C++ 继承笔记
C++ 继承笔记
引言
1 继承的概念及定义
继承是 C++ 实现代码复用的核心机制,通过让 “具有共同特征的类”(子类)复用 “公共类”(父类)的成员,减少重复代码开发。
1.1 继承的核心概念
- 父类(基类,Base Class):封装多个子类的公共属性与行为,作为代码复用的 “模板”。例:
Person
类封装 “姓名(name)、年龄(age)、电话(telphone)”,为Student
、Teacher
等类提供公共成员。 - 子类(派生类,Derived Class):继承父类成员后,扩展自身特有属性与行为。例:
Student
类继承Person
的name
/age
,新增studentID
(学号)、major
(专业)。
1.2 继承的语法格式(【补充】原始笔记未明确语法)
cpp
运行
// 基本语法:class 子类名 : 继承方式 父类名
class Person { // 父类(基类)
public:string name; // 公共属性:姓名int age; // 公共属性:年龄void showInfo() { // 公共行为:显示信息cout << "Name: " << name << ", Age: " << age << endl;}
};// 子类Student:public继承Person(显式指定继承方式,推荐)
class Student : public Person {
public:int studentID; // 子类特有属性:学号void study() { // 子类特有行为:学习cout << "Student " << studentID << " is studying." << endl;}
};
1.3 继承方式与基类成员访问权限
C++ 通过 “继承方式”(public
/protected
/private
)修改基类成员在子类中的访问权限,最终决定 “子类能否访问该成员”。
1.3.1 权限转换表格(原始笔记核心表格细化)
基类成员类型 \ 继承方式 | public 继承 | protected 继承 | private 继承 |
---|---|---|---|
基类的public 成员 | 子类的public 成员 | 子类的protected 成员 | 子类的private 成员 |
基类的protected 成员 | 子类的protected 成员 | 子类的protected 成员 | 子类的private 成员 |
基类的private 成员 | 子类中不可见 | 子类中不可见 | 子类中不可见 |
【注】“不可见” 指:基类
private
成员会被继承到子类对象内存中,但语法禁止子类(类内 / 类外)直接访问,需通过基类的public
/protected
接口间接访问。
1.3.2 核心权限规则(结合语法示例)
-
基类
private
成员的不可见性无论何种继承方式,子类均无法直接访问基类private
成员。例:cpp
运行
class Person { private:int salary; // 基类私有成员:薪资 public:void setSalary(int s) { salary = s; } // 公共接口:设置薪资 };class Teacher : public Person { public:void showSalary() {// cout << salary; // 错误:salary是基类private成员,子类不可见// 正确方式:通过基类public接口访问setSalary(8000); } };
-
protected
权限的设计目的用于 “基类成员不想被类外访问,但需被子类访问” 的场景,是 “为继承设计的权限”。例:cpp
运行
class Person { protected:string idCard; // 保护成员:身份证号(类外不可访问,子类可访问) };class Student : public Person { public:void checkID() {cout << "ID Card: " << idCard << endl; // 正确:子类可访问基类protected成员} };int main() {Person p;// p.idCard; // 错误:类外无法访问protected成员return 0; }
-
权限的 “最小限制” 公式基类非
private
成员在子类的权限 =Min(基类成员权限, 继承方式权限)
(权限等级:public
>protected
>private
)。- 例 1:基类
public
成员 +protected
继承 → 子类protected
成员(protected
限制更严)。 - 例 2:基类
protected
成员 +private
继承 → 子类private
成员(private
限制最严)。
- 例 1:基类
-
默认继承方式
- 用
class
定义子类时,默认继承方式为private
(例:class Student : Person {}
→ 隐式private
继承)。 - 用
struct
定义子类时,默认继承方式为public
(例:struct Student : Person {}
→ 隐式public
继承)。【建议】显式指定继承方式(如class Student : public Person
),避免歧义。
- 用
-
实际开发的继承选择几乎只使用
public
继承,极少用protected
/private
继承:- 后两者会将基类成员降级为
protected
/private
,仅允许子类内部访问,导致 “子类再派生时权限受限”,扩展性极差。
- 后两者会将基类成员降级为
2 基类和派生类对象赋值转换
基类与子类对象的赋值转换遵循 “切片(切割)” 原则,核心是 “子类中父类部分的传递”,反向转换存在严格限制。
2.1 允许的转换场景(子类→基类)
子类对象可直接赋值给 “基类对象、基类指针、基类引用”,本质是 “切取子类中属于基类的成员” 进行赋值,子类特有成员被忽略。
2.1.1 代码示例(结合原始笔记类结构)
cpp
运行
class Person { // 基类
public:string name;string sex;int age;
};class Student : public Person { // 子类
public:int No; // 子类特有成员:学号
};int main() {Student s;s.name = "Zhang San";s.age = 20;s.No = 2024001; // 子类特有成员赋值// 1. 子类对象 → 基类对象(切片:仅复制name/sex/age)Person p = s; // p.No; // 错误:基类无No成员// 2. 子类对象地址 → 基类指针(指针仅能访问基类成员)Person* p_ptr = &s; p_ptr->name = "Li Si"; // 正确:访问基类成员// p_ptr->No = 2024002; // 错误:指针无法访问子类特有成员// 3. 子类对象 → 基类引用(引用仅能访问基类成员)Person& p_ref = s; p_ref.age = 21; // 正确:访问基类成员// p_ref.No = 2024003; // 错误:引用无法访问子类特有成员return 0;
}
2.2 禁止 / 限制的转换场景(基类→子类)
2.2.1 基类对象 → 子类对象:直接禁止
子类成员数量多于基类(含基类成员 + 特有成员),基类对象无法 “补全” 子类的特有成员,直接赋值编译报错。
cpp
运行
Person p;
Student s = p; // 错误:基类对象无法赋值给子类对象
2.2.2 基类指针 → 子类指针:仅在 “基类指针指向子类对象” 时安全
需通过强制类型转换实现,但仅当基类指针实际指向的是子类对象时,转换后访问子类成员才安全;若指向基类对象,转换后访问子类成员会导致内存错乱。
cpp
运行
int main() {Student s;Person* p_ptr = &s; // 基类指针指向子类对象(合法)// 安全转换:基类指针指向子类对象,强制转换为子类指针Student* s_ptr1 = (Student*)p_ptr; s_ptr1->No = 2024004; // 正确:实际访问的是子类对象的No成员// 不安全转换:基类指针指向基类对象,强制转换为子类指针Person p;Person* p_ptr2 = &p; Student* s_ptr2 = (Student*)p_ptr2; s_ptr2->No = 2024005; // 错误:访问非法内存(基类对象无No成员)// 【补充】安全转换方案:dynamic_cast(依赖RTTI)// 仅当基类有虚函数(多态)时可用,转换失败返回nullptrPerson* p_ptr3 = &s;Student* s_ptr3 = dynamic_cast<Student*>(p_ptr3);if (s_ptr3 != nullptr) { // 判断转换是否成功s_ptr3->No = 2024006; // 安全访问}return 0;
}
【补充】RTTI(Run-Time Type Information):运行时类型信息,允许程序在运行时判断对象的实际类型,
dynamic_cast
依赖此机制实现安全转换。
2.3 转换的本质(【补充】原始笔记未提内存层面)
- 子类对象的内存布局:基类成员在前,子类特有成员在后(例:
Student
对象内存为name
→sex
→age
→No
)。 - 切片的本质:赋值时仅复制 “基类成员对应的内存区域”,子类特有成员的内存区域被忽略,因此基类无法访问子类特有成员。
3 继承中的作用域
继承体系中,基类和派生类拥有独立的作用域,同名成员会产生 “隐藏(重定义)”,需明确区分访问。
3.1 独立作用域的特性
- 基类和子类的作用域相互独立,成员名可重复,但属于不同作用域的成员。
- 访问成员时,编译器优先在 “当前类作用域” 查找,若未找到则向上追溯至基类作用域。
3.2 成员的隐藏(重定义)
若子类与基类存在同名成员(变量或函数),子类成员会 “屏蔽” 基类同名成员的直接访问,需通过基类名::成员名
显式访问基类成员。
3.2.1 变量的隐藏(代码示例)
cpp
运行
class Person {
public:string name = "Person"; // 基类同名变量
};class Student : public Person {
public:string name = "Student"; // 子类同名变量(隐藏基类name)void showNames() {cout << "子类name: " << name << endl; // 直接访问:子类name(隐藏生效)cout << "基类name: " << Person::name << endl; // 显式访问:基类name}
};int main() {Student s;s.showNames(); // 输出:// 子类name: Student// 基类name: Personreturn 0;
}
3.2.2 函数的隐藏(更宽松的条件)
仅需函数名相同即构成隐藏,与参数列表、返回值无关(区别于重载)。
cpp
运行
class Person {
public:void print() { // 基类函数:无参数cout << "Person print()" << endl;}
};class Student : public Person {
public:// 函数名相同,参数不同 → 构成隐藏(非重载,因重载需同一作用域)void print(int num) { cout << "Student print(" << num << ")" << endl;}void testPrint() {print(10); // 直接访问:子类print(隐藏生效)// print(); // 错误:基类print被隐藏,无法直接调用Person::print(); // 显式访问:基类print}
};
3.3 隐藏与重载的区别(【补充】原始笔记未细化)
特性 | 隐藏(重定义) | 重载(Overload) |
---|---|---|
作用域 | 不同作用域(基类与子类) | 同一作用域(同一类内) |
函数名 | 必须相同 | 必须相同 |
参数列表 | 可相同、可不同 | 必须不同(个数 / 类型 / 顺序) |
返回值 | 无要求 | 无要求 |
访问方式 | 需显式指定基类作用域访问基类成员 | 直接通过参数列表区分调用 |
3.4 析构函数的隐藏与虚析构的必要性
3.4.1 析构函数的隐藏原因
编译器会将所有类的析构函数统一重命名为destructor
(底层处理),因此基类与子类的析构函数必然同名,构成隐藏。
3.4.2 虚析构的必要性(【补充】原始笔记未提实际问题)
若基类析构函数非虚函数,通过 “基类指针删除子类对象” 时,仅会调用基类析构函数,导致子类特有成员的资源泄漏。
cpp
运行
class Person {
public:// 非虚析构:存在资源泄漏风险~Person() { cout << "Person destructor" << endl;}
};class Student : public Person {
private:int* arr; // 子类特有资源:动态数组
public:Student() { arr = new int[10]; } // 初始化动态数组~Student() { // 子类析构:释放动态数组delete[] arr;cout << "Student destructor" << endl;}
};int main() {// 基类指针指向子类对象Person* p = new Student(); delete p; // 仅调用基类析构,子类析构未调用 → arr内存泄漏// 输出:Person destructor(无Student destructor)return 0;
}
3.4.3 解决方案:基类析构函数声明为virtual
虚析构函数会触发 “多态行为”,通过基类指针删除子类对象时,会先调用子类析构,再调用基类析构,确保资源完全释放。
cpp
运行
class Person {
public:// 虚析构:解决资源泄漏virtual ~Person() { cout << "Person destructor" << endl;}
};// 子类析构自动成为虚析构(无需显式加virtual)
class Student : public Person { /* 同上 */ };int main() {Person* p = new Student(); delete p; // 输出:// Student destructor(先析构子类)// Person destructor(再析构基类)return 0;
}
4 派生类的默认成员函数
派生类的默认成员函数(构造、拷贝构造、赋值运算符重载、析构、取地址、const 取地址)需 “协同基类成员” 工作,遵循特定调用规则。
4.1 派生类构造函数
4.1.1 核心规则
- 派生类构造时,必须先初始化基类部分(即调用基类构造函数),再初始化子类特有成员。
- 若基类无 “无参默认构造函数”(如基类仅含带参构造),派生类必须在初始化列表中显式指定调用基类的哪个构造函数。
4.1.2 代码示例(含两种场景)
cpp
运行
// 场景1:基类有默认构造函数(无参/全缺省)
class Person {
public:string name;// 基类默认构造(无参)Person() : name("Unknown") { cout << "Person default constructor" << endl;}
};class Student : public Person {
public:int No;// 派生类构造:无需显式调用基类构造(自动调用基类默认构造)Student(int no) : No(no) { cout << "Student constructor" << endl;}
};// 场景2:基类无默认构造(仅带参构造)
class Person2 {
public:string name;// 基类仅带参构造(无默认构造)Person2(string n) : name(n) { cout << "Person2 parameter constructor" << endl;}
};class Student2 : public Person2 {
public:int No;// 派生类必须在初始化列表显式调用基类带参构造Student2(string n, int no) : Person2(n), No(no) { cout << "Student2 constructor" << endl;}
};int main() {Student s1(2024001); // 输出顺序:// Person default constructor(先初始化基类)// Student constructor(再初始化子类)Student2 s2("Wang Wu", 2024002); // 输出顺序:// Person2 parameter constructor(显式调用基类带参构造)// Student2 constructor(再初始化子类)return 0;
}
4.2 派生类拷贝构造函数
4.2.1 核心规则
- 派生类拷贝构造时,必须先拷贝基类部分(即调用基类拷贝构造函数),再拷贝子类特有成员。
- 若未显式定义派生类拷贝构造,编译器会生成默认拷贝构造:自动调用基类拷贝构造,对子类特有成员进行 “浅拷贝”。
4.2.2 代码示例(显式定义与默认行为)
cpp
运行
class Person {
public:string name;// 基类拷贝构造Person(const Person& p) : name(p.name) { cout << "Person copy constructor" << endl;}
};class Student : public Person {
public:int No;// 派生类显式拷贝构造:调用基类拷贝构造Student(const Student& s) : Person(s), No(s.No) { cout << "Student copy constructor" << endl;}
};int main() {Student s1("Zhao Liu", 2024003); Student s2 = s1; // 调用派生类拷贝构造// 输出顺序:// Person copy constructor(先拷贝基类)// Student copy constructor(再拷贝子类)return 0;
}
4.3 派生类赋值运算符重载(operator=
)
4.3.1 核心规则
- 派生类赋值时,必须先赋值基类部分(即调用基类赋值运算符),再赋值子类特有成员。
- 若未显式定义派生类赋值运算符,编译器会生成默认版本:自动调用基类赋值运算符,对子类特有成员进行 “浅拷贝”。
- 需注意避免自赋值(防止重复释放资源),返回值类型为
派生类&
(支持链式赋值)。
4.3.2 代码示例(显式定义)
cpp
运行
class Person {
public:string name;// 基类赋值运算符重载Person& operator=(const Person& p) {if (this != &p) { // 避免自赋值name = p.name;}cout << "Person operator=" << endl;return *this;}
};class Student : public Person {
public:int No;// 派生类赋值运算符重载:调用基类赋值运算符Student& operator=(const Student& s) {if (this != &s) { // 避免自赋值Person::operator=(s); // 显式调用基类赋值No = s.No; // 赋值子类特有成员}cout << "Student operator=" << endl;return *this;}
};int main() {Student s1, s2;s1.name = "Qian Qi";s1.No = 2024004;s2 = s1; // 调用派生类赋值运算符// 输出顺序:// Person operator=(先赋值基类)// Student operator=(再赋值子类)return 0;
}
4.4 派生类析构函数
4.4.1 核心规则
- 派生类析构时,先执行子类析构逻辑,再自动调用基类析构函数(无需显式调用),确保 “先析构子类资源,再析构基类资源”,避免资源依赖错误。
- 析构函数的调用顺序与构造顺序完全相反。
4.4.2 代码示例(析构顺序验证)
cpp
运行
class Person {
public:~Person() { // 基类析构cout << "Person destructor" << endl;}
};class Student : public Person {
public:~Student() { // 子类析构cout << "Student destructor" << endl;}
};int main() {Student s; // 构造顺序:Person构造 → Student构造// 析构顺序(对象生命周期结束时):// Student destructor(先析构子类)// Person destructor(再析构基类)return 0;
}
4.5 派生类对象的初始化与析构完整顺序(【补充】原始笔记未汇总)
- 初始化顺序(构造时):基类构造函数 → 子类构造函数(先基后子)。
- 析构顺序(销毁时):子类析构函数 → 基类析构函数(先子后基)。
5 继承与友元(【补充】原始笔记疏漏核心内容)
友元关系不可继承,即基类的友元无法访问子类的私有 / 保护成员,子类的友元也无法访问基类的私有 / 保护成员。
5.1 友元不可继承的代码示例
cpp
运行
class Person {// 基类友元:全局函数showPersonSalaryfriend void showPersonSalary(const Person& p);
private:int salary = 8000; // 基类私有成员
};// 基类友元函数:可访问Person的私有成员
void showPersonSalary(const Person& p) {cout << "Person Salary: " << p.salary << endl;
}class Student : public Person {
private:int scholarship = 2000; // 子类私有成员
};// 尝试用基类友元访问子类私有成员:错误
void showStudentScholarship(const Student& s) {// cout << "Student Scholarship: " << s.scholarship << endl; // 错误:showStudentScholarship不是Student的友元,且Person的友元无法继承
}int main() {Person p;showPersonSalary(p); // 正确:输出Person Salary: 8000Student s;// showStudentScholarship(s); // 错误:无法访问子类私有成员return 0;
}
5.2 注意事项
- 若需让某个函数访问子类的私有成员,需在子类中重新声明该函数为友元,而非依赖基类的友元关系。
- 友元关系仅存在于 “声明友元的类” 与 “友元实体” 之间,与继承层次无关。
6 继承与静态成员
基类的静态成员(static
修饰)在整个继承体系中仅存在一份实例,所有基类对象、子类对象及子类均共享该静态成员,不会因继承而复制多份。
6.1 静态成员共享特性的代码示例
cpp
运行
class Person {
public:// 基类静态成员:统计继承体系中对象的总个数static int totalCount; Person() { totalCount++; } // 构造时计数+1~Person() { totalCount--; } // 析构时计数-1
};// 【补充】静态成员必须在类外初始化(原始笔记未提初始化位置)
int Person::totalCount = 0;class Student : public Person {
public:Student() { /* 无需重新初始化totalCount,共享基类的静态成员 */ }
};class Teacher : public Person {
public:Teacher() { /* 同上 */ }
};int main() {Person p;cout << "Total Count (after Person): " << Person::totalCount << endl; // 输出1Student s;cout << "Total Count (after Student): " << Student::totalCount << endl; // 输出2(共享)Teacher t;cout << "Total Count (after Teacher): " << Teacher::totalCount << endl; // 输出3(共享)return 0;
}
6.2 静态成员的访问方式(【补充】原始笔记未汇总)
- 通过基类名访问:
基类名::静态成员
(例:Person::totalCount
)。 - 通过子类名访问:
子类名::静态成员
(例:Student::totalCount
)。 - 通过对象访问:
对象.静态成员
(例:p.totalCount
、s.totalCount
)。 - 通过指针访问:
指针->静态成员
(例:Person* p_ptr = &s; p_ptr->totalCount
)。
6.3 注意事项
- 静态成员不属于任何对象,存储在全局数据区,生命周期贯穿程序始终。
- 静态成员函数仅能访问静态成员变量,无法访问非静态成员(因无
this
指针)。
7 复杂的菱形继承及菱形虚拟继承
菱形继承是多继承的特殊形态,会导致 “数据冗余” 和 “二义性” 问题,需通过 “虚拟继承” 解决。
7.1 菱形继承的定义与结构
菱形继承的结构为:顶层基类(A)
→ 派生两个中间基类(B、C)
→ 最终派生底层子类(D)
,形成 “菱形” 结构。
7.1.1 菱形继承的代码示例(原始笔记结构)
cpp
运行
// 顶层基类:Person
class Person {
public:string name; // 公共成员:姓名
};// 中间基类1:Student(继承Person)
class Student : public Person {
public:int No; // 特有成员:学号
};// 中间基类2:Teacher(继承Person)
class Teacher : public Person {
public:int id; // 特有成员:工号
};// 底层子类:Assistant(多继承Student和Teacher)→ 菱形继承
class Assistant : public Student, public Teacher {
public:string majorCourse; // 特有成员:主讲课程
};
7.2 菱形继承的问题:数据冗余与二义性
7.2.1 数据冗余
Assistant
对象会继承两份Person
的name
成员(一份来自Student
,一份来自Teacher
),导致内存浪费。
Assistant
对象内存布局:Student::name
→Student::No
→Teacher::name
→Teacher::id
→Assistant::majorCourse
。
7.2.2 二义性
访问Assistant
对象的name
时,编译器无法确定访问的是Student
继承的name
还是Teacher
继承的name
,编译报错。
cpp
运行
int main() {Assistant a;// a.name = "Li Ba"; // 错误:二义性,无法确定name来源// 需显式指定作用域,才能访问(临时解决方案,无法解决数据冗余)a.Student::name = "Li Ba";a.Teacher::name = "Li Ba"; // 重复赋值,数据冗余return 0;
}
7.3 虚拟继承的解决方案
虚拟继承通过在 “中间基类继承顶层基类时” 添加virtual
关键字,让底层子类仅保留一份顶层基类的成员,从而解决数据冗余和二义性。
7.3.1 虚拟继承的语法(【补充】原始笔记未明确关键字位置)
cpp
运行
// 顶层基类:Person(不变)
class Person {
public:string name;
};// 中间基类1:Student → 虚拟继承Person(添加virtual)
class Student : virtual public Person {
public:int No;
};// 中间基类2:Teacher → 虚拟继承Person(添加virtual)
class Teacher : virtual public Person {
public:int id;
};// 底层子类:Assistant(多继承Student和Teacher,无需virtual)
class Assistant : public Student, public Teacher {
public:string majorCourse;
};
7.3.2 虚拟继承的效果
cpp
运行
int main() {Assistant a;a.name = "Li Ba"; // 正确:仅一份name,无歧义cout << a.name << endl; // 输出Li Ba(无数据冗余)return 0;
}
7.4 虚拟继承的原理(结合原始笔记偏移量)
虚拟继承通过 “偏移量表格(Virtual Base Table)” 实现:
- 中间基类(
Student
、Teacher
)的对象中,会存储一个 “偏移量指针(vbp)”,指向 “偏移量表格”。 - 偏移量表格中记录 “当前类成员到顶层基类(
Person
)成员的内存偏移量”。 - 底层子类(
Assistant
)对象访问顶层基类成员时,通过偏移量指针找到表格,计算出顶层基类成员的实际内存地址,从而确保仅访问一份成员。
【注】虚拟继承会增加内存开销(偏移量指针)和计算开销(地址偏移),非菱形继承场景无需使用。
7.5 虚拟继承的注意事项
virtual
关键字仅需添加在 “中间基类继承顶层基类” 时,底层子类多继承时无需添加。- 虚拟继承仅用于解决菱形继承的问题,禁止在非菱形继承场景中滥用(避免不必要的性能损耗)。
- 若顶层基类有带参构造函数,中间基类和底层子类均需在初始化列表中显式调用顶层基类的构造函数(因虚拟继承改变了构造顺序)。
8 继承的总结与反思(【补充】原始笔记疏漏总结)
8.1 继承的核心优势与局限性
优势 | 局限性 |
---|---|
实现代码复用,减少重复开发 | 增加类间耦合度,子类依赖基类实现 |
建立类的层次关系,逻辑清晰 | 多继承(菱形继承)易引发数据冗余和二义性 |
支持多态,提升代码扩展性 | 子类过度依赖基类,基类修改可能影响所有子类 |
8.2 实际开发中的继承使用原则
- 优先使用
public
继承:protected
/private
继承扩展性差,仅在特殊场景(如限制子类对外暴露基类成员)使用。 - 优先用组合(Has-A)替代继承(Is-A):
- 继承是 “Is-A” 关系(如
Student
是Person
),耦合度高; - 组合是 “Has-A” 关系(如
Car
有Engine
),耦合度低,更灵活(符合 SOLID 原则中的 “合成复用原则”)。
- 继承是 “Is-A” 关系(如
- 避免定义同名成员:同名成员会引发隐藏,增加代码维护难度。
- 基类析构函数需声明为
virtual
:若基类可能被继承且通过基类指针删除子类对象,必须用虚析构避免资源泄漏。 - 谨慎使用多继承:仅在必要场景(如实现多个接口)使用,避免菱形继承;若需菱形继承,必须用虚拟继承解决问题。
8.3 常见错误与规避方法
- 未显式调用基类带参构造:基类无默认构造时,派生类必须在初始化列表显式调用基类带参构造,否则编译报错。
- 滥用多继承:未考虑菱形继承风险,直接使用多继承,导致数据冗余和二义性。
- 基类析构函数非虚函数:通过基类指针删除子类对象时,导致子类资源泄漏。
- 友元关系误判:认为基类的友元可访问子类成员,未在子类中重新声明友元。