zoukankan      html  css  js  c++  java
  • 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&),这样就不会产生临时对象。

  • 相关阅读:
    使用PHP建立GIF
    无数据库的详细域名查询程序PHP版
    用PHP实现通过Web执行C/C 程序
    判断一数是否在一已知数组中的函数
    如何将本地项目上传到码云
    IIS服务器,IIS日志文件占用C盘空间,C盘空间不足 常见问题
    谈谈个人能力的系统性
    毕业后5年决定命运
    个人取得工资、薪金所得应当如何缴纳个人所得税
    Chart 控件 for vs2008的安装
  • 原文地址:https://www.cnblogs.com/WonderHow/p/4809781.html
Copyright © 2011-2022 走看看