在C++中,继承与多态是实现代码复用和灵活性的重要机制。继承允许子类获取父类的属性和方法,从而重用已有代码;多态则让我们可以用统一的接口调用不同子类的实现。下面我们从基础语法、典型用法、常见陷阱以及最佳实践四个角度,系统梳理这两大概念。
一、继承的基本语法
class Base {
public:
void show() const { std::cout << "Base::show" << std::endl; }
protected:
int value;
};
class Derived : public Base { // 公开继承
public:
void show() const { std::cout << "Derived::show" << std::endl; }
void setValue(int v) { value = v; } // 访问受保护成员
};
public、protected、private控制访问权限。公开继承保持父类的公共成员不变;受保护继承将父类公共成员变为受保护;私有继承则把所有成员都变为私有。- 默认继承是
private,在类内部写class Derived : Base {}时,所有公共和受保护成员都变成私有。
二、多态的实现与使用
1. 虚函数
class Base {
public:
virtual void speak() const { std::cout << "Base" << std::endl; }
virtual ~Base() {} // 虚析构,防止内存泄漏
};
class Derived : public Base {
public:
void speak() const override { std::cout << "Derived" << std::endl; }
};
virtual声明在基类中,子类使用override确认覆写,帮助编译器捕获错误。- 虚函数表(vtable)使得运行时可以决定调用哪个函数。
2. 纯虚函数与抽象类
class Shape {
public:
virtual double area() const = 0; // 纯虚函数
};
class Circle : public Shape {
double r;
public:
Circle(double rad) : r(rad) {}
double area() const override { return 3.1415926535 * r * r; }
};
- 纯虚函数使类成为抽象类,无法直接实例化。
- 子类必须实现所有纯虚函数,才能成为可实例化的类。
3. 多重继承
class Flyer {
public:
void fly() { std::cout << "Flying" << std::endl; }
};
class Swimmer {
public:
void swim() { std::cout << "Swimming" << std::endl; }
};
class Duck : public Flyer, public Swimmer { };
- 注意 菱形继承:若多个基类共享同一祖先,需使用虚继承 (
virtual) 避免重复子对象。
class Animal { public: void breathe() {} };
class Mammal : virtual public Animal { };
class Bird : virtual public Animal { };
class Bat : public Mammal, public Bird { }; // 只包含一份 Animal
三、常见陷阱与错误
-
忘记虚析构
通过基类指针删除派生对象时,若基类没有虚析构,析构函数只会调用基类析构,导致资源泄漏。 -
不安全的类型转换
直接使用static_cast<Derived*>(ptr)从基类指针转换,若对象实际不是Derived,行为未定义。推荐使用dynamic_cast并检查返回值。 -
多态失效
- 对象切片:
Derived d; Base b = d;只保留基类部分,b.speak()调用的是基类实现。 - 非虚函数:如果忘记在基类声明为
virtual,多态不会生效。
- 对象切片:
-
虚函数表开销
虚函数表在某些小型嵌入式系统会占用大量内存;如果性能极端敏感,可考虑纯函数或模板。
四、最佳实践
| 主题 | 推荐做法 |
|---|---|
| 继承层次 | 尽量保持浅层(≤2层),避免深层继承导致维护困难 |
| 多态接口 | 定义接口(纯虚类)时只保留必要方法,保持兼容性 |
| 命名规范 | 采用 I 前缀(如 IShape)表示接口,区分实现类 |
| 虚构造 | 所有可通过基类指针删除的类都应提供虚析构 |
使用override |
每次覆写时加 override,让编译器检查 |
final 修饰符 |
防止类被再次继承或方法被再次覆写,提升安全性 |
| 移动语义 | 对于大型对象,优先使用移动构造和移动赋值,避免不必要的拷贝 |
| 模板与 CRTP | 对于需要静态多态的情况,可使用模板/CRTP 替代虚函数,提升性能 |
五、实际案例:实现一个插件系统
class Plugin {
public:
virtual void init() = 0;
virtual void shutdown() = 0;
virtual const char* name() const = 0;
virtual ~Plugin() {}
};
class AudioPlugin : public Plugin {
public:
void init() override { std::cout << "Audio init" << std::endl; }
void shutdown() override { std::cout << "Audio shutdown" << std::endl; }
const char* name() const override { return "Audio"; }
};
class VideoPlugin : public Plugin {
public:
void init() override { std::cout << "Video init" << std::endl; }
void shutdown() override { std::cout << "Video shutdown" << std::endl; }
const char* name() const override { return "Video"; }
};
int main() {
std::vector<std::unique_ptr<Plugin>> plugins;
plugins.emplace_back(std::make_unique <AudioPlugin>());
plugins.emplace_back(std::make_unique <VideoPlugin>());
for (auto& p : plugins) p->init();
// 运行时使用
for (auto& p : plugins) p->shutdown();
}
通过统一的 Plugin 接口,主程序可以在不知道具体插件实现的情况下进行初始化、调用和关闭。继承与多态使得添加新插件时,只需实现接口即可。
结语
继承与多态是C++面向对象设计的核心,正确使用它们可以让代码更模块化、可维护、可扩展。然而,滥用或误用同样会导致层次过深、运行时开销大、代码难以理解。本文通过语法、示例、陷阱与最佳实践,帮助你在实际项目中高效、安全地运用继承与多态。祝你编码愉快!