C++对象模型那点事儿(布局篇)

1 前言


在C++中类的数据成员有两种:static和nonstatic。类的函数成员由三种:static,nonstatic和virtual。

上篇我们尽量说一些宏观上的东西,数据成员与函数成员在类中的布局将在微观篇中具体讨论。

每当我们声明一个类。定义一个对象。调用一个函数.....的时候,不知道你有没有一些疑惑--编译器私底下都干了些什么?普通函数。成员函数都是怎么调用的?static成员又是个什么玩意。

假设你对这些东西也感兴趣,那么好,我们一起将class的底层翻个底朝天。修炼好底层的内功,我想对于上层的提供。帮助可不止一点点吧?


2 class总体布局


C语言中“数据“与函数式分开声明的。也就是说C语言并不支持”数据“与函数之间的关联性。

我们来看以下的样例。
typedef struct point3d{ //数据
      float x;
      float y;
      float z;
}Point3d;
void Point3d_print(const Point3d *pd{
      printf("%g,%g,%g",pd->x,pd->y,pd->z);
}

我们再来看看C++中的做法。
class Point3d{
      float _x;
      float _y;
      float _z;
public:
      void point3d_print(){
            printf("%g,%g,%g",_x,_y,_z);
      }
};

在Point3d转换到C++之后。我们可能会问加上封装之后,成本会添加多少?
答案是class Point3d并没有添加成本。三个数据成员(_x,_y,_z)直接内含在每个对象之中。而成员函数虽在类中声明,却不出如今对象之中。例如以下图所看到的:

凡事没有绝对,virtual看起来就会添加C++在布局及存取时间上的额外负担。稍后讨论。

好吧。我承认光说面上(宏观上)的东西东西大家都懂。并且底层的东西注定不会太宏观。

那么以下我们举样例来证明上述的讨论。

须要说明的是以下是在vs2010下的执行结果。若是gcc,可能某些地方会有所差异。
class A{};         // sizeof(A) = 1  有木有非常奇怪?稍后说明
class B{int x;};   //  sizeof(B) = 4
class C{
      int x;
public:
      int get(){return x;}
};                 //  sizeof(C) = 4; 是不是验证了我们上述的论述?


非常奇怪sizeof(A) = 1而不是0吧?
其实A并非空的,他有一个隐藏的1byte大小,那是被编译器安插进去的一个char。

这样做使得用同一个空类定义两个对象的时候得以在内存中配置独一无二的地址。

比如:
A a,b;
if(&a == &b)cout<<"error"<<endl;

我们都知道在C语言中struct优化的时候会进行内存对齐,那么我们来看看class中有没有这个优化。
class A{
	char x;
	int y;
	char z;
};  // sizeof(A) == 12;
class B{
	char x;
	char y;
	int z;
};  // sizeof(B) = 8;
class C{
	int x;
	char y;
	char z;
};  // sizeof(C) = 8;
class D{
	long long x;
	char y;
	char z;
};  //sizeof(D) = 16; 因为longlong为8字节大小,此处以8字节对齐

显然编译器进行类内存对齐的优化。
接着上文,我们知道stroustrup老大的设计(眼下仍在使用)是:nonstatic data members 被置于每个对象之中,static data member则被置于对象之外。

static和nonstatic function members 则被放在对象之外。

class A{
	static int x;
};  //sizeof(A) = 1;
class B{
	int x;
public:
	int get(){
		return x;
	}
};  //sizeof(B) = 4
class C{
	int x;
public:
	virtual int get(){
		return x;
	}
};  //sizeof(C) = 8;

显然验证了上述我所说的。

class A{
	void (*pf)(); //函数指针
};   //sizeof(A) = 4;
class B{
	int *p;   // 指针
};   //sizeof(B) = 4;

所以含有虚函数的时候,object中会包括一个虚表指针。我们知道指针一边占用4个字节,上面的sizeof(C)就好解释了。

3 虚函数

我们都知道虚函数是以下这个样子。

class X{
	int a;
	int b;
public:
	virtual void foo1(){cout<<"X::foo1"<<endl;}
	virtual void foo2(){cout<<"X::foo2"<<endl;}
};

内存布局例如以下:


以下我们来证明这样的布局。

#include<iostream>
using namespace std;
class X{
	int _a;
	int _b;
public:
	virtual void foo1(){cout<<"X::foo1"<<endl;}
	virtual void foo2(){cout<<"X::foo2"<<endl;}
};
typedef void (*pf)();
int main(){
	X a;
	int **tmp = (int **)&a;
	pf ptf;
	for(int i=0;i<2;i++){
		ptf = (pf)tmp[0][i];
		ptf();
	}
}

执行结果例如以下图所看到的:


那么,我们继续往下看。

4 继承


当涉及到继承的时候。情况又会怎样呢?
class A{
      int x;
};
class B:public A{
      int y;
};   //sizeof(B) = 8;
我们来看看涉及到继承的时候内存的布局情况。

我们继续,若基类中包括有虚函数,这时候又会怎样呢?
class C{
public:
	virtual void fooC(){
		cout<<"C::fooC()"<<endl;
	}
};  //sizeof(C) = 4;
class D:public C{
	int a;
public:
	virtual void fooD(){
		cout<<"D::fooD()"<<endl;
	}
};  //sizeof(D) = 8;
内存布局应该是这个样子:


以下我们来验证这样的布局:
typedef void (*pf)();
int main(){
	C a;
	D b;
	int **tmpc = (int **)&a;
	int **tmpb = (int **)&b;
	pf ptf;
	ptf = (pf)tmpc[0][0];
	ptf();
	ptf = (pf)tmpb[0][0];
	ptf();
	ptf = (pf)tmpb[0][1];	
	ptf();
}
执行结果:

显然上述的布局是对的。这个时候须要注意的是:C::fooC()在前,D::fooD()在后,若出现函数覆盖,则D中的函数会覆盖掉继承过来的同名函数,而对于没有覆盖的虚函数则追加在虚表的最后。
我们再来看看以下的涉及到虚函数的多重继承。

class A{
	int _a;
public:
	virtual void fooA(){
		cout<<"C::fooA()"<<endl;
	}
	virtual void poo(){
		cout<<"A::poo()"<<endl;
	}
};  //sizeof(A) = 8;
class B{
	int _b;
public:
	virtual void fooB(){
		cout<<"C::fooB()"<<endl;
	}
	virtual void poo(){
		cout<<"B::poo()"<<endl;
	}
};  ////sizeof(B) = 8;
class C:public A,public B{
	int _c;
public:
	void poo(){
		cout<<"C::poo()"<<endl;
	}
	virtual void hoo(){
        cout<<"C::hoo()"<<endl;
    }
};    //sizeof(C) = 20;


有了上面的布局信息,我们能够猜測类C的布局例如以下:

以下我们来验证这样的猜測。


typedef void (*pf)();
int main(){
	C a;
	int **tmp = (int **)&a;
	pf ptf;
	for(int i=0;i<3;++i){
		ptf = (pf)tmp[0][i];
		ptf();
	}
	cout<<"-----------"<<endl;
	int s = sizeof(A)/4; //指针与int都占用4字节大小
	for(int i=0;i<2;i++){
		ptf = (pf)tmp[2][i];
		ptf();
	}
}

执行结果:


显然与我们的猜測一致。
最后。我们再来看看菱形继承的情况。

class A{
	int _a1;
	int _a2;
};    //sizeof(A) = 8;
class B:virtual public A{
	int b;
};    //sizeof(B) = 16;
class C:virtual public A{
	int c;
};    //sizeof(C) = 16;
class D:public B,public C{
	int d;
};    //sizeof(D) = 28;

我们来看看这时候的内存布局:


我们来验证这样的布局:
int main(){
	D d;
	A *pta = &d;
	B *ptb = &d;
	C *ptc = &d;
	cout<<"D:  "<<&d<<endl;
	cout<<"B:  "<<ptb<<"   C:  "<<ptc<<endl;
	cout<<"A:  "<<pta<<endl;
}


你在尝试的时候地址可能会有所差异。可是偏移量应该会保持一致。至于不同的编译器是否布局都一样,我也不得而知。至于那两个虚指针所指虚表提供的也就是虚基类的成员偏移量信息。大家假设感兴趣,能够自己验证。
至此,宏观布局部分大致说完,欲知后事怎样请转至“成员篇”。


【推广】 免费学中医,健康全家人
原文地址:https://www.cnblogs.com/ldxsuanfa/p/10688678.html