C++中的继承其实是一个C++中的坑,主要体现在其多继承(菱形继承)方面,我们先来了解下继承的概念
一,继承的概念与定义
1.1继承的概念
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许我们在保持原有类特性的基础上进行扩展,增加方法(成员函数)和属性(成员变量),这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的函数层次的复用,继承是类设计层次的复用。
比如说我们来看下面两个例子:
class Student
{
public:
// 进入校园/图书馆/实验室刷二维码等身份认证
void identity()
{
// ...
}
// 学习
void study()
{
// ...
}
protected:
string _name = "peter"; // 姓名
string _address; // 地址
string _tel; // 电话
int _age = 18; // 年龄
int _stuid; // 学号
};
class Teacher
{
public:
// 进入校园/图书馆/实验室刷二维码等身份认证
void identity()
{
// ...
}
// 授课
void teaching()
{
//...
}
protected:
string _name = "张三"; // 姓名
int _age = 18; // 年龄
string _address; // 地址
string _tel; // 电话
string _title; // 职称
};
在这两个例子中,老师和学生的两个类间有许多成员是重复的-这样如果数据特别多的时候,就会造成数据的冗余,而继承便可以很好的帮助我们解决这个问题:
class Person
{
public:
// 进入校园/图书馆/实验室刷二维码等身份认证
void identity()
{
cout << "void identity()" << _name << endl;
}
protected:
string _name = "张三"; // 姓名
string _address; // 地址
string _tel; // 电话
int _age = 18; // 年龄
};
class Student : public Person
{
public:
// 学习
void study()
{
// ...
}
protected:
int _stuid; // 学号
};
class Teacher : public Person
{
public:
// 授课
void teaching()
{
//...
}
protected:
string title; // 职称
};
1.2继承的定义
1.2.1定义格式
继承方式分为公有继承(public),保护继承(protected),私有继承(private).
1.2.2继承成员访问方式的变化
基类的所有private对于派生类全部不可见,而对于继承下来的其他基类成员,它们则属于派生类中的min(继承方式,基类成员类型) 的成员(public > protected > private),我们一般使用的较多的是public继承,日常使用protected,private继承会降低代码的可维护性
TIPS(补充):设置非继承类与继承方式实现stack
如果我们想要设置一个无法被继承的类,一种方法是直接将基类的所有成员都设置为私有,还有一种是在基类之后加上final,表示这是一个最终类,无法被继承:
class Person final
{
public:
// 进入校园/图书馆/实验室刷二维码等身份认证
void identity()
{
cout << "void identity()" << _name << endl;
}
protected:
string _name = "张三"; // 姓名
string _address; // 地址
string _tel; // 电话
int _age = 18; // 年龄
};
当然,由于我们stl中的常用容器比如vector,list等都是类,所以我们也可以通过继承模板类的方式去模拟实现一个stack函数:
// stack和vector的关系,既符合is-a,也符合has-a
template<class T>
class stack : public std::vector<T>
{
public:
void push(const T& x)
{
// 基类是类模板时,需要指定一下类域,
// 否则编译报错:error C3861: “push_back”: 找不到标识符
// 因为stack<int>实例化时,也实例化vector<int>了
// 但是模版是按需实例化,push_back等成员函数未实例化,所以找不到
vector<T>::push_back(x);
//push_back(x);
}
void pop()
{
vector<T>::pop_back();
}
const T& top()
{
return vector<T>::back();
}
bool empty()
{
return vector<T>::empty();
}
};
二,基类(父类)与派生类(子类)之间的转换
我们之前的时候介绍过,对于需要强制类型转换的数据,会产生一个中间变量(常量)去进行转换,如果我们不是常值引用,编译器就会报错,然而,对于基类与派生类之间的转换,不会产生中间变量,派生类指针或引用无法指向基类对象,而基类指针或引用则可以指向派生类对象,这是通过裁剪实现的:
此时指针指向的是student的头部,不过会将派生类中的_No成员裁剪,其实这种转换关系也很好理解-因为派生类会包含基类的所有成员,故可以通过裁剪方式进行转换,然而父类中却不一定包含所有的子类成员,因此无法转换
三,继承中的作用域与默认成员函数
3.1隐藏规则
1. 在继承体系中基类和派生类都有独立的作用域。
2. 派生类和基类中有同名成员,派生类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。
(在派生类成员函数中,可以使用 基类::基类成员 显示访问)
3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
4. 注意在实际中在继承体系里面最好不要定义同名的成员。
3.2四个常见默认成员函数
1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
比特就业课
3. 派生类的operator=必须要调用基类的operator=完成基类的复制。需要注意的是派生类的
operator=隐藏了基类的operator=,所以显示调用基类的operator=,需要指定基类作用域
4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
5. 派生类对象初始化先调用基类构造再调派生类构造。
6. 派生类对象析构清理先调用派生类析构再调基类的析构。
7. 因为多态中一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个属于多态部分)。那么编译器会对析构函数名进行特殊处理,处理成destructor(),所以基类析构函数不加
virtual(菱形继承部分我们会解释)的情况下,派生类析构函数和基类析构函数构成隐藏关系。
所以如果默认的父类构造函数已经够用时,我们便不需要在写基类的构造函数时去调用父类构造函数,如果不够用,则需要以以下方式进行初始化:
class Student : public Person
{
public:
Student(int stuid)
:_stuid(stuid)
, Person()//根据父类的构造函数去输入值
{}
protected:
int _stuid;
};
TIPS(补充):继承与友元
父类的友元无法被子类继承,就好比父亲的朋友不一定是儿子的朋友一样一个道理.
TIPS(补充):继承与静态成员
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个派生类,都只有一个static成员实例。 也就是说子类中的静态成员与父类中的静态成员是一个成员
四,多继承与菱形继承
4.1继承模型
1.单继承:一个派生类只有一个直接基类时称这个继承关系为单继承
2.多继承:一个派生类有两个或以上直接基类时称这个继承关系为多继承,多继承对象在内存中的模型是,先继承的基类在前面,后面继承的基类在后面,派生类成员在放到最后面。
3.菱形继承:菱形继承是多继承的一种特殊情况。菱形继承的问题,从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题,在Assistant的对象中Person成员会有两份。支持多继承就一定会有菱形继承,像Java就直接不支持多继承,规避掉了这里的问题,所以实践中我们也是不建议设计出菱形继承这样的模型的。
菱形继承会导致数据的冗余,即一个派生类Assistant中包含两份Person的数据,虽然我们不推荐去使用菱形继承,但总有无法避免的情况使用菱形继承更好,比如IO库中的菱形继承:
(图片来源于cplusplus.com)
这时候我们就需要使用虚继承来确保Assistant中只含一份Person的数据
4.2虚继承
C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有
菱形虚拟继承,底层实现就很复杂,性能也会有一些损失,所以最好不要设计出菱形继承。多继承可以认为是C++的缺陷之一,后面很多的语言也就不再支持多继承比如java,虚继承的格式如下:
// 使用虚继承Person类
class Student : virtual public Person
{
protected:
int _num; //学号
};
这时候我们去对4.1中第三幅图中的teacher与student都使用虚继承,就可以使Assistant中只有一份Person的数据,但虚继承的底层特别复杂,会带来一定的性能损耗,所以一般情况下我们尽量不要写出菱形继承这样的代码