目录
前言
大家好,这一篇博文主要是详细介绍c++中继承的概念,其中在菱形继承之前的内容都是比较重点的,关于菱形虚继承部分的原理其实有些东西比较复杂,可以简单了解一下,不用过多纠结。实际当中也不咋用,希望大家多多点赞收藏评论!
继承的概念及定义
首先,面向对象编程的三大特性是封装、继承、多态,而今天我们要学习的就是继承
继承是代码复用的一种手段,为什么这么说呢?先看看下面这个案例
有一天学校给张三布置了一个作业,就是完成一个图书管理系统,而在写的过程中,张三先考虑的是图书管理系统中存储的对象,这些对象有学生、有教师、有保安等。于是张三把这些类都实现出来了,当实现出来了以后,张三仔细看了一下代码,这些实现出来的类都有一些共同点,例如名字这些类全部都有,再比如性别、年龄、电话号码等。那么考虑到代码的复用,就去想有没有什么办法解决上述的问题,把这些类共有的数据抽取出来,再把一些类特有的数据分别实现?
而继承就完美解决了上述的问题!
如下:
#include <iostream>
class Person
{
public:
void Print()
{
std::cout << "Person" << std::endl;
}
protected:
//年龄、性别、电话号码都是老师和同学共有的
int _age=0;
int _sex=0;
int _number=0;
//....
};
class Teacher : public Person //Teacher 以公有的方式继承了 Person
{
public:
void TeaPrint()
{
std::cout << "age of teacher is " << _age << std::endl;
}
protected:
size_t _jobid;
};
class Student : public Person //Student 以公有的方式继承了 Person
{
public:
void StuPrint()
{
std::cout << "age of student is " << _age << std::endl;
}
protected:
size_t _stuid;
};
int main()
{
Teacher t;
t.Print();//输出Person
Student s;
s.Print();//输出Person
t.TeaPrint();//输出age of teacher is 0
s.StuPrint();//输出age of student is 0
return 0;
}
代码解析:上述代码把学生和老师类中的共有数据,例如年龄、名字等信息以及成员函数抽离出来变成一个新的类Person,此时当学生和老师继承这个Person类时,就可以访问这个Person类的成员以及方法
其中,我们把这个抽离出来的Person类叫做父类(也可以叫基类)
我们把继承父类(基类)的类称为子类(也可以叫派生类)
类名旁边的public我们称为继承方式,public是继承方式的一种,还有其他的继承方式后面说
继承方式与访问限定符
在c++中有3种继承方式,分别是public继承、protected继承、private继承,如下表
类成员/继承方式 | public继承 | protected继承 | private继承 |
基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
对于上述表,我总结出以下规律:
1.基类的private成员不管什么继承方式都不可见,不可见的意思是虽然这个成员被继承下来了,但是无法通过派生类自己来访问,只能间接通过基类的方法进行访问。
就好比张三的爸爸藏了有私房钱,当张三去问他爸爸拿钱的时候,他爸说没有,实际上钱并不是不存在,而是张三没有权利访问。
2.我们认为在访问权限上,public >protected >private,那么我就能得出以下结论:派生类的成员在子类的访问方式就是成员在基类中的访问限定符和继承方式之间的权限较小值。
例如:基类中的public成员被protected继承方式继承下来,那么取它们中的权限较小值就是protected,那么继承下来以后在派生类中就是protected成员。再比如protected成员被public继承方式继承下来,那么取它们中的权限较小值就是protected,那么继承下来以后在派生类中就是protected成员
补充:继承方式可以不写,若用class定义的类,默认继承方式是private,若用struct定义的类,默认继承方式是public
并且我们一般不用protected继承和private继承,在接下来的内容中也用的是public继承
基类和派生类对象赋值转换
派生类可以赋值给基类对象/引用/指针
它的赋值规则如下
在上图中,Person是一个基类,而Student是一个派生类,当派生类赋值给基类时会把派生类中继承下来的基类成员赋值给这个基类,这个过程我们一般通俗的叫做切割或者切片,也叫做对象的赋值兼容转换
需要注意的是,这里的赋值比较特殊,以前我们没有接触继承时的不同类型之间赋值是通过中间产生临时变量,临时变量赋值的方式完成赋值的,但基类与派生类的赋值之间不存在临时变量,这是一个特殊规定
为了证明上述特殊规定,如下代码:
class Person
{
public:
int _age = 20;
//...
};
class Student : public Person
{
public:
int _stuid = 10;
};
int main()
{
//非继承赋值
int a = 0;
double d1 = a;//正常运行
//double& d2 = a;//编译错误
const double& d3 = a;//正常运行
Student s;
Person& p = s;//正常运行
return 0;
}
代码解析:
上述代码中的double d1 = a,实际上并不是直接把a赋值给d1的,而是在赋值的过程中用a的值生成了一个临时变量,由于临时变量具有常性,这个临时变量是const double类型
而对于double& d2 = a,也是在赋值的过程中用a的值生成了一个临时变量,这个临时变量也是const double类型,但double类型不能作为const double类型的引用(权限放大),故编译报错
同理,const double&与临时变量类型是匹配的,故正常运行
通过如上叙述,得出结论,在非继承下不同类型的赋值确实会在赋值的过程中生成临时变量
而对于派生类Student与基类Person,·p是可以作为s的引用的,如果中间生成了临时变量,临时变量具有常性,那么Person& p = s肯定会报错,所以能得出结论, 派生类和基类的赋值兼容转换的过程中没有生成临时变量
隐藏的概念
实际上对于作用域,我们都很熟悉,我们之前了解过类的作用域、命名空间等,那么在继承中父类的作用域和子类的作用域是否是同一个作用域呢?
怎么证明这一点呢?
实际上,我们之前说过在一个作用域内是不能定义两个同名成员的,那么根据这一点我们可以尝试证明,如下代码
class Person
{
public:
int _a;
int _age = 20;
//...
};
class Student : public Person
{
public:
int _a;
int _stuid = 10;
};
int main()
{
Student s;
Person& p = s;
return 0;
}
上述代码是正常运行的,于是可以得出结论,父类和子类是不同作用域,并且若父类与子类之间的成员是同名的,那么我们就无法直接像之前一样的在子类当中直接访问父类成员,因为编译器默认会从自己的作用域查看,只能指定父类作用域之后访问父类成员,如下
class Person
{
public:
int _a = 0;
int _age = 20;
//...
};
class Student : public Person
{
public:
void Print()
{
std::cout << _a << std::endl;//打印的是Student自己的_a成员,运行结果为1
std::cout << Person::_a << std::endl;//打印的是继承下来的_a成员,运行结果为0
}
int _a = 1;
int _stuid = 10;
};
int main()
{
Student s;
s.Print();
return 0;
}
而上述情况,子类中直接访问同名变量,访问的是子类的同名变量,我们称之为两个同名变量是隐藏关系
在上述中,我们聊的一直都是同名变量的隐藏关系,实际上除了同名变量,同名函数之间也有隐藏关系,如下代码
class Person
{
public:
void Print()
{
std::cout << "I am Person" << std::endl;
}
public:
int _a = 0;
int _age = 20;
//...
};
class Student : public Person
{
public:
void Print()
{
std::cout << "I am Student" << std::endl;
}
int _a = 1;
int _stuid = 10;
};
int main()
{
Student s;
s.Print();//访问的是Student类自己的Print方法
s.Person::Print();//访问的是基类Person的Print方法
return 0;
}
成员函数之间的隐藏关系,只要符合父类和子类的成员函数名相同即可构成隐藏关系
派生类的默认成员函数
要搞懂有关继承的默认成员函数,我们首先需要搞懂的是普通类的默认成员函数,再在这个基础上进一步学习继承的默认成员函数,如果不了解普通类的默认成员函数,可以看看我往期的博客《类与对象》
首先,继承中添加了子类和父类的概念,而父类的默认成员函数我们可以当成是普通类的默认成员函数,接下来我们需要讨论的就是子类(派生类)的默认成员函数
派生类的构造函数
首先,我们先理解一下派生类的默认构造函数,也就是我们不写,编译器默认生成的构造函数会做什么,如下代码
#include <iostream>
class Person
{
public:
Person()
{
std::cout << " 构造 ";
}
Person(const Person& p)
{
std::cout << " 拷贝构造 ";
}
public:
int _a = 0;
int _age = 20;
//...
};
class Student : public Person
{
public:
int _a = 1;
int _stuid = 10;
};
int main()
{
Student s1;
Student s2(s1);
//运行结果:构造 拷贝构造
return 0;
}
在上述代码中,分别运行了一次Person的无参构造和一次Person的拷贝构造
换句话来说,当我们不写,编译器自动生成的派生类的默认构造会自动调用基类的构造函数
当然,上面说的都是对于派生类的默认构造函数,如果我们要显式写派生类的构造函数并且把父类的成员进行初始化,那么我们就需要且只能在初始化列表或者构造函数体内显式调用父类的构造函数完成父类的初始化,如下代码
#include <iostream>
class Person
{
public:
Person(int a,int age)
:_a(a)
,_age(age)
{
std::cout << "Person:" << _a << " " << _age << std::endl;
}
public:
int _a = 0;
int _age = 20;
//...
};
class Student : public Person
{
public:
Student(int stuid,int a,int age)
:_stuid(stuid)
,Person(a,age)//显式调用父类的构造函数
{
std::cout << "Student:" << _stuid << std::endl;
}
private:
int _stuid = 10;
};
int main()
{
Student s2(9,100,20);
//运行结果:
//Person:100 20
//Student:9
return 0;
}
派生类的赋值重载
还是一样的,我们首先来谈默认的赋值重载,即不显式写,编译器默认生成的赋值重载
#include <iostream>
class Person
{
public:
Person(int a,int age)
:_a(a)
,_age(age)
{}
void operator=(const Person& p)
{
std::cout << "I am operator=()" << std::endl;
}
public:
int _a = 0;
int _age = 20;
//...
};
class Student : public Person
{
public:
Student(int stuid,int a,int age)
:_stuid(stuid)
,Person(a,age)//显式调用父类的构造函数
{}
private:
int _stuid = 10;
};
int main()
{
Student s2(9,100,20);
Student s1(10, 11, 12);
s2 = s1;
//运行结果:I am operator=()
return 0;
}
通过上述代码及运行结果,我们证明了其实对于派生类的默认赋值重载,它会自动调用基类的赋值重载,并且我们注意到基类的赋值重载是需要传一个Person类的对象给他的,那么这个Person类的对象怎么来的呢?此时我们前面说的切割(切片)的意义就在这了,上述代码中,是把s1进行切割,把s1中属于父类的成员传给这个Person形参
现在我们搞懂了默认赋值重载,接下来我们要了解一下我们显式写的赋值重载如何实现
显式写的赋值重载同样需要根据父类初始化父类成员,子类初始化子类成员的思路来
需要注意的是,当我们显式写赋值重载的时候不能直接调用operator=()来调用父类的赋值重载,而是要指定父类的作用域,因为我们前面说过父类和子类的同名函数构成隐藏关系,如下代码
#include <iostream>
class Person
{
public:
Person(int a,int age)
:_a(a)
,_age(age)
{}
void operator=(const Person& p)
{
std::cout << "I am operator=()" << std::endl;
}
public:
int _a = 0;
int _age = 20;
//...
};
class Student : public Person
{
public:
Student(int stuid,int a,int age)
:_stuid(stuid)
,Person(a,age)
{}
/*void operator=(const Student& s)
{
operator=(s);
std::cout << "I am Student operator=()" << std::endl;
}*/
//上面为错误写法
//下面为正确写法
void operator=(const Student& s)
{
Person::operator=(s);
std::cout << "I am Student operator=()" << std::endl;
}
private:
int _stuid = 10;
};
int main()
{
Student s2(9,100,20);
Student s1(10, 11, 12);
s2 = s1;
//运行结果:
//I am operator=()
//I am Student operator=()
return 0;
}
派生类的析构函数
首先还是来聊派生类的默认析构函数,派生类的默认析构函数会自动调用基类的析构函数,如下
#include <iostream>
class Person
{
public:
Person(int a,int age)
:_a(a)
,_age(age)
{}
~Person()
{
std::cout << "I am ~Person()" << std::endl;
}
public:
int _a = 0;
int _age = 20;
//...
};
class Student : public Person
{
public:
Student(int stuid,int a,int age)
:_stuid(stuid)
,Person(a,age)
{}
private:
int _stuid = 10;
};
int main()
{
Student s2(9,100,20);
//运行结果:I am ~Person()
return 0;
}
而如果我们显式写派生类的析构函数,如果还按照之前的思路,情况就会比较复杂,如下代码
#include <iostream>
class Person
{
public:
Person(int a,int age)
:_a(a)
,_age(age)
{}
~Person()
{
std::cout << "I am ~Person()" << std::endl;
}
public:
int _a = 0;
int _age = 20;
//...
};
class Student : public Person
{
public:
Student(int stuid,int a,int age)
:_stuid(stuid)
,Person(a,age)
{}
~Student()
{
~Person();//编译错误
std::cout << "I am ~Student()" << std::endl;
}
private:
int _stuid = 10;
};
int main()
{
Student s2(9,100,20);
return 0;
}
上述代码为什么会编译错误呢?子类不是继承了父类的成员函数吗?难道析构函数不被继承?
实际上,父类的析构函数确实被继承给了子类,但子类和父类的析构函数构成隐藏关系,虽然它们名字不相同,但经过编译器的处理以后子类和父类的析构函数都被特殊处理了,函数名统一处理为destructor(),在后面的博文多态部分会详细解释这个原因
#include <iostream>
class Person
{
public:
Person(int a,int age)
:_a(a)
,_age(age)
{}
~Person()
{
std::cout << "I am ~Person()" << std::endl;
}
public:
int _a = 0;
int _age = 20;
//...
};
class Student : public Person
{
public:
Student(int stuid,int a,int age)
:_stuid(stuid)
,Person(a,age)
{}
~Student()
{
Person::~Person();
std::cout << "I am ~Student()" << std::endl;
}
private:
int _stuid = 10;
};
int main()
{
Student s2(9,100,20);
//运行结果
//I am ~Person()
//I am ~Student()
//I am ~Person()
return 0;
}
上述代码简而言之就是当我们显式调用了父类的构造函数之后会析构两次父类
为什么会出现上述这种情况呢?
实际上,编译器也对这里进行了特殊处理,即当我们调用完子类的析构函数之后,编译器会自动调用父类的析构函数,以实现对父类资源的清理,换句话来说我们其实写子类的析构函数时不用显式调用父类的析构函数,这里的原因是因为析构函数必须要先析构子类再析构父类才能保证安全,因为如果先析构父类,那么子类的析构函数就有可能会使用父类的成员,此时就会造成野指针等问题,所以编译器干脆直接统一处理了
继承之友元与静态成员
对于友元实际上就一句话,友元关系不能继承,也就是说如果父类是A类的友元,那么它的子类不是A类的友元,如果需要子类也是A类的友元需要手动定义
对于在继承中的静态成员变量,如果父类定义了一个静态成员变量A,这个父类被继承到了子类,那么这个静态成员虽然会被继承,但是整个继承体系中就只有一个静态成员变量A,也就是无论派生多少子类,它们都共享这个A,我们可以通过静态成员变量的这个性质,来统计父类和它派生的子类的个数是多少,并且在访问这个静态成员变量时即可以指定父类作用域也可以指定子类作用域,因为它们访问的是同一个。如下代码
class A
{
public:
A()
{
++count;
}
static size_t count;
};
size_t A::count = 0;
class B : public A
{
public:
B()
{
++count;
}
};
int main()
{
A a1;
A a2;
B b1;
B b2;
A a3;
std::cout << A::count << std::endl;//运行结果7
B b3;
std::cout << B::count << std::endl;//运行结果9
//下面代码证明子类父类共享一个静态成员变量
std::cout << &A::count << std::endl;
std::cout << &B::count << std::endl;
return 0;
}
菱形继承及菱形虚继承
在了解菱形继承之前我们得先了解两个概念,即单继承和多继承得概念
单继承
所谓单继承,就是一个子类只有一个直接父类时称这个继承关系为单继承,我们之前写的全部都是单继承
就好比一个研究生是一个学生,所以研究生类继承学生类,而一个学生是一个人,所以学生类继承人类,如下图即为单继承
多继承
多继承:至少有一个子类有两个或以上直接父类时称这个继承关系为多继承
需要用到多继承的场景比如:某个大学需要招收学生助教兼职,此时相对于这个学生助教的学生,这个学生助教是一个老师,而相对于学校来说,这个学生助教是学生,于是当我们需要描述这个关系的时候就要用到多继承,如下即为多继承
如下图:
菱形继承
菱形继承是多继承的一种特殊情况
菱形继承的场景比如:某个大学需要招收学生助教兼职,此时相对于这个学生助教的学生,这个学生助教是一个老师,而相对于学校来说,这个学生助教是学生,但不管是学生还是老师都是人,于是这个继承关系就是学生类和老师类继承 人这个类,学生助教继承老师类和学生类,于是它们的关系如下:
上图即为菱形继承,如下即为实现代码
注意:菱形继承带来了两个问题,接下来分别阐述
#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
protected:
string _name;
int _age;
int _tel;
};
class Student : public Person
{
public:
//...
protected:
size_t _stuid;
};
class Teacher : public Person
{
public:
//...
protected:
size_t _jobid;
};
class Assistant : public: Student , public Teacher
{
public:
protected:
//...
}
int main()
{
Assistant a;
a._name = "张三"; //报错
return 0;
}
上述代码出现错误,原因是Assistant这个类既继承了Student类,又继承了Teacher类,而这两个类又分别继承了Person类,于是Assistant类就继承了两个_name,此时菱形继承就出现了第一个问题:二义性
对于上述问题的解决办法我们可以直接指定父类作用域
//....
int main()
{
Assistant a;
a.Student::_name = "张三";
a.Teacher::_name = "小张";
return 0;
}
但对于这种解决办法其实没治到根上,因为归根到底A这个类继承了两个相同的成员,而难道一个人会有两个名字吗?就算有,那么如果是性别呢?身份证号呢?年龄呢?
接下来引入第二个问题,其实就是数据冗余的问题,因为一个人不可能有两个不同的年龄,此时你存储下来了两个年龄。这本质上是资源的一种浪费
我们分析一下上面两个问题,本质上其实都是数据重复定义造成的,而c++给出的方法就是虚继承
虚继承的语法定义就是在“:”之后,继承方式之前加上virtual关键字,但注意,这个关键字只能加到数据重复出现的类,在上述代码中,Assistant类是因为继承了Teacher和Student类导致的数据重复,那么我们只需要在Student类和Teacher类加上virtual关键字即可,如下代码
#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
string _name;
int _age;
int _tel;
};
class Student : virtual public Person
{
public:
//...
size_t _stuid;
};
class Teacher : virtual public Person
{
public:
//...
size_t _jobid;
};
class Assistant : public Student, public Teacher
{
public:
//...
};
int main()
{
Assistant a;
a._name = "张三";//编译通过
return 0;
}
在上述代码中,加上virtual关键字后Assistant类继承的成员就只有一份而不是两份,故没有了二义性,也没有了数据冗余的问题。
虚继承原理
在上面,我们了解了菱形继承带来的两大问题,并且提供了解决方案:虚继承,接下来我们要了解虚继承怎么解决菱形继承问题的
在真正了解虚继承之前,我们先通过内存窗口来看一下没有虚继承的菱形继承的内存分布
如下:
#include <iostream>
class A
{
public:
int _a;
};
class B : public A
{
public:
int _b;
};
class C : public A
{
public :
int _c;
};
class D : public B, public C
{
public:
int _d;
};
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
实际上,通过上图,我们明显能看到有两个从A类继承下来的_a存在,那么如果我们加上虚继承之后,再进行对比一下
如下
class B : virtual public A
//...
class C : virtual public A
//....
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
从上图我们能看到,相比于菱形继承,菱形虚拟继承存储的时候只有一个_a成员,而且这个_a成员被压在了最下面,并且多了两个指针,这两个指针所指的地方我们称之为虚基表(多态部分详细说)我们通过这两个指针最终在这两个指针所指地址的下一行找到了一个数据,那么这个数据是什么呢?
其实这个数据就是我们所说的偏移量,那么是什么的偏移量呢?其实就是我们看到的多出来两个指针地址它们的相对偏移量,例如在上面多出来两个指针的地址分别是0x004FF6F4,0x004FF6FC
而这两个地址分别与它们指针所指的偏移量相加即为0x004FF708,也就是_a成员的地址
ok,上面我们讲了虚继承的基本原理,接下来该回答几个问题
第一个问题:为什么偏移量要存在虚基表的第二行,第一行不能存吗?
这里的虚基表其实不仅仅是要存偏移量的,第一行还有其他作用要完成,接下来多态部分会详细说明
第二个问题:为什么不直接把偏移量存到对象里面,为什么要这么麻烦,存一个指针,然后通过指针找偏移量呢?
首先,如果把偏移量存到对象里面,那么就需要消耗4个字节,而存到虚基表中,只需要消耗1个字节
第三个问题:什么场景下必须通过偏移量来找_a?(为什么存储偏移量?)
实际上,如果单单是D类对象进行访问,是不需要存储偏移量的,因为D类对象之中其实就有_a
而怕就怕发生对象赋值兼容转换,就好比如下代码
//...
void func(B* pb)
{
}
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
B* pb1 = &d;
func(pb1);
func(&d);
return 0;
}
上述代码中,发生了切片,pb指向的是d中属于B类的成员,但注意,B类中没有_a对象,_a对象被存储在了最底下,详细可以看看上面的内存分布,此时我的pb可能接收D类,也可能接受B类,这是不确定的,如果不存偏移量,编译器就要根据是D类还是B类来判断_a所在的位置,对于编译器来说是比较麻烦的,而存了偏移量后,编译器可以把D类和B类按照统一的方式进行访问_a
继承与组合
在实践中,除了有继承的概念,还有组合的概念,接下来谈的都是public继承,其他继承我们用的很少
组合:说的是一个类中的成员是另一个类,那么这两个类就构成组合关系,就好比STL中的Stack和vector之间的关系就是组合,以下代码,A类和B类是组合关系,C类和A类是继承关系
class A
{
public:
//...
protected:
int _a;
};
class B //B类和A类是组合关系
{
//...
protected:
A _a;
int _b;
};
class C : public A //A类和C类是继承关系
{
public:
//...
protected:
int _c;
};
实际上,组合和继承都可以实现代码复用,那么在我们实践中应选用哪一种呢?接下来我们针对这个方向进行讨论
在软件开发中,有一个评判代码优劣的标准,即低耦合
耦合指的是类与类之间的关系,低耦合说的就是类与类之间的关系越独立越好
我们根据这个标准来看,其实组合是比继承更优的
首先,组合是受访问限定符的限制的,那么当有一个类(假设C类)有100个成员,90个成员是保护的,10个成员是公有的,此时有另外两个类,一个是与他组合的(A类),一个是继承它的(B类)
那么如果C类进行修改,只要不修改公有的成员,那么A类是不受影响的,因为A类本身就受C类访问限定符的限制,在A类中使用C类成员本来也只能用公有的
如果C类进行修改,无论修改公有还是保护成员,B类都可能多多少少受影响,因为B类是C类继承下来的,不受protected限定符限制
根据上述,如果要符合低耦合的标准,组合比继承更优,换句话来说,如果组合和继承都能用,并且说得通,那么优先使用组合
当然,除了低耦合以外,评判使用组合还是继承也可以根据这两个哪个更合理进行设计
组合:是一种has-a的关系,也就是 xxx中有xxx
继承:是一种is-a的关系,也就是 xxx是一种特殊的xxx
例1:汽车与轮胎之间的关系,汽车当中有轮胎,那么汽车类与轮胎类就优先使用组合,因为这样更合理,你不能说汽车就是轮胎吧?
例2:学生与人之间的关系,我们只能说学生是特殊的人,而不能说学生中有人,所以优先使用继承
例3:STL之中的Stack和vector之间的关系,Stack是一种特殊的vector,这好像能说通,Stack中有vector,这好像也能说通,那么根据低耦合我们可以优先使用组合
最后,这一期博文就先到这了,对于这一篇的内容,菱形虚继承部分是可以作为了解的,因为这部分的内容本身就很复杂,个人水平也有限,讲的不是很清楚。并且多继承其实是c++设计的缺陷,有了多继承就有了菱形继承,为了解决菱形继承就设计出了菱形虚继承,菱形虚继承又非常复杂,写代码的时候不推荐设计这种结构。下一篇博文将详细介绍的是c++中多态的概念,码文不易,希望大家可以点点赞收藏评论