前言
本文给大家介绍的是关于C++对象继承的内存布局的相关内容,分享出来供大家参考学习,在开始之前说明下,关于单继承和多继承的简单概念可参考此文章
以下编译环境均为:WIN32+VS2015
虚函数表
对C++ 了解的人都应该知道虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。简称为V-Table。在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数。
首先先通过一个例子来引入虚函数表,假如现在有三个类如下:
class A //包含虚函数的类 { public: virtual void func1() {} virtual void func2() {} }; class B//空类 {}; class C //包含成员函数不包含成员变量的类 { void fun() {} }; void Test1() { cout << sizeof(A) << endl; cout << sizeof(B) << endl; cout << sizeof(C) << endl; }
就上述的代码,将会分别输出4,1,1
造成A的大小为4的原因就是:在A中存放了一个指向A类的虚函数表的指针。而32位下一个指针大小为4字节,所以就为4。
A类实例化后在内存中对应如下:
注:在虚函数表中用0来结尾。
通过内存中的显示我们就能知道编译器应该将虚函数表的指针存在于对象实例中最前面的位置,所以可以&a转成int*,取得虚函数表的地址,再强转成(int*)方便接下来可以每次只访问四个字节大小(虚函数表可看做是一个函数指针数组,由于32位下指针是4字节,所以转为(int*))。将取得的int*指针传给下面的打印虚函数表的函数,就能够打印出对应的地址信息。
typedef void(*FUNC) (); //int*VTavle = (int*)(*(int*)&a) //传参完成后就可打印出对应的信息。 void PrintVTable(int* VTable) { cout << " 虚表地址>" << VTable << endl; for (int i = 0; VTable[i] != 0; ++i) { printf(" 第%d个虚函数地址 :0X%x,->", i, VTable[i]); FUNC f = (FUNC)VTable[i]; f(); } cout << endl; }
接下来就来分析各种继承关系中对应的内存模型以及虚函数表
单继承(无虚函数覆盖)
class A { public: virtual void func1() { cout << "A::func1" << endl; } virtual void func2() { cout << "A::func2" << endl; } public: int _a; }; class B : public A { public: virtual void func3() { cout << "B::func3" << endl; } virtual void func4() { cout << "B::func4" << endl; } public: int _b; }; void Test1() { B b; b._a = 1; b._b = 2; int* VTable = (int*)(*(int*)&b); PrintVTable(VTable); }
将内存中的显示和我们写的显示虚函数表对应起来如下:
小结:
1)虚函数按照其声明顺序放于表中。
2)父类的虚函数在子类的虚函数前面。(由于子类单继承父类,直接使用父类的虚函数表)
一般继承(成员变量+虚函数覆盖)
在上面例子进行稍微修改,使得子类中有对父类虚函数的覆盖,进行和之前同样的测试:
class A { public: virtual void func1() { cout << "A::func1" << endl; } virtual void func2() { cout << "A::func2" << endl; } public: int _a; }; class B : public A { public: virtual void func1() { cout << "B::func1" << endl; } virtual void func3() { cout << "B::func3" << endl; } public: int _b; };
小结:
1)覆盖的func()函数被放到了虚表中原来父类虚函数的位置。
2)没有被覆盖的函数依旧。
多重继承(成员变量+虚函数覆盖)
class A { public: virtual void func1() { cout << "A::func1" << endl; } virtual void func2() { cout << "A::func2" << endl; } public: int _a; }; class B { public: virtual void func3() { cout << "B::func1" << endl; } public: int _b; }; class C : public A , public B { //覆盖A::func1() virtual void func1() { cout << "C::func1()"<<endl; } virtual void func4() { cout << "C::func4()" << endl; } public: int _c; };
再次调试观察:
小结:
多重继承后的子类将与自己第一个继承的父类公用一份虚函数表。(上述例子中A为C的第一个继承类)
菱形继承(成员变量 + 虚函数覆盖)
class A { public: virtual void func1() { cout << "A::func1" << endl; } public: int _a; }; class B : public A { public: virtual void func2() { cout << "B::func2" << endl; } public: int _b; }; class C : public A { virtual void func3() { cout << "C::func3()" << endl; } public: int _c; }; class D : public B , public C { virtual void func2() { cout << "D::fun2()" << endl; } virtual void func4() { cout << "D::fun4()" << endl; } public: int _d; }; void Test1() { D d; d.B::_a = 1; d.C::_a = 2; d._b = 3; d._c = 4; d._d = 5; int* VTable = (int*)(*(int*)&d); PrintVTable(VTable); }
掌握了单继承和多继承的规律,按照总结的一步步分析,就可以最终得到D的虚函数表。
由于子类B继承父类A,所以B与A公用一个虚函数表,又因为B是D多继承中的第一个继承的类,所以B,D共用一个虚函数表。
菱形的虚拟继承(成员变量 + 虚函数覆盖)
参考下面这个例子:
class A { public: virtual void func1() { cout << "A::func1()" << endl; } virtual void func2() { cout << "A::func2()" << endl; } public: int _a; }; class B : virtual public A//虚继承A,覆盖func1() { public: virtual void func1() { cout << "B::func1()" << endl; } virtual void func3() { cout << "B::func3()" << endl; } public: int _b; }; class C : virtual public A //虚继承A,覆盖func1() { virtual void func1() { cout << "C::func1()" << endl; } virtual void func3() { cout << "C::func3()" << endl; } public: int _c; }; class D : public B , public C//虚继承B,C,覆盖func1() { virtual void func1() { cout << "D::func1()" << endl; } virtual void func4() { cout << "D::func4()" << endl; } public: int _d; }; typedef void(*FUNC) (); void PrintVTable(int* VTable) { cout << " 虚表地址>" << VTable << endl; for (int i = 0; VTable[i] != 0; ++i) { printf(" 第%d个虚函数地址 :0X%x,->", i, VTable[i]); FUNC f = (FUNC)VTable[i]; f(); } cout << endl; } void Test1() { D d; d.B::_a = 1; d.C::_a = 2; d._b = 3; d._c = 4; d._d = 5; cout <<"sizeof(A) = "<< sizeof(A) << endl; cout << "sizeof(B) = " << sizeof(B) << endl; cout << "sizeof(C) = " << sizeof(C) << endl; //打印d的虚函数表 int* VTable = (int*)(*(int*)&d); PrintVTable(VTable); //打印C的虚函数表 VTable = (int*)*(int*)((char*)&d + sizeof(B)-sizeof(A)); PrintVTable(VTable); //打印A的虚函数表 VTable = (int*)*(int*)((char*)&d + sizeof(B)+sizeof(C)-2*sizeof(A)+4); PrintVTable(VTable); }
接下来就慢慢分析:
1)先通过调试查看内存中是如何分配的,并和我们打印出的虚函数表对应起来:
注:由于B,C是虚继承A,所以编译器为了解决菱形继承所带来的“二义性”以及“数据冗余”,便将A放在最末端,并在子类中存放一个虚基表,方便找到父类;而虚基表的前四个字节存放的是对于自己虚函数表的偏移量,再往下四个字节才是对于父类的偏移量。
2)接下来就抽象出来分析模型
总结
1)虚函数按照其声明顺序放于表中;
2)父类的虚函数在子类的虚函数前面(由于子类单继承父类,直接使用父类的虚函数表);
3)覆盖的func()函数被放到了虚表中原来父类虚函数的位置;
4)没有被覆盖的函数依旧;
5)如果B,C虚继承A,并且B,C内部没有再声明或定义虚函数,则B,C没有对应的虚函数表;
6)在菱形的虚拟继承中,要注意A为B,C所共有的,在打印对应虚函数表时要注意偏移量。
好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对小牛知识库的支持。
我知道多重继承的内存布局没有定义,所以我不应该依赖它。但是,在特殊情况下,我可以依赖它吗?也就是说,一个类只有一个“真正的”超级类。所有其他类都是“空类”,即既没有字段也没有虚拟方法的类(即它们只有非虚拟方法)。在这种情况下,这些附加类不应该在类的内存布局中添加任何内容(更简洁地说,在C 11的措辞中,该类具有标准布局) 我能推断出所有的超类都没有偏移吗?例如。: 在这里, 是类, 是唯一真正的基
本文向大家介绍浅谈C++中派生类对象的内存布局,包括了浅谈C++中派生类对象的内存布局的使用技巧和注意事项,需要的朋友参考一下 主要从三个方面来讲: 1 单一继承 2 多重继承 3 虚拟继承 1 单一继承 (1)派生类完全拥有基类的内存布局,并保证其完整性。 派生类可以看作是完整的基类的Object再加上派生类自己的Object。如果基类中没有虚成员函数,那么派生类与具有相同功能的非派
本文向大家介绍C/C++ 公有继承、保护继承和私有继承的对比详解,包括了C/C++ 公有继承、保护继承和私有继承的对比详解的使用技巧和注意事项,需要的朋友参考一下 C/C++ 公有继承、保护继承和私有继承的区别 在c++的继承控制中,有三种不同的控制权限,分别是public、protected和private。定义派生类时,若不显示加上这三个关键字,就会使用默认的方式,用struct定义的类
本文向大家介绍JavaScript中的对象继承关系,包括了JavaScript中的对象继承关系的使用技巧和注意事项,需要的朋友参考一下 我们今天就来看一下继承中的类继承以及类继承和原型继承的混用,所谓类继承,就是使用call或者apply方法来进行冒充继承: 像上面这种就是我们要使用的类继承,用这种继承,我们可以访问类中的方法和属性,但是无法访问父类原型中的方法和属性,这种方法别名冒充继承,顾
本文向大家介绍详解C#面相对象编程中的继承特性,包括了详解C#面相对象编程中的继承特性的使用技巧和注意事项,需要的朋友参考一下 继承(加上封装和多态性)是面向对象的编程的三个主要特性(也称为“支柱”)之一。 继承用于创建可重用、扩展和修改在其他类中定义的行为的新类。其成员被继承的类称为“基类”,继承这些成员的类称为“派生类”。派生类只能有一个直接基类。但是,继承是可传递的。如果 ClassB 派生
本文向大家介绍C++中的内存对齐实例详解,包括了C++中的内存对齐实例详解的使用技巧和注意事项,需要的朋友参考一下 C++中的内存对齐实例详解 内存对齐 在我们的程序中,数据结构还有变量等等都需要占有内存,在很多系统中,它都要求内存分配的时候要对齐,这样做的好处就是可以提高访问内存的速度。 我们还是先来看一段简单的程序: 程序一 这段程序的功能很