c++菱形继承、多态与类内存模型

1.菱形继承

先看下面的例子,SheepTuo同时继承了SheepTuo,而他们同时继承Animal
image

#include <iostream>
using namespace std;

class Animal
{
    int mAge;
};

class Sheep : public Animal {};
class Tuo : public Animal {};
class SheepTuo : public Sheep, public Tuo {};

int main()
{
    SheepTuo st;
    ////// 1.报错,"SheepTuo::mAge" is ambiguous,mAge成员在两个子类都存在,二义性
    // st.mAge = 18; 

    ////// 2.可以声明作用域,避免成员的二义性
    st.Sheep::mAge = 18;
    st.Tuo::mAge = 100;

    ////// 3.但是在SheepTuo类中mAge成员会复制两份,造成内存浪费
    return 0;
}

1.1.菱形继承的问题

  1. 共享成员二义性——增加作用域可以解决
  2. 内存复制两份-浪费——虚继承解决
    image

1.2.解决办法

#include <iostream>
using namespace std;

// 虚基类
class Animal
{
public:
    int mAge;
};

// virtual --> 虚继承
class Sheep : virtual public Animal {};
class Tuo : virtual public Animal {};
class SheepTuo : public Sheep, public Tuo {};

int main()
{
    SheepTuo st;
    ////// 1.不会报错了
    // st.mAge = 18;

    ////// 2.这样写,下面的mAge都会是100,因此虚继承后,成员不会被复制,只在基类中有一份,子类中维护vbptr(管理不同的偏移量)指向它
    st.Sheep::mAge = 18;
    st.Tuo::mAge = 100;
    
    return 0;
}

SheepTuo继承了SheepTuo的虚基类指针vbptr,这俩的指针会指向他们的虚基类表vbtable,虚基类表中存着其vbptr的偏移量,通过偏移量可以帮助子类正确找到从虚基类继承来的数据
image

2.虚函数与多态

2.1.普通函数不能实现多态

#include <iostream>
using namespace std;

class Animal
{
public:
    void Speak()
    {
        cout << "动物 在说话" << endl;
    }
};

class Cat : public Animal 
{
    void Speak()
    {
        cout << "小猫 在说话" << endl;
    }
};
class Dog : public Animal 
{
    void Speak()
    {
        cout << "小狗 在说话" << endl;
    }
};

// 常对象只能调用常函数
// void DoSpeak(const Animal &animal)
void DoSpeak(Animal &animal)
{
    animal.Speak();
}

int main()
{
    Cat cat;
    DoSpeak(cat);
    
    return 0;
}

输出:动物 在说话
上面的AnimalSpeak是一个普通成员函数,虽然有继承的条件,但是编译器在编译阶段,会DoSpeak函数中把animal.Speak();animal绑定为Animal的地址,无法实现多态

2.2.虚函数(子类重写)+ 父类指向子类——实现多态

#include <iostream>
using namespace std;

class Animal
{
public:
    virtual void Speak()
    {
        cout << "动物 在说话" << endl;
    }
};

class Cat : public Animal 
{
    void Speak()
    {
        cout << "小猫 在说话" << endl;
    }
};
class Dog : public Animal 
{
    void Speak()
    {
        cout << "小狗 在说话" << endl;
    }
};

void DoSpeak(Animal &animal)
{
    animal.Speak();
}

int main()
{
    Cat cat;
    DoSpeak(cat);
    
    return 0;
}

输出:小猫 在说话
和2.1.节相比,只增加了virtual关键字,使得父类普通函数变为虚函数,使得DoSpeak函数中animal.Speak()的animal在运行阶段才会指向真正调用的对象,实现了多态
其运行流程先说明:对象指针->取其虚表指针->取其虚表中函数->call调用

2.3.多态原理

  • case1.普通函数的基类所占内存大小, sizeof(Animal) = 1
class Animal
{
public:
    void Speak()
    {
        cout << "动物 在说话" << endl;
    }
};

image

  • case2.虚函数的基类所占内存大小, sizeof(Animal) = 4
class Animal
{
public:
    virtual void Speak()
    {
        cout << "动物 在说话" << endl;
    }
};

image

case1和case2说明了增加virtual会在类中多用内存,增加的是虚函数指针和虚函数表

  • case3.子类继承虚基类,并且子类重写虚函数
class Animal
{
public:
    virtual void Speak()
    {
        cout << "动物 在说话" << endl;
    }
};

class Cat : public Animal 
{
    void Speak()
    {
        cout << "小猫 在说话" << endl;
    }
};
  • Cat中的虚函数表发生覆盖
    image
  • 总体来看看
    image

3.c++内存模型

C++类中内存存储情况比较复杂,涉及成员数据、函数、静态成员、虚函数等情况
记录结论,详情参考C++类的内存布局
image
image

4.参考

C++类的内存布局
C++多态剖析

posted @ 2024-05-20 23:19  胖白白  阅读(15)  评论(0编辑  收藏  举报