文章较长,并且内容相对来讲比较枯燥,但愿对C++对象的内存布局、虚表指针、虚基类指针等有深刻了解的朋友能够慢慢看。
本文的结论都在VS2013上获得验证。不一样的编译器在内存布局的细节上可能有所不一样。
文章若是有解释不清、解释不通或疏漏的地方,恳请指出。html
引用《深度探索C++对象模型》这本书中的话:前端
有两个概念能够解释C++对象模型:数据结构
- 语言中直接支持面向对象程序设计的部分。
- 对于各类支持的底层实现机制。
直接支持面向对象程序设计,包括了构造函数、析构函数、多态、虚函数等等,这些内容在不少书籍上都有讨论,也是C++最被人熟知的地方(特性)。而对象模型的底层实现机制倒是不多有书籍讨论的。对象模型的底层实现机制并未标准化,不一样的编译器有必定的自由来设计对象模型的实现细节。在我看来,对象模型研究的是对象在存储上的空间与时间上的更优,并对C++面向对象技术加以支持,如以虚指针、虚表机制支持多态特性。函数
这篇文章主要来讨论C++对象在内存中的布局,属于第二个概念的研究范畴。而C++直接支持面向对象程序设计部分则很少讲。文章主要内容以下:布局
至于其余与内存有关的知识,我假设你们都有必定的了解,如内存对齐,指针操做等。本文初看可能晦涩难懂,要求读者有必定的C++基础,对概念一有必定的掌握。测试
C++中虚函数的做用主要是为了实现多态机制。多态,简单来讲,是指在继承层次中,父类的指针能够具备多种形态——当它指向某个子类对象时,经过它可以调用到子类的函数,而非父类的函数。优化
class Base { virtual void print(void); } class Drive1 :public Base{ virtual void print(void); } class Drive2 :public Base{ virtual void print(void); }
Base * ptr1 = new Base; Base * ptr2 = new Drive1; Base * ptr3 = new Drive2;
ptr1->print(); //调用Base::print() prt2->print();//调用Drive1::print() prt3->print();//调用Drive2::print()
这是一种运行期多态,即父类指针惟有在程序运行时才能知道所指的真正类型是什么。这种运行期决议,是经过虚函数表来实现的。设计
若是咱们丰富咱们的Base类,使其拥有多个virtual函数:3d
class Base { public: Base(int i) :baseI(i){}; virtual void print(void){ cout << "调用了虚函数Base::print()"; } virtual void setI(){cout<<"调用了虚函数Base::setI()";} virtual ~Base(){} private: int baseI; };
当一个类自己定义了虚函数,或其父类有虚函数时,为了支持多态机制,编译器将为该类添加一个虚函数指针(vptr)。虚函数指针通常都放在对象内存布局的第一个位置上,这是为了保证在多层继承或多重继承的状况下能以最高效率取到虚函数表。指针
当vprt位于对象内存最前面时,对象的地址即为虚函数指针地址。咱们能够取得虚函数指针的地址:
Base b(1000); int * vptrAdree = (int *)(&b); cout << "虚函数指针(vprt)的地址是:\t"<<vptrAdree << endl;
咱们运行代码出结果:
咱们强行把类对象的地址转换为 int* 类型,取得了虚函数指针的地址。虚函数指针指向虚函数表,虚函数表中存储的是一系列虚函数的地址,虚函数地址出现的顺序与类中虚函数声明的顺序一致。对虚函数指针地址值,能够获得虚函数表的地址,也便是虚函数表第一个虚函数的地址:
typedef void(*Fun)(void); Fun vfunc = (Fun)*( (int *)*(int*)(&b)); cout << "第一个虚函数的地址是:" << (int *)*(int*)(&b) << endl; cout << "经过地址,调用虚函数Base::print():"; vfunc();
这样,咱们就取得了类中的第一个虚函数,咱们能够经过函数指针访问它。
运行结果:
同理,第二个虚函数setI()的地址为:
(int * )(*(int*)(&b)+1)
一样能够经过函数指针访问它,这里留给读者本身试验。
到目前为止,咱们知道了类中虚表指针vprt的由来,知道了虚函数表中的内容,以及如何经过指针访问虚函数表。下面的文章中将常使用指针访问对象内存来验证咱们的C++对象模型,以及讨论在各类继承状况下虚表指针的变化,先把这部分的内容消化完再接着看下面的内容。
在C++中,有两种数据成员(class data members):static 和nonstatic,以及三种类成员函数(class member functions):static、nonstatic和virtual:
如今咱们有一个类Base,它包含了上面这5中类型的数据或函数:
class Base { public: Base(int i) :baseI(i){}; int getI(){ return baseI; } static void countI(){}; virtual ~Base(){} virtual void print(void){ cout << "Base::print()"; } private: int baseI; static int baseS; };
那么,这个类在内存中将被如何表示?5种数据都是连续存放的吗?如何布局才能支持C++多态? 咱们的C++标准与编译器将如何塑造出各类数据成员与成员函数呢?
说明:在下面出现的图中,用蓝色边框框起来的内容在内存上是连续的。
这个模型很是地简单粗暴。在该模型下,对象由一系列的指针组成,每个指针都指向一个数据成员或成员函数,也便是说,每一个数据成员和成员函数在类中所占的大小是相同的,都为一个指针的大小。这样有个好处——很容易算出对象的大小,不过赔上的是空间和执行期效率。想象一下,若是咱们的Point3d类是这种模型,将会比C语言的struct多了许多空间来存放指向函数的指针,并且每次读取类的数据成员,都须要经过再一次寻址——又是时间上的消耗。
因此这种对象模型并无被用于实际产品上。
这个模型在简单对象模型的基础上又添加一个间接层,它把类中的数据分红了两个部分:数据部分与函数部分,并使用两张表格,一张存放数据自己,一张存放函数的地址(也即函数比成员多一次寻址),而类对象仅仅含有两个指针,分别指向上面这两个表。这样看来,对象的大小是固定为两个指针大小。这个模型也没有用于实际应用于真正的C++编译器上。
概述:在此模型下,nonstatic 数据成员被置于每个类对象中,而static数据成员被置于类对象以外。static与nonstatic函数也都放在类对象以外,而对于virtual 函数,则经过虚函数表+虚指针来支持,具体以下:
在此模型下,Base的对象模型如图:
先在VS上验证类对象的布局:
Base b(1000);
可见对象b含有一个vfptr,即vprt。而且只有nonstatic数据成员被放置于对象内。咱们展开vfprt:
vfptr中有两个指针类型的数据(地址),第一个指向了Base类的析构函数,第二个指向了Base的虚函数print,顺序与声明顺序相同。
这与上述的C++对象模型相符合。也能够经过代码来进行验证:
void testBase( Base&p) { cout << "对象的内存起始地址:" << &p << endl; cout << "type_info信息:" << endl; RTTICompleteObjectLocator str = *((RTTICompleteObjectLocator*)*((int*)*(int*)(&p) - 1)); string classname(str.pTypeDescriptor->name); classname = classname.substr(4, classname.find("@@") - 4); cout << "根据type_info信息输出类名:"<< classname << endl; cout << "虚函数表地址:" << (int *)(&p) << endl; //验证虚表 cout << "虚函数表第一个函数的地址:" << (int *)*((int*)(&p)) << endl; cout << "析构函数的地址:" << (int* )*(int *)*((int*)(&p)) << endl; cout << "虚函数表中,第二个虚函数即print()的地址:" << ((int*)*(int*)(&p) + 1) << endl; //经过地址调用虚函数print() typedef void(*Fun)(void); Fun IsPrint=(Fun)* ((int*)*(int*)(&p) + 1); cout << endl; cout<<"调用了虚函数"; IsPrint(); //若地址正确,则调用了Base类的虚函数print() cout << endl; //输入static函数的地址 p.countI();//先调用函数以产生一个实例 cout << "static函数countI()的地址:" << p.countI << endl; //验证nonstatic数据成员 cout << "推测nonstatic数据成员baseI的地址:" << (int *)(&p) + 1 << endl; cout << "根据推测出的地址,输出该地址的值:" << *((int *)(&p) + 1) << endl; cout << "Base::getI():" << p.getI() << endl; }
Base b(1000); testBase(b);
结果分析:
好的,至此咱们了解了非继承下类对象五种数据在内存上的布局,也知道了在每个虚函数表前都有一个指针指向type_info,负责对RTTI的支持。而加入继承后类对象在内存中该如何表示呢?
若是咱们定义了派生类
class Derive : public Base { public: Derive(int d) :Base(1000), DeriveI(d){}; //overwrite父类虚函数 virtual void print(void){ cout << "Drive::Drive_print()" ; } // Derive声明的新的虚函数 virtual void Drive_print(){ cout << "Drive::Drive_print()" ; } virtual ~Derive(){} private: int DeriveI; };
继承类图为:
一个派生类如何在机器层面上塑造其父类的实例呢?在简单对象模型中,能够在子类对象中为每一个基类子对象分配一个指针。以下图:
简单对象模型的缺点就是因间接性致使的空间存取时间上的额外负担,优势则是类的大小是固定的,基类的改动不会影响子类对象的大小。
在表格驱动对象模型中,咱们能够为子类对象增长第三个指针:基类指针(bptr),基类指针指向指向一个基类表(base class table),一样的,因为间接性致使了空间和存取时间上的额外负担,优势则是无须改变子类对象自己就能够更改基类。表格驱动模型的图就再也不贴出来了。
在C++对象模型中,对于通常继承(这个通常是相对于虚拟继承而言),若子类重写(overwrite)了父类的虚函数,则子类虚函数将覆盖虚表中对应的父类虚函数(注意子类与父类拥有各自的一个虚函数表);若子类并没有overwrite父类虚函数,而是声明了本身新的虚函数,则该虚函数地址将扩充到虚函数表最后(在vs中没法经过监视看到扩充的结果,不过咱们经过取地址的方法能够作到,子类新的虚函数确实在父类子物体的虚函数表末端)。而对于虚继承,若子类overwrite父类虚函数,一样地将覆盖父类子物体中的虚函数表对应位置,而若子类声明了本身新的虚函数,则编译器将为子类增长一个新的虚表指针vptr,这与通常继承不一样,在后面再讨论。
咱们使用代码来验证以上模型
typedef void(*Fun)(void); int main() { Derive d(2000); //[0] cout << "[0]Base::vptr"; cout << "\t地址:" << (int *)(&d) << endl; //vprt[0] cout << " [0]"; Fun fun1 = (Fun)*((int *)*((int *)(&d))); fun1(); cout << "\t地址:\t" << *((int *)*((int *)(&d))) << endl; //vprt[1]析构函数没法经过地址调用,故手动输出 cout << " [1]" << "Derive::~Derive" << endl; //vprt[2] cout << " [2]"; Fun fun2 = (Fun)*((int *)*((int *)(&d)) + 2); fun2(); cout << "\t地址:\t" << *((int *)*((int *)(&d)) + 2) << endl; //[1] cout << "[2]Base::baseI=" << *(int*)((int *)(&d) + 1); cout << "\t地址:" << (int *)(&d) + 1; cout << endl; //[2] cout << "[2]Derive::DeriveI=" << *(int*)((int *)(&d) + 2); cout << "\t地址:" << (int *)(&d) + 2; cout << endl; getchar(); }
运行结果:
这个结果与咱们的对象模型符合。
单继承中(通常继承),子类会扩展父类的虚函数表。在多继承中,子类含有多个父类的子对象,该往哪一个父类的虚函数表扩展呢?当子类overwrite了父类的函数,须要覆盖多个父类的虚函数表吗?
其中第二点保证了父类指针指向子类对象时,老是可以调用到真正的函数。
为了方便查看,咱们把代码都粘贴过来
class Base { public: Base(int i) :baseI(i){}; virtual ~Base(){} int getI(){ return baseI; } static void countI(){}; virtual void print(void){ cout << "Base::print()"; } private: int baseI; static int baseS; }; class Base_2 { public: Base_2(int i) :base2I(i){}; virtual ~Base_2(){} int getI(){ return base2I; } static void countI(){}; virtual void print(void){ cout << "Base_2::print()"; } private: int base2I; static int base2S; }; class Drive_multyBase :public Base, public Base_2 { public: Drive_multyBase(int d) :Base(1000), Base_2(2000) ,Drive_multyBaseI(d){}; virtual void print(void){ cout << "Drive_multyBase::print" ; } virtual void Drive_print(){ cout << "Drive_multyBase::Drive_print" ; } private: int Drive_multyBaseI; };
继承类图为:
此时Drive_multyBase 的对象模型是这样的:
咱们使用代码验证:
typedef void(*Fun)(void); int main() { Drive_multyBase d(3000); //[0] cout << "[0]Base::vptr"; cout << "\t地址:" << (int *)(&d) << endl; //vprt[0]析构函数没法经过地址调用,故手动输出 cout << " [0]" << "Derive::~Derive" << endl; //vprt[1] cout << " [1]"; Fun fun1 = (Fun)*((int *)*((int *)(&d))+1); fun1(); cout << "\t地址:\t" << *((int *)*((int *)(&d))+1) << endl; //vprt[2] cout << " [2]"; Fun fun2 = (Fun)*((int *)*((int *)(&d)) + 2); fun2(); cout << "\t地址:\t" << *((int *)*((int *)(&d)) + 2) << endl; //[1] cout << "[1]Base::baseI=" << *(int*)((int *)(&d) + 1); cout << "\t地址:" << (int *)(&d) + 1; cout << endl; //[2] cout << "[2]Base_::vptr"; cout << "\t地址:" << (int *)(&d)+2 << endl; //vprt[0]析构函数没法经过地址调用,故手动输出 cout << " [0]" << "Drive_multyBase::~Derive" << endl; //vprt[1] cout << " [1]"; Fun fun4 = (Fun)*((int *)*((int *)(&d))+1); fun4(); cout << "\t地址:\t" << *((int *)*((int *)(&d))+1) << endl; //[3] cout << "[3]Base_2::base2I=" << *(int*)((int *)(&d) + 3); cout << "\t地址:" << (int *)(&d) + 3; cout << endl; //[4] cout << "[4]Drive_multyBase::Drive_multyBaseI=" << *(int*)((int *)(&d) + 4); cout << "\t地址:" << (int *)(&d) + 4; cout << endl; getchar(); }
运行结果:
菱形继承也称为钻石型继承或重复继承,它指的是基类被某个派生类简单重复继承了屡次。这样,派生类对象中拥有多份基类实例(这会带来一些问题)。为了方便叙述,咱们不使用上面的代码了,而从新写一个重复继承的继承层次:
class B { public: int ib; public: B(int i=1) :ib(i){} virtual void f() { cout << "B::f()" << endl; } virtual void Bf() { cout << "B::Bf()" << endl; } }; class B1 : public B { public: int ib1; public: B1(int i = 100 ) :ib1(i) {} virtual void f() { cout << "B1::f()" << endl; } virtual void f1() { cout << "B1::f1()" << endl; } virtual void Bf1() { cout << "B1::Bf1()" << endl; } }; class B2 : public B { public: int ib2; public: B2(int i = 1000) :ib2(i) {} virtual void f() { cout << "B2::f()" << endl; } virtual void f2() { cout << "B2::f2()" << endl; } virtual void Bf2() { cout << "B2::Bf2()" << endl; } }; class D : public B1, public B2 { public: int id; public: D(int i= 10000) :id(i){} virtual void f() { cout << "D::f()" << endl; } virtual void f1() { cout << "D::f1()" << endl; } virtual void f2() { cout << "D::f2()" << endl; } virtual void Df() { cout << "D::Df()" << endl; } };
这时,根据单继承,咱们能够分析出B1,B2类继承于B类时的内存布局。又根据通常多继承,咱们能够分析出D类的内存布局。咱们能够得出D类子对象的内存布局以下图:
D类对象内存布局中,图中青色表示b1类子对象实例,黄色表示b2类子对象实例,灰色表示D类子对象实例。从图中能够看到,因为D类间接继承了B类两次,致使D类对象中含有两个B类的数据成员ib,一个属于来源B1类,一个来源B2类。这样不只增大了空间,更重要的是引发了程序歧义:
D d; d.ib =1 ; //二义性错误,调用的是B1的ib仍是B2的ib? d.B1::ib = 1; //正确 d.B2::ib = 1; //正确
尽管咱们能够经过明确指明调用路径以消除二义性,但二义性的潜在性尚未消除,咱们能够经过虚继承来使D类只拥有一个ib实体。
虚继承解决了菱形继承中最派生类拥有多个间接父类实例的状况。虚继承的派生类的内存布局与普通继承不少不一样,主要体如今:
为了分析最后的菱形继承,咱们仍是先从单虚继承继承开始。
在C++对象模型中,虚继承而来的子类会生成一个隐藏的虚基类指针(vbptr),在Microsoft Visual C++中,虚基类表指针老是在虚函数表指针以后,于是,对某个类实例来讲,若是它有虚基类指针,那么虚基类指针可能在实例的0字节偏移处(该类没有vptr时,vbptr就处于类实例内存布局的最前面,不然vptr处于类实例内存布局的最前面),也可能在类实例的4字节偏移处。
一个类的虚基类指针指向的虚基类表,与虚函数表同样,虚基类表也由多个条目组成,条目中存放的是偏移值。第一个条目存放虚基类表指针(vbptr)所在地址到该类内存首地址的偏移值,由第一段的分析咱们知道,这个偏移值为0(类没有vptr)或者-4(类有虚函数,此时有vptr)。咱们经过一张图来更好地理解。
虚基类表的第2、第三...个条目依次为该类的最左虚继承父类、次左虚继承父类...的内存地址相对于虚基类表指针的偏移值,这点咱们在下面会验证。
若是咱们的B1类虚继承于B类:
//类的内容与前面相同 class B{...} class B1 : virtual public B
根据咱们前面对虚继承的派生类的内存布局的分析,B1类的对象模型应该是这样的:
咱们经过指针访问B1类对象的内存,以验证上面的C++对象模型:
int main() { B1 a; cout <<"B1对象内存大小为:"<< sizeof(a) << endl; //取得B1的虚函数表 cout << "[0]B1::vptr"; cout << "\t地址:" << (int *)(&a)<< endl; //输出虚表B1::vptr中的函数 for (int i = 0; i<2;++ i) { cout << " [" << i << "]"; Fun fun1 = (Fun)*((int *)*(int *)(&a) + i); fun1(); cout << "\t地址:\t" << *((int *)*(int *)(&a) + i) << endl; } //[1] cout << "[1]vbptr " ; cout<<"\t地址:" << (int *)(&a) + 1<<endl; //虚表指针的地址 //输出虚基类指针条目所指的内容 for (int i = 0; i < 2; i++) { cout << " [" << i << "]"; cout << *(int *)((int *)*((int *)(&a) + 1) + i); cout << endl; } //[2] cout << "[2]B1::ib1=" << *(int*)((int *)(&a) + 2); cout << "\t地址:" << (int *)(&a) + 2; cout << endl; //[3] cout << "[3]值=" << *(int*)((int *)(&a) + 3); cout << "\t\t地址:" << (int *)(&a) + 3; cout << endl; //[4] cout << "[4]B::vptr"; cout << "\t地址:" << (int *)(&a) +3<< endl; //输出B::vptr中的虚函数 for (int i = 0; i<2; ++i) { cout << " [" << i << "]"; Fun fun1 = (Fun)*((int *)*((int *)(&a) + 4) + i); fun1(); cout << "\t地址:\t" << *((int *)*((int *)(&a) + 4) + i) << endl; } //[5] cout << "[5]B::ib=" << *(int*)((int *)(&a) + 5); cout << "\t地址: " << (int *)(&a) + 5; cout << endl;
运行结果:
这个结果与咱们的C++对象模型图彻底符合。这时咱们能够来分析一下虚表指针的第二个条目值12的具体来源了,回忆上文讲到的:
第2、第三...个条目依次为该类的最左虚继承父类、次左虚继承父类...的内存地址相对于虚基类表指针的偏移值。
在咱们的例子中,也就是B类实例内存地址相对于vbptr的偏移值,也便是:[4]-[1]的偏移值,结果即为12,从地址上也能够计算出来:007CFDFC-007CFDF4结果的十进制数正是12。如今,咱们对虚基类表的构成应该有了一个更好的理解。
若是咱们有以下继承层次:
class B{...} class B1: virtual public B{...} class B2: virtual public B{...} class D : public B1,public B2{...}
类图以下所示:
菱形虚拟继承下,最派生类D类的对象模型又有不一样的构成了。在D类对象的内存构成上,有如下几点:
菱形虚拟继承下的C++对象模型为:
下面使用代码加以验证:
int main() { D d; cout << "D对象内存大小为:" << sizeof(d) << endl; //取得B1的虚函数表 cout << "[0]B1::vptr"; cout << "\t地址:" << (int *)(&d) << endl; //输出虚表B1::vptr中的函数 for (int i = 0; i<3; ++i) { cout << " [" << i << "]"; Fun fun1 = (Fun)*((int *)*(int *)(&d) + i); fun1(); cout << "\t地址:\t" << *((int *)*(int *)(&d) + i) << endl; } //[1] cout << "[1]B1::vbptr "; cout << "\t地址:" << (int *)(&d) + 1 << endl; //虚表指针的地址 //输出虚基类指针条目所指的内容 for (int i = 0; i < 2; i++) { cout << " [" << i << "]"; cout << *(int *)((int *)*((int *)(&d) + 1) + i); cout << endl; } //[2] cout << "[2]B1::ib1=" << *(int*)((int *)(&d) + 2); cout << "\t地址:" << (int *)(&d) + 2; cout << endl; //[3] cout << "[3]B2::vptr"; cout << "\t地址:" << (int *)(&d) + 3 << endl; //输出B2::vptr中的虚函数 for (int i = 0; i<2; ++i) { cout << " [" << i << "]"; Fun fun1 = (Fun)*((int *)*((int *)(&d) + 3) + i); fun1(); cout << "\t地址:\t" << *((int *)*((int *)(&d) + 3) + i) << endl; } //[4] cout << "[4]B2::vbptr "; cout << "\t地址:" << (int *)(&d) + 4 << endl; //虚表指针的地址 //输出虚基类指针条目所指的内容 for (int i = 0; i < 2; i++) { cout << " [" << i << "]"; cout << *(int *)((int *)*((int *)(&d) + 4) + i); cout << endl; } //[5] cout << "[5]B2::ib2=" << *(int*)((int *)(&d) + 5); cout << "\t地址: " << (int *)(&d) + 5; cout << endl; //[6] cout << "[6]D::id=" << *(int*)((int *)(&d) + 6); cout << "\t地址: " << (int *)(&d) + 6; cout << endl; //[7] cout << "[7]值=" << *(int*)((int *)(&d) + 7); cout << "\t\t地址:" << (int *)(&d) + 7; cout << endl; //间接父类 //[8] cout << "[8]B::vptr"; cout << "\t地址:" << (int *)(&d) + 8 << endl; //输出B::vptr中的虚函数 for (int i = 0; i<2; ++i) { cout << " [" << i << "]"; Fun fun1 = (Fun)*((int *)*((int *)(&d) + 8) + i); fun1(); cout << "\t地址:\t" << *((int *)*((int *)(&d) + 8) + i) << endl; } //[9] cout << "[9]B::id=" << *(int*)((int *)(&d) + 9); cout << "\t地址: " << (int *)(&d) +9; cout << endl; getchar(); }
查看运行结果:
在C语言中,“数据”和“处理数据的操做(函数)”是分开来声明的,也就是说,语言自己并无支持“数据和函数”之间的关联性。
在C++中,咱们经过类来将属性与操做绑定在一块儿,称为ADT,抽象数据结构。
C语言中使用struct(结构体)来封装数据,使用函数来处理数据。举个例子,若是咱们定义了一个struct Point3以下:
typedef struct Point3 { float x; float y; float z; } Point3;
为了打印这个Point3d,咱们能够定义一个函数:
void Point3d_print(const Point3d *pd) { printf("(%f,%f,%f)",pd->x,pd->y,pd_z); }
而在C++中,咱们更倾向于定义一个Point3d类,以ADT来实现上面的操做:
class Point3d { public: point3d (float x = 0.0,float y = 0.0,float z = 0.0) : _x(x), _y(y), _z(z){} float x() const {return _x;} float y() const {return _y;} float z() const {return _z;} private: float _x; float _y; float _z; }; inline ostream& operator<<(ostream &os, const Point3d &pt) { os<<"("<<pr.x()<<"," <<pt.y()<<","<<pt.z()<<")"; }
看到这段代码,不少人第一个疑问多是:加上了封装,布局成本增长了多少?答案是class Point3d并无增长成本。学过了C++对象模型,咱们知道,Point3d类对象的内存中,只有三个数据成员。
上面的类声明中,三个数据成员直接内含在每个Point3d对象中,而成员函数虽然在类中声明,却不出如今类对象(object)之中,这些函数(non-inline)属于类而不属于类对象,只会为类产生惟一的函数实例。
因此,Point3d的封装并无带来任何空间或执行期的效率影响。而在下面这种状况下,C++的封装额外成本才会显示出来:
不只如此,Point3d类数据成员的内存布局与c语言的结构体Point3d成员内存布局是相同的。C++中处在同一个访问标识符(指public、private、protected)下的声明的数据成员,在内存中一定保证以其声明顺序出现。而处于不一样访问标识符声明下的成员则无此规定。对于Point3类来讲,它的三个数据成员都处于private下,在内存中一块儿声明顺序出现。咱们能够作下实验:
void TestPoint3Member(const Point3d& p) { cout << "推测_x的地址是:" << (float *) (&p) << endl; cout << "推测_y的地址是:" << (float *) (&p) + 1 << endl; cout << "推测_z的地址是:" << (float *) (&p) + 2 << endl; cout << "根据推测出的地址输出_x的值:" << *((float *)(&p)) << endl; cout << "根据推测出的地址输出_y的值:" << *((float *)(&p)+1) << endl; cout << "根据推测出的地址输出_z的值:" << *((float *)(&p)+2) << endl; }
//测试代码 Point3d a(1,2,3); TestPoint3Member(a);
运行结果:
从结果能够看到,_x,_y,_z三个数据成员在内存中紧挨着。
总结一下:
不考虑虚函数与虚继承,当数据都在同一个访问标识符下,C++的类与C语言的结构体在对象大小和内存布局上是一致的,C++的封装并无带来空间时间上的影响。
今有类以下:
class B{}; class B1 :public virtual B{}; class B2 :public virtual B{}; class D : public B1, public B2{}; int main() { B b; B1 b1; B2 b2; D d; cout << "sizeof(b)=" << sizeof(b)<<endl; cout << "sizeof(b1)=" << sizeof(b1) << endl; cout << "sizeof(b2)=" << sizeof(b2) << endl; cout << "sizeof(d)=" << sizeof(d) << endl; getchar(); }
输出结果是:
解析:
转载请注明原出处:http://www.cnblogs.com/QG-whz/p/4909359.html