1. C++对象模型:
其他的两种曾经提出的对象模型:
1. 简单对象模型:
对象是一个slot集合,每一个slot即一个指针,指向成员变量或成员函数
如下图所示,对象是由一系列slot组成,一个slot执行一个类方法或类变量。
2. 表格驱动模型对象:
对象成员变量和成员变量函数各自放在一个表中,对象中有两个指针分别指向这两个表。
3. C++对象模型:
1. nonstatic data memger 放置在每个class object中
2. static data members放在class object之外的地方
3. static 与 nonstatic function members被放在class objects之外的地方
4. virtual functions 以两个步骤支持:
class 产生指向virtural functions的指针,放在表格中,这个表格成为virtural table。
每个对象安插一个指针,指向相关的virtual table,成为vptr。vptr的设定和重置由constructor, destructor, copy assignment完成。
type_info object (它是支持RTTI的)也是由tirtural table指出,位于表格的第一个slot中。
类对象,静态成员,静态方法,虚拟表等部分组成。
2. 对象的内存布局:
nonstatic object member + vptr
vptr 为指针,在32系统中,指针所占的内存是4个字节
#include <iostream>
using namespace std;
class Animal
{
public:
// virtual void Shout(){};
// virtual void Eat(){};
// int GetAge(){return 1;};
int GetAge();
private:
// int age;
};
int Animal::GetAge()
{
return 1;
}
int main()
{
Animal animal;
cout<<"The size of class without member:" << sizeof(animal) <<endl;
cout << "End" << endl;
return 0;
}
上述代码验证:
1. 如果没有虚函数、没有成员变量、没有普通成员函数,类定义的对象的大小为1 (使用sizeof()得到)
这一个字节是为了编译器为了指示该类的存在而设置,如果类有了虚函数或成员变量,则不再需要改填充字节。
2. 如果单纯设置虚函数,类对象的大小为4个字节。因为它包含了指向虚函数表的指针 vptr
此时不需要那一个字节的填充,如果这一个字节是类必须的,此时的类对象的大小应该是 5
3. 同理如果仅仅有一个int型的成员变量age,此时类对象的大小也是4个字节.
4. 如果成员变量age和虚函数都存在则此时的类对象的大小为8个字节,也即int型age占有四个字节, vptr指针占有四个字节
5. 如果是两个纯虚函数,此时的类对象大小仍然是 4个字节
6. 普通成员函数,其并不位于类对象之中,它位于类内存之中,并不占用类对象的内存。
问题:
1. 将子类对象赋给父类对象(非指针或引用形式赋值),对象内存将被裁减(sliced),该裁减如何完成:
复制一个父类对象?还是其他的方法。
2. 裁减之后,虚函数表指针vptr也被复制(不会是单纯的复制)?还是会被重新复制,重新复制由谁完成?
此处的vptr不会被复制,它会被父类的复制构造函数重新赋值新的虚拟函数表。
3. 构造函数
需要合成默认构造函数的情况:
1. 带有默认构造函数的成员类对象(member class object),包含该类对象的类会在自己的默认构造函数中隐式(由编译器完成)加入
成员类对象的默认构造函数,有多个成员类对象时,调用构造函数的顺序按照类对象定义的顺序添加。
2. 带有默认构造函数的基类,其子类中如果没有构造函数,则会合成以讹默认构造函数,在该合成的构造函数中其中会调用基类的默认构造函数。
3. 带有虚函数的类:
1. 类声明或继承一个virtual function
2. 类继承链中有一个类或多个类为虚基类
由于vptr指针要由构造函数类初始化,因此对有虚函数的类或者派生类药合成构造函数来做这些工作
4. 带有虚基类的类:多继承需要确定虚基类成员所在的内存地址,也即生成一个唯一指向虚基类成员的指针,这个指针需要由构造函数完成。
注:
尽管合成构造函数,但是非成员类对象还是不会进行初始化,也即非静态的普通非对象成员变量。
4. 拷贝构造函数
使用拷贝构造函数(copy constructor)的情况:
class Animal{};
1. 使用一个对象初始化另外一个对象时
Animal A;
Animal B(A)
Animal C = A;
Animal D = Animal(A);
2. 以一个类对象作为函数的参数
void GetName(Animal A){}
3. 函数中返回一个类对象
void GetAClass()
{
Animal A;
return A;
}
三种情况下需要用到拷贝构造函数,如果提供了则进行显示调用,如果没有显示提供,则需要合成默认的拷贝构造函数
默认的拷贝构造函数实现的是按位拷贝,其中的指针和引用会按照位进行拷贝,不会对他们指向的内容进行拷贝。
对于成员类对象,如果该类亦没有显示提供拷贝构造函数,它也是按照位拷贝进行。
非位拷贝的情况:
1. 成员类对象有拷贝构造函数
2. 本类所继承的基类具有拷贝构造函数
3. 类中声明了一个或多个虚函数
4. 类继承链中有一个或多个virtual base class 存在。
其中3,4不按照位拷贝是需要对vptr进行正确复制,需要合成拷贝构造函数对vptr进行正确赋值
Bear作为ZooAnimal的子类,将Bear的对象赋值给ZooAnimal类对象,需要对对象的virtual table 做重新赋值。
即如将子类对象初始化基类对象,不能按照位拷贝来进行复制,需要对基类独享的virtual table指针重新赋值,因此需要合成拷贝构造函数。
拷贝构造函数分别对上述三种情况都做了修改:
1.
Animal A;
Animal B(A)
Animal C = A;
Animal D = Animal(A);
改写为:
Animal B;
Animal C;
Animal D;
调用拷贝构造函数
B.Animal::Animal(A);
C.Animal::Animal(A);
D.Animal::Animal(A);
2.
void GetName(Animal A){}
修改为:
Animal temp;
temp.Animal::Animall(A);
void GetName(temp);
3.
Animal GetAClass()
{
Animal A;
return A;
}
改写为:
void GetAClass( Animal & _A)
{
Animal A;
A.Animal::Animal();
_A = &A;
return ;
}
5. 使用member initialization list的情况:
1. 初始化一个reference member时
2. 初始化一个const member时
3. 调用一个base class的constructor时,而它有一组参数
4. 当调用一个member class的constructor,而它具有一组参数时
对于成员类对象放到参数类表中,初始化效率更高
如:
class Animal
{
Animal()
{
name = "";
age = 0;
}
/*
Animal(): name("")
{
age = 0;
}
*/
private:
string name;
int age;
}
Animal()
{
name = "";
age = 0;
}
被改写为:
Animal()
{
string temp;
temp.string::string("");
name = temp;
temp.destrucor();
age = 0;
}
Animal(): name("")
{
age = 0;
}
被改写为:
Animal()
{
name.string::string("");
age = 0;
}
省去了临时对象的创建和销毁的过程。
构造函数会将成员列表中的变量初始化安插在函数体内其他的初始化变量代码前,顺序按照声明顺序进行。
6. 不同编译器对类对象大小的影响:
对于一个空类(不包含任何数据成员和成员函数),为了在内存中有效表示类对象,编译器为对象额外填补了一个字节的数据(无用)
由于编译器的不同,有的编译器这一字节会被其子类所“继承”,那么子类的大小为 1 Byte + sizeof(vptr) = 5 Bytes,
由于C++编译器实现字节对齐,那么最终的类对象大小将是 8 Bytes
对于多继承,如果每一个父类都有虚函数,也即有序函数表,那么子类中将分别继承他们,子类对象中要包含每一个父类的vptr指针。
7. 数据成员:
静态数据成员放在程序的数据段,为类所拥有,不属于任何一个个别类。
非静态数据成员按照不同的访问区(Access sections),按照声明的先后顺序在对象中排列。
数据成员存取:
1. static data menber: 静态数据成员,对应于类放在程序的data segment之中,对其存取不是使用类对象,
而是使用类,如Point3d::chunkSize来实现。
如果多个类有相同的静态变量,那么编译器会将他们改名,按照类来进行。
2. 非静态数据成员(nonstatic data members)
在成员函数中,对nonstatic data members的存取由一个implicit class object(由this指针表达)完成。
被改写为:
为translate()函数自动加入了一个类的this指针。
对于nonstatic data members的存取操作:编译器将class object的其实地址加上 Data member的偏移位置(offset)得到。
Data member的偏移位置offset在编译期间可以获知,继承的member也可以获知。nonstatic data member 效率和存取一个C struct member或
非派生类的成员是一样的。
origin.x = 0.0;
pt->x = 0.0
两者区别,若对于非继承类,那么两者相同,对于Point3d这样的继承类,且其中有一个Virtual base class
那么pt->x 则需要额外工作来确定其真实类型在访问,而对象则不必,它的类型在定义时已经确定。
因为有可能将子类对象赋值给父类对象指针。
8. Data Member分四种情况讨论:
1. 单一继承且不含有virtual functions
2. 单一继承并且包含有virtual functions
3. 多重继承
4. 虚拟继承
1. 对于concrete derive来说,继承不会给子类访问数据成员带来时间和空间上的额外的消耗。
对象数据内存中的布局例子如下:
此时会出现子类对象比你实际理论计算的内存空间要大。
由于Bytes padding(字节填充)父类中会有填充字节,而子类派生父类,添加新的变量,这些变量是在父类的padding字节之后加入。
如下例:
此类对象占用的内存即 int + char + char + char + padding.
将它修改为继承的形式,每一次继承,添加一个成员:
而此时Contrete3的对象内存布局如下图所示:
显然Concrete3的对象所占的内存比Concreate类对象要大不少,但是他们的数据成员是一样的。
是否可以如同Concreate类一样,让Concrete3的成员“挤一下”?
由此图,明显地,他们是无法“挤一下”的,因为这样在父类对象与子类对象之间进行赋值时会出现错误。
2. 单继承带有virtual functions(即实现了多态)
时间与空间上均有增加:父类由于有虚函数,要添加virtual table,并且类对象中要添加vptr指针。对构造函数,如果没有明确定义
那么要隐式合成,完成对vptr的正确赋值。
对于子类,要继承一个父类的vptr,但是如果子类中仍然有virtual functions,在自对象中不会再添加virtural table
为何不会再添加,子类中的virtural function 放到那个virtual table中呢?疑问ing......
3. 多重继承:
多重继承不如单继承那么自然简单。
如多继承例子:
他们之间的继承关系如下图所示:
他们各自的对象在内存中的布局如下:
多继承下,内存的分布如图所示,Vertex3d将所有的父类集成到自己的内存中
4. 虚拟继承:
按照多继承的思路,中间类继承子同一个基类,那么最底层子类中顶层基类会出现两次。
即Vertex3d的对象中要Pointed2d 要出现两次(来自不同的中间基类)
显然有问题,解决方法:
1. 对虚继承中,虚基类在子类中对象中防止一个指针指向该虚基类在子类中的内存位置,对每一个virtual base class
要放一个指针。
2. 将虚基类在子类对象中的offset放在virtual table中,对虚基类进行访问时,首先拿到其在子类对象内存分布中的
offset,计算后访问从虚基类继承的成员变量。
例子:
他们的继承关系如下图:
各个类的对象的数据布局如下图所示:
Vertex3d由于虚继承了Point2d,那么它从Point3d和Vertex中继承的指向Point2d的subobject的指针,在Vertex3d中指向了同一个Point2d的subobject。这样保证了在对象地址中仅仅有一个虚基类(Point2d)的subobject对象存在。
8. Member Function 的调用方式
1. nonstatic Member functions(非静态成员函数)
非静态成员函数要与非成员函数有一样的调用代价,不应带有额外负担。
编译器将member functions转化为了nonmember function。
上述函数由 float Point3d::magnitude3d(){ return sqrt(x*x + y*y + z*z); }方法转化而来。
该转化分为三步:
1)改写函数的原型(signature),安插一个额外的参数到member function中,用以提供一个存取成员变量的管道,使得class object可以调用该函数,该额外参数为this指针。
如:
Point3d Point3d::magnitude(Point3d * const this);
2)将每一个对nonstatic data member的存取操作,改为经由this指针来存取。
{
return sqrt(
this->_x * this->_x +
this->_y * this->_y +
this->_z * this->_z);
}
3)对member function重写一个外部函数,对函数名称进行“mangling”处理,使其在程序中成为独一无二的函数。
Extern magnitude__7Point3dFv( register Point3d * const this);
由此:obj.magnitude(); 操作变为了 Magnitude__7Point3dFv( &obj); 操作
ptr->magnitude(); 操作变为了 Magnitude__7Point3dFv( ptr); 操作
2. 换名处理:
Member 前面会加上class名称,形成一个独一无二的命名,对于重载函数换名后可能是同名函数,处理方法是给它加上参数列表。
3. 虚成员函数:
如果normalize()为虚成员函数,那么ptr->normalize()将转化为
(*ptr->vptr[1])(ptr);
vptr为virtual table,virtual table中的函数也会被改名,主要由于派生产生的问题。
4. static member functions(静态成员函数)
C++有时需要一些独立于class object之外的操作,引入了static member functions。其主要的特征就是没有this指针。
由此:1. 不可以访问nonstatic members;2. 不能声明为const,volatile或virtual类型的方法;3. 不需要经由class object被调用
static function 差不多等同于一个nonmember,它可以成为一个callback函数。
5. virtual member functions(多态问题)
为了支持virtual function机制,必须首先能够对多台类型有某种形式的“执行期类型判断法”(runtime type resolution),也即要得到指针ptr->z()中ptr的某种对象信息。
最简单的方法:
设ptr带有其所对应对象信息,如所参考对象地址或对象类型的某种编码或某个结构,可以唯一标识该类,以便正确决定z()方法的真实地址。
缺点:需要额外增加空间负担,破坏了与C语言的链接兼容性。
其实此处只需要知道两个额外信息:
ptr所指对象的真实地址,从而可以选择正确的z()实例
z()实例的位置,以便可以调用它。
实现中,可以给每一个多态的class object添加两个member:
1. 字符串或数字,标识class的类型
2. 一个指针,指向某表格,表格中持有程序的virtual functions执行期的地址。
要在执行期找到函数:
1. 为了找到表格,每一个class object安插一个有编译器内部产生的指针,指向该表格。
2. 为了找到函数地址,每一个virtual function被指派一个表格索引。
基类Point:
继承类如下:
其中的虚函数处理如下图所示:
在继承类中,需要将虚函数表中的虚函数slot指向的虚函数修改掉。
上述即C++的处理方法:
给虚基类加一个virtual table,其中存放了所有虚函数,而继承类中会对应地放置改写了的基类的virtual function地址。这些函数在子类中进行了重写,virtual bable中放置的是新的函数的地址。
如果子类添加新的virtual functions,这些functions放置在继承的virtual table中,在virtual table中添加新的slot,将函数添加进去
ptr-> z(); 由编译器改写为(*ptr->vptr[4])(ptr);
6. 多重继承
多重继承中支持virtual functions,复杂度主要是来自第二个以及后继的base classes上,必须在执行期间动态调整this指针,让它指向正确的地址。
按照多继承的内存知,如Base2基类,在Base1基类之后,
Base2 * pbase2 = new Derived();
应该调整为指向Base2 subobject,
Derived * temp = new Derived();
Base2 * pbase2 = temp?temp+ sizeof(Base1): 0;
但是delete pbase2; 时,有需要将指针做调整,让其指向内存起始位置。
指针的调整有如下几个方法:
1. 对所有的函数访问,计算偏移地址,再调用。
这种方法会将非虚函数也连累进来。
2. 使用Thunk技术
Thunk为一段汇编代码,用来以适当的offset调整this指针,让它指向正确的内存,在一个作用就是跳转到virtual function去。
Sun中使用的是所谓的”split functions”技术。Microsoft 使用所谓的“address points”来取代thunk技术。
7. 虚拟继承
虚拟继承,将parent class 的subobject的偏移量放置到virtual table中,用于访问父类的subobject。
8. inline functions
内联函数在编译期间,将整个函数做优化,如果结果是常量值,直接使用常量值替代;计算的式子,直接将式子集成到代码中,替代函数。
内联函数类似define声明的常量替换,但较它更安全。内联函数会将函数中的代码嵌入到主代码中,使得代码量增大,对于较大的函数不宜声明为内联。内联由于进行了优化,可以极大地提高函数调用的速度。
9. 特殊指针——指向对象成员的指针
1. 指向成员变量的指针
成员变量按照声明顺序和访问区顺序依次排列,以如下的类为例:
class Point3d
{
public:
virtual ~Point3d();
public:
static Point3d origin;
float x, y, z;
};
根据vptr放置位置不同,三个坐标值在对象布局中的offset不同,如果vptr放置在结尾位置,三个坐标值的offset为 0,4,8。如果vptr放置在对象开始处,对象布局中的offset就分别是4,8,12。而事实上,去data members的地址,传回来的值总是多1,也就是1,5,9,或5,9,13等。
这么处理的原因是区分一个“没有指向任何data member”的指针,和一个指向“第一个data member”的指针。如:
float Point3d::*p1 = 0;
float Point3d::*p2 = &Point3d::x;
由于p1和p2相等,因此无法区分。因此给指向数据成员的指针加1,以示区别。
但是测试上述类的结果如下:
打印data member的地址。
cout << "Address of Member Values:" << endl;
printf("&Point3d::x = %p\n", &Point3d::x);
printf("&Point3d::y = %p\n", &Point3d::y);
printf("&Point3d::z = %p\n", &Point3d::z);
结果:
解释了“指向data members的指针”之后,可以解释如下两个式子:
& Point3d::z; 和 & rgin.z;
“个nonstatic data member的地址,将会得到它在class中的offset”,取一个“绑定于真正class object身上的data member”的地址,将会得到该member在内存中的真正地址。
多重继承之下,若要将第二个(或后继)base class的指针,和一个“与derived class object绑定”的member结合起来,那么将会因为“需要加入offset值”而变得相当复杂。
2. 指向Member Function的指针
取一个nonstatic member function的地址,如果该函数是nonvirtual,得到的结果是他在内存中的真正地址,这个值也是不完全的,需要绑定到某个class object上,才能够调用该函数。所有的nonstatic member functions都需要对象的地址(以参数this指出)
double 返回类型
( Point::* 指针类型
pmf) 指针的名称
(); 参数列表
Double (Point::* coord)() = &Point::x;
Coord = &Point::y;
调用函数:
(origin.*coord)()
(ptr->*coord)()
指向member function的指针的声明语法,以及指向”member selection运算符”的指针,其作用是作为this指针的空间保留者,这也是为何static member functions(没有this指针)的类型是“函数指针”,而不是“指向member function的指针”的缘故。
指向virtual member functions的指针
类似虚成员数据,获得的是一个在类中的偏移setoff,而nonvirtual function获得的是真正的内存地址。这就需要在执行时区别是否是virtual 函数指针。
一部分编译器中的做法是将指针与~127做位与操作,如果结果为0,说明是虚函数指针,否则是非虚函数指针。从而在程序中执行不同的代码:
(((int) mpf)& ~127) ? (*pmf)(ptr) : (*ptr->vptr[(int)pmf](ptr));
多继承之下,指向member function的指针。
为指针专门设计一个结构:
struct __mptr
{
int delta;
int index;
union{
ptrtofunc faddr;
int v_offset;};
};
Index 和 faddr 分别持有virtual table索引和nonvirtual member function地址。从而根据index值是否是0,从而进行不同的调用。
(ptr->*pmf)();
改写为:
(pmf.index < 0) ? ( *pmf.faddr)( ptr) : (* ptr-> vptr[pmf.index](ptr));
10. 构造函数
1.
class Abstruct_base
{
public:
virtual ~Abstract_base() = 0;
virtual void interface() const = 0;
virtual const char * mumble() const {return _mumble;}
protected:
char * _mumble;
};
抽象的Base class,其中有pure virtual function,使得Abstract base不可能拥有实例,但是仍然需要提供一个显式的构造函数以初始化data member _mumble,没有这个构造函数,derived class的对象将无法初始化_mumble的初值。
2. 纯虚函数可以被调用,只能以静态方式调用(invoked statically),不可以经过虚函数机制调用:在子类的方法中调用Abstract_base::interface();
唯一例外的情况是pure virtual destructor,class设计者必须得定义它,每一个derived class constructor会被编译器加以扩展,以静态调用的方式调用其中每一个 virtual base class以及上一层base class的destructor,只要缺少一个base class destructor的定义,就会导致继承链断裂而编译失败。
#include <iostream>
using namespace std;
class Abstract_base
{
public:
virtual ~Abstract_base() = 0;
virtual void interface() = 0;
virtual const char * mumble() const {return _mumble;}
protected:
char * _mumble;
};
class Concrete_derived: public Abstract_base
{
public:
Concrete_derived();
};
class Third_Derived : public Concrete_derived
{
public:
~Third_Derived() {};
};
int main()
{
Third_Derived thirdDerived;
cout << "Hello world!" << endl;
return 0;
}
出现错误:
只要将base class的析构函数定义为纯虚函数,此错误就会出现,与中间子类是否定义析构函数没有关系。
\Constructor\main.cpp:(.text$_ZN16Concrete_derivedD2Ev[Concrete_derived::~Concrete_derived()]+0x16)||undefined reference to `Abstract_base::~Abstract_base()'|
疑问ing????????
较好的解决方法:不要将virtual destructor声明为pure。
不将destructor声明为pure,就不会出现错误。
3. const 函数:
声明函数为const,然后发现其继承类的实例会修改 data member,由于声明为const,无法修改成员。简单的解决方法是不再用const声明函数。
4. 无继承的对象构造:Point local; Point * heap = new Point();
对于new 操作,并不会有default constructor施行与其传回的Point object上,而delete时,如果没有显式的destructor存在,则也不会调用类的析构函数,仅仅简单地将内存释放即可。
如果没有显示的copy constructor,那么返回、赋值等操作完全是简单的为拷贝操作。
例如:
type struct
{
float x, y, z;
}Point;
观念上,编译器会为Point声明trival default constructor、一个trival destructor、一个trival copy constructor,以及一个trival copy assignment operator。但是实际上,编译器会分析这个声明,为它贴上Plain Ol’ Data标签。
在实际的使用中,如声明对象,销毁对象,对象间赋值,赋值符号的使用都不会调用这些函数。
4. 带有虚函数
一旦有了虚函数,就需要对原有的函数等进行扩展,定义constructor代码中,会在参数列表中加入对象的this指针,以便正确初始化vptr参数。
需要合成copy constructor 和 copy assignment operator,而且操作不再是trival的,不能使用默认的bitwise或memberwise的拷贝或赋值方式,同时这些方法也带有this指针参数,以便正确初始化vptr参数。
继承体系下的对象构造,construtor可能含有大量的隐藏代码,因为编译器会扩充每一个constructor,扩充程度是class T的继承体系而定。一般而言编译器所作的扩充大约包含以下几个方面:
1. 记录member initialization list中的data members初始化操作会被放到construtor函数体内部,以menbers 的声明顺序为序。
2. 如果有一个member 并没有出现在member initialization list之中,但它有一个default constructor,那么该default constructor必须备调用。
3. 在那之前如果class object有virtual table pointers,他们必须设定初始值以指向适当的virtual tables。
4. 在那之前,所有上一层的base class constructors必须被调用,以base class 的声明顺序为顺序(与member initialization list中的顺序没关联)
5. virtual base class constructors必须备调用,从左到有,从浅到深。
5. 虚拟继承(virtual inheritance)
由于继承中只能有一个base class subobject存在,那么对与base class 的构造函数调用,只能调用一次。
对与vptr指针的初始化,最适当的地方是base class constructor调用之后,但是在程序员提供的代码或是member initialization list中的menber初始化操作之前。
解决方法:
有些编译器将constructor分裂为二,一个针对完整的object,一个针对subobject。
11. 拷贝函数
如果没有显式声明拷贝构造函数,则使用默认的拷贝构造函数则实现的是bitwise拷贝。以下的情况中,不会进行位拷贝:
1. 当class内含有一个member object,而class 有一个copy assignment operator时。
2. 当一个class的base class有一个copy assignment operator时。
3. 当一个class 声明了任何virtual functions,此时需要对vptr进行正确赋值,因此不可用bitwise拷贝方式
4. 当class继承一个virtual base class,不论此base class有没有copy operator时。
12. 析构函数
如果class没有定义destructor,那么只有在class内涵的member object抑或class自己的base class拥有destructor的情况下,编译器才会自动合成一个出来。否则destructor被视为不需要,也就不需要被合成。
在有constructor时,destructor的调用顺序是按照与constructor的顺序相反的顺序来调用,即从最底层派生类开始,一次向上调用。
13. 对象的构造和析构
一般而言,constructor和destructor的安插都如期望一样:
// C++
{
Point point;
// point.Point::Point();
…
// point.Point::~Point();
}
如果一个区段,或函数中有一个以上的离开点,情况就会复杂很多,Destructor必须被放在每一个离开点之前。
那么为了避免一些不必要的对象创建与销毁,可以将local对象的定义放在临近使用的地方,这样在之前如果函数或区段返回,则不再会创建和销毁该对象。但许多Pascal或C程序员习惯将所有的objects放在函数或区段的起始处声明与定义。
全局对象:
C++中的所有global objects都被放在了程序的data segment中,如果显示给定值,则以该值为初值,否则初始化为0。C语言中并不自动为全局变量赋初值。
对于全局对象的构造函数的调用,是在程序启动时进行的,这样需要一个静态初始化。在一部分的编译器中的解决方法是:
为每一个需要静态初始化的文件爱你产生一个_sti()函数,内含有必要的constructor调用操作或inline expansions。类似地有一个静态的内存释放操作(static deallocation),构造一个_std()函数,包含了destructor调用操作。这样提供了一组runtime library “munch”函数:一个_main()函数(用以调用可执行文件中的所有_sti()函数),以及一个exit()函数(以类似方式调用所有的_std()函数)。
统计这些构造函数和析构函数的方法:
使用nm命令,将所有的obj文件的符号表格输入该命令,从其中过滤器其中的构造_sti()和析构_std()函数。
另外一个解决方法是system V 1.0版中的做法,每一个可执行文件是System V COFF格式,这些文件中包含了_link nodes,这样将所有的_sti()和_std()函数找到。
建议在程序中不要用那些需要静态初始化的global objects。
局部静态变量:
局部静态变量同全局变量一样,其在内存位置也是data segment。由于是静态变量,在整个程序(函数)的运行期间,在程序起始时构造出来对象,在程序结束时才析构掉,中间再次调用该函数也不应该再对该函数进行构造。
一个做法是导入一个临时的对象,以保护静态变量只被构造一次。析构同样可以参考该临时的对象。
对象数组:
如果对应的类有构造函数,则需要在分配内存之后,对每一个对象调用构造函数进行初始化,否则只是单纯分配内存。
同样对于有构造函数的类,分配数组后,会有相应函数对其进行处理。
vec_new( void * array, size_t elem_size, int elem_count, void (*constructor)(void *), void (*destructor)(void *, char));
同样需要一个delete 函数释放掉这些对象。
vec_new( void * array, size_t elem_size, int elem_count, void (*destructor)(void *, char));
14. new 和delete
new 与 delete 其中除了分配资源之外,还做了错误处理。
int * pi = new int(5);
int * pi = __new (sizeof(int));
*pi = 5;
作出错误处理,应该如下:
int * pi;
if ( pi = __new( sizeof( int ) ) )
* pi = 5;
同理delete pi;
if ( pi != 0)
__delete( pi );
对于数组:
在使用new构造时,同样是使用vec_new()方法来进行。
而在delete 时,对于数组,在前编译器版本中,需要显式给出数组大小。
如delete [ array_size] p_array;
而后期的编译器虽然不需要给出大小,但是需要加上[],如delete [] p_array; delete会自动寻找维数。否则将出现delete了数组第一个元素的错误。
14. 临时对象
临时对象的产生,要注意其析构的时机,不能够在使用之前就析构了该对象,否则出现错误。
15. Template(模板)
Template原本被看作是对Container classes如lists和Arrays的一项支持。,但是它目前已经成为了标准模板库的一部分,或者用于一项所谓的tempalte metaprogram技术。
如:
template<class type>
class Point
{
public:
enum Status { unallocated, normalized};
Point( Type x = 0.0, Type y = 0.0, Type z = 0.0);
~Point();
void * operator new( size_t);
void operator delete( void *, size_t);
private:
static Point<Type> * freeList;
Type _x, _y, _z;
}
类模板中的静态元素或函数只能使用template Point class的某个实力来存取或操作
因此存取Status类型,如下写:
Point< float >:: Status s; 不可以如此:Point::Status s;
对于静态数据成员:
freeList,也必须使用模板类的某个实例来调用:
Point< float >:: freeList; 不可以如此:Point::freeList;
对于模板类实例的指针赋值0,则该指针赋值为空,而引用类型无法引用空对象,因此对于const Point< float > & ref = 0;默认扩展如下:
Point< float > temporary(float( 0));
const Pooint< float > & ref = temporary;
对于template function不应该被“实例化”,至少对那些未被使用过的。只有在member functions被使用的时候,C++ Standard才要求被“实例化”。但是目前的编译器,并不是都支持这个机制。使用使用者主导“实例化(instantiation)”规则,原因如下:
1. 空间和时间效率的考虑。对于有些类根本不会用到某些函数,将他们实例化,结果占用内存,且编译效率低下。
2. 尚未实现的机制,并不是一个template实例化的所有类型都能够完全支持一组member functions所需要的所有的运算符。
模板机制现在很不完善,存在一些缺陷。如所有与类型有关的及爱你查,牵扯到了template的参数,都必须延迟到阵阵的实例化(instantiation)操作发生才会进行检查。
Template中的名称决议法:
scope of the template definition 也就是定义出template的程序端
scope of the template instantiation 实例化template的程序端
template <class type>
class ScopeRules
{
public:
void invariant()
{
_member = foo(_val);
}
type type_dependent()
{
return foo(_member);
}
private:
Int _val;
Type _member;
};
情况一:
extern int foo(int);
ScopeRules < int > sr0;
sr0.invariant();
在invariant()调用的为那个foo实例呢?
对于一个nonmember name的决议,是根据name的使用是否与“用以实例化template的参数类型”有关决定。如果其使用互不相关,那么以scope of the template definition来决定name,如果其使用互有关联,那么就以scope of the template instantiation来决定name。
Sr0.type_dependent();方法与类型相关,其使用scope of the template instantiation来决定,调用extern int foo(int);。
对于函数的实例化行为:
两种策略,一种是编译时期策略,程序代码必须在program text file中备妥可用。另一个是连接时期策略。
16. 异常处理(exception handling)
C++的异常处理三个主要词汇:
1. 一个throw子句。它在某处发出一个exception,被抛出的exception可以是内建类型,也可以是自定义的异常类。
2. 一个或多个catch字句,每一个catch字句都是一个exception handler,它用来表示说,这个字句准备处理某种类型的exception,并且封闭的大括号中提供实际的处理程序。
3. 一个try区段,它被围绕一系列的语句组成,这些语句可能引发异常。
一旦发生了异常,当前函数对程序的控制权将被剥夺,默认的处理程序terminate()会被调用,函数堆栈中的改函数也会被弹出。Unwinding the stack 程序调用时,每个函数被弹出堆栈,其局部的对象都会被释放。
对与Exception Handling的支持
1. 检验发生throw操作的函数
2. 决定throw操作是否发生在try区段中
3. 若是,编译系统必须吧exception type 拿来和每一个catch子句进行比较
4. 如果比较吻合,流程控制交到catch子句手中
5. 如果throw的发生并不在try区域,或没有一个catch子句吻合,那么系统必须摧毁所有active local objects,从堆栈中将目前的函数unwind掉。进行到程序堆栈的下一个函数中去,然后重复上述步骤。
在catch语句中,与对象进行比较时,它里面产生的是一个临时对象,这个对象被传递到catch内进行处理,如果在catch内进行再一次抛出,那么抛出的是本临时对象,不会将原Exception对象抛出。此处一定注意。
17. RTTI(执行期类型识别)
Type-safe Downcast(保证安全的向下转换操作)
此处是将父类动态转化为子类的对象
Type-safe Dynamic Cast(保证安全的动态转换)
动态的转化类型,将子类对象转化为父类对象。
References并不是Pointers:
将dynamic_cast施加到引用上,如果转换成功则返回true,否则引发bad_cast exception。
结束:
如果对与Conponent Object Model<COM>感兴趣,推荐两本书:
Essential COM(Don Box/ Addison Wesley):第一章和第二章吧软件组件的本质,问题所在以及COM的解决之道解释非常好,带着读者以一般的,纯粹的C++语言完成一个COM程序结构。但是第三章之后较晦涩难懂
Inside COM,全书清爽简易,但是最好先通读Essential COM的前两章之后,有了扎实的基础再来读这本书。