1. 操作符重载
- 函数重载
- 名同、参数不同 ,返回值不同没有用的:参数顺序、参数类型匹配(找到最佳匹配)
- 静态绑定
- 歧义控制:
- 顺序:
- 最佳匹配:
- 原则一:这个匹配每一个参数不比其他的匹配更差
- 原则二:这个匹配有一个参数更精确匹配
- 整形提升:更好的,标准转换(标准转换都是一视同仁的)
- 窄转换?允许的,大->小
- 操作符重载(变为一种函数)
- 动机:操作符语义
- built_in 类型
- 自定义数据类型
- 作用:
- 提高可读性
- 提供可扩充性
- 动机:操作符语义
1.1 + 操作符重载
- 一种设计思想是: c++不希望区分内置类型和用户自定义类型的操作, 希望用户自定义类型也能使用类似内置类型的操作符进行操作.
cpp
class Complex {
double real, imag;
public:
Complex() {real = 0; imag = 0;}
Complex(double r, double i) { real = r; imag = i; }
Complex add(Complex& x);
};
Complex a(1,2),b(3,4),c;
c=a.add(b);//想要写成 a + b
- 为了实现上述目标, C++ 提供了操作符重载的机制, 允许用户为自定义类型重新定义操作符的含义.
cpp
class Complex {
double real, imag;
public:
Complex() { real = 0; imag = 0; }
Complex(double r, double i) { real = r; imag = i; }
Complex operator + (Complex& x) {
Complex temp;
temp.real = real + x.real;
temp.imag = imag + x.imag;
return temp;
}
};
Complex a(1,2),b(3,4),c;
c = a.operator + (b);
- 注意, 我们这里重载的是
+操作符, 而且是成员函数形式的重载;
同样, 也可以使用友元函数+全局函数的形式进行重载.
cpp
class Complex {
double real, imag ;
public :
Complex() { real = 0 ; imag = 0 ; }
Complex(double r, double i) {
real = r;
imag = 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),c;
c = a + b;//自动进行翻译
- 注意 : 友元函数和全局函数的重载至少包含一个用户自定义类型的参数 , 否则会和内置类型的操作符冲突
1.2 自增自减的重载
cpp
enum Day { SUN, MON, TUE, WED, THU, FRI, SAT};
// 我们希望 Day 类型可以进行自增操作, 也希望 Day 可以直接输出
Day& operator++(Day& d){
return d= (d==SAT)? SUN: Day(d+1);
}
ostream& operator<<(ostream& os, Day d){
const char* dayName[] = {"SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"};
os << dayName[d];
return os;
}
int main(){
Day today = WED;
cout << "Today is " << today << endl;
Day tomorrow = ++today;
cout << "Tomorrow is " << tomorrow << endl;
}
- ps: 上面的
+操作符重载, 返回值是值类型, 这里的++/<<操作符重载, 返回值是引用类型, 我们希望确实修改了原来的对象
1.3 可以重载的操作符
- 不可以重载的操作符 :
.(成员访问操作符)、.*(成员指针访问运算符/解引用访问,如下)、::(域操作符)、?:(条件操作符)、sizeof:也不重载
- 三目, 实际上编译器看成是 if else, 但是两个参数需要优先计算, 所以不可以重载
- 重载基本原则:
- 方法:(大多数都支持,但是有的不支持)
- 要么类成员函数
- 要么带有类参数的全局函数
- 遵循原有语法
- 单目/双目:一一对应
- 优先级, 结合性等等都不变
- 方法:(大多数都支持,但是有的不支持)
- 限制:
=()[]->必须是类成员函数重载, 不可以是全局函数重载!
- 顺序是固定的? 因为第一个参数一定是用户自定义类型
- 对于
=赋值操作符, 如果不重载, 编译器会提供一个默认的赋值操作符 ,
成员函数优先级更高, 所以你定义在全局的赋值操作符是没有意义的!
全局函数作为补充的情况/双目运算符一般友元函数, 单目运算符一般成员函数
obj + 10和10 + obj这两种情况同时支持, 那么就可以直接友元函数- 双目运算, 交换律
cpp
class Box {
double width;
public:
Box(double w) { width = w; }
friend Box operator+(Box b, double val); // 友元函数
friend Box operator+(double val, Box b); // 友元函数
};
不建议重载 || 和 &&
cpp
if (p != 0 && strlen(p) > 0) { ... }
- 注意, 当
p==0的时候, 第二个条件不会被计算, 但是如果重载了&&操作符, 那么两个条件都会被计算, 可能会引发错误, - 短路原则被破坏了!(逻辑与和逻辑或, 一般不可以重载)
1.4 双目运算符返回值总结
- 返回类型的问题: 如果没有&的时候,第一个return出现了对象拷贝,避免:临时变量不能返回拷贝
- 对于加减乘除等操作符, 一般返回值是值类型, 而不是引用类型
这里的例子是用来说明, 假如我们对于加减乘除等操作符, 返回值是引用类型, 会出现什么问题:
cpp
class Rational {
public:
Rational(int,int);
const Rational& operator *(const Rational& r) const;//const写不写都行,写了更好
private:
int n, d;
};
// operator * 的函数体
return Rational(n * r.n, d * r.d);
Rational *result = new Rational(n*r.n, d*r.d);
return *result;//返回引用的问题?
// w = x * y * z出现问题:出现内存泄露的问题
static Rational result;//声明为static
result.n = n * r.n;
result.d = d * r.d;
return result;//static是全局的,可以吗?不可以,同时出现两个的结果会出现问题
//if((a * b) == (c * d)) ->永真式
1.5 单目运算符重载
a++vs++a- prefix ++ 返回的是左值, postfix ++ 返回的是右值(加一之前的临时变量)
- 实际上我们要区分两种情况, 方法是改变参数列表方便重载
cpp
class Counter{
int value;
public:
Counter() { value = 0; }
Counter& operator ++() // ++a, 效率更高, 返回引用, 参数列表为空
{ value++;
return *this;
}
Counter operator ++(int) //a++
{ Counter temp=*this; //这里的int值是什么意义?区分两个函数,dummy argument,哑元变量
value++;
return temp;
}
}
- 这里的返回值,
++a返回的是引用类型,a++返回的是值类型 - 这里也可以发现, 一般来说 ++a 效率更高, 因为没有创建临时变量
1.6 操作符 = 的重载
cpp
A a;
A b = a; // 这里调用的是拷贝构造函数
A c, d;
c = d; // 这里调用的一定不是拷贝构造, 而是赋值操作符重载函数
- 如果不提供赋值操作符重载, 编译器会提供一个默认的赋值操作符, 它会将对象的每个成员逐一进行赋值
- 赋值操作符重载不能继承? 为什么:
- 因为拷贝构造,派生出来的类有一些新的部分. 编译器会为每个类自动生成赋值操作符,无法继承。子类需要根据自身成员重新定义赋值操作符。
- 返回值的要求? 默认返回的是
*this的引用, 但是没有强制要求- 支持链式赋值:
a = b = c;
- 支持链式赋值:
深拷贝的良好实践
- 希望解决问题: 自我赋值问题, 异常安全问题
cpp
class A{
public:
A& operator = (A& a) {
//赋值
x = a.x;
y = a.y;
delete []p;
p = new char[strlen(a.p)+1];
strcpy(p,a.p);
return *this;//也会出现悬垂
}//还有问题,就是赋值自身会出现问题
}; // 方法一, 如果 new 失败, 那么抛出异常, 对象被破坏! p 成为悬垂指针
class A{
public:
A& operator = (A& a) {
//更安全的拷贝,先new再delete
char *pOrig = p;
p = new char ...
strcpy();
delete pOrig;
return *this;
}
};
自赋值问题
cpp
class A{
public:
A& operator = (A& a) {
if (this == &a) //检测自赋值
return *this;
//赋值
...
}
};
1.7 操作符 [ ] 的重载
- 当你的类内部维护一个数组, 希望通过
obj[index]的形式访问数组元素 时, 可以重载[]操作符 - 下面是一个 string 的类
cpp
class string {
char *p;
public :
string(char *p1){
p = new char [strlen(p1)+ 1];
strcpy(p,p1);//#pragma warning(disable:4996)来屏蔽问题
}
char& operator[] (int i) {
return p[i];
}
const char operator[](int i) const {
return p[i];
}
//可以用两个重载函数吗?是可以的
virtual ~string() { delete[] p ; }
};
int main() {
string s("aabbcc");
s[0] = 'z'; // 调用 char& operator[](int i)
const string cs("const");
char ch = cs[1]; // 调用 const char operator[](int i) const
cs[0] = 'x'; // 错误, 不能修改 const 对象
}
- 这里的返回值是不是需要加引用?
- 如果我们希望对他进行赋值, 那么需要加引用
char & operator[](int i) const这是一个奇怪的现象, 虽然理论上这是一 const, 但是返回值是引用, 所以可以进行赋值操作, 修改了 const 的语义!- 所以我们需要提供两个重载版本, 一个是 const 版本(返回左值), 一个是非 const 版本(返回右值), 对于可变对象, 调用非 const 版本; 对于不可变对象, 调用 const 版本 (醍醐灌顶, 这个时候, 我们人为保证了 const 的语义安全性!)
- 这里的参数列表实质上不同,
this指针的类型不同, 所以可以重载成功 - 下标操作符的重载都是成对出现的
- 这里的参数列表实质上不同,
多维数组 + []
- 如果是一个二维数组的话, 我们怎么重载
- 如果是一个三维数组
arr[i][j][k], 我们期望中的是第一次, 偏移i * (n2 * n3), 第二次偏移j * n3, 第三次偏移k;
但是, 如果第一次返回的是int *,第二次就做不到运算符重载, 因为是内置类型;
我们可以使用 自定义类型(代理类)!
cpp
class Array2D{
public:
class Array1D{
public:
Array1D(int *p) {this->p = p;}
int& operator[](int j) {
return p[j];
}
const int& operator[](int j) const {
return p[j];
}
private:
int *p;
};
Array2D(int rows, int cols) {
this->rows = rows;
this->cols = cols;
data = new int[rows * cols];
}
~Array2D() {
delete[] data;
}
Array1D operator[](int i) {
return Array1D(data + i * cols);
}
const Array1D operator[](int i) const {
return Array1D(data + i * cols);
}
private:
int rows;
int cols;
int *data;
};
1.9 操作符 () 的重载
- 函数调用操作符重载, 我们可以传入一个函数对象
cpp
class Func{
double para;
int lowerBound, upperBound;
public:
double operator()(double, int, int);
};
Func f;
f(3.14, 1, 10); // 调用 operator()
- 比如我们传入一个 class errorlogger, 对象已经绑定到了日志文件, 我们将这个对象当成函数来调用, 传入错误信息, 这样就可以实现日志记录功能
- 再比如, cpp 中的 sort,
可以传入一个函数指针(但是不能保存对象的状态);
可以传入函数对象, 定义一个类, 并且重载 () 操作符(可以保存状态);
可以传入匿名函数, lambda 函数;
函数指针 vs 函数对象:
- 函数指针
- 不能保存状态,
- 编译器无法内联优化, 效率较低
- 函数对象
- 对象, 可以携带成员变量
- 是类, 编译器可以内联优化, 效率较高
lambda 函数
可以捕获作用域之内的变量, 是创建匿名函数对象的方式, 编译器会自动生成一个类, 并且重载 () 操作符
语法:
- 捕获:
[], 不捕获任何变量[=], 按值捕获所有变量[&], 按引用捕获所有变量[x, &y], 按值捕获 x, 按引用捕获 y
举例:
cpp
vector<string> str_filter(vector<string> &vec, function<bool(const string&)> matched) {
vector<string> result;
for (string tmp: vec) {
if (matched(tmp)) {
result.push_back(tmp);
}
}
return result;
}
int main() {
vector<string> vec = {"www.example.com", "test.org", "sample.com", "hello.net"};
string pattern = ".com";
// 使用 lambda 函数作为过滤条件
vector<string> filtered = str_filter(vec,
[&](const string &s) -> bool {
return s.find(pattern) != string::npos;
}
);
}
std::function灵活的封装函数, 可以保存函数指针, 函数对象, lambda 函数等, 作为参数传递给其他函数
1.10 类型转换 操作符重载
- 类型转换操作符重载, 允许我们定义如何将一个自定义类型转换为另一个类型
- 基本类型 / 用户自定义类型
- 可以减少混合计算中需要定义的重载操作符的数量
cpp
class Rational {
private: int n, d;
public:
Rational(int n, int d) : n(n), d(d) {}
operator double() const {
return static_cast<double>(n) / d;
}
};
- 比如:
ostream f("abc.txt"); if (f) {...}, 这就是调用了operator bool()
问题:
- 如果我们定义了
operator double(), 那么Rational对象可以隐式转换为double, 可能会引发意外的类型转换, 需要谨慎使用; - 如果有多个类型转换操作符, 可能不能确定调用哪个, 需要避免歧义; explicit 关键字可以避免隐式转换引发的问题
cpp
class A {
public:
operator int() { return 1; }
explicit operator double() { return 2.0; }
};
A a;
cout << a + 1; // 如果没有 explicit, 那么错误, 不确定调用哪个转换操作符
// 如果有 explicit, 那么只能调用 operator int(), double 需要显式转换
cout << static_cast<double>(a) + 1.0; // 显式调用
1.11 操作符 -> 的重载
->实际上是二元运算符, 但是重载的时候按照一元操作符重载描述;- 一个非常重要的应用是实现智能指针
cpp
A a;
a->f();
a.operator->(f);
a.operator->()->f(); //重载时按一元操作符重载描述,这时,a.operator->()返回一个指针(或者是已经重定义过->的对象)
这里的两个 -> 不需要都重载, 只需要重载第一个 -> 即可, 返回一个指针类型 或者是一个已经重载过 -> 的对象
Prevent memory Leak, 避免内存泄漏
问题描述代码:
cpp
class A{
public:
void f();
int g(double);
void h(char);
};
void test(){
A *p = new A;
p->f();//如果出错不能顺序执行, 会内存泄漏
p->g(1.1);//返回值
p->h('A');
delete p;
}
使用智能指针的方案:
cpp
// 使用智能指针
class AWrapper{//不包含逻辑
A* p;// ? T p; 支持多个类型
public:
AWrapper(A *p) { this->p = p;}
~AWrapper() { delete p;}
A*operator->() { return p;}
};
void test(){
AWrapper p(new A);
p->f();// 自动调用AWrapper::operator->()
p->g(1.1);
p->h('A');
} // 退出的时候自动调用AWrapper::~AWrapper(), 按照编译器控制的生命周期
// RAII 资源获取即初始化
- 智能指针的局限性, 只能按照栈对象的生命周期进行管理, 不能跨函数传递
1.12 一个空的类包含哪些函数
cpp
class Empty{
Empty() {} // 默认构造函数
Empty(const Empty& ) {} // 拷贝构造函数
Empty(Empty&& ) noexcept {} // 移动构造函数
~Empty() {} // 析构函数
Empty& operator=(const Empty& ) { return *this; } // 拷贝赋值
Empty& operator=(Empty&& ) noexcept { return *this; } // 移动赋值
Empty *operator &(); // 取地址
const Empty* operator &() const; // 取地址 const 版本
}
智能指针的重要例子(考试重点)
- 异常处理 / 析构函数
- 异常处理 可以解决的问题
- 析构函数 可以解决的问题
cpp
template <typename T>
class auto_ptr{
private:
T* ptr;
public:
auto_ptr(T *p = 0): ptr(p) {}
~auto_ptr() { delete ptr;}
T* operator->() const {return ptr;} // 当作指针使用
T& operator *() const {return *ptr;} // 指针解引用
};
-
一个类似的实际应用,在GUI中
cppvoid displayInfo(const Information& info) { WINDOW_HANDLE w(createWindow()); // display info in window corresponding to w destroyWindow(w); // 如果中间代码抛出异常, 这里不会被执行, 导致内存泄漏 }解决方案:(智能指针, 但是没有重载 ->, 而是重载类型转换, 也是一种解决方案)
cppclass WindowHandleWrapper { public: WindowHandleWrapper(WINDOW_HANDLE handle) : w(handle) {} ~WindowHandleWrapper() { destroyWindow(w); } operator WINDOW_HANDLE() const { return w; } private: WINDOW_HANDLE w; WindowHandleWrapper(const WindowHandleWrapper&); // 禁止拷贝 WindowHandleWrapper& operator=(const WindowHandleWrapper&); // 禁止赋值 }; void displayInfo(const Information& info) { WindowHandleWrapper w(createWindow()); // display info in window corresponding to w } // 退出时自动调用析构函数, 释放资源
1.14 new/delete 操作符重载
- 频繁调用系统的存储管理, 影响效率
- 程序自身管理内存, 提高效率
- 方法:
- 调用系统存储分配, 申请一块大内存, 自行管理
- 针对该内存, 自己管理存储分配, 去配
- 通过重载new和delete操作符实现
- 重载的 new 和 delete 必须是静态成员函数或者全局函数
- 重载的 new 和 delete 遵循类的访问控制, 可以继承
1.14.1 重载 new
void *operator new (size_t size, ...), 可以有多个参数- 名:operator new
- 返回类型:void *
- 第一个参数:size_t(unsigned int)
- 系统自动计算对象的大小, 并且传递给 new 操作符重载函数
- 其他参数:用户自定义
A *p = new (arg1, arg2, ...) A;
- 如果重载一个 new, 那么不再调用系统默认的 new, 而是调用用户自定义的 new
- 重载了 new, 但是不代表 new [] 也重载了, 需要单独重载 new []
一些特殊的new:
void *operator new (size_t) throw (std::bad_alloc);标准版本的 operator new, 如果内存分配失败, 抛出异常 std::bad_allocvoid *operator new (size_t, void* p);
- placement new, 定位 new , 允许在指定的内存位置创建对象, 不进行内存分配;适合我们实现内存池等高级内存管理技术
- 直接调用构造函数在已有内存地址上构造对象
void *operator new (size_t, std::nothrow_t&) noexcept;
- 不抛出异常的 new, 如果内存分配失败, 返回nullptr
1.14.2 重载 delete
void operator delete (void *p, size_t size);, 第二个参数可有可无- delete 的重载只能有一个
- 如果重载了 delete, 那么不再调用系统默认的 delete, 而是调用用户自定义的 delete