前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >深入探究C++虚函数表——从内存的角度

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

作者头像
C语言与CPP编程
发布2023-04-30 11:47:42
3670
发布2023-04-30 11:47:42
举报
文章被收录于专栏:c语言与cpp编程

在正式讨论虚函数前,我们需要明确c++的设计思想——零成本抽象

对于下面的这个类

代码语言:javascript
复制
class A {
public:
    int x;
};

这个类的大小为4,也就是一个int的大小。

我们在跑这个类,等同于在跑一个单独的int

代码语言:javascript
复制
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

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

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

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

代码语言:javascript
复制
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

这个时候我们发现,函数是不占空间的。

我们写出一个继承

代码语言:javascript
复制
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的位置

代码语言:javascript
复制
printf("%p\n", &A::show);
printf("%p\n", &B::show);

输出

00007FF75D8A152D 00007FF75D8A152D

我们整个带虚函数的类

代码语言:javascript
复制
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放点东西

代码语言:javascript
复制
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的空间放了一个指针。

我们就直接放出内存模型

我们来一步步的解析啊。

代码语言:javascript
复制
typedef long long u64;
typedef void(*func)();
A a;
u64* p = (u64*)&a;

然后我们再

代码语言:javascript
复制
u64* arr = (u64*)*p;

我们用函数指针接着

代码语言:javascript
复制
func fa = (func)arr[0];
func fb = (func)arr[1];
func fc = (func)arr[2];
fa(); fb(); fc();

此时我们就指向了虚函数

代码语言:javascript
复制
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的实例化,那个指针都是指向同一块

代码语言:javascript
复制
A a1, a2;
u64* p = (u64*)&a1;
cout << *p << endl;
p = (u64*)&a2;
cout << *p << endl;

输出

140695023172728 140695023172728

现在我们来个A的派生

代码语言:javascript
复制
class B :public A {
public:
    virtual void b() { cout << "B b()" << endl; }
};

按照上面的代码跑一下

代码语言:javascript
复制
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()

我们来对比下二者的虚函数的指向

代码语言:javascript
复制
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

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

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

代码语言:javascript
复制
A *a = new B;

我们来对比下指向的那个数组

代码语言:javascript
复制
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,多放些数据

代码语言:javascript
复制
class B :public A {
public:
    int z;
    virtual void b() { cout << "B b()" << endl; }
};

那内存模型为

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

我们先定义个C

代码语言:javascript
复制
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

代码

代码语言:javascript
复制
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变成了什么呢,就由不得编译器了(

完整代码

代码语言:javascript
复制
#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 是否可以访问到

代码语言:javascript
复制
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呢

答案是可以的

代码语言:javascript
复制
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; }
};

这个时候我们

代码语言:javascript
复制
 A* a = new B(1,3,5);

实际上这个a是指向了

而z的位置,处于y的下面

所以我们写出这样的代码

代码语言:javascript
复制
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

原文链接:https://zhuanlan.zhihu.com/p/563418849

——

EOF

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-04-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 C语言与CPP编程 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 在正式讨论虚函数前,我们需要明确c++的设计思想——零成本抽象
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档