inside the C++ Object model总结
一. 关于对象
1.内联函数:能够除去函数调用的开支,每一处内联函数的调用都是代码的复制。这是一种空间换取时间的做法,若函数代码量大或者有循环的情况下,不宜内联(这件事有些编译器会自动帮你做)。在类中若直接将函数实现写在类体内,默认内联。如果函数因其复杂度或构建等问题被判断为不能成为inline函数,它将被转化为一个static函数。如果一个inline函数被调用太多次的话,会产生大量的扩展码,使程序大小暴涨。
2.C++对象模型:
3.组合,而非继承是把C和C++结合的唯一可行方法。
4.C++程序的三种程序设计范式:
(1)procedural model,即C语言的模式
(2)ADT model,即所谓的封装,将属性与方法封装在一起
(3)object-oriented model,支持继承、多态(只有通过指针或引用来使用,往往只有到执行期才知道指向哪种object)。
5.class object所占内存计算:
nonstatic data member总和+aligment填补(32位机器为4)+支持虚拟机制产生的额外内存(如果有的话)
这里需要说明一下支持虚拟机制产生的额外内存,一般含有虚函数的类会产生一个指向虚拟表的指针(vptr),所以会增加4byte的内存。对于虚拟继承来讲,所有父类的virtual table指针全部被保留。举例说明:
1 class point 2 { 3 public: 4 virtual void show(){}; 5 protected: 6 int _x,_y; 7 };
sizeof结果为:12
1 class point3d : public point 2 { 3 protected: 4 int _z;
5 };
sizeof结果为:16
1 class point3d : virtual public point 2 { 3 protected: 4 int _z; 5 }; 6 7 class vertex : virtual public point 8 { 9 protected: 10 vertex* next; 11 };
这两个类的size均为20
1 class vertex3d : public point3d,public vertex 2 { 3 protected: 4 int a; 5 };
sizeof结果为32,包括5个4byte的变量,和三个父类虚函数指针。
二.构造函数语意学
1.在编译需要的时候,编译器为自动生成default constructor,若类已有constructor,编译器会将必要的代码安插进去(安插在user code之前)。有些类没有声明constructor,会有一个default constructor被隐式声明出来(所以在用户申明了构造函数的情况下,并不会有default 构造函数被合成),但声明出来的将是一个trivial constructor(并不会被编译器合成),除非是在必要的情况下,一个nontrivial constructor才会被编译器合成,包括以下几种情况:
(1)带有Default constructor的Member Class Object。即某一个成员含有构造函数,假设类A的对象是类B的成员变量,若B包含有多个构造函数,均没有调用A的构造函数,则B的每个构造函数将由编译器安插调用A的构造函数的代码。
(2)带有Default constructor的base Class。与上面类似,但需要注意:调用base class的constructor优先于member class的constructor。
(3)带有virtual function的class。constructor要负责生成并初始化指向虚函数表的指针vptr。
(4)带有virtual base class 的class。不同的编译器对虚基类的处理不同,但是总是会产生一个指针去实现虚拟机制,MSVC将虚函数表与虚基类表合并,均使用指针vpt寻址。
2.在用户没有定义拷贝构造函数的情况下,默认采用memberwise初始化,即逐成员初始化,并进行位逐次拷贝。但在一些情况下,一个nontrivial的拷贝构造函数将被合成:
(1)类的member class object声明了一个copy constructor
(2)类的base class 声明了一个copy constructor
(3)类声明了虚函数
(4)类的继承串链中包含虚拟继承。
以(3)举例,A中声明了一个虚函数,B是A的子类,现进行如下操作B b;A a=b;此时A会合成一个nontrivial copy constructor显示设定a的vptr指向类A的虚函数表,而不是直接把B的虚函数表指针拷贝过来。
如果您声明的类的成员变量包含指针,请务必声明一个copy constructor!!!!
3.关于程序的转化:
(1)显式的copy初始化操作会转化为两阶段,如:X x1(x0)会被编译器转化为两个阶段 1.定义,即占内存:X x1 2.调用拷贝构造函数x1.X::X(x0)
(2)函数参数的初始化,一种最常用的策略便是会形成一个临时变量来存储传进来的参数,函数结束后被销毁。如果函数的形参是引用或指针,则不会这样咯。
(3)函数返回值的初始化,还是会形成一个临时变量,例如:函数X foo(){X xx;....; return xx }会被转化为:void foo(X & _result){X xx;....;_result.X::X(xx);return;},因此在程序员写下X xx=foo();将被转化为:X xx; foo(xx); 这就是所谓的具名优化(NRV优化),如果你的声明包含copy constructor,就很可能被具名优化,当然只是很可能,到底优化没有还得看编译器,因为你根本不知道编译器会干些什么!!
4.必须使用成员初始化队伍初始化的情况:
(1)初始化一个reference member时。
(2)初始化一个const member时
(3)调用一个base class的constructor时
(4)调用一个member class的constructor时
需要说明的是:成员初始化队伍的初始化方法比在构造函数里面复制效率要高一点,因此应该多用哦!举例说明:
class man{public: man(){_name =0;_age=0;};priavte: string _name;int _age;}其构造函数中的赋值由于=的存在,必须产生一个临时变量,此时构造函数会被转化为:man(){_name.string::string();//占内存咯 string temp=String(0); _name.string::operator=(temp); temp.string::~string(); _age=0; } 临时变量的产生与析构将拉低效率。而若以初始化队伍初始化,即man():_name(0),_age(0){}将被转化为:man(){_name.string::string(0);_age=0; }哪个快显而易见吧?经自己实验测试确实是快了一点,但是仍然在一个数量级
但是这里有一点需要注意,在成员初始化队伍中。初始化的顺序并不是按照这个list的顺序,而是按照类里面成员声明的顺序,例如:class x{int i;int j; X (int val):j(val),i(j){};},这段代码会出现异常,因为i会比j先初始化,而此时j还没有被初始化,所以i(j)肯定会异常咯。但是如果这样就对了:class x{int i;int j; X (int val):j(val){i=j};},因为编译器会把初始化队伍的代码放在explicit user code之前。
对象在使用前初始化是个好习惯,尤其是在对象有大量成员变量的情况下。
三.Data语意学
1.一个空类的声明将占1个字节的内存,使得整个class的不同object得以在内存中配置独一无二的地址。但是当你在这个空类中声明一个非静态成员变量时(比如int _a),不同的编译器会产生不同的效果,一般情况下类会变成4字节,因为原来的那1字节已经不需要了,但是有的编译器会保留那1字节,这样有用的字节数就为5字节,再加上32机器的补全(alignment机制),这个类将有8个字节。
2.将类的函数放在类体外中定义,对函数本体的分析会在整个class声明都出现后才开始,因此这是一个良好的习惯咯。
3.关于Data member的布局:
(1)Nonstatic data member在class object中的顺序将和被声明的数据顺序一样。即同一access section(如:private、protected等)较晚出现的member在class object中有较高的地址,但是各个members之间并不一定是连续排列的,因为members的边界调整(alignment)可能就需要填补一些byte。举个栗子:class test{char _a;int _b; char _c;} sizeof的结果是12,没错确实不是8,是12!!!!!!
(2)C++标准虽然也允许多个access section之中的data members自由排列,但目前各家编译器都是将一个以上的access sections连锁在一起,依照声明的顺序成为一个连续的区块,因为这样毕竟效率高嘛。
(3)编译器产生的vptr将被允许安插在对象的任何位置,但是大部分编译器还是将其安插在所有显式声明的成员变量之后,但也有放最前面的。
4.关于data member的存取:
(1)对于静态成员变量,通过对象的指针和对象存取是完全相同的。因为static成员比昂两并不存在类内,而是放在程序的data segment。如果出现两个不同的类声明了相同名字的static member,那它们都被放在data segment中会导致命名冲突,编译器的解决方法是对每一个static member进行name-mangling,使之名字唯一。(name-mangling还会在函数重载中用到)
(2)对于非静态成员函数,其直接存取效率和存取一个C struct一样。但是需要注意如果某个类继承自抽象类(包含虚函数),那么如果用指针存取就会降低效率,因为一直到执行其才能知道父类的指针到底指向的是子类还是父类,因此这种间接性会降低效率。同理虚拟继承时,当子类要存取父类的成员变量时,由于间接性的原因通过对象或指针存取都将降低效率。
5.继承条件下的data member分布:
(1)单一继承:很简单
(2)关于继承链产生alignment的情况:
对于以下三个类:class concrete1{int val;char bit1}; class concrete2:public concrete1 {char bit2}; class concrete3:public concrete2 {char bit3} ;
concrete1所占内存为8,concrete2为12,concrete3为16.
(3)抽象类(包含虚函数)作为父类的单一继承内存分布:
(4)抽象类(包含虚函数)作为父类的多重继承内存分布,如:
其内存分布应如下:两个虚表指针都保存。
(5)虚拟继承下的内存分布:
其内存布局如下有2种策略:
a.每个类除了虚表指针外,再添加一个虚基类指针,以指向自己的虚基类,如:(cfront)
b.扩展虚函数表,也将虚基类的指针存进来。一般的存取策略是:若offset为正存取的是虚函数地址,为负存取的是虚基类地址:
四.Function语意学
1.关于不同成员函数的调用方式:
(1)非静态成员函数,编译器内部会将成员函数实例转换为对等的nonmember函数实例。其过程如下:
a.改写函数原型,提供额外的参数(即指向对象的this指针),如类point的方法 point point::foo();会被转化为point point::foo(point * const this);
b.若函数体内有对对象成员变量的操作,全部替换为带this指针的操作。如函数体内若将两个成员变量相加,_x+_y会被转化为this->_x+this->_y;
c.对程序进行name-mangling处理(前面三.4.(1)页提到过),使函数成为整个程序中唯一的词汇,如:foo_6pointFv(point * const this);注意:这里可以想到重载的函数经过mangling之后名字就不一样了,所以调用起来没问题。
而对象对此函数的调用会由obj.foo()转化为foo_6pointFv(&obj)
(2)虚拟成员函数通过虚拟表存取。这就是C++多态的实现途径,以一个public base class指针寻址出一个derived class object。
a.单一继承下virtual function的布局:
当某个父类的指针调用虚函数时,虽然我们并不知道父类指针指向的究竟是什么(可能是父类对象也可能是子类对象),但通过vptr可以去到该对象的virtual table,而每个函数在virtual table中的顺序是固定的,恩,多态就是这么实现的。
b.多重继承下的virual table布局:
于是,当你将一个子类的地址赋予一个Base1类的指针或子类指针,被处理的virtual table是图中的第一个表格,当你讲一个子类赋予一个Base2类的指针时,被处理的virtual table是图中第二个表格。
有三种情况下第二个基类会影响对virtual function的支持:
- 指向第二个基类的指针调用子类的函数:如Base2 *ptr = new Derived();delete ptr;后面这一句要调用虚析构函数,因此ptr需移动sizeof(Base1)个byte。(即从base2 subobject开头处移动到derived对象的开头处)
- 指向子类的指针调用第二个base class中继承而来的虚函数:如 Derived *pder=new Derived(); pder->mumble();从图上看到mumble()函数是第二个基类的虚函数,为了能调用它,需将pder移动sizeof(Base1)个byte。(即从derived对象的开头处移动到base2 subobject开头处)
- 函数的返回值如果是Base2指针:如Base2 *pb1= new Derived; Base2 * pb2=pb1->clone(); pb1->clone()会传回一个指向子类对象起始位置的指针,该对象地址在赋予pb2之前会经过调整以指向base2指针。
(3)static成员函数具有以下特性:
a.不能直接存取其class类的非静态成员变量。
b.不能被声明为const、volatile或virtual
c.不需非得经由class obj调用。
五.进一步深入构造、析构、拷贝语意学
1.类的对象是可以经由explicit initialization list初始化的,如class point {point(){};public:float _x,_y,_z;},point local = {1.0,1.0,1.0}; 虽然这样效率高一点,但是这样做有条件:只有在class member是常量的情况下奏效;list里面只能是常量;初始化失败可能性很高呢。如果在某些程序中,可能需要将大量的常量数据倾倒给程序,可以考虑此法。
2.constructor的执行算法通常如下:
(1)所有virtual base class和上一层base class的constructor被调用
(2)对象的vptr(s)初始化,指向相关类的虚表。
(3)如果有member initialization list的话,将他们在constructor体内扩展开。
(4)最后调用程序员自己的代码。
3.不准将一个class object复制给另一个class object的方法:将copy assignment operator(即=)设为private。
4.如果类没有定义析构函数,那么只有在类内含member object或类的父类拥有析构函数的情况下编译器才会自动合成一个来,否则析构函数被视为不需要,也就不用被合成和调用。
5.C++之父强调:“你应该拒绝那种被我陈伟’对称策略‘的奇怪想法:你已经定义了一个constructor,所以你以为提供一个destructor也是天经地义的事情,事实上,你应该根据需要而非感觉定义析构函数!”。
6.在C++程序设计中,将所有的object声明放在函数或某个区段的起始处完全是个陋习!因为首先出现在函数起始处的应该是各种各样的检查,检查如果不符合就会跳出函数,那你声明的变量不是白声明了。
六.执行期语意学
1.全局对象,C++所有的全局对象都被放置在程序的data segment中,如果显式地给了它一个值,这便是全局变量的初值,否则设初值为0。C++保证一定会在main()函数第一次使用全局变量前将它构造出来。
2.动态初始化与静态初始化:一般而言,局部变量是在程序运行到某处后再栈中申请分配地址,这就是动态初始化。而全局变量或静态变量在程序开始的时候就分配好了地址,可以让程序放心使用,这就叫静态初始化。
3.new运算符是以标准的malloc()完成的,delete运算总是以标准的C free()完成的。
4.如果你new了一个对象数组,如point * ptr=new point[10];则删除必须要delete [] ptr;如果delete ptr;那将只有一个元素被析构。
5.T c=a+b;总是比 c=a+b有效率一些,因为后者总会产生临时变量来存放a+b的结果。而编译器厂商一般都会实现T opratior+ (const T&,const T&),这样就不会产生临时对象。
posted on 2015-09-20 16:22 Wonder奇迹奇迹 阅读(1468) 评论(0) 编辑 收藏 举报