【c/c++】多重继承时的内存模型
今天查 bug 时遇到一种情况。有一个c++对象发生了 use after free。这个对象涉及的业务比较复杂,并且有算法在里面,因此想定位 bug 是比较困难的。因此,我们在几个关键的节点,把这个指针地址打印了出来。两个关键的位置是
- 基类构造函数里面,打印出 this
- 对象入栈和出栈时分别打印一下指针
当 bug 复现时,根据 use after free 报出的指针地址,发现在所有的构造函数打印的 this 指针都没有。于是我怀疑这个指针的差异跟对象的多重继承有关。于是我找了相近的指针。果然有一个指针与它相差8个字节。
这个现象引起了我们对 c++ 中多重继承时内存模型的讨论。我写了如下样例。
多重继承的情况
#include <iostream>
#include <memory>
using namespace std;
class A {
public:
int64_t a;
};
class B {
public:
int b;
};
class C : public A, public B {
public:
C() : c(10) {
cout << "construct c " << this << endl;
}
private:
int c;
};
void func(B *pb) {
cout << "convert to B: " << pb << endl;
}
int main()
{
auto pc = make_shared<C>();
func(pc.get());
return 0;
}
发现当 C 指针转换成 B 指针时,编译器会自动将指针地址偏移 sizeof(A)。
那么为什么呢?为什么编译器一定要把 C 的指针做一次偏移之后,才能赋给 B 呢?解决这个问题,需要把视角放小。
思考自然语言需要关注上下文,而编程语言经常是上下文无关的。我们拿出上面代码的一部分来看。
对于 func 函数来说,b 指针必须指向 b 的数据。而由于 C 多重继承于 A 和 B,A 和 B 的数据谁放在前面谁放在后面,总要有个先后。gcc/llvm都选择了把 B 放在后面,C -> B 转换时,需要偏移 sizeof(A);
class B {
public:
int b;
};
void func(B* b) {
cout << "convert to B: " << pb << endl;
}
根据这个结果,我们能可以画出如下的内存模型图。
线性继承的情况
作为对比,当继承是线性的,会发生什么呢?测试代码如下,答案是不会偏移。个中区别读者可以自己思考,文末会放一张图,可以与前面那张图做对比,二者有微妙的差别。提示:还是要把视角放小,做局部思考。
#include <iostream>
using namespace std;
class A {
public:
A() {
cout << "A" << this << endl;
}
int64_t da;
};
class B : public A {
public:
B() {
cout << "B" << this << endl;
}
int64_t db;
};
class C : public B {
public:
C() {
cout << "C" << this << endl;
}
int64_t dc;
};
void func(B * b) {
cout << b << endl;
}
int main() {
C* c = new C;
cout << c << endl;
func(c);
return 0;
}
在这里插入图片描述
此图与多重继承的结构图差别有二
- 类 B 包含了 类 A 的数据
- 类 B 的内存块前部,没有虚表。也就是说 A B C 共用一张虚表。
虚函数表
实际上,在多继承时,虚函数还有一些微妙的不同。比如说,如果 A 没有虚函数,B 有虚函数,那么 B 将会被提前,这样可以减少指针的转换次数,因为可以让 C 和 B 共用一张虚函数表。这里很微妙,后面有机会再详细解释。
如果 A B 都有虚函数表的话,那才是回到最初的那一张图。在这种情况下,发生对 B 的虚函数调用时,需要两次指针偏移,上面会把 B 的排布提前也是因为这个问题,原因后续有机会再详述。
可以使用如下代码,来验证。
/// A B 都有虚函数
#include <iostream>
#include <memory>
using namespace std;
class A {
public:
int64_t a;
virtual void funcA() {
cout << "A::funcA" << endl;
}
};
class B {
public:
int b;
virtual void funcB() {
cout << "B::funcB" << endl;
}
};
class C : public A, public B {
public:
C() : c(10) {
cout << "construct c " << this << endl;
}
void funcB() override {
cout << "C::funcB" << endl;
cout << this << endl;
cout << a << endl; // 访问了 a 的数据
}
void funcA() override {
cout << "C::funcA" << endl;
cout << this << endl;
cout << a << endl; // 访问了 a 的数据
}
private:
int c;
};
void func(B *pb) {
cout << "convert to B: " << pb << endl;
cout << pb->b << endl;
pb->funcB();
}
void funcA(A *pa) {
cout << "convert to A: " << pa << endl;
pa->funcA();
cout << pa->a << endl;
}
int main()
{
auto pc = make_shared<C>();
func(pc.get());
funcA(pc.get());
return 0;
}
#include <iostream>
#include <memory>
using namespace std;
class A {
public:
int64_t a;
void funcA() {
cout << "A::funcA" << endl;
}
};
class B {
public:
int b;
virtual void funcB() {
cout << "B::funcB" << endl;
}
};
class C : public A, public B {
public:
C() : c(10) {
cout << "construct c " << this << endl;
}
void funcB() override {
cout << "C::funcB" << endl;
cout << this << endl;
cout << a << endl; // 访问了 a 的数据
}
void funcA() {
cout << "C::funcA" << endl;
cout << this << endl;
cout << a << endl; // 访问了 a 的数据
}
private:
int c;
};
void func(B *pb) {
cout << "convert to B: " << pb << endl;
cout << pb->b << endl;
pb->funcB();
}
void funcA(A *pa) {
cout << "convert to A: " << pa << endl;
pa->funcA();
cout << pa->a << endl;
}
int main()
{
auto pc = make_shared<C>();
func(pc.get());
funcA(pc.get());
return 0;
}
结论
在c++中尽量不要多继承,尤其是多实现几个接口的时候。
原文地址:https://blog.csdn.net/weixin_43233774/article/details/143637146
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!