第十三讲:C++ 继承
继承是面向对象程序设计的核心特性之一,核心目标是实现类级别的代码复用,建立类的层次结构,描述 “is-a(是一种)” 的类间关系。下面从基础语法到进阶坑点,系统梳理所有核心知识点。
一、继承的基本概念
1. 为什么需要继承
没有继承时,相似类会存在大量代码冗余。例如Student和Teacher都有姓名、年龄、地址等属性,都有身份认证方法,但各自独立实现,重复代码多。
继承的核心思路:提取公共成员到基类(父类),派生类(子类)继承基类后,只需编写自己特有的成员。
2. 基本语法与术语
// 基类(父类):存放所有子类的公共成员
class Person {
public:
void identity() { cout << "身份认证:" << _name << endl; }
protected:
string _name; // 姓名
int _age; // 年龄
};
// 派生类(子类):public方式继承Person
class Student : public Person {
public:
void study() { cout << _name << "在学习" << endl; }
private:
int _stuId; // 学号:Student类特有属性
};
class Teacher : public Person {
public:
void teaching() { cout << _name << "在授课" << endl; }
private:
string _title; // 职称:Teacher类特有属性
};
-
基类 / 父类:被继承的类(如Person),存放通用成员
-
派生类 / 子类:继承基类的类(如Student、Teacher),只扩展特有成员
二、继承方式与成员访问权限
1. 三种继承方式
继承方式决定了基类成员在派生类中的最终访问权限,共有public、protected、private三种。
| public | public | protected | private |
| protected | protected | protected | private |
| private | 不可直接访问 | 不可直接访问 | 不可直接访问 |
2. 核心规则(必记)
private 成员永远不可直接访问:基类的私有成员会被继承到派生类对象的内存中,但语法上派生类(类内、类外)都无法直接访问。
权限取更小值:派生类中成员的访问权限 = min(基类成员权限, 继承方式),权限等级:public > protected > private。
默认继承方式:class默认private继承,struct默认public继承。工程开发必须显式写出继承方式。
protected 的意义:专门为继承设计的访问限定符。类外无法访问,但派生类内可以访问,平衡了封装性和继承复用。
工程惯例:几乎只使用public继承。protected/private继承会降低成员访问权限,扩展性差、耦合度高。
三、基类与派生类的对象转换(赋值兼容规则)
public继承下,派生类是一种特殊的基类,支持以下类型转换规则:
1. 支持的安全转换
Student s;
// 1. 派生类对象 → 基类指针(安全,无拷贝)
Person* p = &s; // 指针指向s对象中基类Person的部分
// 2. 派生类对象 → 基类引用(安全,无拷贝)
Person& r = s; // 引用绑定s对象中基类Person的部分
// 3. 派生类对象 → 基类对象(发生切片)
Person p2 = s; // 调用基类拷贝构造,只拷贝基类部分,丢弃派生类部分
-
切片(切割):派生类对象赋值给基类对象时,会 “切掉” 派生类特有的成员,只保留基类部分。
2. 不支持的转换
Person p;
Student s = p; // 编译报错!基类对象不能直接赋值给派生类对象
3. 强制转换
基类指针 / 引用如果实际指向派生类对象,可以强制转换为派生类指针 / 引用,但直接强转不安全。多态场景下推荐使用dynamic_cast进行安全的向下转型。
四、继承中的作用域与同名隐藏
1. 核心规则
-
基类和派生类拥有独立的作用域。
-
同名隐藏:派生类中如果定义了和基类同名的成员,派生类成员会屏蔽基类的同名成员,直接访问默认访问派生类的。
-
访问基类同名成员:必须显式指定类域 基类名::成员名。
2. 函数隐藏的特点
函数隐藏只看函数名相同,和参数列表、返回值无关。
⚠️ 注意和函数重载的区别:重载发生在同一个作用域,隐藏发生在不同作用域(基类和派生类)。
3. 经典例题解析
class A {
public:
void fun() { cout << "A::fun()" << endl; }
};
class B : public A {
public:
void fun(int i) { cout << "B::fun(int): " << i << endl; }
};
int main() {
B b;
b.fun(10); // 调用B::fun(int),正常
b.fun(); // 编译报错!B的fun隐藏了A的fun,找不到无参版本
b.A::fun();// 显式指定类域,调用A::fun(),正常
return 0;
}
结论:A::fun()和B::fun(int)是隐藏关系,不是重载。
4. 工程建议
继承体系中尽量不要定义同名成员,避免混淆和误用。
五、派生类的默认成员函数
类有 6 个默认成员函数,继承体系中,派生类的默认函数会自动处理基类部分,规则如下:
1. 构造函数
-
派生类构造函数必须先调用基类构造函数,初始化基类的成员。
-
如果基类有默认构造函数,派生类可以不显式调用,编译器自动调用。
-
如果基类没有默认构造函数,派生类必须在初始化列表中显式调用基类构造。
2. 拷贝构造函数
-
派生类拷贝构造必须调用基类的拷贝构造函数,完成基类部分的拷贝初始化。
3. 赋值运算符重载
-
派生类的operator=会隐藏基类的operator=。
-
必须显式调用基类的赋值运算符:基类::operator=(派生类对象)。
4. 析构函数
-
派生类析构函数执行完后,编译器自动调用基类析构函数。
-
调用顺序:构造函数「先基类,后派生类」;析构函数「先派生类,后基类」。
-
析构函数也构成隐藏:编译器会把所有析构函数名统一处理为destructor(),因此基类和派生类析构函数是隐藏关系。
5. 完整代码示例
class Person {
public:
// 构造函数
Person(const char* name = "peter") : _name(name) {
cout << "Person()" << endl;
}
// 拷贝构造函数
Person(const Person& p) : _name(p._name) {
cout << "Person(const Person&)" << endl;
}
// 赋值运算符重载
Person& operator=(const Person& p) {
cout << "Person& operator=(const Person&)" << 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&)" << endl;
}
// 赋值运算符:显式调用基类赋值
Student& operator=(const Student& s) {
cout << "Student& operator=(const Student&)" << endl;
if (this != &s) {
Person::operator=(s); // 调用基类赋值
_num = s._num;
}
return *this;
}
// 析构函数
~Student() { cout << "~Student()" << endl; }
protected:
int _num;
};
六、特殊主题
1. 不能被继承的类
方法 1:C++98 构造函数私有化
派生类必须调用基类构造函数,基类构造私有则派生类无法调用,无法实例化对象。
class NonInherit {
private:
NonInherit() {} // 构造函数私有
};
方法 2:C++11 final 关键字
用final修饰类,直接禁止该类被继承。
class Base final { // final修饰,不能被继承
public:
void func() {}
};
2. 友元与继承
友元关系不能继承。基类的友元函数只能访问基类的成员,不能访问派生类的私有 / 保护成员。
class Student;
class Person {
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; // 合法:Person的友元可以访问Person的成员
cout << s._stuNum << endl; // 编译报错!友元关系不继承,不能访问Student的保护成员
}
3. 静态成员与继承
基类定义的静态成员,整个继承体系只有一份,所有基类、派生类对象共享同一个静态成员。
class Person {
public:
static int _count; // 静态成员
};
int Person::_count = 0;
class Student : public Person {};
int main() {
Person p;
Student s;
// 地址相同,说明是同一个静态成员
cout << &p._count << endl;
cout << &s._count << endl;
// 都可以通过类名访问
cout << Person::_count << endl;
cout << Student::_count << endl;
return 0;
}
七、多继承与菱形继承
1. 基本概念
-
单继承:一个派生类只有一个直接基类。
-
多继承:一个派生类有两个或以上直接基类。
class A {};
class B {};
class C : public A, public B {}; // 多继承
2. 菱形继承(多继承的经典问题)
定义
两个派生类继承同一个基类,最派生类同时继承这两个派生类,形成菱形结构。
Person(公共基类)
/ \\
Student Teacher(中间类)
\\ /
Assistant(最派生类)
核心问题
数据冗余:Assistant对象中会有两份Person的成员(分别来自 Student 路径和 Teacher 路径)。
二义性:访问Person的成员时,编译器不知道走哪条继承路径,报错 “访问不明确”。
临时解决二义性
显式指定访问哪个基类路径的成员,但数据冗余问题仍然存在。
Assistant a;
a.Student::_name = "张三"; // 显式指定Student路径的_name
a.Teacher::_name = "李四"; // 显式指定Teacher路径的_name
3. 虚继承(解决菱形继承问题)
语法
在继承方式前加virtual关键字,让中间类虚继承公共基类。
class Person { public: string _name; };
// 虚继承Person
class Student : virtual public Person {
protected: int _num;
};
// 虚继承Person
class Teacher : virtual public Person {
protected: int _id;
};
class Assistant : public Student, public Teacher {
protected: string _majorCourse;
};
效果
-
最派生类中只有一份公共基类(Person)的成员,彻底解决数据冗余。
-
访问公共基类成员不再有二义性。
注意事项
虚继承底层通过虚基表指针实现,有额外的内存和性能开销。
虚基类的构造由最派生类负责调用,保证只构造一次。
工程建议:尽量避免设计菱形继承结构,多继承本身会大幅增加代码复杂度。
八、继承 vs 组合
1. 核心区别
| 继承 | is-a(是一种) | 白箱复用:基类内部对派生类可见 | 破坏基类封装 | 高 |
| 组合 | has-a(有一个) | 黑箱复用:对象内部不可见 | 保持封装 | 低 |
2. 设计原则
优先使用对象组合,而非类继承。
-
组合耦合度低、维护性好,更灵活。
-
只有当类之间是明确的is-a关系,且需要实现多态时,才使用继承。
3. 示例
-
Car和Tire(轮胎):是 has-a 关系,用组合。
-
BMW和Car:是 is-a 关系,用继承。
-
stack和vector:既可以继承也可以组合,推荐用组合。
核心知识点总结
继承核心是代码复用,描述 is-a 关系;class 默认私有继承,struct 默认公有继承。
基类 private 成员不可直接访问;成员最终权限取基类权限和继承方式的更小值。
public 继承下,派生类对象可赋值给基类指针 / 引用 / 对象(发生切片),反向不可行。
同名成员构成隐藏(不是重载);访问基类同名成员必须显式加类域。
构造顺序:先基类后派生类;析构顺序:先派生类后基类;拷贝构造和赋值要显式调用基类版本。
菱形继承存在数据冗余和二义性,可用虚继承解决,但工程上尽量避免菱形结构。
面向对象设计原则:优先使用组合,其次使用继承。



