c++内存对象模型&虚函数表vtable布局研究

目录

1.linux查看内存对象模型的方法

2.没有虚函数的内存布局

3.有虚函数的内存布局

1)子类不重写父类的虚函数

2)子类重写父类的虚函数

4.多重继承的内存布局

1)子类不重写父类的虚函数

2)子类重写父类的虚函数


       

        今天学习了《面试系列之C++的对象布局》一文,收获很多。记录下自己的实验和心得体会。

原文链接:面试系列之C++的对象布局 - 知乎

1.linux查看内存对象模型的方法

查看内存模型的方法:

clang -Xclang -fdump-record-layouts -stdlib=libc++  -std=gnu++11 xxx.cpp

为了测试方便,写了个简单的脚本./xxx.sh:
clang -Xclang -fdump-record-layouts -stdlib=libc++  -std=gnu++11  -c $1

./xxx.sh xxx.cpp

使用方法:./xxx.sh xxx.cpp

查看虚函数表的方法

clang -Xclang -fdump-vtable-layouts -stdlib=libc++ -std=gnu++11  -c  xxx.cpp

2.没有虚函数的内存布局

示例代码:

struct Base {
    Base() = default;
    ~Base() = default;
	
	void FuncA() {}
    int a;
    int b;
};

int main() {
   Base base1;

   return 0;
}

内存布局:

Base有两个int型的成员变量,共计4+4=8个字节

3.有虚函数的内存布局

示例代码:

struct BaseA {
    BaseA() = default;
    virtual ~BaseA() = default;
	
	void FuncA() {}
	virtual void FuncB() {}
    int a;
    int b;
};

int main() {
	BaseA base2;

    return 0; 
}

内存布局:

BaseA vtable pointer是一个虚函数表指针vtable pointer,占用8个字节

BaseA共计占用8+4+4=16个字节。

vtable:

共有3个虚函数,其中,虚析构函数有两个版本,没有深入研究,网上的一种说法是:

BaseA::~BaseA() [complete],当在栈上构造该对象时,析构时用这个版本;

BaseA::~BaseA() [deleting],当在堆上构造该对象时,析构时用这个版本。

1)子类不重写父类的虚函数

示例代码:

struct BaseA {
    BaseA() = default;
    virtual ~BaseA() = default;
	
	void FuncA() {}
	virtual void FuncB() {}
    int a;
    int b;
};

struct Derived1: public BaseA {	
};

int main() {
	Derived1 derived1;

    return 0; 
}

 内部布局:

Derived1没有新增成员变量或者成员函数,所以,它的内存模型和其父类的模型一致。派生类从其基类拷贝一份内存中的数据。

vtable:

pic1表示派生类所有的虚函数,从中可知,派生类继承了基类的虚函数void BaseA::FuncB()。

pic2表示派生类自己的虚函数。

2)子类重写父类的虚函数

示例代码:

struct BaseA {
    BaseA() = default;
    virtual ~BaseA() = default;
	
	void FuncA() {}
	virtual void FuncB() {}
    int a;
    int b;
};

struct Derived2: public BaseA {
	virtual void FuncB() override {}
};

int main() {
	Derived2 derived2;

    return 0; 
}

vtable:

从图中可以看出来,

派生类重写了虚函数FuncB,此时,派生类实现的版本void Derived2::FuncB()会覆盖基类的版本void BaseA::FuncB()。

4.多重继承的内存布局

1)子类不重写父类的虚函数

示例代码:

struct BaseA {
    BaseA() = default;
    virtual ~BaseA() = default;
	
	void FuncA() {}
	virtual void FuncB() {}
    int a;
    int b;
};

struct BaseB {
    BaseB() = default;
    virtual ~BaseB() = default;
	
	void FuncA() {}
	virtual void FuncC() {}
    int a;
    int b;
};

struct Derived3: public BaseA, BaseB {	
};

int main() {
	Derived3 derived3;

    return 0; 
}

内部布局:

从图中可以看出,派生类分别从其两个父类拷贝一份内存中的数据,派生类占用的字节数为16+16=32个字节。内存的排布按照声明时的顺序排列。struct Derived4: public BaseA, BaseB {};BaseA在前,BaseB在后。

