继承的概念
继承(inheritance)机制是面向对象程序设计使代码可以复用的重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能。继承呈现了面向对象程序设计的层次结构,之前我们接触的复用都是函数复用,今天我们所讨论的继承是类设计层次的复用。
子类继承父类的成员后,父类的 成员函数 和 成员变量 都会变成子类的一部分,其中父类的成员函数与子类的成员函数使用的是同一个函数(构造函数除外),成员变量是每个类各自拥有,相当于把成员变量重新拷贝了一份。
注意:
- 友元关系不能被继承
- 基类若定义了static静态成员,则整个继承体系里面只有一个这样的成员。
继承方式
根据继承方式和访问限定符,共有九种继承方式:
类成员/继承方式 | public继承 | protected继承 | private继承 |
基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
通过上面的表格可以发现:基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private。比如,基类的public成员 与 private继承方式,因为public > private,所以在派生类的访问方式为private。
基类private成员在派生类中是不能被直接访问的;protected的意义就是基类成员不能被类外的对象直接访问,但是可以被派生类直接访问。
class Person
{
public:
void Print(){
cout << "age:" << _age << endl;
}
private:
int _age = 18; // 年龄
};
class Student : public Person
{
protected:
int _stuid; // 学号
};
int main()
{
// 虽然Student类继承了Person类,但是_age被Person设为私有
// 所以Student不能直接访问_age, 但可以通过调用父类的成员函数间接访问
Student st;
st.Print(); // 结果为:age:18
}
基类与派生类的复制兼容转换
- 派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。叫做切片或者切割。
- 基类对象不能赋值给派生类对象
ps. Student类继承Person类
继承中的作用域
- 在继承体系中基类和派生类都有独立的作用域。
- 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
- 如果是成员函数的隐藏,只需要函数名相同就构成隐藏
下面有一道练习题,大家可以试着做一下
class A{
public:
void fun()
cout << "func()" << endl;
};
class B : public A
{
public:
void fun(int i)
cout << "func(int i)->" <<i<<endl;
};
int main(){
B b;
b.fun();
return 0;
}//不定项选择
// A选项:fun构成重载
// B选项:fun构成隐藏
// C选项:编译报错
// D选项:运行报错
答案:BC 解析:A选项,函数名相同而且在同一作用域才构成重载;B选项,正确;C选项,对象b首先会去派生类里面寻找fun,如果派生类没有,才回去基类中寻找。而本题中派生类中有fun函数,而且需要传参才可以,但是b对象调用时却没有传参,所以会编译报错,故而正确;D选项,先编译后运行,编译都报错了,根本进行不到运行这一步。
派生类的默认成员函数
“默认”的意思就是指我们不写,编译器会为我们自动生成一个,那么在派生类中,这几个默认成员函数是如何生成的呢?
- 派生类的构造函数 必须调用 基类的构造函数 初始化 基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
- 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
- 派生类的operator=必须要调用基类的operator=完成基类的复制。
- 派生类的析构函数会在被 调用完成后 在自动调用 基类的析构函数 清理基类成员。因为这样才能保证派生类对象 先清理派生类成员 再 清理基类成员的顺序。
// 派生类的拷贝构造,赋值与之类似
class Person
{
public:
Person(const char* name = "")
: _name(name)
{}
Person(const Person& p)
: _name(p._name)
{}
protected:
string _name;
};
class Student : public Person
{
public:
Student() = default; // 强制生成默认构造
// s为子类对象
Student(const Student& s) // 拷贝构造
// 切割,切割出子类中父类的那一部分。调用 基类的拷贝构造 来完成基类的拷贝初始化
: Person(s)
, _num(s._num)
{}
protected:
int _num;
};
派生类的默认成员函数可以分为三个部分:父类的那一部分、自定义类型部分、内置类型部分。对于父类那一部分会调用父类对象相应的默认成员函数(复用)、自定义类型部分调用自定义类型的默认成员函数、内置类型不做处理。
菱形继承与菱形虚拟继承
单继承:一个子类只有一个直接父类时称这个继承关系为单继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
菱形继承: 两个派生类继承同一个基类,而同时又有某个类 同时继承了这两个派生类,这种继承被称为菱形继承,菱形继承是多继承的一种特殊情况。
从上图可以看出菱形继承存在数据冗余和二义性的问题。为了解决这个问题,后续又提出了菱形虚拟继承的概念,即在上例中的Student和Teacher的继承Person时使用虚拟继承,即可解决问题。
请看代码:
class Person
{
public :
string _name ; // 姓名
};
class Student : virtual public Person // 虚拟继承
{
protected :
int _num ; //学号
};
class Teacher : virtual public Person // 虚拟继承
{
protected :
int _id ; // 职工编号
}