一、概念

C++ 的多态机制主要通过三个核心概念实现,它们在编译和运行时有着截然不同的处理方式:

概念 定义 绑定时机 核心特征
函数重载 同一作用域内,函数名相同但参数列表不同的函数 编译时 静态多态,基于参数列表区分
函数重写 派生类中重新定义基类中的虚函数,函数签名完全相同 运行时 动态多态,基于对象实际类型调用
函数隐藏 派生类中定义的函数遮蔽基类中同名函数,无论参数是否相同 编译时 名称遮蔽,基类函数被隐藏

注:函数签名包括函数名、参数类型和顺序,不包括返回值类型

二、函数重载解析

函数重载是 C++ 实现静态多态的基础机制,允许在同一作用域内定义多个同名函数,通过参数列表的差异进行区分。

重载的实现原理

编译器在编译阶段会对重载函数进行名称修饰(Name Mangling),根据函数名和参数列表生成唯一的内部名称,因此重载函数在底层实际上拥有不同的标识符。

重载示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <iostream>
#include <string>

class Calculator {
public:
// 整数加法
int add(int a, int b) {
std::cout << "int add: ";
return a + b;
}

// 双精度浮点数加法(参数类型不同)
double add(double a, double b) {
std::cout << "double add: ";
return a + b;
}

// 三个整数加法(参数数量不同)
int add(int a, int b, int c) {
std::cout << "int add 3 params: ";
return a + b + c;
}

// 字符串拼接(参数类型不同)
std::string add(std::string a, std::string b) {
std::cout << "string add: ";
return a + b;
}
};

int main() {
Calculator calc;
std::cout << calc.add(2, 3) << std::endl; // 调用int版本
std::cout << calc.add(2.5, 3.7) << std::endl; // 调用double版本
std::cout << calc.add(1, 2, 3) << std::endl; // 调用3参数版本
std::cout << calc.add("Hello, ", "World!") << std::endl; // 调用string版本

return 0;
}

执行结果

1
2
3
4
int add: 5
double add: 6.2
int add 3 params: 6
string add: Hello, World!

重载的规则与限制

  • 必须满足:参数个数、类型或顺序至少有一个不同

  • 不能仅通过返回值类型不同来重载函数

  • 作用域限制:重载函数必须在同一作用域内

三、函数重写(覆盖)解析

函数重写是实现动态多态的核心机制,允许派生类重新实现基类中声明的虚函数。

重写的实现原理

C++ 通过虚函数表(vtable)和虚表指针(vptr)实现重写机制:

  • 每个包含虚函数的类都有一个虚函数表

  • 类的每个对象都包含一个指向该类虚函数表的指针

  • 当调用虚函数时,通过对象的虚表指针找到对应的虚函数表,再调用相应的函数

重写示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#include <iostream>

// 基类
class Shape {
public:
// 虚函数,可被重写
virtual void draw() const {
std::cout << "绘制基本形状" << std::endl;
}

// 纯虚函数,必须被派生类重写
virtual double area() const = 0;

// 虚析构函数,确保正确析构派生类对象
virtual ~Shape() {
std::cout << "Shape析构函数" << std::endl;
}
};

// 派生类Circle
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}

// 重写基类的draw函数
void draw() const override { // 使用override关键字明确表示重写
std::cout << "绘制圆形" << std::endl;
}

// 重写基类的area函数
double area() const override {
return 3.14159 * radius * radius;
}

~Circle() override {
std::cout << "Circle析构函数" << std::endl;
}
};

// 派生类Rectangle
class Rectangle : public Shape {
private:
double width;
double height;
public:
Rectangle(double w, double h) : width(w), height(h) {}

// 重写基类的draw函数
void draw() const override {
std::cout << "绘制矩形" << std::endl;
}

// 重写基类的area函数
double area() const override {
return width * height;
}

~Rectangle() override {
std::cout << "Rectangle析构函数" << std::endl;
}
};

int main() {
// 基类指针指向派生类对象
Shape* shape1 = new Circle(5.0);
Shape* shape2 = new Rectangle(4.0, 6.0);

// 动态绑定:调用的是对象实际类型的函数
shape1->draw(); // 输出"绘制圆形"
std::cout << "面积: " << shape1->area() << std::endl;

shape2->draw(); // 输出"绘制矩形"
std::cout << "面积: " << shape2->area() << std::endl;

delete shape1;
delete shape2;

return 0;
}

执行结果

1
2
3
4
5
6
7
8
绘制圆形
面积: 78.5397
绘制矩形
面积: 24
Circle析构函数
Shape析构函数
Rectangle析构函数
Shape析构函数

