零成本抽象
对于如下类,大小为4字节,也就是一个int的大小,跑这个类如同跑一个单独的int
1 2 3 4
| class A { public: int x; };
|
类这个概念,只存在于编译时期。
也就是,我们可以写出修改类中的私有变量的代码(因为,私有这个东西,只在编译时期中存在):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| class A { private: int x; public: int getx() { return x; } }; int main() { cout << sizeof(A) << endl; A a; int* p = (int*)&a; *p = 114514; cout << a.getx() << endl; return 0; }
|
这个时候我们发现,函数是不占空间的。
我们写出一个继承:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| class A { public: int x, y; void show() { cout << "show" << endl; } }; class B :public A { public: int z; }; int main(){ cout << sizeof(A) << endl; cout << sizeof(B) << endl; return 0; printf("%p\n", &A::show); printf("%p\n", &B::show); }
|
两个类共享一个show,这个show不会占用类的空间(全局数据区存放全局变量,静态数据和常量;所有类成员函数和非成员函数代码存放在代码区;为运行函数而分配的局部变量、函数参数、返回数据、返回地址等存放在栈区;余下的空间都被称为堆区)
带有虚函数的类
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class A1 { public: virtual void a() { cout << "A1 a()" << endl; } virtual void b() { cout << "A1 b()" << endl; } virtual void c() { cout << "A1 c()" << endl; } };
class A2 { public: virtual void a() { cout << "A2 a()" << endl; } virtual void b() { cout << "A2 b()" << endl; } virtual void c() { cout << "A2 c()" << endl; } int x, y; };
|
输出发现A1大小为8字节,A2大小为16字节。也就是,只要有虚函数,无论多少个,都会增加8的大小(64位系统),说明增加了指针。
此时内存模型:
探索一下虚函数表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| class A { public: virtual void a() { cout << "A a()" << endl; } virtual void b() { cout << "A b()" << endl; } virtual void c() { cout << "A c()" << endl; } int x, y; };
int main(){ typedef long long u64; typedef void(*func)(); A a; u64* p = (u64*)&a; u64* arr = (u64*)*p; func fa = (func)arr[0]; func fb = (func)arr[1]; func fc = (func)arr[2]; fa(); fb(); fc(); return 0; }
|
对于A的实例化,虚函数指针都是指向同一块的(指向虚函数表)。
派生一个B:
1 2 3 4 5
| class B :public A { public: int z; virtual void b() { cout << "B b()" << endl; } };
|
待补充
多继承情况下对象和虚表的布局、thunk这种编译器魔法
总结:
-
每个类,只要含有虚函数,new出来的对象就包含一个虚函数指针,指向这个类的虚函数表(这个虚函数表一个类用一张)
-
子类继承父类,会形成一个新的虚函数表,但是虚函数的实际地址还是用的父类的,如果子类重写了某个虚函数,那么子类的虚函数表中存放的就是重写的虚函数的地址
-
不同类之间可以通过强制转型调用其他类的虚函数
注:转载自C++虚函数表的位置——从内存的角度 - 知乎 (zhihu.com)