vtable:

派生类的vtable也由两个父类的vtable组成。由于Derived3和BaseA的起始地址相同,所以offset_to_top为0。BaseB的起始地址和Derived3的起始地址相差16个字节,所以offset_to_top为16。

2)子类重写父类的虚函数

示例代码:

struct BaseA {
    BaseA() = default;
    virtual ~BaseA() = default;
	
	void FuncA() {}
	virtual void FuncB() {}
    int a;
    int b;
};

struct BaseB {
    BaseB() = default;
    virtual ~BaseB() = default;
	
	void FuncA() {}
	virtual void FuncC() {}
    int a;
    int b;
};


struct Derived4: public BaseA, BaseB {
	virtual void FuncB() override {}
	virtual void FuncC() override {}
	virtual void FuncD() {}
};

struct Derived5: public BaseB, BaseA {
	virtual void FuncB() override {}
	virtual void FuncC() override {}
};


int main() {
    Derived4 derived4;
    Derived5 derived5;

    return 0; 
}

vtable:

结论1:多重继承时,派生类新增的虚函数,会被添加到排在最前面的vtable中,且只添加一份。从图中可以看出,派生类新增的虚函数FuncD,被添加到了BaseA的vtable中,没有被添加到BaseB的vtable中。

Derived4重写了FuncB和FuncC,为什么其vtable中BaseA部分有FuncB和FuncC,而BaseB部分只有FuncC?

对于这个问题,没有找到确切的答案,下面是我的理解,不正确的话不吝指教。

step1:struct Derived4: public BaseA, BaseB {};,声明Derived4时,BaseA排在前面。先构造(这么说可能不是很准确)BaseA,发现FuncB被重写了,所以用void Derived4::FuncB()去覆盖void BaseA::FuncB()。

step2:对于BaseA来说,void Derived4::FuncC()是新增的虚函数,根据前文的“结论1”,把void Derived4::FuncC()添加到BaseA的vtable中。

step3:然后构造BaseB部分,发现FuncC被重写了,所以用void Derived4::FuncC()覆盖之前的版本。void Derived4::FuncB()对于BaseB来说是新增的函数,根据“结论1”,只需要添加到排在前面的对象的vtable中,由于BaseA部分已经有一个void Derived4::FuncB()了,所以,不需要再添加到BaseB部分了。

为了验证上述结论,在声明Derived4时交换继承的顺序,改为struct Derived5: public BaseB, BaseA {};那么按照step3中的分析,先构造BaseB部分,则会用void Derived5::FuncB()覆盖之前的版本,把void Derived5::FuncC()作为新增的函数添加进来。然后构造BaseA部分,只需要用void Derived5::FuncB()覆盖之前的版本就可以了。下图中的vtable布局,和上述分析吻合。

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
c++中的内存模型包括四个部分:栈区、堆区、全局/静态区、常量区。而虚函数内存划分则涉及vptr和vtable。 首先,栈区是用于存储局部变量、函数参数和函数调用时的临时数据。虚函数的调用会先找到对象虚函数指针vptr,然后通过vptr找到虚函数vtable。而vptr是属于对象的一部分,它存储在对象内存布局中的最开始位置。这意味着vptr会占用对象内存空间。 vtable是一个指针数组,它存储了虚函数的地址。在vtable中,每个虚函数的地址根据其在类中的声明顺序进行排列。当通过vptr找到vtable后,就可以通过虚函数在vtable中的位置偏移找到对应的虚函数的地址。虚函数的地址存储在vtable的字节中,这些字节也会占用对象内存空间。 在堆区,存储的是通过new关键字动态分配的内存。当对象动态分配内存时,vptr会随着对象内存块一起存储在堆中。 在全局/静态区,存储的是全局变量和静态变量。由于全局变量和静态变量是整个程序共享的,它们的内存布局中不包含vptr和vtable。 最后,常量区存储的是程序中的字符串常量和其他常量。常量区的内存布局中也不包含vptr和vtable。 总的来说,虚函数的内部具体内存划分包含vptr和vtable,它们存储在对象内存空间中,占用一定的字节。而全局/静态区、常量区和堆区中的内存布局则不包含有关虚函数内存划分。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值