
🦌云深麋鹿
专栏 :C++ | 用C语言学数据结构 | Java

回顾:上一篇我们结束了 模板,接下来这篇文章让我们进入到新的内容 继承 的学习,体会新的设计思路吧~
放个目录
- [一 介绍继承](#一 介绍继承)
-
- [1.1 上代码](#1.1 上代码)
- [1.2 语法格式](#1.2 语法格式)
- [1.3 继承方式&访问限定符](#1.3 继承方式&访问限定符)
-
- [1.3.1 介绍](#1.3.1 介绍)
- [1.3.2 怎么访问继承下来的不可访问成员?](#1.3.2 怎么访问继承下来的不可访问成员?)
- [1.4 继承类模板](#1.4 继承类模板)
-
- [1.4.1 写一个stack继承vector](#1.4.1 写一个stack继承vector)
- [二 基类和派生类之间的转换](#二 基类和派生类之间的转换)
-
- [2.1 指针/引用赋值](#2.1 指针/引用赋值)
-
- [2.1.1 (回顾)类型转换](#2.1.1 (回顾)类型转换)
- [2.1.2 复制兼容转换](#2.1.2 复制兼容转换)
- [2.2 派生类对象赋值给基类对象](#2.2 派生类对象赋值给基类对象)
- [2.3 基类对象不能赋值给派生类对象](#2.3 基类对象不能赋值给派生类对象)
- [三 继承中的作用域](#三 继承中的作用域)
-
- [3.1 规则](#3.1 规则)
- [3.2 选择题](#3.2 选择题)
-
- [3.2.1 俩fun构成关系](#3.2.1 俩fun构成关系)
- [3.2.2 程序运行结果](#3.2.2 程序运行结果)
- [3.3 总结](#3.3 总结)
- [四 派生类的默认成员函数](#四 派生类的默认成员函数)
-
- [4.1 常见的默认成员函数](#4.1 常见的默认成员函数)
-
- [4.1.1 构造函数](#4.1.1 构造函数)
- [4.1.2 拷贝构造](#4.1.2 拷贝构造)
- [4.1.3 赋值重载](#4.1.3 赋值重载)
- [4.1.4 析构函数](#4.1.4 析构函数)
- [4.2 实现不被继承的类](#4.2 实现不被继承的类)
-
- [4.2.1 让构造函数私有化](#4.2.1 让构造函数私有化)
- [4.2.2 给final关键字](#4.2.2 给final关键字)
- [五 继承和友元](#五 继承和友元)
- [六 继承与静态成员](#六 继承与静态成员)
-
- [6.1 定义static成员变量](#6.1 定义static成员变量)
- [6.2 测试](#6.2 测试)
- [6.3 再写个同名变量](#6.3 再写个同名变量)
- [七 多继承及其菱形继承问题](#七 多继承及其菱形继承问题)
-
- [7.1 继承模型](#7.1 继承模型)
-
- [7.1.1 写个 Assistant 类](#7.1.1 写个 Assistant 类)
- [7.1.2 测试](#7.1.2 测试)
- [7.1.3 菱形继承的问题](#7.1.3 菱形继承的问题)
- [7.2 解决方案:虚继承](#7.2 解决方案:虚继承)
- [7.3 来个选择题](#7.3 来个选择题)
- [7.4 菱形继承特殊情况](#7.4 菱形继承特殊情况)
- [八 继承和组合](#八 继承和组合)
一 介绍继承
1.1 上代码
cpp
class Person {
public:
void identity()
{
cout << "void identity()" << _name << endl;
}
private:
int _age = 18;
string _name = "xxx";
};
class Student : public Person{
public:
void study()
{
//...
}
private:
int _score = 0;
};
1.2 语法格式
cpp
class sub_name : public base_name{};
- 这里用public继承。
1.3 继承方式&访问限定符
1.3.1 介绍
- 简单来说,min(访问限定符 , 继承方式)。
- 一般来说最多用public继承。
- 不写继承方式,使用class默认是私有继承;使用struct默认是私有继承。
1.3.2 怎么访问继承下来的不可访问成员?
在父类里访问,函数设置成public。
cpp
public:
string name() {
return _name;
}
1.4 继承类模板
1.4.1 写一个stack继承vector
(1)上代码
cpp
template<class T>
class stack01 :public vector<T> {
public:
void push(const T& x)
{
vector<T>::push_back(x);
}
void pop()
{
vector<T>::pop_back();
}
const T& top()
{
return vector<T>::back();
}
bool empty()
{
return vector<T>::empty();
}
};
注意点
- 模板调用之后才实例化,调用之前编译器不知道基类里有什么成员,会报错。

- 所以调用基类成员函数需要指明类域,或者加this指针。
cpp
void push(const T& x)
{
//vector<T>::push_back(x);
this->push_back(x);
}
(2)比较之前的写法
适配器的方式就是组合。
cpp
template<class T,class Container = vector<T>>
class stack02 {
public:
void push(const T& x)
{
c.push_back(x);
}
void pop()
{
c.pop_back();
}
const T& top()
{
return c.back();
}
bool empty()
{
return c.empty();
}
private:
Container c;
};
二 基类和派生类之间的转换
2.1 指针/引用赋值
public继承的派生类对象 可以赋值给 基类的指针 / 基类的引⽤。形象地说就是切片或切割,指向派生类中切出来的基类那部分。
cpp
wyzy::Student s;
wyzy::Person* p = &s;
wyzy::Person& r = s;
调试:

为什么可以直接引用?
2.1.1 (回顾)类型转换
隐式类型转换,会产生临时对象,引用需要加const。
cpp
// false: Base& ref1 = Base();
const Base& ref2 = Base(); // true
2.1.2 复制兼容转换
这是一种特殊处理,没有产生临时对象,引用的是派生类的切片。
2.2 派生类对象赋值给基类对象
编译器会调用基类的拷贝构造函数(没写就默认生成),用切片构造一个基类对象。
cpp
wyzy::Student s;
wyzy::Person p = s;
调试:

2.3 基类对象不能赋值给派生类对象
cpp
wyzy::Person p;
wyzy::Student s = p;
报错:

三 继承中的作用域
3.1 规则
- 基类和派生类有独立的作用域。
- 基类和派生类有同名成员,派生类会隐藏基类的同名成员。
① 如果实在要访问基类的同名成员,就指定基类类域。
② 基类里访问成员变量,通过public函数继承给派生类。
3.2 选择题
cpp
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(10);
b.fun();
return 0;
}
3.2.1 俩fun构成关系
俩fun构成隐藏关系。
3.2.2 程序运行结果
编译报错,基类函数隐藏后调用不到。
怎么才能不报错?
还是指定基类类域。
cpp
b.A::fun();
3.3 总结
- 建议不要跟派生类不要跟基类搞同名成员。
- 同名函数就构成隐藏,即使参数不同。
四 派生类的默认成员函数
4.1 常见的默认成员函数
4.1.1 构造函数
(1)编译器生成的
继承自基类那部分(当作一个整体),调用基类的默认构造。
- 内置类型初始化为随机值。
- 自定义类型调用默认构造。
(2)我们自己实现
①声明的时候给缺省值
cpp
private:
int _age = 18;
string _name = "xxx";
②走初始化列表
如果我们初始化基类成员会报错(基类被当作一个整体)。

那我们怎么初始化基类成员嘞?
1.语法
cpp
:Base(member),
...
2.初始化顺序
基类成员排在前面初始化(初始化顺序按声明顺序来)。

4.1.2 拷贝构造
没有什么额外的资源开销,就不需要写这个。
(1)怎么自己实现?
先实现一个基类:
cpp
Person(const Person& p)
:_age(p._age)
, _name(p._name)
{
cout << "Person(const Person& p)" << endl;
}
函数体里打印个东西方便观察。
初始化列表里怎么调用基类的拷贝构造?
用到上面的切片。
cpp
:Base(...),
运用上述语法,实现派生类的拷贝构造:
cpp
Student(const Student& s)
:Person(s)
, _score(s._score)
{
cout << "Student(const Student& s)" << endl;
}
(2)测试
cpp
wyzy::Student s1;
wyzy::Student s2(s1);
运行输出:

监视窗口:

4.1.3 赋值重载
写不写跟4.2一样的情况。
(1)怎么自己实现?
基类实现:
cpp
Person& operator=(const Person& p)
{
cout << "Person operator=(const Person& p)" << endl;
if (this != &p)
_name = p._name;
return *this;
}
一样整个打印方便观察。
怎么调用基类的复制重载?
注意需要指明基类类域。
cpp
Base::operator=(...);
派生类实现:
cpp
Student& operator=(const Student& s)
{
cout << "Student& operator= (const Student& s)" << endl;
if (this != &s){
Person::operator =(s);
_score = s._score;
}
return *this;
}
(2)测试
cpp
wyzy::Student s1;
wyzy::Student s2(20,"lll",100);
s2 = s1;
运行输出:

调试:

4.1.4 析构函数
写不写情况也一样。
(1)怎么自己实现?
基类:
cpp
~Person(){
cout << "~Person()" << endl;
}
显式调用基类析构?
cpp
~Student() {
~Person();
cout << "~Student()" << endl;
}
编译报错,因为一些场景下要析构函数构成多态,需要 派生类 和 基类 析构名称相同。

这里底层把析构函数的名称统一处理成destructor,所以和派生类的析构函数重名了,被隐藏了,需要指定基类类域。
cpp
Person::~Person();
(2)测试
cpp
wyzy::Student s;
运行结果:

为什么析构函数调用了两次?
- 默认先析构派生类成员,再析构基类成员。
- 所以我们不需要调用基类的析构函数,基类析构完会自动调用。
cpp
~Student() {
//~Person();
cout << "~Student()" << endl;
}
4.2 实现不被继承的类
4.2.1 让构造函数私有化
把构造放到public前。
cpp
Person(int age = 18, string name = "xxx")
:_age(age)
, _name(name)
{}
public:
// ...
编译报错:

4.2.2 给final关键字
cpp
class Person final{
// ...
}
编译报错:

五 继承和友元
给基类写一个友元:
cpp
class Person {
public:
friend void Display(const Person& p);
// ...
}
void Display(const Person& p)
{
cout << p._name << endl;
}
编译通过。
- 友元关系不能被继承,基类的友元不能访问派生类的private和protected成员。
cpp
class Person {
public:
friend void Display(const Person& p, const Student& s);
// ...
}
void Display(const Person& p, const Student& s)
{
cout << p._name << endl;
cout << s._stuNum << endl;
}
编译报错:

六 继承与静态成员
6.1 定义static成员变量
基类定义一个static成员,在整个继承体系中只有这一个成员。
cpp
class Person {
// ...
private:
// ...
static int _count;
}
int Person::_count = 0;
6.2 测试
cpp
wyzy::Student s1;
wyzy::Student s2;
调试:

6.3 再写个同名变量
在派生类写个同名成员:
cpp
class Student : public Person{
// ...
private:
// ...
int _count = 0;
}
调试:
可以发现static _count被隐藏了。
七 多继承及其菱形继承问题
7.1 继承模型
单继承:派生类继承自一个基类。
多继承:派生类继承自多个基类。
菱形继承:多继承可能导致菱形继承。

7.1.1 写个 Assistant 类
依照上图,Assistant代表上图的Dderived。
cpp
class Assistant : public Student, public Teacher {
public:
Assistant(int age = 20, string name = "aaa", int score = 0, int salary = 0, string majorCourse = "x")
/*:_name("yyy")*/
:Student(age, name, score)
,Teacher(age, name, salary)
, _majorCourse(majorCourse)
{}
private:
string _majorCourse = 0;
};
7.1.2 测试
cpp
wyzy::Assistant a;
调试:

7.1.3 菱形继承的问题
菱形继承有 数据冗余 和 ⼆义性(有两份基类切片) 的问题。
在Assistant里写个函数访问_age:
cpp
void grow(int years = 1) {
_age += years;
}
编译报错:

7.2 解决方案:虚继承
引入菱形虚拟继承,会损失性能,最好不要设计出这种虚继承。
怎么实现?
加virtual关键字。
cpp
class Student : virtual public Person{
// ...
}
class Teacher : virtual public Person{
// ...
}
报错问题就不一样了:

具体实现:构造函数也有多份,析构函数也有多份,比较复杂。
7.3 来个选择题
是多继承中指针偏移的问题:
cpp
class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };
int main()
{
Derive d;
Base1* p1 = &d;
Base2* p2 = &d;
Derive* p3 = &d;
return 0;
}
需要选出正确的一个:
A:p1 == p2 == p3 B:p1 < p2 < p3
C:p1 == p3 != p2 D:p1 != p2 != p3
这里涉及多继承中的切片。
选C:

7.4 菱形继承特殊情况
怎么特殊嘞?如图:

- virtual加在最先触及多继承那一层。
这里就加在Derived1和Derived2上。
八 继承和组合
- 继承是 is-a ,组合是 has-a。
- 这两种都是一种代码复用。
组合优于继承
- 组合相比于继承,更加体现低耦合思想。
- 当然特定场景下,该用继承还是要用继承。
继承 的学习就到这里,下一篇也是学习 C++面向对象的特性[多态],不久后就会更出来啦~

