虚函数
虚函数允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。
在基类中用virtual声明成员函数为虚函数,在派生类中重新定义此函数并可改变其功能。当一个成员函数被声明为虚函数后,其派生类中的同名函数都自动成为虚函数,但如果派生类中没有覆盖基类的虚函数,则调用时调用基类的函数定义。
多重继承或多继承
由多个基类共同派生出新的类,这样的继承结构被称为多重继承或多继承。举个例子
#include<iostream>
using namespace std;
class Animal
{
public:
int m_Age;
};
class Sheep : /*virtual*/ public Animal {
};
class Tuo : /*virtual*/ public Animal {
};
class SheepTuo : virtual public Sheep , public Tuo{
};
羊驼是由羊类和驼类一起衍生出来的派生类,这种继承就是多继承。
菱形继承(二义性与数据冗杂问题)
菱形继承大概就是这样:
只要符合继承之后有两份数据之后就是菱形继承!
在多继承结构中,存在着很多问题,比如从不同基类中继承了同名成员,派生类中也定义了同名成员,这种二义性问题很好解决,加上要访问的基类的类名限制就可以了。例如
#include<iostream>
using namespace std;
class Animal
{
public:
int m_Age;
};
class Sheep : public Animal {
};
class Tuo : public Animal {
};
class SheepTuo : virtual public Sheep , public Tuo{
};
void Test1() {
SheepTuo st;
st.Sheep::m_Age = 18;
st.Tuo::m_Age = 20;
// 菱形继承,两个父类都有相同变量,需要加作用域进行区分
cout << "st.Sheep::m_Age = " << st.Sheep::m_Age << endl; //18
cout << "st.Tuo::m_Age = " << st.Tuo::m_Age << endl; //20
}
int main() {
Test1();
system("pause");
return 0;
}
通过上面这样增加作用域,我们可以把二义性的问题很好的解决了,但是,数据冗杂的问题依旧存在,那我们该如何来解决呢?
菱形虚拟继承
我们通过一个关键字virtual,来修饰羊类和驼类即如下面所示
#include<iostream>
using namespace std;
class Animal
{
public:
int m_Age;
};
// 虚继承解决菱形继承多一份数据的问题
// 在继承之前加上virtual关键字
class Sheep : virtual public Animal {
};
class Tuo : virtual public Animal {
};
class SheepTuo : virtual public Sheep , public Tuo{
};
void Test1() {
SheepTuo st;
st.Sheep::m_Age = 18;
st.Tuo::m_Age = 20;
// 菱形继承,两个父类都有相同变量,需要加作用域进行区分
cout << "st.Sheep::m_Age = " << st.Sheep::m_Age << endl;
cout << "st.Tuo::m_Age = " << st.Tuo::m_Age << endl;
//cout << "st.m_Age = " << st.m_Age << endl;//20
// 虚继承之后,都共享一份数据
// 我们只需要一份
// 使用虚函数之后,虚继承之后,打印结果都为20
}
int main() {
Test1();
system("pause");
return 0;
}
这样就解决了我们所困扰的数据冗杂问题了
我们通过命令行,clion命令来观察一下对象模型的底层
可以发现,未使用virtual 关键字的时候,在两个父类中都继承了一份m_Age属性,这就导致了数据冗杂问题
而使用virtual 关键字之后,来自不同父类的同一属性,数据共享了,只有一份m_Age,这就也解决了
我们在羊驼类的内存中可以发现有vbptr在驼类和羊类的下方,说明这俩都共享同一份从基类继承下来的数据
接下来我们看vbtable中,offset即偏移量,虚指针中其实就存储的是偏移量,总基类Animal中4,然后存储了4个字节的int类型,加上就到了第二基类Sheep中的8.
虚指针存储偏移量,方便我们去找到基类里面的属性,只要找到第一个,其他基类的属性是连在一起的,就在下面。所以只会有一个偏移量,编译器不会把基类的属性放在不连续的地址上。因为都是归属最后一个类对象的。不是单独的属于羊驼类里面的对象了,所以需要通过偏移量去找
多个对象的是也是可以通过虚基表去找对应的偏移量,虚基表里面可能还有其他值要放,所有不能把指向虚基表的地址直接替换成偏移量的值
虚基类表中存放着虚函数的指针等信息。当通过虚基类指针调用虚函数时,虚基类指针会根据虚基类表中的信息准确地找到对应的虚函数实现。这使得在多态场景下,即使通过基类指针或引用操作派生类对象,也能正确地调用到派生类重写后的虚函数,从而实现动态绑定的多态行为。
继承的总结和反思
- 很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题。
- 多继承可以认为是C++的缺陷之一,很多后来的OOP语言都没有多继承,如Java。
- 继承和组合
- public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
- 组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
- 优先使用对象组合,而不是类继承 。
- 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
- 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。
- 实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承。类之间的关系可以用继承,可以用组合,就用组合。
附录
分别附上未使用virtual和使用virtual的运行结果,如下
未使用:
使用:
特别的,在使用virtual关键字之后,由于共享数据,所以我们并不需要声明作用域