c++菱形继承、多态与类内存模型
1.菱形继承
先看下面的例子,SheepTuo
同时继承了Sheep
和Tuo
,而他们同时继承Animal
类
#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.解决办法
#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
继承了Sheep
和Tuo
的虚基类指针vbptr
,这俩的指针会指向他们的虚基类表vbtable
,虚基类表中存着其vbptr的偏移量,通过偏移量可以帮助子类正确找到从虚基类继承来的数据
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;
}
输出:动物 在说话
上面的Animal
中Speak
是一个普通成员函数,虽然有继承的条件,但是编译器在编译阶段,会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;
}
};
- case2.虚函数的基类所占内存大小, sizeof(Animal) = 4
class Animal
{
public:
virtual void Speak()
{
cout << "动物 在说话" << endl;
}
};
case1和case2说明了增加virtual
会在类中多用内存,增加的是虚函数指针和虚函数表
- case3.子类继承虚基类,并且子类重写虚函数
class Animal
{
public:
virtual void Speak()
{
cout << "动物 在说话" << endl;
}
};
class Cat : public Animal
{
void Speak()
{
cout << "小猫 在说话" << endl;
}
};
Cat
中的虚函数表发生覆盖
- 总体来看看
3.c++内存模型
C++类中内存存储情况比较复杂,涉及成员数据、函数、静态成员、虚函数等情况
记录结论,详情参考C++类的内存布局