在C++中,公有继承(public inheritance)从来都不是一个简单的语法糖,它实际上承载着两种截然不同但又紧密相关的责任:接口继承和实现继承。理解这个双重性质是掌握面向对象设计的关键。
接口继承意味着派生类承诺"我能做基类能做的所有事情"。这是一种契约,保证派生类对象在任何需要基类对象的地方都能正常工作。而实现继承则更具体,它决定了派生类如何完成这些操作。这两种继承的混合使用,直接体现在基类成员函数的不同声明方式上。
举个例子,考虑一个图形绘制系统:
cpp复制class Shape {
public:
virtual void draw() const = 0; // 纯虚函数
virtual void rotate(double angle); // 虚函数
Point center() const; // 非虚函数
};
在这个设计中,draw()是纯虚函数,强制所有派生类必须实现自己的绘制逻辑;rotate()提供了默认实现但允许派生类覆盖;center()则是一个所有派生类都必须一致使用的实现。
纯虚函数通过=0语法明确表示:"你必须实现这个功能,但我不知道具体怎么实现"。这种声明方式在设计中特别有用,当我们知道所有派生类都需要某个操作,但基类无法提供有意义的默认实现时。
典型应用场景:
代码示例:
cpp复制class DatabaseAccess {
public:
virtual void connect() = 0;
virtual void disconnect() = 0;
virtual std::string query(const std::string& sql) = 0;
};
这个数据库访问接口要求所有具体数据库实现(如MySQL、PostgreSQL)都必须提供连接、断开连接和查询功能。
注意:虽然纯虚函数可以有实现(通过
ClassName::function()方式调用),但这种用法相对少见,通常用于提供一些基础功能片段供派生类复用。
虚函数提供了"最好自定义,但也可以使用默认"的灵活方案。这种设计在框架开发中特别常见,框架提供默认行为,但允许应用开发者根据需要覆盖。
风险警示:
cpp复制class Logger {
public:
virtual void write(const std::string& msg) {
// 默认写入控制台
std::cout << msg << std::endl;
}
};
class FileLogger : public Logger {
// 忘记重写write方法!
};
上面的设计存在严重问题:如果开发者忘记重写write方法,FileLogger会错误地使用控制台输出而非文件输出。
更安全的模式:
cpp复制class Logger {
public:
void log(const std::string& msg) { // 非虚接口
doWrite(msg); // 调用真正的实现
}
private:
virtual void doWrite(const std::string& msg) = 0; // 真正的实现
};
class ConsoleLogger : public Logger {
void doWrite(const std::string& msg) override {
std::cout << msg << std::endl;
}
};
这种模式被称为"模板方法模式"或"NVI(Non-Virtual Interface)",它确保了接口的非虚性,同时保留了实现的可定制性。
非虚函数代表的是"所有派生类都必须一致遵守"的行为。这类函数通常表示那些不应该随派生类变化的核心功能。
典型用例:
objectID())cpp复制class Account {
public:
double balance() const { return balance_; } // 非虚函数
virtual void withdraw(double amount); // 虚函数
private:
double balance_;
};
在这个账户类中,查询余额的操作应该是统一的,不论是什么类型的账户(储蓄账户、支票账户等),因此balance()被设计为非虚函数。
一个经常被忽视但至关重要的规则是:任何作为基类使用的类都应该声明虚析构函数。如果没有这样做,通过基类指针删除派生类对象会导致未定义行为。
错误示例:
cpp复制class Base {
public:
~Base() { /* 非虚析构函数 */ }
};
class Derived : public Base { /*...*/ };
Base* ptr = new Derived();
delete ptr; // 未定义行为!
正确做法:
cpp复制class Base {
public:
virtual ~Base() = default; // 虚析构函数
};
现代C++提供了更精细的控制虚函数重写的机制:
override:明确表示要重写基类虚函数final:禁止进一步重写cpp复制class Base {
public:
virtual void foo();
virtual void bar() final; // 禁止派生类重写
};
class Derived : public Base {
public:
void foo() override; // 明确表示重写
// void bar() override; // 错误!bar是final的
};
虽然虚函数调用比普通函数调用稍慢(需要间接跳转),但在现代CPU上这种开销通常可以忽略。真正需要关注的是虚函数对编译器优化的阻碍:
优化建议:
在设计框架时,通常建议采用"接口与实现分离"的原则:
cpp复制// 接口类
class Drawable {
public:
virtual ~Drawable() = default;
virtual void draw() const = 0;
};
// 实现类
class Circle : public Drawable {
public:
void draw() const override { /*...*/ }
};
虚函数是实现插件架构的核心技术:
cpp复制// 核心系统定义的接口
class Plugin {
public:
virtual ~Plugin() = default;
virtual void initialize() = 0;
virtual void execute() = 0;
};
// 插件实现的示例
class MyPlugin : public Plugin {
public:
void initialize() override { /*...*/ }
void execute() override { /*...*/ }
};
虽然虚函数很强大,但不应滥用。以下情况应该避免使用虚函数:
std::function和lambda在某些场景下,可以使用std::function替代虚函数实现更灵活的回调:
cpp复制class Button {
public:
using ClickHandler = std::function<void()>;
void setClickHandler(ClickHandler handler) {
handler_ = std::move(handler);
}
void click() {
if (handler_) handler_();
}
private:
ClickHandler handler_;
};
C++20引入的概念(concepts)提供了另一种实现多态的方式:
cpp复制template <typename T>
concept Drawable = requires(T t) {
{ t.draw() } -> std::same_as<void>;
};
template <Drawable T>
void render(const T& obj) {
obj.draw();
}
类型擦除(如std::any、std::variant)也可以在某些场景下替代传统的继承体系:
cpp复制class AnyDrawable {
struct Concept {
virtual ~Concept() = default;
virtual void draw_() const = 0;
};
template <typename T>
struct Model : Concept {
Model(T t) : data(std::move(t)) {}
void draw_() const override { data.draw(); }
T data;
};
std::unique_ptr<Concept> ptr_;
public:
template <typename T>
AnyDrawable(T t) : ptr_(std::make_unique<Model<T>>(std::move(t))) {}
void draw() const { ptr_->draw_(); }
};
在实际工程中,选择继承还是替代方案应该基于具体需求、性能要求和团队习惯综合考虑。虚函数和继承体系仍然是C++中实现运行时多态最直接和高效的方式,特别是在需要明确的接口契约和层次关系的场景中。