重写的规则与限制

  • 基类函数必须声明为virtual

  • 派生类函数必须与基类函数有完全相同的函数签名(名称、参数列表)

  • 派生类函数的返回值类型必须与基类函数相同,或为协变返回类型

  • C++11 引入override关键字,显式指明函数是重写基类虚函数,增强代码可读性并让编译器检查是否符合重写规则

四、函数隐藏解析

函数隐藏指派生类中的函数遮蔽了基类中同名函数,无论它们的参数列表是否相同。这是一种名称查找机制导致的现象。

隐藏的实现原理

编译器在查找函数名称时,会先在当前类的作用域中查找,如果找到匹配的名称,则不会继续在基类中查找,从而导致基类中的同名函数被隐藏。

隐藏示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <iostream>

class Base {
public:
void print(int x) {
std::cout << "Base::print(int): " << x << std::endl;
}

void show() {
std::cout << "Base::show()" << std::endl;
}
};

class Derived : public Base {
public:
// 情况1:函数名相同,参数不同 - 隐藏基类的print(int)
void print(double x) {
std::cout << "Derived::print(double): " << x << std::endl;
}

// 情况2:函数名相同,参数相同 - 隐藏基类的show()
void show() {
std::cout << "Derived::show()" << std::endl;
}
};

int main() {
Derived d;

// 调用Derived的print(double)
d.print(3.14); // 正确

// 尝试调用Base的print(int),但被隐藏
// d.print(10); // 编译错误:无法将int转换为double

// 必须显式指定作用域才能调用基类被隐藏的函数
d.Base::print(10); // 正确

// 调用Derived的show()
d.show(); // 正确

// 显式调用Base的show()
d.Base::show(); // 正确

// 基类指针指向派生类对象
Base* b = &d;
b->print(20); // 调用Base的print(int),因为非虚函数,静态绑定
b->show(); // 调用Base的show(),因为非虚函数,静态绑定

return 0;
}

执行结果

1
2
3
4
5
6
Derived::print(double): 3.14
Base::print(int): 10
Derived::show()
Base::show()
Base::print(int): 20
Base::show()

隐藏的规则与限制

  • 只要派生类中定义的函数与基类中的函数同名,无论参数是否相同,基类函数都会被隐藏

  • 通过派生类对象直接调用同名函数时,只会调用派生类中的版本

  • 要访问基类中被隐藏的函数,必须使用作用域解析运算符::

  • 对于非虚函数,即使通过基类指针调用,也只会根据指针类型(静态类型)调用相应类的函数

五、三者的核心区别对比

特性 函数重载 函数重写 函数隐藏
作用域 同一类中 基类与派生类之间 基类与派生类之间
函数名 相同 相同 相同
参数列表 不同 必须相同 可以相同或不同
基类函数要求 无特殊要求 必须是虚函数 无特殊要求
绑定方式 静态绑定(编译时) 动态绑定(运行时) 静态绑定(编译时)
调用依据 函数参数列表 对象实际类型 指针 / 引用的静态类型
关键字 override(C++11)
实现机制 名称修饰 虚函数表 名称查找规则

六、实际开发中的应用建议

函数重载的最佳实践

  1. 一致性原则:重载函数应实现相似或相关的功能
1
2
3
4
// 推荐:功能相似的重载
void print(int x);
void print(double x);
void print(const std::string& s);
  1. 避免过度重载:过多的重载版本会降低代码可读性
  2. 优先使用重载而非默认参数:当参数组合复杂时,重载更清晰

函数重写的最佳实践

  1. 始终使用override关键字:明确表示重写意图,让编译器帮助检查错误
1
2
3
4
5
// 推荐
void draw() const override;

// 不推荐
void draw() const; // 无法确定是重写还是新函数
  1. 基类析构函数应声明为虚函数:确保删除基类指针时能正确调用派生类析构函数

  2. 保持函数签名完全一致:包括const修饰符等细节

函数隐藏的注意事项

  1. 避免无意识的隐藏:派生类中定义与基类同名的函数时要格外小心

  2. 明确指定作用域:当需要调用基类中被隐藏的函数时,使用Base::function()

  3. 区分隐藏与重写:如果希望实现多态,应使用虚函数重写而非隐藏

总结

C++ 的重载、重写和隐藏机制是实现代码复用和多态的重要工具,它们各自有明确的应用场景和行为特征:

  • 重载用于在同一类中实现功能相似但参数不同的操作,提供编译时多态

  • 重写用于在继承体系中实现动态多态,使派生类可以自定义基类的行为

  • 隐藏是名称查找机制的自然结果,需谨慎使用以避免意外行为