自学内容网 自学内容网

【c/c++】多重继承时的内存模型

今天查 bug 时遇到一种情况。有一个c++对象发生了 use after free。这个对象涉及的业务比较复杂,并且有算法在里面,因此想定位 bug 是比较困难的。因此,我们在几个关键的节点,把这个指针地址打印了出来。两个关键的位置是

  1. 基类构造函数里面,打印出 this
  2. 对象入栈和出栈时分别打印一下指针

当 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;
}

在这里插入图片描述
在这里插入图片描述
此图与多重继承的结构图差别有二

  1. 类 B 包含了 类 A 的数据
  2. 类 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)!