在正式讨论虚函数前,我们需要明确c++的设计思想——零成本抽象
对于下面的这个类
class A {
public:
int x;
};
这个类的大小为4,也就是一个int的大小。
我们在跑这个类,等同于在跑一个单独的int
class A {
public:
int x;
};
int main()
{
cout << sizeof(A) << endl;
A a;
int* p = (int*)&a;
*p = 23333;
cout << a.x << endl;
return 0;
}
输出
4 23333
实际上,在汇编的角度上,更能看出来
所以,类这个概念,只存在于编译时期。
也就是,我们可以写出修改类中的私有变量的代码(因为,私有这个东西,只在编译时期中存在)
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;
}
输出
4 114514
这个时候我们发现,函数是不占空间的。
我们写出一个继承
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;
}
输出
8 12
内存模型为
两个类共享一个show,这个show不会占用类的空间(放在别的地方了
输出下show的位置
printf("%p\n", &A::show);
printf("%p\n", &B::show);
输出
00007FF75D8A152D 00007FF75D8A152D
我们整个带虚函数的类
class A {
public:
virtual void a() { cout << "A a()" << endl; }
virtual void b() { cout << "A b()" << endl; }
virtual void c() { cout << "A c()" << endl; }
};
输出下大小发现是8。
很怪?我们多给A放点东西
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;
};
大小为16
也就是,只要有虚函数,无论多少个,都会增加8的大小。
8就是64位,显然我的电脑是64位系统(
也就是,这个8应该是个指针。
实际上,A的内存模型为
开头8的空间放了一个指针。
我们就直接放出内存模型
我们来一步步的解析啊。
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();
此时我们就指向了虚函数
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 a() A b() A c()
对于A的实例化,那个指针都是指向同一块
A a1, a2;
u64* p = (u64*)&a1;
cout << *p << endl;
p = (u64*)&a2;
cout << *p << endl;
输出
140695023172728 140695023172728
现在我们来个A的派生
class B :public A {
public:
virtual void b() { cout << "B b()" << endl; }
};
按照上面的代码跑一下
B b;
u64* p = (u64*)&b;
u64* arr = (u64*)*p;
func fa = (func)arr[0];
func fb = (func)arr[1];
func fc = (func)arr[2];
fa(); fb(); fc();
输出
A a() B b() A c()
我们来对比下二者的虚函数的指向
A a;
u64* pa = (u64*)&a;
u64* arra = (u64*)*pa;
B b;
u64* pb = (u64*)&b;
u64* arrb = (u64*)*pb;
for (int i = 0; i < 3; i++) {
cout << hex << arra[i] << " " << arrb[i] << endl;
}
输出
7ff6889a159b 7ff6889a159b 7ff6889a1596 7ff6889a15c3 7ff6889a155f 7ff6889a155f
也就是说,内存模型是这样的
这个时候我们看下任何虚函数教程都有的
A *a = new B;
我们来对比下指向的那个数组
A* a1 = new A;
A* a2 = new A;
A* a3 = new B;
B* b = new B;
cout << hex << *(u64*)a1 << endl;
cout << hex << *(u64*)a2 << endl;
cout << hex << *(u64*)a3 << endl;
cout << hex << *(u64*)b << endl;
输出
7ff626e6bc78 7ff626e6bc78 7ff626e6bc18 7ff626e6bc18
内存模型为
如果我们的B,多放些数据
class B :public A {
public:
int z;
virtual void b() { cout << "B b()" << endl; }
};
那内存模型为
那么我们可以整一个究极花活
我们先定义个C
class C {
public:
virtual void d() { cout << "C d()" << endl; }
virtual void e() { cout << "C e()" << endl; }
virtual void f() { cout << "C f()" << endl; }
};
长成这个样子
那么我们移花接木一下A
代码
A* a = new A;
C* c = new C;
*(u64*)a = *(u64*)c;
a->a(); a->b(); a->c();
输出
C d() C e() C f()
因为编译器只知道,函数a()去找arr[0],b()去找arr[1],c()去找arr[2]。
但是到底arr变成了什么呢,就由不得编译器了(
完整代码
#include <iostream>
#include <stdio.h>
using namespace std;
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;
};
class C {
public:
virtual void d() { cout << "C d()" << endl; }
virtual void e() { cout << "C e()" << endl; }
virtual void f() { cout << "C f()" << endl; }
};
int main(){
typedef long long u64;
typedef void(*func)();
A* a = new A;
C* c = new C;
*(u64*)a = *(u64*)c;
a->a(); a->b(); a->c();
return 0;
}
好了,相信看到这里,大家应该都知道虚函数在哪里了吧。
剩下的一些分配策略什么的,去看看别人的就可以了。
如果你觉得自己懂了的话,可以尝试用C语言模拟一遍。
经人提醒,实际上数组前面还有一块
不过太细节的地方大家还是自己去看吧。
评论区有人提了个问题
如果我们B中有个新的虚函数,然后我们 A∗a=newB 是否可以访问到
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 = 3, y = 5;
};
class B :public A {
public:
virtual void d() { cout << "B d()" << endl; }
};
int main(){
typedef unsigned long long u64;
typedef void(*func)();
A* a = new B;
u64* arr = (u64*)*(u64*)a;
func f = (func)arr[3];
f();
return 0;
}
输出
B d()
实际上就是
当然是可以的
但是吧, 不要继续深究这个了,越来越UB了。
评论区又提问了
b多放些数据那里a3是不是也应该有z呢
答案是可以的
class B :public A {
public:
int z;
B(int _x, int _y, int _z) { x = _x, y = _y, z = _z; }
virtual void d() { cout << "B d()" << endl; }
};
这个时候我们
A* a = new B(1,3,5);
实际上这个a是指向了
而z的位置,处于y的下面
所以我们写出这样的代码
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 = 3, y = 5;
};
class B :public A {
public:
int z;
B(int _x, int _y, int _z) { x = _x, y = _y, z = _z; }
virtual void d() { cout << "B d()" << endl; }
};
int main(){
A* a = new B(1,3,5);
cout << *(&(a->y) + 1) << endl;
return 0;
}
输出
5