C++类的数据对齐+虚函数内存情况探究
首先我们都知道如果类里有虚函数(自己覆盖了/声明了新的虚函数或者继承了父类的虚函数都算),会在类的开头有一个虚函数表指针,所以一个空类如果有一个虚函数,那么sizeof该类等于4。
类的大小和内部数据有关系,和成员函数无关,函数都是放在代码段的,不包含在每个对象的实例中(要是每个实例里面都有一大堆函数,这么费空间还定义类干嘛)。
比如一个类里有int a,char b,char c三个数据。
看下不同顺序声明成员变量会对该类的大小有什么影响:
int ,char,char:
char,int,char:
char,char,int:
原因:类会按照其占用空间最大的成员变量大小来对齐数据:
int和char比。int占4个字节,最大。就会用4来作为对齐的量度:
所以第一种int ,char,char的:第一个int占了0~3字节,第二个char占了之后4~7字节中的4号字节,第三个char占了4~7中的5号字节。最后对齐为4的倍数,所以sizeof等于8
第二种char,int,char的:第一个char占了0号字节,0~4的剩余3个字节不够接下来的int用的,所以int只能占4~7字节。前面的1~3字节就空在那里。第三个char使用第8个字节。最后对齐为4的倍数,所以是12。
第三种不解释了
另外虚函数表会多占用4字节。
比如:
并且虚函数表指针永远在类的开头位置,并且虚函数表指针会占用最大对齐长度的空间,比如成员中最大为
栗子:
解释:
虚函数表指针在类开头0~3字节,数据从4号字节开始。a在4字节,b放不下,所以5~7空着。b放到8~11字节,c放到12字节。最后整体对齐为4的倍数,所以是16字节。
把b换为double:
解释:虚函数表指针占0~3字节,但由于所有数据里最大的是double,占8字节,所以虚函数表指针也占8字节。数据从字节8开始。
用VS验证一下,其中的内存是取得b的地址:
如果派生类的虚函数覆盖了基类的虚函数,那么这两个函数地址是不同的。如果派生类只是简单的继承了基类的虚函数,并没有覆盖它,那么这两个函数的地址是相同的:
1 #pragma warning(disable:4996) 2 #include <iostream> 3 #include<istream> 4 #include <string> 5 #include <cctype> 6 #include<vector> 7 #include<list> 8 #include<cstring> 9 #include<random> 10 #include<typeinfo> 11 #include<set> 12 #include<map> 13 #include<deque> 14 #include<regex> 15 #include<sstream> 16 #include<cstdlib> 17 #include<queue> 18 #include<stdlib.h> 19 #include<stdio.h> 20 #include<stack> 21 #include<algorithm> 22 #include<thread> 23 #include<mutex> 24 #include<assert.h> 25 #include<fstream> 26 #include<unordered_map> 27 #include<unordered_set> 28 using namespace std; 29 30 31 32 class A 33 { 34 }; 35 36 class B 37 { 38 char ch; 39 virtual void func0() { } 40 }; 41 42 class C 43 { 44 char ch1; 45 char ch2; 46 virtual void func() { } 47 virtual void func1() { } 48 }; 49 50 class D : public A, public C 51 { 52 int d; 53 virtual void func() { } 54 virtual void func1() { } 55 }; 56 57 class E : public B, public C 58 { 59 int e; 60 virtual void func0() { } 61 virtual void func1() { } 62 }; 63 64 int main(void) 65 { 66 A a; 67 B b; 68 C c; 69 D d ; 70 E e; 71 cout << "A=" << sizeof(A) << endl; //result=1 72 cout << "B=" << sizeof(B) << endl; //result=8 73 cout << "C=" << sizeof(C) << endl; //result=8 74 cout << "D=" << sizeof(D) << endl; //result=12 75 cout << "E=" << sizeof(E) << endl; //result=20 76 return 0; 77 }
其中D类覆盖了C类的func和func1,那么可以看到,两个func和两个func1都是不同地址的:
(上面图的圈圈画错了,不过应该能看明白吧)
但E类没有覆盖C类的func函数,所以他俩是相同的地址:
像E类这样继承了多个类的类,它的内部结构也是按不同类来分的,E中首先存B类数据,再存C类数据,再存自己的数据:
可以看到,E类有两个虚函数表指针,一个是B类的,一个是C类的。
一点小问题:
但是如果基类声明了虚函数,但是没有继承的派生类,那么在内存中查看,就会发现比较奇怪:
下面C类声明了几个虚函数,但没有其他派生类继承C类,可以看到虚函数表里的函数名都不对了,这个原因就太高深了,大概是编译器发现了这个类没有被继承,所以做了一些优化和处理。
再比如,E类声明了一个新的虚函数,但是虚函数表中找不到相应的函数指针。
虚继承问题:
在继承方式前面加上 virtual 关键字就是虚继承,虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类(Virtual Base Class),本例中的 A 就是一个虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员。
比如下图A派生出B和C类,D继承于B和C类,那么D就会有两份A的数据。用虚继承可以保证D只有一份A的数据。
事实上继承是非常复杂的,而且看到别的博客有讲虚函数表中的函数地址也不是实际上的函数调用地址,而是一个函数符号表的地址(可能就是为什么上面的某张图里虚函数表中的函数名奇奇怪怪的)。这就超出我的能力范围了,暂时先会这些