多态
含义
- 同一论域中一个元素可有多种解释
- 在同一个上下文中,一个元素可以有多种不同的表现形式或行为方式。
- 允许程序以一种统一的方式处理不同类型的对象,从而提高代码的灵活性和可扩展性。
好处
- 提高语言灵活性
多态的实现方式
- 一名多用
- 函数重载
- 类属(泛型)
- 模板
- OO程序设计
- 继承和虚函数
重载
函数重载
cpp
void print(int x); // 一个版本
void print(double x); // 另一个版本
- 函数名同、参数的类型或数量不同
- 静态绑定
- 在编译时根据参数类型决定调用哪个函数
- 歧义控制
- 当多个重载函数可能匹配同一个调用时,编译器必须做出选择
- 按照如下顺序进行选择:
- 完美匹配
- 提升 (不会造成精度损失,如
int -> long,float -> double) - 标准转换(可能造成精度损失)
- 用户自定义转换(A 的构造函数的参数为 B)
- 如果无法确定唯一一个函数(比如存在两个同一等级),则不通过编译。
操作符重载
动机
- 让用户自定义的数据类型也能支持熟悉的运算符语义
- 提高代码可读性、直观性、可扩充性
- 提供结合性(例如
a + b + c,只要+重载返回一个类即可实现链式调用)
如何实现
- built_in 类型:由编译器预定义行为
- 自定义数据类型:由程序员自己定义
歧义控制
- 和函数重载的歧义控制一致,操作符重载本质上是特殊的函数重载
- 成员操作符重载和全局操作符重载会放在一起比较(C++ 经过 Cfront 编译后 C 语言后,不会有类的概念)
两个例子
重载 +
成员函数重载(可以省略第一个参数)
cpp
class Complex {
public:
Complex operator+(Complex& x); // 成员函数
};
Complex a(1,2), b(3,4);
Complex c = a + b; // OK
Complex d = b + a; // OK(对称)
全局函数重载(不能省略第一个参数)
cpp
class Complex {
double real, imag;
public:
Complex() { ... }
Complex(double r, double i) { ... }
friend Complex operator+(Complex& c1, Complex& c2); // 声明为友元
};
Complex operator+(Complex& c1, Complex& c2) {
Complex temp;
temp.real = c1.real + c2.real;
temp.imag = c1.imag + c2.imag;
return temp;
}
Complex a(1,2), b(3,4);
Complex c = a + b; // OK
Complex d = b + a; // OK(对称)
注意:全局操作符重载必须包含一个自定义类型!!!
绝对不能被重载的操作符
..*::?:
它们是语言的基础,一旦重载,整个语言的可靠性将崩溃。
重载的基本原则
- 重载方式
- 类成员函数重载
- 带有类参数的全局函数重载
- 遵循原有语义(否则语法错误)
- 单目/双目
- 优先级
- 结合性(左结合或右结合,比如
a = b = c为右结合,从右到左计算,等价于a = (b = c))
双目操作符重载
类成员函数重载
-
格式
cpp<ret type> operator # (<arg>)- 返回类型可以是
T或者T& - 参数个数为 1 个,因为隐含了
this #是你想要重载的操作符
- 返回类型可以是
-
调用方式
cppa # b; // 等价于: a.operator#(b); // 显式调用
全局函数形式重载
-
格式
cppfriend <ret type> operator # (<arg1>, <arg2>); // 必须声明为友元 <ret type> operator # (<arg1>, <arg2>) // 两个参数都需显式输入 -
示例
cppComplex a(1,2), b(3,4); Complex c = a + b; // 调用 operator+(a, b) -
注意
=,(),[],->不能作为全局函数重载 这些操作符的语义与对象内部状态紧密绑定,要求左操作数必须是该类类型的可修改对象,作为全局函数重载破坏了对象的完整性。- 全局函数重载两次即可实现交换律
cpp// 全局函数 1:支持 int + CL CL operator+(int i, CL& a) { return CL(i + a.count); // 假设 CL 构造函数接受 int } // 全局函数 2:支持 CL + int CL operator+(CL& a, int i) { return CL(a.count + i); } -
警告
- 不要重载
&&和||,因为这会破坏短路机制,可能引发严重 bug。
- 不要重载
单目操作符
类成员函数重载
cpp
<ret type> operator # ()
全局函数重载
cpp
<ret type> operator # (<arg>)
前置自增和后置自增
- 两者的区别:
++a:先加 1,再返回修改后的对象a++:加 1,但是返回旧值的副本
- 实现示例:
- 后置自增,加上一个哑元用以区分(没有其他任何用处)
cpp
class Counter {
int value;
public:
Counter() { value = 0; }
Counter& operator ++() { // ++a
value++;
return *this;
}
Counter operator ++(int) { // a++
Counter temp = *this;
value++;
return temp;
// 能不能返回 Counter&?绝对不行,temp 为栈上对象,一旦退出函数就会析构
}
};
特殊操作符重载
= 的重载
-
默认赋值操作符(编译器自动生成)
- 逐个成员赋值(类似默认拷贝函数)
- 数据成员中有对象则递归
-
赋值操作符不能被继承
=的行为与类的内存布局高度相关,子类可能有额外成员变量,继承父类的=,会导致额外成员未被赋值
-
返回值------
return *this- 以支持链式赋值(
a = b = c)
- 以支持链式赋值(
-
如果一个类中有额外申请的资源,必须重载赋值操作符(深拷贝和浅拷贝)
- 其实更广的应该遵从三法则 和五法则
-
三法则和五法则
- 三法则 :如果你显式定义了以下三个特殊成员函数中的任意一个,那么你很有可能需要重新定义全部三个
- 析构函数
- 拷贝构造函数
- 拷贝赋值操作符
- 五法则:加入移动构造函数和移动赋值操作符
- 为什么需要这样? 主要还是额外资源可能需要深拷贝,移动时将被移动的对象的额外资源置为空。
- 三法则 :如果你显式定义了以下三个特殊成员函数中的任意一个,那么你很有可能需要重新定义全部三个
-
避免自我赋值
cppclass A { int* ptr; public: A& operator=(const A& other) { delete ptr; // 1. 删除旧内存 ptr = new int(*other.ptr); // 2. 分配新内存 return *this; } }; // 考虑 a = a 的情况解决方法:加入证同测试(对象身份 object identity 是否相同,对象身份由对象内存地址决定)。
cppA& operator=(const A& other) { if (this != &other) { // 证同测试,查看地址是否相同 delete ptr; ptr = new int(*other.ptr); } return *this; }
[] 的重载
-
读写控制(返回非 const 引用还是 const 引用)
- 例子
cppclass string { char* p; // 动态分配的字符数组 public: string(char* p1) { p = new char[strlen(p1)+1]; strcpy(p, p1); } virtual ~string() { delete[] p; } // 1. 非常量版本:允许修改 char& operator[](int i) { return p[i]; } // 2. 常量版本:只读访问 const char& operator[](int i) const { return p[i]; } }; string s("aacd"); // 创建可变字符串 s[2] = 'b'; // ✅ 合法!调用非 const 版本 cout << s[0]; // ✅ 输出 'a' const string cs("const"); // 创建常量字符串 cout << cs[0]; // ✅ 合法!调用 const 版本 cs[0] = 'D'; // ❌ 编译错误!不能修改 const 对象- 关键点
- 如果没有常量版本,那么
const string cs("const"); cout << cs[0];会报错,因为非 const 引用不能绑定 const 变量。 - 所以 const string 则会自动调用常量版本,多个重载函数只需要有一个符合即可。
- 如果没有常量版本,那么
-
如何实现多维数组访问
- C++ 中用一维数组标识多维数组,类似如下的方式无法解决多维数组访问的问题
cppclass Array2D { int n1, n2; int* p; // 指向整个二维数组的连续内存 public: Array2D(int i, int j) : n1(i), n2(j) { p = new int[n1*n2]; } virtual ~Array2D() { delete[] p; } int& operator[](int i) { return p[i]; } // ❌ 只能返回 int& };- 解决方案:使用 Array1D 代理类
cppclass Array1D { int* q; // 指向某一行的数据 public: int& operator[](int j) { return q[j]; } }; class Array2D { int n1, n2; int* p; // 指向整个二维数组的连续内存 public: Array2D(int i, int j) : n1(i), n2(j) { p = new int[n1*n2]; } virtual ~Array2D() { delete[] p; } // 关键:返回 Array1D 对象! Array1D operator[](int i) { Array1D temp; temp.q = p + i * n2; // 指向第 i 行的起始地址 return temp; } };
() 的重载
- 让类可以像函数一样被调用,从而实现函数对象。
cpp
class Func {
double para;
int lowerBound, upperBound;
public:
double operator()(double x, int i, int j) {
// 实现某种计算逻辑
return para * x + i * j;
}
};
Func f; // 创建一个 Func 对象
f(2.4, 0, 8); // ✅ 调用 f.operator()(2.4, 0, 8)
sort就用到了函数对象
cpp
template <class RandomAccessIterator, class Compare>
void sort(RandomAccessIterator first,
RandomAccessIterator last,
Compare comp);
它要求
comp必须实现了()的重载
三种可调用对象
- 普通函数
- 成员函数
- 函数指针:即一般的函数
cpp
int multiply_by_2(int x) { return x * 2; }
// 声明一个指向该函数类型的指针
int (*func_ptr)(int); // func_ptr 是一个函数指针
// 赋值:让指针指向 multiply_by_2 函数
func_ptr = &multiply_by_2; // & 可省略
- 函数对象 :重载了
operator()的类示例- 和函数指针的最大区别是它可以携带状态
cpp
class Multiply {
int factor; // 成员变量:保存状态
public:
Multiply(int f) : factor(f) {} // 构造函数初始化状态
int operator()(int x) const { // 重载 () 操作符
return x * factor;
}
};
// 函数对象:可以参数化
Multiply m(3);
m(4); // 返回 12
- Lambda 函数
- 这是创建匿名函数对象的语法糖,并没有实现新的功能,只是让你不用自己创建一个函数对象,编译器会自动为你生成一个匿名类。
cpp
int add5 = [base = 5](int x) { return x + base; };
cout << add5(10); // 输出 15
- 语法结构
cpp
[capture-list] (params) -> return-type { body }
->return-type可省略,自动推断。[]为捕获列表,即捕获作用域中的变量(类似于初始化)
| 捕获方式 | 含义 |
|---|---|
[] |
不捕获任何变量 |
[&] |
以引用方式捕获所有在作用域内被引用的变量 |
[=] |
以值拷贝方式捕获所有在作用域内被引用的变量 |
[=, &foo] |
以值拷贝方式捕获所有被引用的变量,但 foo 变量以引用方式捕获 |
[&, foo] |
以引用方式捕获所有被引用的变量,但 foo 以值拷贝方式捕获 |
[bar] |
仅以值拷贝方式捕获 bar;不捕获任何其他变量 |
function------通用函数封装器
- 可以存储并调用任何可调用对象,实现类型统一和接口抽象,将所有能调用的对象都当作同一类处理。
cpp
void doSomething(std::function<void(int)> callback) {
int result = 42;
callback(result); // 调用用户提供的逻辑
}
function 的语法
cpp
bool cmpInt(int a, int b) { return a < b; }
class CmpInt {
bool operator()(const int a, const int b) const {
return a < b;
}
};
std::function<bool(int, int)> f1(cmpInt);
std::function<bool(int, int)> f2(CmpInt);
std::function<bool(int, int)> f3([](int a, int b) { return a < b; });
function 的实现(个人认为了解即可)
- 模板参数不同,则实例化出的对象是完全不同的类,所以
function包裹的函数参数不同就会调用不同的call,传入不同的参数。
cpp
struct _Base {
virtual void call() = 0; // 纯虚函数:多态调用点
virtual ~_Base() {} // 虚析构函数
};
template<class F>
struct _Model : _Base {
F f; // 存储实际的可调用对象
_Model(F func) : f(func) {}
void call() override { f(); } // 多态调用点!
};