继承是面向对象编程(OOP)的三大核心特性之一(封装、继承、多态),它通过 “复用基类代码 + 扩展新功能” 的方式,实现类设计层次的代码复用,构建清晰的类层次结构。但 C++ 的继承机制并非简单的 “复制粘贴”,其包含访问权限控制、作用域隐藏、默认成员函数生成、菱形继承等复杂问题。本文将从 “基础概念→核心特性→实战避坑” 的路径,系统讲解 C++ 继承的全貌,帮你彻底掌握这一核心机制。
一、继承的基础:概念与定义
1.1 为什么需要继承?—— 解决代码冗余
在未使用继承时,相似类会存在大量重复代码。例如Student和Teacher类都包含 “姓名、地址、电话、年龄” 等成员变量,以及 “身份认证” 等成员函数,这些重复代码不仅增加开发量,还会导致维护困难(修改一处需改多处)。
继承的核心思想是:将公共属性和行为提取到基类(如Person),派生类(如Student、Teacher)通过继承基类,直接复用这些公共成员,同时扩展自身独有的成员(如学号、职称)。
优化前后对比:
优化前(无继承):Student和Teacher各自定义重复的_name、identity()等;
优化后(有继承):Person定义公共成员,Student和Teacher继承Person,仅扩展独有成员。
代码示例(优化后):
// 基类(父类):存储公共成员
class Person {
public:
// 公共行为:身份认证
void identity() {
cout << "身份认证:" << _name << endl;
}
protected:
// 公共属性:姓名、地址、电话、年龄(protected:派生类可访问,类外不可访问)
string _name = "张三";
string _address;
string _tel;
int _age = 18;
};
// 派生类(子类)Student:继承Person,扩展学号
class Student : public Person {
public:
// 独有行为:学习
void study() {
cout << _name << "正在学习" << endl;
}
protected:
int _stuid; // 独有属性:学号
};
// 派生类Teacher:继承Person,扩展职称
class Teacher : public Person {
public:
// 独有行为:授课
void teaching() {
cout << _name << "正在授课" << endl;
}
protected:
string _title; // 独有属性:职称
};
// 测试:派生类复用基类成员
int main() {
Student s;
Teacher t;
s.identity(); // 复用Person的identity(),输出“身份认证:张三”
t.identity(); // 同上
s.study(); // 调用Student的独有方法
t.teaching(); // 调用Teacher的独有方法
return 0;
}
1.2 继承的定义格式
C++ 继承的语法格式为:
class 派生类名 : 继承方式 基类名 {
// 派生类的成员(扩展部分)
};
基类(Base Class):被继承的类,也称 “父类”;
派生类(Derived Class):继承基类的类,也称 “子类”;
继承方式:控制基类成员在派生类中的访问权限,有public、protected、private三种。
关键:继承方式与访问权限的关系
基类成员在派生类中的访问权限,由 “基类成员自身的访问限定符” 和 “继承方式” 共同决定,核心规则是:派生类中成员的访问权限 = min (基类成员访问限定符,继承方式)(权限优先级:public > protected > private)。
具体对应关系如下表:
| 基类成员访问限定符 | public 继承 | protected 继承 | private 继承 |
|---|---|---|---|
| public 成员 | 派生类 public 成员 | 派生类 protected 成员 | 派生类 private 成员 |
| protected 成员 | 派生类 protected 成员 | 派生类 protected 成员 | 派生类 private 成员 |
| private 成员 | 派生类中不可见 | 派生类中不可见 | 派生类中不可见 |
重要说明:
- “不可见” 的含义:基类的
private成员会被继承到派生类对象中,但语法上限制派生类(无论类内还是类外)都无法访问 —— 若需让派生类访问基类成员,应将其定义为protected(而非private)。 - 默认继承方式:使用
class关键字时,默认继承方式为private;使用struct时,默认继承方式为public(建议显式写出继承方式,避免歧义)。 - 实际使用场景:几乎只使用
public继承 ——protected/private继承会将基类成员限制在派生类内部,扩展性极差,难以维护。
二、继承的核心特性:转换、作用域与默认成员函数
2.1 基类与派生类的对象转换(切片 / 切割)
在public继承下,派生类对象与基类对象之间存在特定的转换规则,核心是 “派生类对象可以隐式转换为基类对象”,反之则不行。这种转换被称为 “切片”(Slicing),即仅将派生类中 “基类部分” 切出来赋值给基类。
允许的转换(安全):
- 派生类对象 → 基类对象;
- 派生类对象的地址 → 基类指针;
- 派生类对象 → 基类引用。
禁止的转换(不安全):
基类对象 / 指针 / 引用 → 派生类对象 / 指针 / 引用(需强制类型转换,且仅在基类指针指向派生类对象时安全)。
代码示例:
class Person {
protected:
string _name;
int _age;
};
class Student : public Person {
public:
int _stuid; // 学号
};
int main() {
Student sobj;
// 1. 派生类对象 → 基类对象(切片:仅赋值基类部分)
Person pobj = sobj;
// 2. 派生类对象地址 → 基类指针(指向基类部分)
Person* pp = &sobj;
// 3. 派生类对象 → 基类引用(引用基类部分)
Person& rp = sobj;
// 错误:基类对象不能赋值给派生类对象(基类没有派生类的独有成员)
// sobj = pobj;
return 0;
}
2.2 继承中的作用域:隐藏规则(重点)
基类和派生类拥有独立的作用域,若两者存在同名成员(变量或函数),派生类成员会 “隐藏” 基类成员 —— 即直接访问时,优先使用派生类成员;若需访问基类成员,必须显式指定基类作用域(基类名::成员名)。
隐藏的两种场景:
- 同名成员变量:派生类变量隐藏基类变量(无论类型是否相同);
- 同名成员函数:仅需函数名相同即构成隐藏(无需参数列表、返回值相同 —— 与重载不同,重载要求同一作用域内函数名相同且参数列表不同)。
代码示例(成员变量隐藏):
class Person {
protected:
int _num = 111; // 基类:身份证号
};
class Student : public Person {
public:
void Print() {
cout << "身份证号:" << Person::_num << endl; // 显式访问基类成员
cout << "学号:" << _num << endl; // 访问派生类成员(隐藏基类)
}
protected:
int _num = 999; // 派生类:学号(隐藏基类的_num)
};
int main() {
Student s;
s.Print();
// 输出:
// 身份证号:111
// 学号:999
return 0;
}
代码示例(成员函数隐藏):
class A {
public:
void func() { cout << "A::func()" << endl; }
};
class B : public A {
public:
// 函数名相同,构成隐藏(无论参数是否相同)
void func(int i) { cout << "B::func(int): " << i << endl; }
};
int main() {
B b;
b.func(10); // 调用B::func(int)
// b.func(); // 错误:基类func()被隐藏,需显式访问
b.A::func(); // 正确:显式访问基类func()
return 0;
}
避坑建议:继承体系中不要定义同名成员—— 隐藏会导致代码歧义,增加维护成本。
2.3 派生类的默认成员函数生成规则
C++ 类有 6 个默认成员函数(默认构造、拷贝构造、赋值重载、析构、取地址、const 取地址),当派生类未显式定义时,编译器会自动生成,但生成规则与普通类不同 ——必须先处理基类部分的初始化 / 清理。
核心规则(4 个关键默认成员函数):
-
派生类构造函数:
- 必须调用基类的构造函数初始化 “基类部分”;
- 若基类无默认构造函数(无参 / 全缺省),派生类必须在初始化列表中显式调用基类构造函数。
-
派生类拷贝构造函数:
- 必须调用基类的拷贝构造函数,拷贝 “基类部分”;
- 编译器默认生成的拷贝构造,会调用基类的拷贝构造。
-
派生类赋值重载(operator=):
- 必须调用基类的赋值重载,赋值 “基类部分”;
- 派生类的赋值重载会隐藏基类的赋值重载,需显式指定基类作用域调用。
-
派生类析构函数:
- 派生类析构函数执行完后,会自动调用基类的析构函数(无需手动调用);
- 目的:保证 “先清理派生类成员,再清理基类成员” 的顺序(与构造顺序相反);
- 注意:编译器会将析构函数名统一处理为
destructor(),因此基类析构不加virtual时,派生类析构与基类析构构成隐藏(非重写,多态章节会详细讲解)。
代码示例(派生类默认成员函数):
class Person {
public:
// 基类构造函数
Person(const char* name) : _name(name) {
cout << "Person(const char*)" << endl;
}
// 基类拷贝构造
Person(const Person& p) : _name(p._name) {
cout << "Person(const Person&)" << endl;
}
// 基类赋值重载
Person& operator=(const Person& p) {
if (this != &p) {
_name = p._name;
}
cout << "Person::operator=" << endl;
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(const char*, int)" << endl;
}
// 派生类拷贝构造:调用基类拷贝构造
Student(const Student& s)
: Person(s) // 切片:s的基类部分传给Person拷贝构造
, _num(s._num) {
cout << "Student(const Student&)" << endl;
}
// 派生类赋值重载:显式调用基类赋值重载
Student& operator=(const Student& s) {
if (this != &s) {
Person::operator=(s); // 显式调用基类赋值(避免隐藏)
_num = s._num;
}
cout << "Student::operator=" << endl;
return *this;
}
// 派生类析构:无需手动调用基类析构
~Student() {
cout << "~Student()" << endl;
}
protected:
int _num;
};
// 测试:构造与析构顺序
int main() {
Student s1("Jack", 1001);
// 构造顺序:Person → Student
// 输出:Person(const char*) → Student(const char*, int)
Student s2 = s1;
// 拷贝构造顺序:Person拷贝 → Student拷贝
// 输出:Person(const Person&) → Student(const Student&)
Student s3("Rose", 1002);
s1 = s3;
// 赋值顺序:Person赋值 → Student赋值
// 输出:Person::operator= → Student::operator=
// 析构顺序:Student → Person(与构造相反)
// 输出:~Student() → ~Person() → ~Student() → ~Person() → ~Student() → ~Person()
return 0;
}
三、继承的进阶问题:友元、静态成员与菱形继承
3.1 继承与友元:友元关系不可继承
基类的友元函数 / 类不能访问派生类的私有 / 保护成员—— 友元关系是 “单向且非传递” 的,仅对基类生效,与派生类无关。
代码示例(友元不可继承):
class Student; // 前向声明
class Person {
// 友元函数:可访问Person的私有/保护成员
friend void Display(const Person& p, const Student& s);
protected:
string _name;
};
class Student : public Person {
protected:
int _stuid; // 派生类私有成员
};
// 友元函数:可访问Person::_name,但不能访问Student::_stuid
void Display(const Person& p, const Student& s) {
cout << p._name << endl; // 正确:Person的友元可访问_name
// cout << s._stuid << endl; // 错误:友元关系不可继承,无法访问_stuid
}
// 解决方法:将Display也声明为Student的友元
class Student : public Person {
friend void Display(const Person& p, const Student& s); // 新增
protected:
int _stuid;
};
3.2 继承与静态成员:全继承体系共享一份
基类定义的static成员,在整个继承体系中仅存在一份实例—— 无论派生出多少个子类,所有类(基类 + 派生类)共享同一个静态成员。
代码示例(静态成员共享):
class Person {
public:
static int _count; // 静态成员:统计总人数
protected:
string _name;
};
// 静态成员必须在类外初始化
int Person::_count = 0;
class Student : public Person {
protected:
int _stuid;
};
class Teacher : public Person {
protected:
int _id;
};
int main() {
Person p;
Student s;
Teacher t;
// 所有对象访问的是同一个_count
p._count++;
s._count++;
t._count++;
// 输出:3(共享一份,三次自增)
cout << Person::_count << endl;
cout << Student::_count << endl;
cout << Teacher::_count << endl;
return 0;
}
3.3 多继承与菱形继承:C++ 的 “坑”
3.3.1 什么是多继承与菱形继承?
单继承:一个派生类仅有一个直接基类(如Student继承Person);
多继承:一个派生类有两个或以上直接基类(如Assistant同时继承Student和Teacher);
菱形继承:多继承的特殊情况 —— 两个直接基类(如Student和Teacher)共同继承自同一个间接基类(如Person),形成 “菱形” 结构。
3.3.2 菱形继承的问题:数据冗余与二义性
菱形继承会导致派生类对象中存在两份间接基类的成员,引发两个问题:
- 数据冗余:间接基类成员被存储两次(如
Assistant对象中有两份Person::_name); - 访问二义性:直接访问间接基类成员时,编译器无法确定访问哪一份(编译报错)。
代码示例(菱形继承问题):
// 间接基类
class Person {
public:
string _name; // 姓名
};
// 直接基类1:继承Person
class Student : public Person {
protected:
int _stuid; // 学号
};
// 直接基类2:继承Person
class Teacher : public Person {
protected:
int _id; // 职工编号
};
// 派生类:多继承Student和Teacher(菱形继承)
class Assistant : public Student, public Teacher {
protected:
string _major; // 主修课程
};
int main() {
Assistant a;
// 错误:访问二义性(_name有两份,无法确定访问Student还是Teacher的)
// a._name = "Peter";
// 解决二义性:显式指定基类作用域(但数据冗余问题仍存在)
a.Student::_name = "Peter";
a.Teacher::_name = "Peter";
return 0;
}
3.3.3 解决方案:虚继承(Virtual Inheritance)
C++ 引入 “虚继承” 机制,通过virtual关键字修饰继承方式,使间接基类在派生类中仅存储一份实例,从而解决数据冗余和二义性问题。
使用方法:在 “直接基类继承间接基类” 时,添加virtual关键字(如Student和Teacher继承Person时用虚继承)。
代码示例(虚继承解决菱形继承):
// 间接基类
class Person {
public:
string _name;
};
// 直接基类1:虚继承Person
class Student : virtual public Person {
protected:
int _stuid;
};
// 直接基类2:虚继承Person
class Teacher : virtual public Person {
protected:
int _id;
};
// 派生类:多继承Student和Teacher(菱形继承,但间接基类仅一份)
class Assistant : public Student, public Teacher {
protected:
string _major;
};
int main() {
Assistant a;
a._name = "Peter"; // 正确:仅一份_name,无歧义
return 0;
}
避坑建议:尽量避免设计菱形继承
虚继承的底层实现复杂(通过 “虚基表” 和 “虚基指针” 间接访问基类成员),会增加内存开销和访问延迟;且菱形继承本身破坏了类层次的简洁性,难以维护。Java 等语言直接禁止多继承,从根源上规避了菱形继承问题 ——C++ 虽支持多继承,但实际开发中应尽量避免。
四、继承与组合:如何选择?
在代码复用场景中,除了继承,还有 “组合”(Composition)机制 —— 两者的核心区别在于类之间的关系:
继承(is-a 关系):派生类是基类的一种(如Student是Person的一种);
组合(has-a 关系):一个类包含另一个类的对象(如Car包含Tire对象)。
4.1 继承 vs 组合:核心差异
| 特性 | 继承(is-a) | 组合(has-a) |
|---|---|---|
| 关系本质 | 派生类是基类的特例 | 类包含另一个类的对象 |
| 代码复用方式 | 白箱复用(基类内部细节对派生类可见) | 黑箱复用(被组合类仅暴露接口,细节隐藏) |
| 封装性 | 破坏基类封装(基类修改影响派生类) | 封装性好(被组合类修改不影响组合类) |
| 耦合度 | 高(派生类依赖基类) | 低(仅依赖被组合类的接口) |
| 扩展性 | 差(基类接口变化需修改所有派生类) | 好(替换被组合类仅需修改组合类内部) |
4.2 选择原则:优先使用组合
除非满足以下条件,否则优先选择组合:
- 类之间明确是 “is-a” 关系(如
BMW是Car的一种); - 需要实现多态(多态必须依赖继承,通过虚函数实现)。
代码示例(组合 vs 继承):
// 组合:Car包含Tire(has-a关系)
class Tire { // 轮胎类
protected:
string _brand; // 品牌
int _size; // 尺寸
};
class Car { // 汽车类(组合Tire)
protected:
string _color;
Tire _t1, _t2, _t3, _t4; // 包含4个Tire对象
};
// 继承:BMW是Car的一种(is-a关系)
class BMW : public Car { // 继承Car,扩展自身功能
public:
void Drive() {
cout << "BMW:操控性好" << endl;
}
};
五、总结
C++ 继承是一把 “双刃剑”:它能高效复用代码,构建清晰的类层次,但也带来访问权限、作用域隐藏、菱形继承等复杂问题。核心要点总结如下:
- 基础概念:
public继承是主流,基类成员访问权限由 “基类限定符 + 继承方式” 决定; - 核心特性:派生类对象可隐式转换为基类(切片),同名成员会隐藏,默认成员函数需处理基类部分;
- 进阶问题:友元不可继承,静态成员全继承体系共享,菱形继承需用虚继承解决(但建议避免);
- 复用选择:优先使用组合(低耦合、高封装),仅在 “is-a” 关系或需多态时使用继承。

988

被折叠的 条评论
为什么被折叠?



