第二十九天:重载、重写和覆盖
重载(Overloading)、重写(Overriding,也叫覆盖)
- 重载(Overloading)、重写(Overriding,也叫覆盖)是重要的概念,它们在函数多态性和继承体系中有不同的表现和用途:
重载(Overloading)详解
-
定义:函数重载(Function Overloading)是静态多态(编译时多态)的典型体现,它允许程序员在同一个作用域(如类作用域或命名空间)内定义多个具有相同名称但不同参数列表的函数。编译器会根据调用时传入的实际参数来决定调用哪个具体实现。具体区分标准包括:
- 参数个数不同(如
func(int)
和func(int, int)
) - 参数类型不同(如
func(int)
和func(double)
) - 参数顺序不同(如
func(int, double)
和func(double, int)
)
重要注意事项:
- 仅返回类型不同(如
int func()
和void func()
)不能作为重载依据,会导致编译错误 - 对于成员函数,const修饰可以形成重载(如
void func() const
和void func()
) - 典型用途是为功能相似但需要处理不同数据类型的操作提供统一接口
- 参数个数不同(如
-
典型应用场景:
-
数学运算函数
- 实现支持多种数值类型的运算,如:
int add(int a, int b); double add(double a, double b);
- 可以扩展支持复数、矩阵等特殊类型的运算
- 实现支持多种数值类型的运算,如:
-
构造函数重载
- 提供多种对象初始化方式,如:
class Person { public:Person(); // 默认构造Person(string name); // 单参数构造Person(string name, int age); // 多参数构造 };
- 提供多种对象初始化方式,如:
-
输入/输出操作
- 处理不同类型数据的I/O,如C++的
<<
运算符重载:ostream& operator<<(ostream& os, int i); ostream& operator<<(ostream& os, double d);
- 可扩展到自定义类型的输出格式化
- 处理不同类型数据的I/O,如C++的
-
工具类方法
- 如字符串处理函数支持不同参数形式:
String substring(int begin); String substring(int begin, int end);
- 如字符串处理函数支持不同参数形式:
-
图形绘制API
- 支持多种绘制方式:
def draw(shape: Circle): def draw(shape: Rectangle):
- 支持多种绘制方式:
-
-
完整示例代码:
#include <iostream>
#include <string>class Printer {
public:// 重载print函数void print(int i) {std::cout << "Printing int: " << i << std::endl;}void print(double f) {std::cout << "Printing float: " << f << std::endl;}void print(const std::string& s) {std::cout << "Printing string: " << s << std::endl;}// const成员函数重载void display() const {std::cout << "Const display" << std::endl;}void display() {std::cout << "Non-const display" << std::endl;}
};int main() {Printer prt;prt.print(5); // 调用print(int)prt.print(3.14); // 调用print(double)prt.print("Hello"); // 调用print(string)const Printer cprt;cprt.display(); // 调用const版本prt.display(); // 调用非const版本return 0;
}
重写(Overriding,覆盖)详解
-
详细定义:重写(Override)是面向对象编程中实现动态多态(运行时多态)的核心机制,它允许派生类重新定义基类的虚函数实现。必须严格满足以下技术条件:
- 继承关系:必须存在于具有明确继承关系的类体系中(如 class Derived : public Base)
- 虚函数声明:基类中的目标函数必须使用virtual关键字显式声明(例如:virtual void draw() const)
- 函数签名一致性:派生类中的重写函数必须保持与基类完全相同的:
- 函数名称(区分大小写)
- 参数列表(参数类型、数量和顺序)
- 返回类型(除协变返回类型特例外)
- 访问权限灵活性:虽然语法允许派生类修改访问限定符(如基类protected改为public),但会破坏设计一致性,因此不推荐这种实践
-
深入解析重要特性:
-
协变返回类型:
- 允许派生类重写函数返回基类返回类型的派生类
- 典型应用场景:克隆模式
class Base { public:virtual Base* clone() const { return new Base(*this); } }; class Derived : public Base { public:Derived* clone() const override { return new Derived(*this); } // 协变返回 };
-
final关键字(C++11引入):
- 用法示例:
class Base { public:virtual void api() final; // 禁止所有派生类重写 }; class Derived : public Base {// void api() override; // 编译错误 };
- 设计意义:保护关键接口不被意外修改,增强代码稳定性
-
纯虚函数(抽象函数):
- 语法特征:在声明后添加=0标记(virtual void pure() = 0)
- 强制实现要求:使类成为抽象类,派生类必须实现所有纯虚函数才能实例化
- 典型设计模式应用:作为接口类的核心实现方式(如策略模式中的策略接口)
-
-
补充注意事项:
- 与重载(overload)的本质区别:重写是运行时行为,重载是编译期行为
- override关键字(C++11):显式标记重写关系,增强代码可读性并防止意外隐藏(name hiding)
- 动态绑定的实现原理:通过虚函数表(vtable)实现运行时函数地址解析
-
完整示例代码:
#include <iostream>
#include <memory>class Shape {
public:virtual ~Shape() = default;virtual double area() const = 0; // 纯虚函数virtual void draw() const {std::cout << "Drawing shape" << std::endl;}virtual Shape* clone() const = 0; // 用于协变示例
};class Circle : public Shape {double radius;
public:explicit Circle(double r) : radius(r) {}// 重写纯虚函数double area() const override {return 3.14159 * radius * radius;}// 重写虚函数void draw() const override final { // 使用final禁止后续重写std::cout << "Drawing circle with radius " << radius << std::endl;}// 协变返回类型示例Circle* clone() const override {return new Circle(*this);}
};class Rectangle : public Shape {double width, height;
public:Rectangle(double w, double h) : width(w), height(h) {}double area() const override {return width * height;}void draw() const override {std::cout << "Drawing rectangle " << width << "x" << height << std::endl;}Rectangle* clone() const override {return new Rectangle(*this);}
};int main() {// 多态调用示例std::unique_ptr<Shape> shapes[] = {std::make_unique<Circle>(5.0),std::make_unique<Rectangle>(4.0, 6.0)};for (const auto& shape : shapes) {shape->draw();std::cout << "Area: " << shape->area() << std::endl;// 克隆测试协变auto cloned = shape->clone();cloned->draw();delete cloned;}return 0;
}
重载与重写的深入对比
特性 | 重载(Overloading) | 重写(Overriding) |
---|---|---|
作用域 | 同一作用域(类内或全局) | 继承体系中的不同类 |
函数关系 | 同名函数 | 派生类函数与基类虚函数 |
参数要求 | 必须不同 | 必须相同 |
返回类型 | 可以不同 | 必须相同(允许协变) |
virtual关键字 | 不需要 | 基类函数必须为virtual |
多态时机 | 编译时决定 | 运行时决定 |
const修饰 | 可以构成重载 | 不能仅靠const构成重写 |
默认参数 | 可以有不同默认值 | 必须保持相同默认值 |
访问权限 | 可以不同 | 可以不同(但不建议) |
异常规范 | 可以不同 | 派生类不能比基类抛出更多异常 |
实际开发建议:
-
对于重载:
- 保持重载函数的功能一致性,确保所有重载版本都实现相同的核心功能,只是参数不同。例如,一个计算面积的函数可能有多个重载版本,但都应该返回面积值。
- 避免过度重载导致代码混淆。建议一个函数名最多不超过5个重载版本,否则应考虑使用不同的函数名或重构代码结构。
- 在重载函数间保持参数顺序和类型的一致性,例如所有重载版本都把必选参数放在前面。
-
对于重写:
- 从C++11开始,始终使用override关键字明确表示函数重写。这不仅提高代码可读性,还能让编译器检查重写是否正确。
- 如果基类可能被继承,必须将析构函数声明为virtual,确保通过基类指针删除派生类对象时能正确调用派生类的析构函数。
- 对于不应被进一步重写的函数,使用final修饰符。这在设计框架类时特别有用,可以锁定关键接口的实现。
- 重写时要遵循Liskov替换原则,确保派生类对象可以替代基类对象使用而不会产生意外行为。
-
在大型项目中:
- 使用静态分析工具(如Clang-Tidy、Coverity等)定期检查重写是否正确实现。这些工具可以检测出忘记override关键字、签名不匹配等问题。
- 建立代码审查机制,特别关注重写函数的实现是否符合预期。
- 考虑使用单元测试来验证重写行为,特别是多态调用时的表现。
- 在文档中明确记录哪些函数可以被重写,以及重写时需要遵循的约束条件。