【深入探究C++虚函数表——从内存的角度】

文章详细探讨了C++中类的内存布局,特别是涉及虚函数时的情况。指出类在编译时期的概念,而在运行时,可以通过指针对私有变量进行操作。解释了虚函数不占用类的大小,但会增加8字节(64位系统)的空间,这8字节是一个指向虚函数表的指针。通过实例展示了如何通过指针访问虚函数表并调用相应函数。文章还讨论了派生类如何继承和重写虚函数,以及动态类型转换的可能性。
摘要由CSDN通过智能技术生成

在正式讨论虚函数前,我们需要明确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

实际上,在汇编的角度上,更能看出来

1

所以,类这个概念,只存在于编译时期。

也就是,我们可以写出修改类中的私有变量的代码(因为,私有这个东西,只在编译时期中存在)

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

内存模型为

2

两个类共享一个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的内存模型为

3

开头8的空间放了一个指针。

我们就直接放出内存模型

4

我们来一步步的解析啊。

typedef long long u64;
typedef void(*func)();
A a;
u64* p = (u64*)&a;

5

然后我们再

u64* arr = (u64*)*p;

6

我们用函数指针接着

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的实例化,那个指针都是指向同一块

7

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

也就是说,内存模型是这样的

8

这个时候我们看下任何虚函数教程都有的

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

内存模型为

9

如果我们的B,多放些数据

class B :public A {
public:
    int z;
    virtual void b() { cout << "B b()" << endl; }
};

那内存模型为

10

那么我们可以整一个究极花活

我们先定义个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; }
};

长成这个样子

11

那么我们移花接木一下A

12

代码

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语言模拟一遍。


经人提醒,实际上数组前面还有一块

13

不过太细节的地方大家还是自己去看吧。

评论区有人提了个问题

如果我们B中有个新的虚函数,然后我们 A∗a=newB 是否可以访问到

14

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()

实际上就是

15

当然是可以的

但是吧, 不要继续深究这个了,越来越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是指向了

16

而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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值