
本文属于 《C++ 进阶篇系统教程》第 1 篇 ,前面我们已经系统吃透了 C++ 基础语法、类和对象、STL 全容器、模板初阶与进阶,今天正式开启面向对象三大特性的核心篇章 ------继承。这是 C++ 进阶的第一道门槛,也是校招面试笔试的绝对高频考点!
面向对象的三大特性是封装、继承、多态 。封装解决了代码模块化和数据安全的问题,而继承解决了代码复用和层次化设计的问题 ------ 让我们可以在已有类的基础上,快速扩展出新的类,不用重复写相同的代码。
但继承的细节非常多,坑也特别多:三种继承方式的区别、同名成员隐藏、构造析构顺序、菱形继承的二义性...... 很多新手学完继承还是一头雾水,面试一问就懵。
这篇文章我们从基础语法到底层原理,结合完整代码示例和面试考点,把继承的所有核心知识点、细节和易错点一次性讲透!
一、什么是继承?为什么要用继承?
1.1 核心概念
继承是一种类与类之间的关系 ,允许我们在一个已有类(基类 / 父类 )的基础上,创建一个新的类(派生类 / 子类 )。派生类会自动拥有基类的所有非私有成员(成员变量和成员函数),并且可以在派生类中添加新的成员,或者重写基类的成员。
继承的本质是 is-a(是一个) 关系:比如 "学生是一个人""老师是一个人",那么Person就是基类,Student和Teacher就是派生类。
1.2 为什么要用继承?------ 解决代码复用问题
如果没有继承,我们要写Student和Teacher两个类,就需要重复写很多相同的代码:
cpp
// 没有继承:重复写相同的代码
class Student {
public:
string _name;
int _age;
void ShowInfo() {
cout << "姓名:" << _name << ",年龄:" << _age << endl;
}
int _student_id; // 学生特有
};
class Teacher {
public:
string _name;
int _age;
void ShowInfo() {
cout << "姓名:" << _name << ",年龄:" << _age << endl;
}
int _teacher_id; // 老师特有
};
用继承后,我们把共同的属性和方法抽出来放到基类Person中,派生类只需要写自己特有的部分:
cpp
// 用继承:代码复用,只写特有部分
class Person {
public:
string _name;
int _age;
void ShowInfo() {
cout << "姓名:" << _name << ",年龄:" << _age << endl;
}
};
// Student 继承自 Person
class Student : public Person {
public:
int _student_id; // 学生特有
};
// Teacher 继承自 Person
class Teacher : public Person {
public:
int _teacher_id; // 老师特有
};
int main() {
Student s;
s._name = "张三";
s._age = 18;
s._student_id = 2026001;
s.ShowInfo(); // 自动继承基类的方法
Teacher t;
t._name = "李四";
t._age = 35;
t._teacher_id = 1001;
t.ShowInfo(); // 自动继承基类的方法
return 0;
}
二、继承的基本语法与三种继承方式(重点 + 易错点)
2.1 基本语法
cpp
class 派生类名 : 继承方式 基类名 {
// 派生类特有成员
};
继承方式有三种:public(公有继承)、protected(保护继承)、private(私有继承)
如果不写继承方式,class 默认是 private 继承,struct 默认是 public 继承(新手最容易忘的细节)
2.2 三种继承方式的核心区别
继承方式决定了基类成员在派生类中的访问权限,以及派生类对象能否访问基类成员。
核心规则:基类的 private 成员,在任何继承方式下,派生类内部都不能直接访问 ;基类的 public 和 protected 成员,在派生类中的访问权限等于 "继承方式和原权限的最小值"。
一张表讲清楚所有情况:
| 基类成员权限 | public 继承后在派生类中的权限 | protected 继承后在派生类中的权限 | private 继承后在派生类中的权限 |
|---|---|---|---|
| public | public | protected | private |
| protected | protected | protected | private |
| private | 不可直接访问 | 不可直接访问 | 不可直接访问 |
代码示例:验证继承方式的权限
cpp
class Person {
public:
string _name; // public
protected:
int _age; // protected
private:
string _id_card; // private
};
// 1. public继承
class Student : public Person {
public:
void Func() {
_name = "张三"; // 可以访问(public→public)
_age = 18; // 可以访问(protected→protected)
// _id_card = "123456"; // 不能访问(基类private)
}
};
// 2. protected继承
class Teacher : protected Person {
public:
void Func() {
_name = "李四"; // 可以访问(public→protected)
_age = 35; // 可以访问(protected→protected)
// _id_card = "789012"; // 不能访问
}
};
// 3. private继承
class Doctor : private Person {
public:
void Func() {
_name = "王五"; // 可以访问(public→private)
_age = 40; // 可以访问(protected→private)
// _id_card = "345678"; // 不能访问
}
};
int main() {
Student s;
s._name = "张三"; // public继承,派生类对象可以访问基类public成员
// s._age = 18; //protected成员,类外不能访问
Teacher t;
// t._name = "李四"; // protected继承,基类public成员变成protected,类外不能访问
Doctor d;
// d._name = "王五"; // private继承,基类所有成员变成private,类外不能访问
return 0;
}
2.3 实际开发中的选择
1.99% 的场景都用 public 继承 :这是最符合 is-a 关系的继承方式,派生类对象可以当作基类对象使用
2.protected 和 private 继承几乎不用:它们表示 "has-a" 或 "用一个" 的关系,不如用组合(把基类对象作为派生类的成员)更清晰
三、继承中的作用域与同名成员隐藏(面试高频 + 易错点)
3.1 核心规则:派生类和基类有独立的作用域
每个类都有自己独立的作用域,派生类的作用域嵌套在基类的作用域之内 。当派生类中有和基类同名的成员时,派生类的成员会隐藏 基类的同名成员 ------ 这就是隐藏规则。
3.2 同名成员变量的隐藏
cpp
class Person {
public:
int _age = 10; // 基类的_age
};
class Student : public Person {
public:
int _age = 20; // 派生类的_age,隐藏基类的_age
};
int main() {
Student s;
cout << s._age << endl; // 输出:20(访问派生类的_age)
// 如何访问基类的_age?加基类作用域限定符
cout << s.Person::_age << endl; // 输出:10(访问基类的_age)
return 0;
}
3.3 同名成员函数的隐藏(最容易踩的坑!)
重点:只要函数名相同,不管参数列表是否相同,派生类的函数都会隐藏基类的所有同名函数 ------这不是重载!重载必须在同一个作用域内。
cpp
class Person {
public:
void Show() {
cout << "Person::Show()" << endl;
}
void Show(int age) {
cout << "Person::Show(int):" << age << endl;
}
};
class Student : public Person {
public:
void Show() {
cout << "Student::Show()" << endl;
}
};
int main() {
Student s;
s.Show(); // 调用派生类的Show()
// s.Show(18); // 编译错误,基类的Show(int)被隐藏了,无法直接调用
// 如何调用基类的Show(int)?加基类作用域限定符
s.Person::Show(18); // 调用基类的Show(int)
return 0;
}
3.4 总结
1.派生类的同名成员会隐藏基类的所有同名成员(变量和函数)
2.要访问基类的同名成员,必须加 ** 基类名:: ** 作用域限定符
3.隐藏和重载的区别:重载在同一个作用域,隐藏在不同作用域
四、派生类的默认成员函数(核心 + 面试必考)
我们之前讲过,每个类都有 6 个默认成员函数。在继承中,派生类的默认成员函数会自动处理基类部分,但有很多细节需要注意。
4.1 构造函数
派生类的构造函数必须先调用基类的构造函数,初始化基类部分,再初始化派生类自己的部分。
1.如果派生类的构造函数没有显式调用基类的构造函数,编译器会自动调用基类的默认构造函数
2.如果基类没有默认构造函数,派生类必须在初始化列表中显式调用基类的带参构造函数
cpp
class Person {
public:
// 基类有带参构造函数,没有默认构造函数
Person(string name, int age)
: _name(name)
, _age(age)
{
cout << "Person构造函数" << endl;
}
string _name;
int _age;
};
class Student : public Person {
public:
// 派生类构造函数必须在初始化列表中显式调用基类的带参构造
Student(string name, int age, int student_id)
: Person(name, age) // 调用基类构造函数初始化基类部分
, _student_id(student_id) // 初始化派生类自己的部分
{
cout << "Student构造函数" << endl;
}
int _student_id;
};
int main() {
Student s("张三", 18, 2026001);
// 输出顺序:
// Person构造函数
// Student构造函数
return 0;
}
4.2 析构函数
**析构函数的调用顺序和构造函数完全相反:**先调用派生类的析构函数,再调用基类的析构函数。
重要细节:派生类的析构函数会自动调用基类的析构函数,不需要我们手动调用。如果手动调用基类析构函数,会导致重复析构,程序崩溃。
cpp
class Person {
public:
~Person() {
cout << "Person析构函数" << endl;
}
};
class Student : public Person {
public:
~Student() {
cout << "Student析构函数" << endl;
}
};
int main() {
Student s;
// 输出顺序:
// Student析构函数
// Person析构函数
return 0;
}
4.3 拷贝构造函数
派生类的拷贝构造函数会自动调用基类的拷贝构造函数,初始化基类部分。如果我们显式写派生类的拷贝构造函数,必须在初始化列表中显式调用基类的拷贝构造函数。
cpp
class Person {
public:
Person(string name, int age) : _name(name), _age(age) {}
Person(const Person& p)
: _name(p._name)
, _age(p._age)
{
cout << "Person拷贝构造" << endl;
}
string _name;
int _age;
};
class Student : public Person {
public:
Student(string name, int age, int student_id)
: Person(name, age)
, _student_id(student_id)
{}
// 显式写派生类拷贝构造,必须调用基类拷贝构造
Student(const Student& s)
: Person(s) // 把派生类对象传给基类拷贝构造(切片,后面讲)
, _student_id(s._student_id)
{
cout << "Student拷贝构造" << endl;
}
int _student_id;
};
int main() {
Student s1("张三", 18, 2026001);
Student s2 = s1;
// 输出顺序:
// Person拷贝构造
// Student拷贝构造
return 0;
}
4.4 赋值运算符重载
和拷贝构造类似,派生类的赋值运算符重载必须显式调用基类的赋值运算符重载,赋值基类部分。
cpp
class Person {
public:
Person& operator=(const Person& p) {
if (this == &p) return *this;
_name = p._name;
_age = p._age;
cout << "Person赋值重载" << endl;
return *this;
}
string _name;
int _age;
};
class Student : public Person {
public:
Student& operator=(const Student& s) {
if (this == &s) return *this;
// 调用基类赋值重载,赋值基类部分
Person::operator=(s);
// 赋值派生类自己的部分
_student_id = s._student_id;
cout << "Student赋值重载" << endl;
return *this;
}
int _student_id;
};
4.5 总结:派生类默认成员函数的调用顺序
| 函数 | 调用顺序 | 注意事项 |
|---|---|---|
| 构造函数 | 基类构造 → 派生类构造 | 必须先调用基类构造 |
| 析构函数 | 派生类析构 → 基类析构 | 自动调用基类析构,不要手动调用 |
| 拷贝构造 | 基类拷贝构造 → 派生类拷贝构造 | 显式写时必须调用基类拷贝构造 |
| 赋值重载 | 基类赋值 → 派生类赋值 | 显式写时必须调用基类赋值 |
五、赋值兼容规则(切片 / 切割)
赋值兼容规则是 public 继承特有的规则,它允许派生类对象赋值给基类对象、基类指针、基类引用, 这个过程叫做 切片(切割)------ 只取派生类中基类的部分,切掉派生类自己的部分。
cpp
class Person {
public:
string _name;
int _age;
};
class Student : public Person {
public:
int _student_id;
};
int main() {
Student s;
s._name = "张三";
s._age = 18;
s._student_id = 2026001;
// 1. 派生类对象赋值给基类对象(切片)
Person p = s;
cout << p._name << " " << p._age << endl; // 输出:张三 18
// p._student_id = 123; // 编译错误!p是Person对象,没有_student_id
// 2. 派生类对象地址赋值给基类指针
Person* pp = &s;
cout << pp->_name << " " << pp->_age << endl; // 输出:张三 18
// 3. 派生类对象赋值给基类引用
Person& rp = s;
cout << rp._name << " " << rp._age << endl; // 输出:张三 18
// 反向不成立:基类对象不能赋值给派生类对象
// Student s2 = p; // 错误!
return 0;
}
赋值兼容规则是多态的基础,我们下一篇讲多态的时候会详细展开。
六、菱形继承与虚继承(重难点!)
6.1 什么是菱形继承?
菱形继承是多继承 的一种特殊情况:一个派生类同时继承自两个基类,而这两个基类又继承自同一个基类。
比如:Assistant(助教)既是Student(学生)又是Teacher(老师),而Student和Teacher都继承自Person,这就形成了一个菱形:

