【C++】13. 继承
- 一、继承的概念及定义
-
- [1.1 继承的概念](#1.1 继承的概念)
- [1.2 继承定义](#1.2 继承定义)
-
- [1.2.1 定义格式](#1.2.1 定义格式)
- [1.2.2 继承关系和访问限定符](#1.2.2 继承关系和访问限定符)
- [1.2.3 继承基类成员访问方式的变化](#1.2.3 继承基类成员访问方式的变化)
- 二、基类和派生类对象的赋值兼容转换
- 三、继承中的作用域
-
- [3.1 隐藏规则](#3.1 隐藏规则)
- [3.2 考察继承作用域相关选择题](#3.2 考察继承作用域相关选择题)
-
- [3.2.1 A和B类中的两个func构成什么关系(==B==)](#3.2.1 A和B类中的两个func构成什么关系(==B==))
- [3.2.2 上面程序的编译运行结果是什么(==A==)](#3.2.2 上面程序的编译运行结果是什么(==A==))
- 四、派生类的默认成员函数
-
- [4.1 四个常见默认成员函数](#4.1 四个常见默认成员函数)
-
- [4.1.1 ==派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。==](#4.1.1 ==派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。==)
- [4.1.2 ==派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。==](#4.1.2 ==派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。==)
- [4.1.3 ==派生类的 operator= 必须要调用基类的 operator= 完成基类的复制。设计子类析构时只要保证自己的资源正确释放即可。==](#4.1.3 ==派生类的 operator= 必须要调用基类的 operator= 完成基类的复制。设计子类析构时只要保证自己的资源正确释放即可。==)
- [4.1.4 ==派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。==](#4.1.4 ==派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。==)
- [4.1.5 派生类对象初始化先调用基类构造再调派生类构造。](#4.1.5 派生类对象初始化先调用基类构造再调派生类构造。)
- [4.1.6 派生类对象析构清理先调用派生类析构再调基类的析构。](#4.1.6 派生类对象析构清理先调用派生类析构再调基类的析构。)
- [4.2 实现一个不能被继承的类](#4.2 实现一个不能被继承的类)
- 五、继承与友元
- 六、继承与静态成员
- 七、多继承及其菱形继承问题
-
- [7.1 继承模型](#7.1 继承模型)
-
- [7.1.1 单继承](#7.1.1 单继承)
- [7.1.2 多继承](#7.1.2 多继承)
- [7.1.3 菱形继承](#7.1.3 菱形继承)
-
- (1)菱形继承定义
- [(2)菱形继承产生的问题 ------ 二义性+数据冗余](#(2)菱形继承产生的问题 —— 二义性+数据冗余)
- [7.2 虚继承](#7.2 虚继承)
-
- [(1)虚继承:解决 菱形继承产生二义性和数据冗余的问题](#(1)虚继承:解决 菱形继承产生二义性和数据冗余的问题)
- [(2)虚继承 解决数据冗余和二义性的原理](#(2)虚继承 解决数据冗余和二义性的原理)
- 八、继承的总结和反思
一、继承的概念及定义
1.1 继承的概念


cpp
// 基类
class Person
{
protected:
void print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "张三"; // 姓名
int _age = 18; // 年龄
};
// 派生类
class Student : public Person
{
protected:
int _stuid; // 学号
};
// 派生类
class Teacher : public Person
{
protected:
int _jobid; // 工号
};
int main()
{
Person p;
p.Print();
Student s;
s.print();
Teacher t;
t.print();
return 0;
}

继承后父类 Person 的成员(成员函数 + 成员变量)都会变成子类的一部分。
这里体现出了 Student 和 Teacher 复用了 Person 的成员。
1.2 继承定义
1.2.1 定义格式


1.2.2 继承关系和访问限定符

1.2.3 继承基类成员访问方式的变化


实例 演示三种继承关系下基类成员的各类型成员访问关系的变化:
cpp
// 基类
class Person
{
public :
void Print ()
{
cout << _name << endl;
}
protected :
string _name ; // 姓名
private :
int _age ; // 年龄
};
// 派生类
//class Student : protected Person
//class Student : private Person
class Student : public Person
{
protected :
int _stunum ; // 学号
};

二、基类和派生类对象的赋值兼容转换



cpp
// 基类
class Person
{
protected :
string _name; // 姓名
string _sex; // 性别
int _age; // 年龄
};
// 派生类
class Student : public Person
{
public :
int _id; // 学号
};
void Test ()
{
Student sobj ;
// 1.子类对象可以赋值给父类对象/指针/引用
Person pobj = sobj ;
Person* pp = &sobj;
Person& rp = sobj;
// 2.基类对象不能赋值给派生类对象
sobj = pobj;
// 3.基类的指针可以通过强制类型转换赋值给派生类的指针
pp = &sobj
Student* ps1 = (Student*)pp; // 这种情况转换是可以的
ps1->_No = 10;
pp = &pobj;
Student* ps2 = (Student*)pp; // 这种情况转换虽然可以,但是会存在越界访问的问题
ps2->_No = 10;
}
三、继承中的作用域
3.1 隐藏规则

cpp
// Student的_num和Person的_num构成隐藏关系,虽然这样的代码虽然能跑,但是非常容易混淆
// 基类
class Person
{
protected :
string _name = "李四"; // 姓名
int _num = 333444; // 身份证号
};
// 派生类
class Student : public Person
{
public:
void Print()
{
cout << "姓名:" << _name << endl;
cout << "身份证号:" << Person::_num << endl; // 类名::成员显示访问
cout << "学号:" << _num << endl; // 打印的是派生类的学号
}
protected:
int _num = 111222; // 学号
};
int main()
{
Student s1;
s1.Print();
return 0;
};

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;
//改为:
//A::fun(); //类名::成员 还是可以访问的
//cout << "func(int i)->" << i << endl;
}
};
int main()
{
B b;
b.fun(10);
return 0;
};
3.2.1 A和B类中的两个func构成什么关系(B)
A. 重载 B.隐藏 C.没关系
3.2.2 上面程序的编译运行结果是什么(A)
A. 编译报错 B.运行报错 C.正常运行


四、派生类的默认成员函数
4.1 四个常见默认成员函数


4.1.1 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。
如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。


4.1.2 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。


4.1.3 派生类的 operator= 必须要调用基类的 operator= 完成基类的复制。设计子类析构时只要保证自己的资源正确释放即可。

4.1.4 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。
(因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。(要符合先定义的先析构))

4.1.5 派生类对象初始化先调用基类构造再调派生类构造。
4.1.6 派生类对象析构清理先调用派生类析构再调基类的析构。


可以使用以下代码 验证上面的结论:
派生类的构造函数、拷贝构造、赋值重载都要手动调用基类的对应函数,只有析构函数不用(编译器自动调)
cpp
#include <iostream>
#include <string>
using namespace std;
// ========== 基类:Person(人) ==========
// 基类定义了4个默认成员函数,演示它们的行为
class Person
{
public:
// ===== 1. 构造函数(带缺省参数) =====
// 参数name有默认值"Peter",所以可以不传参
// 作用:初始化Person对象的_name成员
Person(const char* name = "Peter")
: _name(name) // 使用初始化列表,将name赋值给_name
{
cout << "Person()" << endl; // 打印表示构造函数被调用
}
// ===== 2. 拷贝构造函数 =====
// 作用:用已有的Person对象p,拷贝构造一个新的Person对象
// 参数是const引用,防止拷贝时无限递归
Person(const Person& p)
: _name(p._name) // 从p拷贝_name成员
{
cout << "Person(const Person& p)" << endl; // 打印表示拷贝构造被调用
}
// ===== 3. 赋值重载函数 =====
// 作用:将p的值赋给当前对象(对象已存在)
// 返回自身引用,支持链式赋值(a = b = c)
Person& operator=(const Person& p)
{
cout << "Person operator=(const Person& p)" << endl; // 打印表示赋值被调用
// 防止自己给自己赋值(例如:p1 = p1)
if (this != &p) // this是当前对象的地址,&p是参数对象的地址
_name = p._name; // 将p的_name赋值给当前对象的_name
return *this; // 返回当前对象(支持链式赋值)
}
// ===== 4. 析构函数 =====
// 作用:Person对象销毁时自动调用,释放资源
// 这里没有动态分配的资源,只打印表示被调用
~Person()
{
cout << "~Person()" << endl; // 打印表示析构函数被调用
}
protected:
string _name; // 姓名(protected:派生类可以访问)
};
// ========== 派生类:Student(学生) ==========
// Student继承自Person,拥有Person的所有成员
// 演示派生类的4个默认成员函数如何正确实现
class Student : public Person
{
public:
// ===== 1. 派生类的构造函数 =====
// 作用:初始化Student对象(包括基类部分和派生类自己的部分)
// 参数:name传给基类构造,num初始化自己的_num
Student(const char* name, int num)
: Person(name) // ✅ 关键:显式调用基类构造函数,初始化继承来的_name
, _num(num) // 初始化派生类自己的成员_num
{
cout << "Student()" << endl; // 打印表示派生类构造函数被调用
// 注意:先调用基类构造,再执行派生类构造体
}
// ===== 2. 派生类的拷贝构造函数 =====
// 作用:用已有的Student对象s,拷贝构造一个新的Student对象
// 参数:const引用,接收被拷贝的对象
Student(const Student& s)
: Person(s) // ✅ 关键:显式调用基类拷贝构造
// 这里发生"切片":把s当作Person用,只拷贝基类部分(_name)
, _num(s._num) // 拷贝派生类自己的成员_num
{
cout << "Student(const Student& s)" << endl; // 打印表示派生类拷贝构造被调用
// 注意:先调用基类拷贝构造,再执行派生类拷贝构造体
}
// ===== 3. 派生类的赋值重载函数 =====
// 作用:将s的值赋给当前Student对象
// 返回自身引用,支持链式赋值
Student& operator=(const Student& s)
{
cout << "Student& operator= (const Student& s)" << endl; // 打印表示派生类赋值被调用
// 防止自己给自己赋值
if (this != &s)
{
// ✅ 关键:显式调用基类的赋值重载函数
// 这里发生"切片":把s当作Person用,只处理基类部分(_name)
Person::operator=(s);
// 赋值派生类自己的成员_num
_num = s._num;
}
return *this; // 返回当前对象
}
// ===== 4. 派生类的析构函数 =====
// 作用:Student对象销毁时自动调用
~Student()
{
cout << "~Student()" << endl; // 打印表示派生类析构被调用
// ✅ 关键:不需要手动调用基类析构!
// 执行完派生类析构体后,编译器会自动调用基类的析构函数
}
protected:
int _num; // 学号(派生类自己的成员)
};
// ========== 主函数:测试所有默认成员函数 ==========
int main()
{
// ===== 测试1:构造函数 =====
// 创建s1,调用构造函数
// 执行顺序:Person() → Student()
Student s1("Jack", 18);
// ===== 测试2:拷贝构造函数 =====
// 用s1拷贝构造s2
// 执行顺序:Person(const Person&) → Student(const Student&)
Student s2(s1);
// ===== 测试3:构造函数 =====
// 创建s3
// 执行顺序:Person() → Student()
Student s3("Rose", 17);
// ===== 测试4:赋值重载函数 =====
// 将s3赋值给s1
// 执行顺序:Student::operator= → Person::operator=
s1 = s3;
// ===== main结束,所有对象析构 =====
// 析构顺序:后创建的先析构(s3 → s2 → s1)
// 每个对象的析构顺序:~Student() → ~Person()
return 0;
}
4.2 实现一个不能被继承的类
方法1:基类的构造函数私有,派生类的构成必须调用基类的构造函数,但是基类的构成函数私有化以后,派生类看不见就不能调用了,那么派生类就无法实例化出对象。
方法2:C++11新增了⼀个final关键字,final修改基类,派生类就不能继承了。
cpp
// C++11
的⽅法
class Base final
{
public:
void func5()
{
cout << "Base::func5" << endl;
}
protected:
int a = 1;
private:
// C++98的⽅法
/*Base()
{}*/
};
class Derive :public Base
{
void func4() { cout << "Derive::func4" << endl;
}
protected:
int b = 2;
};
int main()
{
Base b;
Derive d;
return 0;
}
五、继承与友元

cpp
// 前置声明:告诉编译器Student是一个类,后面会定义
class Student;
// ========== 基类:Person ==========
class Person
{
public:
// 声明Display函数是Person的友元
// 友元函数可以访问Person类的私有/保护成员
friend void Display(const Person& p, const Student& s);
protected:
string _name; // 姓名(保护成员)
};
// ========== 派生类:Student ==========
class Student : public Person
{
protected:
int _stuNum; // 学号(保护成员)
// 注意:Student没有声明Display为友元
};
// ========== 全局函数:Display ==========
void Display(const Person& p, const Student& s)
{
// ✅ 可以访问:Display是Person的友元
cout << p._name << endl;
// ❌ 报错:Display不是Student的友元
// 友元关系不继承!即使Display是Person的友元
// 也不能访问Student的protected/private成员
cout << s._stuNum << endl; // 编译错误:不可访问
}
int main()
{
Person p;
Student s;
Display(p, s); // 调用Display函数
return 0;
}
六、继承与静态成员

cpp
//无论继承多少层,大家用的始终是同一个变量
// 基类
class Person
{
public :
Person () {++ _count ;}
protected :
string _name ; // 姓名
public :
static int _count; // 统计人的个数
};
int Person :: _count = 0;
class Student : public Person
{
protected :
int _stuNum ; // 学号
};
class Graduate : public Student
{
protected :
string _seminarCourse ; // 研究科目
};
void TestPerson()
{
Student s1 ;
Student s2 ;
Student s3 ;
Graduate s4 ;
cout << " 人数 :" << Person ::_count << endl;
Student ::_count = 0;
cout << " 人数 :" << Person ::_count << endl;
}
七、多继承及其菱形继承问题
7.1 继承模型
7.1.1 单继承


7.1.2 多继承


7.1.3 菱形继承
(1)菱形继承定义


(2)菱形继承产生的问题 ------ 二义性+数据冗余


cpp
class Person
{
public:
string _name; // 姓名
};
// Student继承Person
class Student : public Person
{
protected:
int _num; // 学号
};
// Teacher继承Person
class Teacher : public Person
{
protected:
int _id; // 职工编号
};
// Assistant同时继承Student和Teacher(菱形继承)
// 助教:既是学生又是老师
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse; // 主修课程
};
int main()
{
Assistant a;
// ❌ 二义性:_name不知道是从Student继承来的,还是从Teacher继承来的
// 因为Student和Teacher都有一份从Person继承来的_name
a._name = "Peter"; // 编译报错:ambiguous(二义性)
// ✅ 解决二义性:指定从哪个路径访问
// 但数据冗余问题依然存在(内存中有两份_name)
a.Student::_name = "xxx"; // 访问从Student继承来的_name
a.Teacher::_name = "yyy"; // 访问从Teacher继承来的_name
// 此时内存中有两份_name:
// a.Student::_name = "xxx"
// a.Teacher::_name = "yyy"
// 浪费内存,且容易混淆
return 0;
}
7.2 虚继承
(1)虚继承:解决 菱形继承产生二义性和数据冗余的问题

cpp
class Person
{
public:
string _name; // 姓名
};
// Student虚继承Person,解决菱形继承的数据冗余问题
class Student : virtual public Person
{
protected:
int _num; // 学号
};
// Teacher虚继承Person,解决菱形继承的数据冗余问题
class Teacher : virtual public Person
{
protected:
int _id; // 职工编号
};
// Assistant同时继承Student和Teacher
// 由于Student和Teacher都是虚继承Person,所以Person部分只有一份
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse; // 主修课程
};
int main()
{
Assistant a;
// ✅ 可以直接访问,因为_name只有一份(虚继承解决了数据冗余)
a._name = "Peter";
// 通过不同路径访问,实际上是同一个变量
// 因为虚继承让Person部分只有一份
a.Student::_name = "xxx"; // 通过Student路径访问
a.Teacher::_name = "yyy"; // 通过Teacher路径访问
// 最终a._name的值是"yyy"
// 因为Student::_name和Teacher::_name指向同一块内存
// 后赋值的"yyy"覆盖了之前的"xxx"
return 0;
}
(2)虚继承 解决数据冗余和二义性的原理
为了研究虚拟继承原理,我们给出了一个简化的菱形继承继承体系,再借助内存窗口观察对象成 员的模型。
cpp
class A
{
public:
int _a;
};
// class B : public A
class B : virtual public A
{
public:
int _b;
};
// class C : public A
class C : virtual 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;
}
下图是菱形继承的内存对象成员模型:这里可以看到数据冗余。





下面是上面的 Person 关系菱形虚拟继承的原理解释:

八、继承的总结和反思


cpp
// Car和BMW Car和Benz构成is-a的关系
class Car{
protected:
string _colour = "白色"; // 颜色
string _num = "粤DK0896"; // 车牌号
};
class BMW : public Car{
public:
void Drive()
{
cout << "好开-操控" << endl;
}
};
class Benz : public Car{
public:
void Drive()
{
cout << "好坐-舒适" << endl;
}
};
// Tire和Car构成has-a的关系
class Tire{
protected:
string _brand = "Michelin"; // 品牌
size_t _size = 17; // 尺寸
};
class Car{
protected:
string _colour = "白色"; // 颜色
string _num = "粤DK0896"; // 车牌号
Tire _t; // 轮胎
};