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&),这样就不会产生临时对象。

转载于:https://www.cnblogs.com/WonderHow/p/4809781.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Inside the C++ Object Model focuses on the underlying mechanisms that support object-oriented programming within C++: constructor semantics, temporary generation, support for encapsulation, inheritance, and "the virtuals"--virtual functions and virtual inheritance. This book shows how your understanding the underlying implementation models can help you code more efficiently and with greater confidence. Lippman dispells the misinformation and myths about the overhead and complexity associated with C++, while pointing out areas in which costs and trade offs, sometimes hidden, do exist. He then explains how the various implementation models arose, points out areas in which they are likely to evolve, and why they are what they are. He covers the semantic implications of the C++ object model and how that model affects your programs. Highlights *Explores the program behavior implicit in the C++ Object Model's support of object-oriented programming. *Explains the basic implementation of the object-oriented features and the trade offs implicit in those features. *Examines the impact on performance in terms of program transformation.* Provides abundant program examples, diagrams, and performance measurements to relate object-oriented concepts to the underlying object model. If you are a C++ programmer who desires a fuller understanding of what is going on "under the hood," then Inside the C++ Object Model is for you! Get a value-added service! Try out all the examples from this book at www.codesaw.com. CodeSaw is a free online learning tool that allows you to experiment with live code from your book right in your browser.

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值