6.2 菱形继承的两个致命问题
1.数据冗余 :Assistant对象中会有两份Person的成员(一份来自 Student,一份来自 Teacher)
2.二义性 :访问Person的成员时,编译器不知道该访问哪一份
代码示例:菱形继承的问题
cpp
class Person {
public:
int _age;
};
class Student : public Person {};
class Teacher : public Person {};
class Assistant : public Student, public Teacher {};
int main() {
Assistant a;
// a._age = 18; // 编译错误!二义性,不知道访问Student::_age还是Teacher::_age
// 只能加作用域限定符访问,但数据冗余问题依然存在
a.Student::_age = 18;
a.Teacher::_age = 35;
cout << a.Student::_age << endl; // 输出:18
cout << a.Teacher::_age << endl; // 输出:35
// Assistant对象的大小是8字节(两个int),有两份_age,数据冗余
cout << sizeof(a) << endl; // 输出:8
return 0;
}
6.3 解决方案:虚继承
C++ 用虚继承 来解决菱形继承的问题。在中间层的继承方式前加virtual关键字,让最顶层的基类(Person)成为虚基类,这样派生类中只会有一份虚基类的成员。
cpp
class Person {
public:
int _age;
};
// 虚继承:加virtual关键字
class Student : virtual public Person {};
class Teacher : virtual public Person {};
class Assistant : public Student, public Teacher {};
int main() {
Assistant a;
a._age = 18; // 没有二义性,只有一份_age
cout << a._age << endl; // 输出:18
cout << a.Student::_age << endl; // 输出:18
cout << a.Teacher::_age << endl; // 输出:18
// 注意:虚继承后对象大小会变大(因为多了虚基表指针)
// 32位系统下是8字节(4字节虚基表指针 + 4字节_age)
// 64位系统下是16字节(8字节虚基表指针 + 4字节_age + 4字节对齐)
cout << sizeof(a) << endl; // 64位输出:16
return 0;
}
6.4 虚继承的底层原理(面试高频)
虚继承的底层是通过 ** 虚基表指针(vbptr)和虚基表(vbtable)** 实现的:
1.每个虚继承的派生类对象中,会多一个虚基表指针 ,指向自己的虚基表
2.虚基表中存储了虚基类成员相对于当前对象的偏移量
3.访问虚基类成员时,通过虚基表指针找到虚基表,计算出偏移量,然后访问成员
这样,无论有多少个派生类继承自虚基类,最终的派生类对象中只会有一份虚基类的成员,解决了数据冗余和二义性的问题。
6.5 虚继承的注意事项
1.虚继承会增加对象的大小(多了虚基表指针),也会降低访问效率(需要通过虚基表计算偏移量)
2.虚继承的构造函数调用顺序会变化:先调用虚基类的构造函数,再调用中间层的构造函数,最后调用最派生类的构造函数
3.实际开发中尽量避免多继承,更要避免菱形继承,C++ 的很多设计模式都可以替代多继承
七、继承的其他重要细节与易错点
7.1 友元关系不能继承
基类的友元函数 / 友元类,不能访问派生类的私有成员。友元关系是单向的、不可传递的、不可继承的。
cpp
class Person {
friend void ShowPerson(const Person& p);
private:
int _age = 10;
};
void ShowPerson(const Person& p) {
cout << p._age << endl; // 友元可以访问
}
class Student : public Person {
private:
int _student_id = 2026001;
};
int main() {
Student s;
ShowPerson(s); // 可以访问s中Person的部分
// cout << s._student_id << endl; //不能访问Student的私有成员
return 0;
}
7.2 静态成员变量被所有派生类共享
基类的静态成员变量属于整个类,被所有派生类共享,整个程序中只有一份。
cpp
class Person {
public:
static int _count;
Person() { _count++; }
};
int Person::_count = 0;
class Student : public Person {};
class Teacher : public Person {};
int main() {
Student s1, s2;
Teacher t1;
cout << Person::_count << endl; // 输出:3
cout << Student::_count << endl; // 输出:3
cout << Teacher::_count << endl; // 输出:3
return 0;
}
八、总结
1.继承的核心是代码复用 ,本质是 is-a 关系,99% 的场景用 public 继承。
2.三种继承方式 决定了基类成员在派生类中的访问权限,基类 private 成员在任何继承方式下都不能直接访问。
3.同名成员隐藏 :派生类的同名成员会隐藏基类的所有同名成员,要访问基类成员必须加作用域限定符。
4.派生类默认成员函数 :构造先基后派,析构先派后基,拷贝构造和赋值重载必须显式调用基类的对应函数。
6.赋值兼容规则 :派生类对象可以赋值给基类对象、指针、引用,这是多态的基础。
6.菱形继承 会导致数据冗余和二义性,用虚继承解决,底层通过虚基表指针和虚基表实现。
