【C++进阶】C++中的继承
【C++进阶】C++中的继承
1. 继承的概念及定义
1.1 什么是继承?
继承是面向对象程序设计中最核心的机制之一,它允许我们在保持原有类特性的基础上进行扩展,增加新的功能,从而产生新的类(派生类)。
与之前学习的函数复用不同,继承是类设计层次的复用,体现了由简单到复杂的认知过程。
1.2 继承的基本语法
class Person {
public:void Print() {cout << "name:" << _name << endl;cout << "age:" << _age << endl;}
protected:string _name = "peter";int _age = 18;
};// Student 继承自 Person
class Student : public Person {
protected:int _stuid; // 学号
};class Teacher : public Person {
protected:int _jobid; // 工号
};
2. 继承方式与访问权限
2.1 三种继承方式
| 类成员/继承方式 | public继承 | protected继承 | private继承 |
|---|---|---|---|
| 基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
| 基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
| 基类的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
2.2 重要规则
- 基类private成员在派生类中无论以什么方式继承都是不可见的
- 基类成员在子类的访问方式 = Min(成员在基类的访问限定符,继承方式)
- class默认private继承,struct默认public继承(建议显式写出)
- 实际开发中主要使用public继承
3. 基类和派生类对象赋值转换
3.1 切片(切割)概念
class Person {
protected:string _name;string _sex;int _age;
};class Student : public Person {
public:int _No; // 学号
};void Test() {Student sobj;// 1. 派生类对象可以赋值给基类对象/指针/引用Person pobj = sobj; // 切片Person* pp = &sobj; // 切片Person& rp = sobj; // 切片// 2. 基类对象不能赋值给派生类对象// sobj = pobj; // 错误!// 3. 基类指针可以通过强制类型转换赋值给派生类指针pp = &sobj;Student* ps1 = (Student*)pp; // 安全转换
}
核心要点:
- 派生类→基类:天然支持(切片)
- 基类→派生类:需要强制转换,且可能不安全
4. 继承中的作用域
4.1 隐藏(重定义)规则
class Person {
protected:string _name = "小李子";int _num = 111; // 身份证号
};class Student : public Person {
public:void Print() {cout << "姓名:" << _name << endl;cout << "身份证号:" << Person::_num << endl; // 显式访问cout << "学号:" << _num << endl; // 访问自己的_num}
protected:int _num = 999; // 学号 - 与父类_num构成隐藏
};
重要:
- 子类和父类同名成员会构成隐藏
- 函数只需要函数名相同就构成隐藏(不要求参数列表)
- 建议:在继承体系中不要定义同名成员
5. 派生类的默认成员函数
5.1 六大默认成员函数的继承特性
class Person {
public:Person(const char* name = "peter") : _name(name) {cout << "Person()" << endl;}Person(const Person& p) : _name(p._name) {cout << "Person(const Person& p)" << endl;}Person& operator=(const Person& p) {cout << "Person operator=(const Person& p)" << endl;if (this != &p)_name = p._name;return *this;}~Person() {cout << "~Person()" << endl;}protected:string _name;
};class Student : public Person {
public:Student(const char* name, int num) : Person(name) // 必须调用基类构造函数, _num(num) {cout << "Student()" << endl;}Student(const Student& s) : Person(s) // 调用基类拷贝构造, _num(s._num) {cout << "Student(const Student& s)" << endl;}Student& operator=(const Student& s) {cout << "Student& operator=(const Student& s)" << endl;if (this != &s) {Person::operator=(s); // 调用基类operator=_num = s._num;}return *this;}~Student() {cout << "~Student()" << endl;// 析构函数完成后会自动调用基类析构函数}protected:int _num;
};
5.2 构造和析构顺序
- 构造顺序:基类→派生类
- 析构顺序:派生类→基类
6. 继承与友元、静态成员
6.1 友元关系不能继承
class Student;
class Person {
public:friend void Display(const Person& p, const Student& s);
protected:string _name;
};class Student : public Person {
protected:int _stuNum;
};void Display(const Person& p, const Student& s) {cout << p._name << endl; // OK:友元访问// cout << s._stuNum << endl; // 错误:友元不能继承
}
6.2 静态成员共享
class Person {
public:Person() { ++_count; }
protected:string _name;
public:static int _count; // 整个继承体系共享
};int Person::_count = 0;class Student : public Person {
protected:int _stuNum;
};
7. 复杂的菱形继承及虚拟继承
7.1 菱形继承的问题
class Person {
public:string _name;
};class Student : public Person {
protected:int _num;
};class Teacher : public Person {
protected:int _id;
};class Assistant : public Student, public Teacher {
protected:string _majorCourse;
};void Test() {Assistant a;// a._name = "peter"; // 错误:二义性a.Student::_name = "xxx"; // 需要显式指定a.Teacher::_name = "yyy"; // 数据冗余!
}
问题:
- 二义性:无法确定访问哪个_name
- 数据冗余:Person成员在Assistant中有两份
7.2 虚拟继承解决方案
class Person {
public:string _name;
};class Student : virtual public Person { // 虚拟继承
protected:int _num;
};class Teacher : virtual public Person { // 虚拟继承
protected:int _id;
};class Assistant : public Student, public Teacher {
protected:string _majorCourse;
};void Test() {Assistant a;a._name = "peter"; // 现在没有二义性,且只有一份_name
}
7.3 虚拟继承原理
虚拟继承通过虚基表指针和虚基表来实现:
- 虚基表中存储偏移量
- 通过偏移量找到共享的基类成员
- 解决了数据冗余和二义性问题
8. 继承的总结与反思
8.1 继承 vs 组合
| 特性 | 继承 | 组合 |
|---|---|---|
| 关系 | is-a | has-a |
| 耦合度 | 高 | 低 |
| 复用类型 | 白箱复用 | 黑箱复用 |
| 灵活性 | 较低 | 较高 |
8.2 使用建议
- 优先使用组合,而不是继承
- 如果关系确实是"is-a",使用public继承
- 避免多继承,特别是菱形继承
- 要实现多态,必须使用继承
8.3 代码示例
// 继承:is-a关系
class Car {
protected:string _colour = "白色";string _num = "陕ABIT00";
};class BMW : public Car {
public:void Drive() { cout << "好开-操控" << endl; }
};// 组合:has-a关系
class Tire {
protected:string _brand = "Michelin";size_t _size = 17;
};class Car {
protected:string _colour = "白色";string _num = "陕ABIT00";Tire _t; // 组合
};
9. 常见面试题
9.1 基础概念题
-
什么是菱形继承?菱形继承的问题是什么?
- 菱形继承是多继承的特殊情况,形成钻石形状的继承结构
- 问题:数据冗余和二义性
-
什么是菱形虚拟继承?如何解决数据冗余和二义性?
- 在菱形继承的中间类使用virtual继承
- 通过虚基表指针和虚基表实现共享基类成员
-
继承和组合的区别?什么时候用继承?什么时候用组合?
- 继承:is-a关系,代码复用,多态实现
- 组合:has-a关系,降低耦合度,提高灵活性
- 优先使用组合,确需is-a关系或实现多态时使用继承
