1. 继承的概念与基本语法
在C++面向对象编程中,继承是最核心的特性之一。它允许我们基于已有的类创建新类,新类将自动获得父类的属性和方法。这种机制完美体现了代码复用的思想,也是实现多态的基础。
先看一个最简单的继承示例:
cpp复制class Animal {
public:
void eat() {
cout << "I can eat!" << endl;
}
};
class Dog : public Animal {
public:
void bark() {
cout << "I can bark! Woof woof!" << endl;
}
};
这里Dog类通过: public Animal的语法继承了Animal类。冒号后面的public表示继承方式,这决定了父类成员在子类中的访问权限。继承方式有三种:
- public继承:父类的public成员在子类中保持public,protected保持protected
- protected继承:父类的public和protected成员在子类中都变为protected
- private继承:父类的所有成员在子类中都变为private
提示:实际开发中public继承最常用,因为它保持了"is-a"的语义关系。其他继承方式会破坏这种关系,应当谨慎使用。
2. 继承中的构造函数与析构函数
继承关系中,对象的构造和析构遵循特定顺序,这是理解继承机制的关键点。
2.1 构造函数的调用顺序
当创建子类对象时:
- 先调用父类的构造函数
- 然后调用子类成员对象的构造函数(按声明顺序)
- 最后调用子类自身的构造函数
示例:
cpp复制class Base {
public:
Base() { cout << "Base constructor" << endl; }
};
class Member {
public:
Member() { cout << "Member constructor" << endl; }
};
class Derived : public Base {
Member m;
public:
Derived() { cout << "Derived constructor" << endl; }
};
// 输出顺序:
// Base constructor
// Member constructor
// Derived constructor
2.2 析构函数的调用顺序
与构造函数完全相反:
- 先调用子类自身的析构函数
- 然后调用子类成员对象的析构函数
- 最后调用父类的析构函数
这种相反的调用顺序确保了资源被正确释放。
2.3 构造函数初始化列表
子类构造函数需要通过初始化列表显式调用父类构造函数:
cpp复制class Base {
int value;
public:
Base(int v) : value(v) {}
};
class Derived : public Base {
public:
Derived(int v) : Base(v) { // 必须通过初始化列表调用
// 其他初始化
}
};
注意:如果父类没有默认构造函数,子类必须显式调用父类的某个构造函数,否则会编译错误。
3. 函数重写与多态
继承不仅仅是代码复用,更重要的是实现多态。这是通过虚函数(virtual function)机制实现的。
3.1 虚函数与重写
在父类中将函数声明为virtual,子类中可以重写(override)该函数:
cpp复制class Shape {
public:
virtual void draw() {
cout << "Drawing a shape" << endl;
}
};
class Circle : public Shape {
public:
void draw() override {
cout << "Drawing a circle" << endl;
}
};
使用override关键字(C++11引入)可以明确表示这是重写父类函数,如果签名不匹配会报错,避免意外创建新函数。
3.2 多态的实现原理
多态通过虚函数表(vtable)实现:
- 每个包含虚函数的类都有一个vtable
- 每个对象包含一个指向vtable的指针(vptr)
- vtable中存储了虚函数的实际地址
当通过基类指针或引用调用虚函数时,会根据实际对象的vptr找到正确的函数实现。
3.3 纯虚函数与抽象类
纯虚函数使类成为抽象类,不能实例化:
cpp复制class Shape {
public:
virtual void draw() = 0; // 纯虚函数
};
class Circle : public Shape {
public:
void draw() override {
cout << "Drawing a circle" << endl;
}
};
抽象类强制子类实现特定接口,是设计模式中常用的技巧。
4. 多重继承与菱形问题
C++支持多重继承,即一个类可以继承多个父类。这带来了强大的表达能力,但也引入了复杂性。
4.1 基本多重继承
cpp复制class A {
public:
void funcA() {}
};
class B {
public:
void funcB() {}
};
class C : public A, public B {
// 拥有funcA和funcB
};
4.2 菱形继承问题
当继承关系形成菱形时,会产生二义性:
code复制 A
/ \
B C
\ /
D
D会包含两份A的成员,导致访问歧义。
4.3 虚继承解决方案
使用虚继承可以解决菱形问题:
cpp复制class A {};
class B : virtual public A {};
class C : virtual public A {};
class D : public B, public C {};
虚继承确保D中只有一份A的成员,但会增加对象大小和访问开销。
实际建议:尽量避免多重继承,特别是复杂的菱形继承。多数情况下,单继承加接口(纯虚类)是更好的选择。
5. 继承中的访问控制
理解继承中的访问控制对设计健壮的类层次结构至关重要。
5.1 三种访问修饰符
- public:任意位置可访问
- protected:仅类内和子类可访问
- private:仅类内可访问
5.2 继承方式的影响
继承方式影响父类成员在子类中的可见性:
| 父类成员 | 继承方式 | 子类中的访问性 |
|---|---|---|
| public | public | public |
| public | protected | protected |
| public | private | private |
| protected | public | protected |
| protected | protected | protected |
| protected | private | private |
| private | 任意 | 不可访问 |
5.3 使用using改变访问权限
子类可以使用using声明改变继承成员的访问权限:
cpp复制class Base {
protected:
void protectedFunc() {}
};
class Derived : public Base {
public:
using Base::protectedFunc; // 提升为public
};
6. 继承与组合的选择
继承(is-a)和组合(has-a)是两种主要的代码复用方式,需要根据场景合理选择。
6.1 继承适用场景
- 子类确实是父类的特殊类型
- 需要利用多态特性
- 子类需要扩展而非修改父类行为
6.2 组合适用场景
- 只是需要使用某个类的功能
- 需要动态更换组件
- 避免过度复杂的继承层次
6.3 LSP原则
里氏替换原则(Liskov Substitution Principle)指出:子类对象应该能够替换父类对象而不影响程序正确性。这是判断继承关系是否合理的重要标准。
7. 现代C++中的继承特性
C++11/14/17引入了若干改进继承机制的特性。
7.1 override和final
- override:明确表示重写虚函数
- final:禁止类被继承或虚函数被重写
cpp复制class Base {
public:
virtual void func() {}
};
class Derived : public Base {
public:
void func() override final {} // 重写并禁止进一步重写
};
class MostDerived : public Derived {
// 不能重写func()
};
7.2 委托构造函数
C++11允许构造函数调用同类其他构造函数,减少重复代码:
cpp复制class Base {
int x, y;
public:
Base(int a) : x(a), y(0) {}
Base(int a, int b) : Base(a) { y = b; }
};
7.3 继承构造函数
C++11允许继承父类构造函数:
cpp复制class Base {
public:
Base(int);
Base(int, double);
};
class Derived : public Base {
public:
using Base::Base; // 继承Base的所有构造函数
};
8. 继承的常见陷阱与最佳实践
8.1 切片问题
将子类对象赋值给父类对象会导致"切片"——子类特有部分被切掉:
cpp复制Derived d;
Base b = d; // 切片,丢失Derived特有成员
解决方案:使用指针或引用。
8.2 虚析构函数
如果类可能被继承,析构函数应该声明为virtual,否则通过基类指针删除子类对象会导致未定义行为:
cpp复制class Base {
public:
virtual ~Base() {} // 虚析构函数
};
8.3 避免过度继承
过深的继承层次会增加复杂性,一般建议不超过3层。
8.4 优先使用组合
"组合优于继承"是面向对象设计的重要原则,组合提供了更大的灵活性。
9. 继承在项目中的应用实例
让我们通过一个实际项目案例来理解继承的应用。
9.1 图形编辑器设计
考虑一个图形编辑器,支持多种图形:
cpp复制class Shape {
protected:
Point center;
Color color;
public:
virtual void draw() = 0;
virtual void resize(double factor) = 0;
virtual ~Shape() {}
};
class Circle : public Shape {
double radius;
public:
void draw() override { /* 实现 */ }
void resize(double factor) override { radius *= factor; }
};
class Rectangle : public Shape {
double width, height;
public:
void draw() override { /* 实现 */ }
void resize(double factor) override {
width *= factor;
height *= factor;
}
};
9.2 工厂模式应用
使用工厂方法创建具体图形:
cpp复制class ShapeFactory {
public:
virtual Shape* createShape() = 0;
};
class CircleFactory : public ShapeFactory {
public:
Shape* createShape() override {
return new Circle();
}
};
10. 继承与设计模式
许多设计模式都基于继承机制,下面介绍几个典型例子。
10.1 模板方法模式
在父类中定义算法框架,子类实现具体步骤:
cpp复制class DataProcessor {
public:
void process() {
loadData();
analyze();
saveResult();
}
protected:
virtual void loadData() = 0;
virtual void analyze() = 0;
void saveResult() { /* 通用实现 */ }
};
class CSVProcessor : public DataProcessor {
protected:
void loadData() override { /* CSV特有实现 */ }
void analyze() override { /* CSV特有实现 */ }
};
10.2 策略模式
通过继承实现可互换的算法:
cpp复制class SortStrategy {
public:
virtual void sort(vector<int>&) = 0;
};
class QuickSort : public SortStrategy {
public:
void sort(vector<int>& data) override { /* 快速排序实现 */ }
};
class MergeSort : public SortStrategy {
public:
void sort(vector<int>& data) override { /* 归并排序实现 */ }
};
10.3 装饰器模式
通过继承扩展对象功能:
cpp复制class Stream {
public:
virtual void write(string data) = 0;
};
class FileStream : public Stream {
public:
void write(string data) override { /* 文件写入 */ }
};
class BufferedStream : public Stream {
Stream* stream;
public:
BufferedStream(Stream* s) : stream(s) {}
void write(string data) override {
// 添加缓冲功能
stream->write(data);
}
};
11. 继承与性能考量
使用继承特别是虚函数会带来一定的性能开销,需要权衡。
11.1 虚函数调用开销
虚函数调用比普通函数调用多一次间接寻址,大约有10-20%的性能损失。在性能关键代码中应避免过度使用。
11.2 对象大小影响
包含虚函数的类会增加一个vptr指针的大小(通常4或8字节)。多重继承可能导致对象更大。
11.3 缓存友好性
深度继承层次可能影响缓存局部性,降低性能。扁平化的类结构通常更高效。
12. 继承的替代方案
现代C++提供了几种继承的替代方案。
12.1 类型擦除
使用std::function或void*等技术实现运行时多态而不需要继承:
cpp复制class AnyCallable {
std::function<void()> f;
public:
template<typename F>
AnyCallable(F&& func) : f(std::forward<F>(func)) {}
void operator()() { f(); }
};
12.2 CRTP模式
奇异递归模板模式(Curiously Recurring Template Pattern)实现编译时多态:
cpp复制template<typename T>
class Base {
public:
void interface() {
static_cast<T*>(this)->implementation();
}
};
class Derived : public Base<Derived> {
public:
void implementation() {
// 具体实现
}
};
12.3 概念与约束
C++20引入的概念(concepts)提供了另一种抽象机制:
cpp复制template<typename T>
concept Drawable = requires(T t) {
{ t.draw() } -> std::same_as<void>;
};
template<Drawable T>
void render(T&& obj) {
obj.draw();
}
13. 继承在标准库中的应用
C++标准库中有许多继承的典型应用。
13.1 输入输出流
iostream继承体系是经典的继承应用:
code复制ios_base → ios → istream/ostream → iostream
↑ ↑
ifstream ostringstream
13.2 异常体系
标准异常都继承自exception类:
code复制exception → logic_error → invalid_argument
→ runtime_error → overflow_error
13.3 STL容器分配器
std::allocator通过继承实现自定义分配器:
cpp复制template<typename T>
class MyAllocator : public std::allocator<T> {
// 自定义实现
};
14. 跨平台开发中的继承问题
在不同平台上使用继承时需要注意的一些问题。
14.1 二进制兼容性
虚函数表布局可能随编译器不同而变化,影响动态库的二进制兼容性。
14.2 异常处理
异常抛出和捕获的继承关系在不同平台上可能有不同表现。
14.3 动态加载
通过dlopen/LoadLibrary加载的类可能面临类型识别问题。
15. 继承的未来发展
C++标准的发展趋势对继承机制的影响。
15.1 契约编程
C++20引入的契约(Contracts)可能改变类继承的设计方式。
15.2 反射提案
静态反射提案将提供更多关于类继承关系的元信息。
15.3 模式匹配
模式匹配提案可能提供比虚函数更灵活的多态机制。
