C++ 继承进阶:默认成员函数、多继承、虚继承与组合设计

🔥 星恒随风: 个人主页 ❄️ 个人专栏: 《指针合集》 | 《C语言基础》 | 《数据结构》 | 《机器学习导论》 | 《前端基础》 | 《python基础》 | 《C++从入门到入土》 ✨ 数据即知识,压缩即智能
文章目录
- [C++ 继承进阶:默认成员函数、多继承、虚继承与组合设计](#C++ 继承进阶:默认成员函数、多继承、虚继承与组合设计)
-
- 前言
- 一、派生类对象由哪些部分组成?
-
- [1.1 基类子对象和派生类成员](#1.1 基类子对象和派生类成员)
- [1.2 对象初始化不是简单地执行构造函数体](#1.2 对象初始化不是简单地执行构造函数体)
- 二、派生类构造函数如何初始化基类?
-
- [2.1 基类有默认构造函数](#2.1 基类有默认构造函数)
- [2.2 基类没有默认构造函数](#2.2 基类没有默认构造函数)
- 三、派生类的拷贝构造
-
- [3.1 编译器默认生成时会处理基类部分](#3.1 编译器默认生成时会处理基类部分)
- [3.2 手动编写时要显式拷贝基类](#3.2 手动编写时要显式拷贝基类)
- 四、派生类的赋值运算符
-
- [4.1 赋值也要处理两部分](#4.1 赋值也要处理两部分)
- [4.2 可以优先使用 = default](#4.2 可以优先使用 = default)
- 五、构造和析构顺序
-
- [5.1 构造顺序](#5.1 构造顺序)
- [5.2 析构顺序](#5.2 析构顺序)
- [六、基类析构函数什么时候要加 virtual?](#六、基类析构函数什么时候要加 virtual?)
-
- [6.1 通过基类指针删除派生对象](#6.1 通过基类指针删除派生对象)
- [6.2 哪些基类需要虚析构?](#6.2 哪些基类需要虚析构?)
- 七、如何禁止一个类被继承?
-
- [7.1 使用 final](#7.1 使用 final)
- [7.2 私有构造函数的旧思路](#7.2 私有构造函数的旧思路)
- 八、友元关系不会沿继承链传播
-
- [8.1 基类友元只能访问基类](#8.1 基类友元只能访问基类)
- [8.2 友元也不具有传递性](#8.2 友元也不具有传递性)
- 九、静态成员在继承体系中只有一份
-
- [9.1 普通成员每个对象各有一份](#9.1 普通成员每个对象各有一份)
- [9.2 static 成员属于类](#9.2 static 成员属于类)
- 十、多继承是什么?
-
- [10.1 单继承](#10.1 单继承)
- [10.2 多继承](#10.2 多继承)
- [10.3 多继承中的同名成员二义性](#10.3 多继承中的同名成员二义性)
- 十一、多继承中的指针调整
-
- [11.1 不同基类指针可能具有不同地址值](#11.1 不同基类指针可能具有不同地址值)
- [11.2 不要依赖具体布局](#11.2 不要依赖具体布局)
- 十二、菱形继承为什么麻烦?
-
- [12.1 菱形结构](#12.1 菱形结构)
- [12.2 数据冗余](#12.2 数据冗余)
- [12.3 访问二义性](#12.3 访问二义性)
- 十三、虚继承如何解决菱形问题?
-
- [13.1 声明虚继承](#13.1 声明虚继承)
- [13.2 virtual 写在什么位置?](#13.2 virtual 写在什么位置?)
- 十四、虚基类由谁负责构造?
-
- [14.1 普通继承中的基类构造](#14.1 普通继承中的基类构造)
- [14.2 虚继承中的最派生类规则](#14.2 虚继承中的最派生类规则)
- [14.3 单独构造中间类时仍由中间类负责](#14.3 单独构造中间类时仍由中间类负责)
- 十五、标准库中的虚继承例子
- 十六、继承和组合如何选择?
-
- [16.1 继承表达 is-a](#16.1 继承表达 is-a)
- [16.2 组合表达 has-a](#16.2 组合表达 has-a)
- [16.3 白盒复用和黑盒复用](#16.3 白盒复用和黑盒复用)
- [16.4 优先组合,但不是拒绝继承](#16.4 优先组合,但不是拒绝继承)
- [16.5 一个简单判断流程](#16.5 一个简单判断流程)
- 十七、常见错误整理
-
- [17.1 在构造函数体中"调用"基类构造](#17.1 在构造函数体中“调用”基类构造)
- [17.2 手动写拷贝构造却忘记基类部分](#17.2 手动写拷贝构造却忘记基类部分)
- [17.3 派生类赋值时只复制自己的成员](#17.3 派生类赋值时只复制自己的成员)
- [17.4 多态基类没有虚析构函数](#17.4 多态基类没有虚析构函数)
- [17.5 把友元理解成可继承权限](#17.5 把友元理解成可继承权限)
- [17.6 认为每个对象都有一份 static 成员](#17.6 认为每个对象都有一份 static 成员)
- [17.7 用类域解决菱形继承后认为问题全部消失](#17.7 用类域解决菱形继承后认为问题全部消失)
- [17.8 依赖多继承对象的具体内存排列](#17.8 依赖多继承对象的具体内存排列)
- [17.9 为了复用实现随意使用 public 继承](#17.9 为了复用实现随意使用 public 继承)
- 总结
前言
掌握继承的基本语法后,接下来会遇到一些更容易出错的问题:
- 派生类构造时,基类部分由谁初始化?
- 拷贝构造和赋值运算符是否需要处理基类?
- 为什么构造顺序是从基类到派生类,析构顺序却正好相反?
- 基类析构函数什么时候必须是虚函数?
- 友元关系和静态成员会不会被继承?
- 多继承为什么会产生二义性?
- 菱形继承为什么会造成数据冗余?
- 虚继承如何让多个路径共享同一个基类子对象?
- 继承和组合到底应该如何选择?
这些内容看起来比较分散,实际上都围绕一个核心问题:
一个派生类对象中,不仅有派生类自己的成员,还包含基类子对象。
接下来我们就围绕着这点展开
一、派生类对象由哪些部分组成?
1.1 基类子对象和派生类成员
假设有下面两个类:
cpp
#include <string>
using namespace std;
class Person
{
protected:
string _name;
int _age = 0;
};
class Student : public Person
{
protected:
int _studentId = 0;
};
一个 Student 对象可以抽象成:
text
Student 对象
├── Person 基类子对象
│ ├── _name
│ └── _age
└── Student 自己的成员
└── _studentId
这里的 Person 不是独立于 Student 的另一个对象,而是 Student 对象内部的一个基类子对象。
因此,构造 Student 时,需要先让 Person 部分进入有效状态,再初始化学生自己的成员。

1.2 对象初始化不是简单地执行构造函数体
构造对象时,真正的顺序大致是:
text
虚基类
→ 直接非虚基类
→ 成员变量
→ 派生类构造函数体
其中:
- 基类按照继承列表中的声明顺序初始化
- 成员变量按照它们在类中的声明顺序初始化
- 和初始化列表中的书写先后无关
因此不要故意把初始化列表写成与声明顺序不同的样子,虽然程序可能仍能编译,但阅读起来很容易产生误解。
二、派生类构造函数如何初始化基类?
2.1 基类有默认构造函数
cpp
#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
Person()
{
cout << "Person()" << endl;
}
protected:
string _name;
};
class Student : public Person
{
public:
Student()
{
cout << "Student()" << endl;
}
private:
int _studentId = 0;
};
创建对象:
cpp
Student student;
输出顺序是:
text
Person()
Student()
编译器会先构造基类子对象,再执行派生类构造函数。
2.2 基类没有默认构造函数
假设基类只能通过姓名构造:
cpp
class Person
{
public:
explicit Person(const string& name)
: _name(name)
{
cout << "Person(const string&)" << endl;
}
protected:
string _name;
};
派生类必须在初始化列表中明确调用基类构造函数:
cpp
class Student : public Person
{
public:
Student(const string& name, int studentId)
: Person(name)
, _studentId(studentId)
{
cout << "Student(const string&, int)" << endl;
}
private:
int _studentId;
};
不能写成:
cpp
Student(const string& name, int studentId)
{
Person(name);
}
因为构造函数体开始执行时,基类子对象早就应该完成初始化了。
函数体中的:
cpp
Person(name);
只会创建一个临时的 Person 对象,并不能重新初始化当前对象中的基类部分。
三、派生类的拷贝构造
3.1 编译器默认生成时会处理基类部分
cpp
class Person
{
public:
Person(const Person& other)
: _name(other._name)
{
}
protected:
string _name;
};
class Student : public Person
{
private:
int _studentId = 0;
};
如果没有手动编写 Student 的拷贝构造,编译器生成的版本会依次拷贝:
text
Person 基类子对象
Student 自己的成员
3.2 手动编写时要显式拷贝基类
cpp
class Student : public Person
{
public:
Student(const string& name, int studentId)
: Person(name)
, _studentId(studentId)
{
}
Student(const Student& other)
: Person(other)
, _studentId(other._studentId)
{
}
private:
int _studentId;
};
其中:
cpp
Person(other)
负责调用基类的拷贝构造,复制基类部分。
如果忘记写这一项,编译器会尝试默认构造基类,而不是自动猜测你想调用基类拷贝构造。
如果基类没有默认构造函数,就会直接编译失败。
四、派生类的赋值运算符
4.1 赋值也要处理两部分
派生类赋值时,需要复制:
text
基类部分
派生类自己的部分
可以这样实现:
cpp
class Student : public Person
{
public:
Student(const string& name, int studentId)
: Person(name)
, _studentId(studentId)
{
}
Student& operator=(const Student& other)
{
if (this != &other)
{
Person::operator=(other);
_studentId = other._studentId;
}
return *this;
}
private:
int _studentId;
};
这里需要明确写:
cpp
Person::operator=(other);
原因是派生类自己的 operator= 会隐藏基类中的同名函数。
如果只写:
cpp
operator=(other);
很可能再次调用当前派生类的赋值函数,形成错误递归。

4.2 可以优先使用 = default
如果成员本身都支持正确的拷贝和赋值,通常不需要手动写这些函数。
cpp
class Student : public Person
{
public:
Student(const Student&) = default;
Student& operator=(const Student&) = default;
private:
int _studentId = 0;
};
编译器生成的版本会自动处理基类子对象和成员变量。
只有在类需要特殊资源管理、日志或额外约束时,才有必要手动实现。
五、构造和析构顺序
5.1 构造顺序
假设:
cpp
class Base
{
public:
Base()
{
cout << "Base()" << endl;
}
};
class Member
{
public:
Member()
{
cout << "Member()" << endl;
}
};
class Derived : public Base
{
public:
Derived()
{
cout << "Derived()" << endl;
}
private:
Member _member;
};
创建:
cpp
Derived object;
输出:
text
Base()
Member()
Derived()
构造顺序为:
text
基类
→ 成员对象
→ 派生类构造函数体

5.2 析构顺序
对象销毁时顺序反过来:
text
派生类析构函数体
→ 成员对象
→ 基类
如果加入析构输出,结果大致是:
text
~Derived()
~Member()
~Base()
为什么析构要反过来?
因为派生类可能还依赖基类提供的状态。只有先清理最外层的派生类,再逐层清理它依赖的内部部分,才能保持对象生命周期的完整性。
可以把它理解成搭积木:
text
搭建时:先底座,再上层
拆除时:先上层,再底座
六、基类析构函数什么时候要加 virtual?
6.1 通过基类指针删除派生对象
看下面的代码:
cpp
class Base
{
public:
~Base()
{
cout << "~Base()" << endl;
}
};
class Derived : public Base
{
public:
~Derived()
{
cout << "~Derived()" << endl;
}
};
如果执行:
cpp
Base* ptr = new Derived;
delete ptr;
基类析构函数不是虚函数时,通过基类指针删除派生对象会产生未定义行为。
正确设计通常是:
cpp
class Base
{
public:
virtual ~Base() = default;
};
此时:
cpp
Base* ptr = new Derived;
delete ptr;
会先调用:
text
Derived::~Derived()
再调用:
text
Base::~Base()

6.2 哪些基类需要虚析构?
如果一个类满足下面的情况:
text
它具有虚函数
它会被当作多态基类使用
外部可能通过 Base* 删除派生对象
那么它通常应该具有公有虚析构函数:
cpp
virtual ~Base() = default;
如果明确禁止通过基类指针删除对象,也可以把析构函数设计成保护的非虚函数,但这种设计需要更明确的约束。
七、如何禁止一个类被继承?
7.1 使用 final
C++11 提供了 final:
cpp
class Utility final
{
public:
void run()
{
}
};
下面的代码会编译失败:
cpp
class Derived : public Utility
{
};
final 明确表示:
当前类不允许继续派生。
它比通过特殊构造函数权限绕开继承更加直观。
7.2 私有构造函数的旧思路
在旧式设计中,也可以把基类构造函数设为私有:
cpp
class Base
{
private:
Base() = default;
};
派生类无法调用基类构造函数,因此通常无法正常构造。
不过这种写法表达意图不够直接,也会影响基类本身的创建方式。现代 C++ 中,禁止继承优先使用 final。
八、友元关系不会沿继承链传播
8.1 基类友元只能访问基类
cpp
class Student;
class Person
{
friend void display(const Person&, const Student&);
protected:
string _name;
};
class Student : public Person
{
protected:
int _studentId = 0;
};
函数:
cpp
void display(const Person& person, const Student& student)
{
cout << person._name << endl;
// cout << student._studentId << endl; // 错误
}
虽然 display 是 Person 的友元,但它不是 Student 的友元。
友元关系不会自动继承。
如果确实需要访问学生的非公有成员,需要在 Student 中再次声明:
cpp
class Student : public Person
{
friend void display(const Person&, const Student&);
protected:
int _studentId = 0;
};
8.2 友元也不具有传递性
即使:
text
A 是 B 的友元
B 是 C 的友元
也不能推出:
text
A 是 C 的友元
友元是一种由类明确授予的访问权限,不应当沿关系链自动扩散。
九、静态成员在继承体系中只有一份
9.1 普通成员每个对象各有一份
cpp
class Person
{
public:
string _name;
};
class Student : public Person
{
};
创建:
cpp
Person person;
Student student;
person._name 和 student._name 属于不同对象,地址也不同。

9.2 static 成员属于类
cpp
class Person
{
public:
static int _count;
};
int Person::_count = 0;
class Student : public Person
{
};
无论通过哪个类名访问:
cpp
Person::_count = 10;
cout << Person::_count << endl;
cout << Student::_count << endl;
访问的都是同一个静态成员。
也可以通过对象访问:
cpp
Person person;
Student student;
cout << &person._count << endl;
cout << &student._count << endl;
两个地址相同。
静态成员不属于某个具体对象,而属于整个类及其继承体系所引用的那一个声明。
十、多继承是什么?
10.1 单继承
一个派生类只有一个直接基类:
cpp
class Student : public Person
{
};
关系为:
text
Person
↓
Student
10.2 多继承
一个派生类同时拥有两个或更多直接基类:
cpp
class Scanner
{
public:
void scan()
{
}
};
class Printer
{
public:
void print()
{
}
};
class AllInOneMachine : public Scanner, public Printer
{
};
此时:
text
Scanner Printer
\ /
\ /
AllInOneMachine
对象可以同时使用两个基类提供的接口:
cpp
AllInOneMachine machine;
machine.scan();
machine.print();
10.3 多继承中的同名成员二义性
如果两个基类都有同名函数:
cpp
class Scanner
{
public:
void start()
{
cout << "Scanner::start()" << endl;
}
};
class Printer
{
public:
void start()
{
cout << "Printer::start()" << endl;
}
};
class AllInOneMachine : public Scanner, public Printer
{
};
调用:
cpp
AllInOneMachine machine;
// machine.start(); // 错误:不知道调用哪个 start
需要明确指定路径:
cpp
machine.Scanner::start();
machine.Printer::start();
多继承会让名字查找、构造过程和对象转换变得更加复杂,因此不宜为了简单复用几个函数就随意使用。
十一、多继承中的指针调整
11.1 不同基类指针可能具有不同地址值
cpp
class Base1
{
public:
int _value1 = 1;
};
class Base2
{
public:
int _value2 = 2;
};
class Derived : public Base1, public Base2
{
public:
int _value3 = 3;
};
执行:
cpp
Derived object;
Base1* ptr1 = &object;
Base2* ptr2 = &object;
Derived* ptr3 = &object;
编译器会让每个基类指针指向它对应的基类子对象。
因此,从 Derived* 转换为某个基类指针时,编译器可能需要调整地址。
可以抽象理解为:
text
Derived 对象
├── Base1 子对象 ← Base1*
├── Base2 子对象 ← Base2*
└── Derived 成员
11.2 不要依赖具体布局
某些编译器中,第一个非虚基类子对象可能位于对象起始位置,第二个基类会产生偏移。
但具体对象布局和偏移属于实现相关细节,不能把某个编译器观察到的地址关系当成通用语言规则。
应当依赖 C++ 的类型转换,让编译器完成正确的指针调整,而不是手动计算地址。
十二、菱形继承为什么麻烦?
12.1 菱形结构
考虑下面的类层次:
cpp
class Person
{
public:
string _name;
};
class Student : public Person
{
protected:
int _studentId = 0;
};
class Teacher : public Person
{
protected:
int _teacherId = 0;
};
class Assistant : public Student, public Teacher
{
};
结构为:
text
Person
/ \
Student Teacher
\ /
Assistant
形状像一个菱形,因此称为菱形继承。

12.2 数据冗余
Student 中有一份 Person 子对象。
Teacher 中也有一份 Person 子对象。
因此 Assistant 中会出现两份普通的 Person 子对象:
text
Assistant
├── Student
│ └── Person
│ └── _name
└── Teacher
└── Person
└── _name
一个助教对象中出现两个姓名,通常不是我们真正想表达的模型。
12.3 访问二义性
cpp
Assistant assistant;
// assistant._name = "小王"; // 错误:访问路径不明确
编译器不知道要访问:
cpp
assistant.Student::_name
还是:
cpp
assistant.Teacher::_name
可以通过指定类域解决访问二义性:
cpp
assistant.Student::_name = "学生路径";
assistant.Teacher::_name = "教师路径";
但两份数据仍然存在,所以数据冗余没有消失。
十三、虚继承如何解决菱形问题?
13.1 声明虚继承
让中间层虚继承 Person:
cpp
class Student : virtual public Person
{
protected:
int _studentId = 0;
};
class Teacher : virtual public Person
{
protected:
int _teacherId = 0;
};
class Assistant : public Student, public Teacher
{
};
此时,最派生的 Assistant 对象中只保留一份共享的 Person 虚基类子对象:
text
Person
↗ ↖
Student Teacher
↘ ↙
Assistant
对象可以抽象成:
text
Assistant
├── Student 部分
├── Teacher 部分
├── Assistant 自己的成员
└── 共享的 Person 虚基类子对象
于是:
cpp
Assistant assistant;
assistant._name = "小王";
不再有两份普通 Person 子对象造成的二义性。

13.2 virtual 写在什么位置?
下面两种写法等价:
cpp
class Student : virtual public Person
{
};
cpp
class Student : public virtual Person
{
};
一般使用第一种或团队统一的风格即可。
十四、虚基类由谁负责构造?
这是虚继承中最容易忽略的规则。
14.1 普通继承中的基类构造
普通继承中,直接派生类负责构造自己的直接基类:
cpp
class Student : public Person
{
public:
Student(const string& name)
: Person(name)
{
}
};

14.2 虚继承中的最派生类规则
虚基类由最派生类负责初始化。
cpp
class Person
{
public:
explicit Person(const string& name)
: _name(name)
{
}
protected:
string _name;
};
class Student : virtual public Person
{
public:
Student(const string& name, int studentId)
: Person(name)
, _studentId(studentId)
{
}
private:
int _studentId;
};
class Teacher : virtual public Person
{
public:
Teacher(const string& name, int teacherId)
: Person(name)
, _teacherId(teacherId)
{
}
private:
int _teacherId;
};
最终派生类:
cpp
class Assistant : public Student, public Teacher
{
public:
Assistant(const string& name)
: Person(name)
, Student("Student 路径", 1001)
, Teacher("Teacher 路径", 2001)
{
}
};
创建:
cpp
Assistant assistant("最终姓名");
共享的虚基类 Person 使用的是:
cpp
Person(name)
也就是 Assistant 初始化列表中的参数。
Student 和 Teacher 初始化列表中对 Person 的构造,在创建最派生的 Assistant 对象时不会负责最终虚基类的初始化。
14.3 单独构造中间类时仍由中间类负责
如果直接创建:
cpp
Student student("学生姓名", 1001);
此时 Student 本身就是最派生类,所以它负责初始化虚基类 Person。
"最派生类"不是固定指某个类名,而是指这一次实际创建对象时最外层的完整类型。
十五、标准库中的虚继承例子
C++ 输入输出流体系是虚继承的经典例子。
可以简化理解为:
text
basic_ios
/ \
basic_istream basic_ostream
\ /
basic_iostream
输入流和输出流都需要共享基础的流状态。
如果采用普通菱形继承,basic_iostream 中会出现两份基础流状态。使用虚继承后,最派生流对象只保留一份共享的 basic_ios 虚基类子对象。
这说明虚继承并非完全不能使用。
但它适合真正存在共享基类语义的场景,而不是为了展示语法而主动设计复杂菱形结构。
十六、继承和组合如何选择?
16.1 继承表达 is-a
公有继承通常表达:
text
Derived is a Base
例如:
text
Student is a Person
Circle is a Shape
Dog is an Animal
派生类应当能够在需要基类的地方自然替代基类。

16.2 组合表达 has-a
组合表示一个类拥有另一个类对象:
cpp
class Tire
{
private:
string _brand = "Michelin";
};
class Car
{
private:
Tire _frontLeft;
Tire _frontRight;
Tire _rearLeft;
Tire _rearRight;
};
关系为:
text
Car has Tires
汽车拥有轮胎
汽车不是一种轮胎,因此不应该写:
cpp
class Car : public Tire
{
};
16.3 白盒复用和黑盒复用
继承有时被称为白盒复用。
派生类能够看到基类的公有和保护成员,容易依赖基类内部实现。基类一旦发生改变,派生类可能受到较大影响。
组合通常被称为黑盒复用。
组合类主要通过另一个对象的公有接口使用它,不需要了解其内部细节,耦合度通常更低。
16.4 优先组合,但不是拒绝继承
"优先组合而不是继承"不是说继承不能使用。
更准确的理解是:
text
只有确实存在稳定的 is-a 关系时,才使用 public 继承。
只是为了复用实现时,优先考虑组合。
例如,栈内部使用 vector:
cpp
template<class T>
class Stack
{
public:
void push(const T& value)
{
_data.push_back(value);
}
void pop()
{
_data.pop_back();
}
T& top()
{
return _data.back();
}
private:
vector<T> _data;
};
这里:
text
Stack has a vector
比:
text
Stack is a vector
更符合实际语义。
16.5 一个简单判断流程
设计类关系时,可以依次问:
text
1. 派生类能否自然替代基类?
能:考虑 public 继承
2. 当前类只是需要另一个类提供能力?
是:考虑组合
3. 只是为了复用几段实现代码?
是:优先组合或普通成员函数复用
4. 是否需要运行时多态?
是:通常需要基类接口和 public 继承
十七、常见错误整理
17.1 在构造函数体中"调用"基类构造
错误:
cpp
Student(const string& name)
{
Person(name);
}
正确:
cpp
Student(const string& name)
: Person(name)
{
}
17.2 手动写拷贝构造却忘记基类部分
错误:
cpp
Student(const Student& other)
: _studentId(other._studentId)
{
}
正确:
cpp
Student(const Student& other)
: Person(other)
, _studentId(other._studentId)
{
}
17.3 派生类赋值时只复制自己的成员
错误:
cpp
Student& operator=(const Student& other)
{
_studentId = other._studentId;
return *this;
}
正确:
cpp
Student& operator=(const Student& other)
{
if (this != &other)
{
Person::operator=(other);
_studentId = other._studentId;
}
return *this;
}
17.4 多态基类没有虚析构函数
如果会通过基类指针删除派生对象,应当使用:
cpp
virtual ~Base() = default;
17.5 把友元理解成可继承权限
基类的友元不是派生类的友元。
每个类都需要单独授予友元权限。
17.6 认为每个对象都有一份 static 成员
静态成员属于类,不属于具体对象。继承体系中引用的是同一个基类静态成员声明。
17.7 用类域解决菱形继承后认为问题全部消失
指定:
cpp
assistant.Student::_name
只能解决访问路径二义性。
普通菱形继承中的两份 Person 数据仍然存在。
17.8 依赖多继承对象的具体内存排列
不同编译器和 ABI 可能采用不同布局。
正确做法是通过语言提供的指针转换访问基类子对象,而不是手工计算偏移。
17.9 为了复用实现随意使用 public 继承
public 继承会建立公开的类型替代关系。
如果只需要另一个类提供的功能,组合通常更合适。
总结
继承进阶部分的规则虽然多,但都可以从"派生类对象包含基类子对象"这一点出发理解。
需要重点掌握:
- 构造派生类时,必须先初始化基类子对象
- 手动实现拷贝构造和赋值时,要同时处理基类部分
- 构造顺序是基类到派生类,析构顺序相反
- 通过基类指针删除派生对象时,基类通常需要虚析构函数
- final 可以明确禁止一个类继续被继承
- 友元关系不会继承,静态成员在继承体系中共享
- 多继承可能产生名字二义性和指针调整
- 普通菱形继承会产生基类数据冗余
- 虚继承让多个路径共享一个虚基类子对象
- 虚基类由最派生类负责初始化
- public 继承表达 is-a,组合表达 has-a
- 仅为了复用实现时,通常优先考虑组合
继承本身不是越多越好。
一个清晰、稳定的类层次可以减少重复并支持多态;一个勉强拼出来的继承体系,则会让访问权限、对象生命周期和依赖关系越来越复杂。
设计继承关系时,先判断类型语义,再考虑代码复用,通常会更加稳妥。