运算符重载引入
1. 从函数重载说起
- 函数重载是在一定作用域内,多个相同名称但不同参数列表的函数重载
- 编译时由编译器根据实际调用时给的实参情况来判定本次实际用哪个函数, 这个过程叫重载决策
- 重载函数本质上就是多个独立函数, 重载机制在编译时发生,运行时不参与
- 函数重载的意义就是避免我们胡乱起名,方便编写类库覆盖所有可能操作,是一种语法糖
2. 什么是运算符重载
- 什么是运算符?譬如 + - * / % 等算术运算符和 > < == != 等关系运算符就是典型的可重载运算符(但不是所有的运算符都可以重载,譬如 sizeof )
- 运算符诞生于C语言中,用来对变量进行某种 “预定义” 的运算操作,这种预定义是编译器预制好的,编译时会翻译对应到CPU机器码
- 面向对象时代带来新的问题:两个对象如何运算?譬如:Person a, b, c; c = a + b; 此处的 + 让编译器如何解读?
3. 运算符重载示例
- class coordinate,两个属性 x 和 y,直接执行 +,编译不通过
- 重载 + 运算符后,再编译并执行,查看结果,分析结果
class Coordinate
{
public:
int x;
int y;
Coordinate();
Coordinate(int x0, int y0);
void print(void);
Coordinate operator+(const Coordinate& other);
};
Coordinate::Coordinate()
{
x = 0;
y = 0;
}
Coordinate::Coordinate(int x0, int y0)
{
x = x0;
y = y0;
}
void Coordinate::print(void)
{
cout << "(" << this->x << ", " << this->y << ")" << endl;
}
Coordinate Coordinate::operator+(const Coordinate& other)
{
Coordinate temp;
temp.x = this->x + other.x;
temp.y = this->y + other.y;
return temp;
}
int main(void)
{
Coordinate a(1, 4);
a.print();
Coordinate b(2, 3);
b.print();
Coordinate c(5, 2);
c.print();
c = a + b; //翻译:c = a.operator+(b);
c.print();
system("pause");
return 0;
}
深度理解运算符重载
1. 运算符重载的本质
- 表面上,运算符重载是对 C++ 源生运算符的意义,在某个 class 中做重定义
- 本质上,运算符被映射到执行相应的成员函数,所以运算符重载其实是重定义对象的运算符所对应的函数
2. 运算符重载的意义
- 运算符重载是一种语法特性,C++ 全面支持,Java 不支持,python 有限度的支持
- 没有运算符重载照样写代码,所有操作全部通过显式调用相应成员函数来完成即可
- 运算符重载是一种语法糖,可以让代码“看起来更简洁,更优雅”,将复杂实现隐藏在类的内部
- 运算符重载机制加大了类库作者的工作量,减少了调用类库写功能的人的书写量
- C++ 支持运算符重载机制有其理念和历史原因,是对写法简洁和效率优秀的综合考量
- 赋值运算符 = 重载和引用结合,可有效提升代码效率
- 赋值运算符 = 重载时要注意有指针成员时会涉及浅拷贝和深拷贝
- 运算符重载一定程度上体现了 C++ 的多态性,因为同样的运算符在不同的 class 中表现是不同的
理解运算符重载的关键点
1. 通过代码实践来深度理解运算符重载函数
class Coordinate
{
public:
int x;
int y;
Coordinate();
Coordinate(int x0, int y0);
void print(void);
Coordinate operator+(const Coordinate& other);
Coordinate operator=(const Coordinate& other);
};
Coordinate::Coordinate()
{
x = 0;
y = 0;
}
Coordinate::Coordinate(int x0, int y0)
{
x = x0;
y = y0;
}
void Coordinate::print(void)
{
cout << "(" << this->x << ", " << this->y << ")" << endl;
}
Coordinate Coordinate::operator+(const Coordinate& other)
{
Coordinate temp; // c = a + b; 运算符 + 前用 this,后用 other
temp.x = this->x + other.x;
temp.y = this->y + other.y;
return temp;
}
Coordinate Coordinate::operator=(const Coordinate& other)
{
// c = a; 运算符 = 前用 this,后用 other,c=a 整个表达式的值为返回值即 c
this->x = other.x;
this->y = other.y;
return *this; //返回当前对象
}
int main(void)
{
Coordinate a(1, 4);
a.print();
Coordinate b(2, 3);
b.print();
Coordinate c(5, 2);
c.print();
c = a + b; //翻译:c = a.operator+(b);
c.print();
c = a;
c.print();
system("pause");
return 0;
}
-
运算符 + 的重载对应关系
总结:a + b; -> a.operator+(b); a 对应 this,b 对应函数参数 other,a + b 函数表达式的值对应函数返回值 -
运算符 = 的重载对应关系
总结:c = a; -> c.operator=(a); c 对应 this,a 对应函数参数 other,c = a 整个表达式的值(即 c 值)对应函数的返回值 -
运算符 += 的重载对应关系
总结:a += b; -> a.operator+=(b); a 对应 this,b 对应函数参数 other,a += b 函数表达式的值对应函数返回值
void Coordinate::operator+=(const Coordinate& other)
{
//重载函数实际操作按照 a=a+b的效果
// a是 this,b是 other,操作完成后 a的值被改变,但是 b的值不变
this->x = this->x + other.x;
this->y = this->y + other.y;
}
//c = (a += b); //因为我们一般不会这样写代码,所以 a+=b整个表达式的值不会再拿来给别人赋值用,重载函数返回值无用
a += b;
- 运算符 > 的重载对应关系
总结:a > b; -> a.operator>(b); a 对应 this,b 对应函数参数 other,a > b 函数表达式的 bool 值对应函数返回值
2. 运算符 = 默认提供问题
- = 运算符有点特殊,编译器会提供一个默认的 = 运算符的重载,因此不提供时也可以用
- 如果自己显式写了 = 运算符的重载函数,则会覆盖编译器自动提供的那一个
运算符重载规则总结:
运算符左边的是 this,右边的是 other,运算符加操作数的整个表达式的返回值就是返回值
赋值运算符重载函数返回引用
1. 返回引用优点
-
提升程序效率
- 赋值运算符重载函数返回值可以返回对象类型,也可以返回对象引用类型,都可正常工作
- 区别:返回引用可以避免一次返回值值传递的对象复制,这需要消耗资源
- 总结:虽然 C++ 语法并未强求,但应优先选择返回引用
-
允许连续赋值式
- 返回对象而不是引用时,在连续赋值 (c = a = b;) 时,编译会报错
- 原因:先执行 a=b 操作,返回值再作为第二次赋值运算符的右值
- 总结:连等在返回对象和引用时都可以,但是在返回 void 时不可以
2. 思考:传参为什么要传引用
- 如果传对象,则调用时是值传递,调用时需要复制一次,增加额外开销
- 如果传指针,则重载后,使用时为了符合函数参数格式必须写成 a=&b; 这种,不符合 C 语言写法习惯,也有歧义
- 实际测试发现真的传指针时,写 a=b; a=&b; 都能编译通过,且运行正确,但确实别扭
- 总结:有资料表明,其实 C++ 早期发明引用概念时,就是为了解决此处运算符重载传参尴尬
String 类的赋值运算符重载
1. 标准库中 String 类的运算符重载
- String 类提供了 = 和 + 等多个运算符重载函数的成员
- 标准库只给 API,没有给实现,下面代码为自己实现 = 运算符重载
2. String 的 = 运算符重载演练
- 自己编写一个 MyString 类,成员变量 char *pBuf 指向实际存储字符串的 buf,new 动态内存来使用
- 实践:先完成构造函数,成员函数 print,len 等基础设计,写代码测试
- 编写 = 运算符重载函数,引出浅拷贝深拷贝问题
MyString.h 文件
#pragma once
#include <iostream>
#include <string>
using namespace std;
class MyString
{
public:
//构造函数
MyString();
MyString(const char *pSrc);
//拷贝构造函数
//析构函数
~MyString();
//普通成员函数
void print(void); //打印字符串长度和内容
//运算符重载函数
void operator=(const MyString& other);
private:
char *pBuf; //指针指向实际存储字符串内容的 buffer空间,采用动态分配
int cnt; //用来记录 MyString对象中存储有效字符的个数
};
MyString.c 文件
#include "MyString.h"
#include <iostream>
#include <string>
using namespace std;
MyString::MyString()
{
//不写,后续不实现
}
MyString::MyString(const char *pSrc)
{
//先计算 pSrc字符串的大小,方便之后去复制字符串,并且记录到 this->cnt
const char *p = pSrc;
while (*p != '\0')
{
this->cnt++;
p++;
}
cout << "cnt of pSrc = " << cnt << endl;
//给 pBuf分配动态内存,方便之后去复制字符串
this->pBuf = new char[cnt + 1];
//把 pSrc指向的源字符串内容,复制给当前 MyString对象来存储即可生成
p = pSrc; //要复制的源字符串
char *p2 = this->pBuf;
while (*p != '\0')
{
*p2++ = *p++;
}
*p2 = '\0';
}
void MyString::print(void) //打印字符串长度和内容
{
cout << "cnt of MyString" << this->cnt << endl;
char *p = this->pBuf;
while (*p != '\0')
{
cout << *p++;
}
cout << endl;
}
MyString::~MyString()
{
//析构主要是为了释放构造函数中分配的动态内存
delete[] this->pBuf;
}
void MyString::operator=(const MyString& other)
{
delete[] this->pBuf;
// this是返回值,左值是 this,右值是 other
//重新给 this->pBuf分配足够大的内存空间
this->pBuf = new char[other.cnt + 1]; //造成原来动态内存丢失
//解决办法:先释放掉原来的动态内存
//赋值操作,其实就是把 other里的字符串内容拷贝给 this里的字符串即可
char *p1 = this->pBuf;
char *p2 = other.pBuf;
while (*p2 != '\0')
{
*p1++ = *p2++;
}
}
main.c 文件
#include "MyString.h"
#include <iostream>
#include <string>
using namespace std;
int main(void)
{
MyString s1("linux");
MyString s2("aston");
s1.print();
s2.print();
s1 = s2;
cout << "---------" << endl;
s1.print();
s2.print();
return 0;
}
总结
- 运算符重载技术本身很简单,就是个语法糖
- 运算符重载会牵出其他技术点(譬如浅拷贝深拷贝)共同完成某个工作,这就有点难,讲究功底
- C++ 编程中涉及到动态内存的地方一定要慎重
++ 和 – 运算符的前置后置如何实现
1. 可前置可后置的运算符
- int a = 5; a++; ++a; 结果不同
- 重载 ++ 和 – 无意义