侯捷翻译的《深度探索C++对象模型》一书,详细介绍了对象在内存中的布局,对于深入理解C++语言特性具有很好的启发作用。因此在各大公司面试C++相关岗位的时候,也会提问到相关的问题。


1、从sizeof对象大小谈起

      有一次,B哥去面试问到了一个题目,问sizeof(A)是多少?

class A{};//答案:sizeof(A) =1
       在C++对象模型中,空类的对象也是占用1个字节的空间的。假设将类A修改为如下所示,sizeof(A)还是为1,此时这个1个字节就是char ab占用的空间。
class A{   char ab; //成员变量,char类型占一个字节内存};//sizeof(A) =1
        同样的,如果将A类修改如下所示,得到的sizeof(A)是4,代表int类型占的字节数。
class A{   int ab; //int类型占4个字节};//sizeof(A) =1

        最后,假设这个类定义成员函数如下所示,此时sizeof对象的大小还是1。

class A{public:  void func()  {}; //成员函数  void func1() {}; //成员函数  void func2() {}; //成员函数};//sizeof(A)=1

总结:1、空类或没有成员变量的类,占用1个字节空间。

          2、类的成员函数不占用类对象的内存空间。

          3、成员变量是包含在每个对象中的,是占用对象字节的。

  4、成员函数跟着类走,与这个类产生了多少个实际对象无关。


2、this指针的调整

class A {public:  int a;  A(){    printf("A::A()的this指针是:%p!\n", this);  }  void funcA(){    printf("A::funcA()的this指针是:%p!\n", this);  }};
class B {public:  int b;  B(){     printf("B::B()的this指针是:%p!\n", this);  }  void funcB(){     printf("B::funcB()的this指针是:%p!\n", this);  }};
class C : public A, public B{public:  int c;  C(){    printf("C::C()的this指针是:%p!\n", this);  }  void funcC(){    printf("C::funcC()的this指针是:%p!\n", this);  }  void funcB(){    printf("C::funcB()的this指针是:%p!\n", this);  }};

      从运行的结果来看,C对象的this指针和A的this指针是指向同一个地址,B的this指针在A地址的后面,相差4个字节。这是因为A对象sizeof(A)=4.

结论:1、派生类对象它是包含基类子对象的。

  2、如果派生类只从一个基类继承的话,那么这个派生类对象的地址和基类子对象的地址相同。

  3、但如果派生类对象同时继承多个基类,那么第一个基类子对象的开始地址和派生类对象的开始地址相同。后续这些基类子对象的开始地址和派生类对象的开始地址相差多少呢?相差前面基类子对象所占用的内存空间。

总结:调用哪个子类的成员函数,这个this指针就会被编译器自动调整到对象内存布局中对应该子类对象的起始地址上;


3、类的构造函数

        编译器会在哪些必要的时候帮助我们把默认的构造函数合成出来呢?(1)这里类C没有任何构造函数,但包含一个类B类型的成员变量mb,而该对象ma所属于的类B有一个缺省的构造函数。这个时候,编译器就会为该类C生成一个 “合成默认的构造函数”,合成的目的是为了调用B里的默认构造函数。

class A {public:  A() {//默认构造函数     cout << "A" << endl;  }};class B {public:  B(){ //默认构造函数    cout << "B" << endl;  }};class C {public:  int m_i;  int m_j;  B mb; //类类型成员变量  void func(){    cout << "C::func()" << endl;  }};
    (2)父类带缺省构造函数,子类没有任何构造函数,那因为父类这个缺省的构造函数要被调用,所以编译器会为这个子类合成出一个默认构造函数。      合成的目的是为了调用这个父类的构造函数。换句话说,编译器合成了Child类默认的构造函数,并在其中安插代码,调用其父类base的缺省构造函数。
class base {public:  base(){     cout << "base()" << endl; }};class Child  :public base {public:   int m_i;   int m_j;   Child(){ }};
(3)如果一个类含有虚函数,但没有任何构造函数时,需要合成构造函数。     编译器会给我们生成一个基于该类的虚函数表vftable。然后编译给我们合成了一个构造函数,并且在其中安插代码:把类的虚函数表地址赋给类对象的虚函数表指针 (赋值语句/代码)。
class Base {public:  Base(){     cout << "Base()" << endl;  }};class Child :public Base{ public:    int m_i;   int m_j;  void funct() {    cout << "funct()" << endl;  }  virtual void mvirfunc()  {    cout << "mvirfunc" << endl;  }};
(4)如果一个类带有虚基类,编译器也会为它合成一个默认构造函数。在虚基类结构中,编译器会为子类和父类都产生了“合成的默认构造函数”。
class Grand {   public:};class A1 : virtual public Grand {    public:};class A2 : virtual public Grand{    public:};class C :public A1, public A2{   public:      C() {}};


4、小结

      最后,总结一下本文的全部内容。当对象为空,或者没有成员变量的时候,类的实际对象占用的大小为1个字节。如果对象有实际的成员变量,那么占用的大小就是这个实际成员的内存大小。
     编译器会在4种情况下自动合成构造函数,第一,子类继承自父类,父类有构造函数,此时需要为子类合成构造函数并调用父类构造函数。第二,类中含有其他类的成员变量,并且其他类有构造函数。此时会给这个类合成一个默认的构造函数。第三,存在虚函数的类,需要合成默认构造函数并赋值虚函数表指针vptr。第四,存在虚基类表的情况下,也会合成默认构造函数。
     当深入理解了编译器在何时给我们合成默认构造函数,我们也就能更好的在编写程序的时候掌握好细节了。下一节,我们将介绍编译器何时给我们合成默认拷贝构造函数,以及类成员初始化方式。