C++程序设计08——运算符重载
崔毅东C++程序设计——运算符重载的笔记。
1 引入:平面向量类 2D Vector Class
1.1 在C++中描述平面向量
C++ STL vector: 变长数组
向量数据成员:
double x, double y
或者 std::array
v_; 运算
a. 数乘、点积
b. 求长度和方向
| (1, 2)| : √ ( 11 + 22)
dir (1, 2) : arctan ( 1/2 )
==, !=, <, <=, >, >=
类型转换:
- 转为double,即求向量长度;
- 转为string
负值
自加1,自减1
1.2 TDD开发设计方法
Test-Driven Development (TDD),测试驱动开发
一种开发设计方法,值得一看。Kent Beck 《测试驱动开发》
步骤
(1) 先编写测试代码,而不是功能代码
(2) 编译运行测试代码,发现不能通过
(3) 做一些小小的改动(编写功能代码),尽快地让测试程序可运行
(4) 重构代码,优化设计
1.3 Vector 2D 类成员函数
1 | double atan(double x); //返回x的反正切值,以弧度为单位。有其他类型重载 |
2 重载运算符基本介绍
2.1 运算符与函数的异同
运算符可以看做是函数
不同之处
(1) 语法有区别
(2) 不能自定义新的运算符,只能重载已经存在的运算符
(3) 函数可overload, override产生任何想要的结果,但运算符作用于内置类型的行为不能修改
函数式编程语言的观念——一切皆是函数【Haskell】
Emacs calculator软件
2.2 C++运算符函数
2.2.1 可重载的运算符
类型转换运算符:double, int, char, ……
new/delete, new []/delete[]
“”_suffix 用户自定义字面量运算符(自C++11起)
一般运算符:
2.2.2 不可重载的运算符
Operator | Name |
---|---|
. | 类属关系运算符 |
.* | 成员指针运算符 |
:: | 作用域运算符 |
?: | 条件运算符 |
# | 预编译符号 |
2.2.3 运算符重载的限制
- 优先级和结合性不变
- 不可创造新的运算符
2.2.4 运算符函数
【注意】
- v2作为参数传递给v1的operator函数
- this指针的几个要点(见上图),调用v1 < v2 时,this指向v1
- 传参的时候可以不用引用吗——可以
- 使用引用的好处——提高调用效率,不需要调用拷贝构造函数!
2.2.5 确定运算符的调用形式
2.3 左值、纯右值与将亡值
2.3.1 C++03 的左值和右值
(1) 能放在等号左边的是lvalue
(2) 只能放在等号右边的是rvalue
(3) lvalue可以作为rvalue使用
2.3.2 C++11 的左值和右值
左值
指定了一个函数或者对象,它是一个可以取地址的表达式
举例:
(1) 解引用表达式p:&(\p)
(2) 字符串字面量”abc”:字符串存储在静态区,是有地址的,首地址是一个指针。
(3) 前置自增/自减表达式 ++i / —i:前置表达式操作过程是先对i进行自增自减运算,再取地址,&(++i)等价于&i。后置自增/自减表达式不能进行取地址操作
(4) 赋值或复合运算符表达式(x=y或m*=n等)
纯右值
纯右值:不和对象相关联的值(字面量),或者其求值结果是字面量或者一个匿名的临时对象
举例:
(1) 除字符串字面量以外的字面量,比如 32, ‘a’
(2) 返回非引用类型的函数调用 int f() { return 1;}
(3) 后置自增/自减表达式 i++/i—
(4) 算术/逻辑/关系表达式(a+b、a&b、a<=b、a<b)
(5) 取地址(&x)
左值可以当成右值使用
2.3.3 C++11 将亡值
将亡值:将亡值也指定了一个对象,是一个将纯右值转换为右值引用的表达式
- 右值引用:int&& rvr1{ 22 };
举例:
1
2
3
4
5
6
7
8
9int prv(int x) { return 6 * x; } // pure rvalue
int main() {
const int& lvr5{ 21 }; // 常量左值引用可引用纯右值
int& lvr6{ 22 }; // 错!非常量左值引用不可引用纯右值
int&& rvr1{ 22 }; // 右值引用可以引用纯右值
int& lvr7{ prv(2) }; // 错!非常量左值引用不可引用纯右值
int&& rvr2{ prv(2) }; // 右值引用普通函数返回值
rvr1 = ++rvr2; // 右值引用做左值使用
}右值引用可以看作延续了纯右值的生命期,比如line7,原本执行完prv(2)之后返回值就找不到了,使用右值引用就可以继续对返回值进行修改,比如line8。
3 重载实例
3.1 重载一般二元算术运算符
3.1.1 调用一般二元运算符
1 | Vec2D a{1,2}, b{3, 6}; double z {1.3}; |
【注意】最后一行错误原因:double 类型是内建类型,其运算符不能被重载
3.1.2 函数原型
1 | struct Vec2D { |
【注意】友元函数需要两个参数,不能像成员函数一样默认本对象为调用者
3.1.3 关于返回值
1 | Matrice Matrice::operator+(const Matrice& second_m) const { |
【注意】类内如果有指针成员,在返回的时候一定要格外小心深拷贝、浅拷贝。
如果要用上面代码实现矩阵加法的重载,一定要先重载拷贝构造函数,将默认的浅拷贝重载成深拷贝的方式,不然将会出现内存泄漏——也就是直接返回了 resultMatrice 这个局部对象,而+运算的函数体结束后这个对象会立刻被析构,其中的矩阵指针也会立刻被释放,这时继续使用返回值里的矩阵指针,显然是内存泄漏!!
3.1.4 举例
1 | Vec2D v1(2, 4); |
3.2 重载复合二元算术运算符(+=, -=, *=, and /=)
3.2.1 复合运算符操作的特殊之处
1 | v1 += v2; // 语句执行后,v1的值被改变了(这非常重要!!) |
3.2.2 复合运算符的重载
1 | Vec2D Vec2D::operator +=(const Vec2D& secondVec2D ) { |
如果将以上代码对应v1+=v2; 则有:
- secondVec2D 是 v2
- (*this) 是改变后的 v1
如果将
1
2*this = this->add(secondVec2D);
return (*this);修改为:
1
return this->add(secondVec2D);
这个重载是否还有效?
3.2.3 举例
1 | Vec2D v2(2, 4); |
3.3 重载数组下标运算符
3.3.1 重载[]运算符
为什么重载[]运算符
[]重载后可用类似数组的语法格式访问对象内容
重载代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16Vec2D v {8, 9};
cout << "v.x_: " << v[0] << "v.y_: " << v[1] << endl;
double Vec2D::operator[](const int &index) {
if (index == 0) {
return x_;
}
else if (index == 1) {
return y_;
}
else {
cout << "index out of bound" << endl;
exit(0);
}
}
/*这种重载方法只能读取数据*/3.3.2 数组下标运算符作为访问器和修改器
访问器和修改器:作为访问器就是用[]运算符读取数据,修改器就是用[]运算符修改数据
使r2[]成为左值的方法:使[]返回一个引用
1
2
3
4
5double& Vec2D::operator[](const int &index) { //lvalue
if (index == 0)
return x_; //x_ can be modified
//...... Now, the Vec2D class is mutable.
}
3.4 重载一元运算符
3.4.1 单目运算符
主要的单目运算符:—, ++, -(负号), *(解引用)
编译器执行过程:
若operator @是在obj的类的成员函数,则调用obj.operator @()【无参数】
若operator @是obj的类的友元函数,则调用operator @(obj)
3.4.2 重载负号运算符
3.4.2.1 调用
1 | Vec2D v1(2, 3); |
3.4.2.2 重载负号运算符
1 | Vec2D* Vec2D::operator-(){ |
3.4.3 重载++和- -运算符
3.4.3.1 前置、后置运算符与操作顺序
前置:先增减后取值,表达式是lvalue
后置:先取值(存在某个地方)后增减,表达式是prvalue(纯右值)
- 举例:
- b = (a++) / 2 这一算式中,会先把a的值取出来放到某处,假设为temp(temp将被用于参加整个表达式的后续计算),之后a会被+1,a的值被改变。
- b = (++a) / 2 这一算式,则直接将a值+1,并用a进行后续计算,没有temp这一环节。
- 代码示例:
- 举例:
1 | Vec2D v1(2, 3); |
3.4.3.2 前置与后置在函数定义中的区别
前置++/—重载无参数,返回引用类型
后置++/—重载带参数——“dummy”
- 这个参数用于表示这是一个后置运算符,实际调用的时候并不会传参
- 若在类外定义,则不论前置后置都需要参数
3.4.3.3 重载实例
- 前置++运算符
1 | Vec2D& Vec2D::operator++(){ |
- 后置++运算符
1 | Vec2D Vec2D::operator++(int dummy){ |
- 两条返回语句的不同导致了前置++和后置++的区别
3.5 重载流插入(<<)/提取(>>)运算符
3.5.1 重载<</>>的目的
能够把对象信息直接输出,比如:
1 | cout << vec2d; |
3.5.2 重载为友元函数
为什么不能重载为成员函数
运算符重载为类成员函数后,当调用该运算符时,左操作数必须是该类的实例。若<<和>>重载为成员函数,则只能用 v1<<cout; 如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17/*重载为成员函数*/
class Vec2D {
public:
ostream &operator<<(ostream &stream);
istream &operator>>(istream &stream);
};
Vec2D v1;
v1 << cout; //Vec2D对象只能作为第一个操作数
/*重载为友元函数*/
struct Vec2D {
friend ostream &operator<<(ostream &stream, Vec2D &v);
friend istream &operator>>(istream &stream, Vec2D &v);
};
Vec2D v1;
cout << v1; //更符合编程习惯为什么友元函数的返回值是ostream&类型
要实现cout<<x<<y; 这类连续输出的代码,想要输出y,cout<<x这一部分应该返回一个ostream类型,所以使用ostream&作为返回值
返回ostream类型与&有什么联系?为什么一定是&呢
3.6 重载对象转换运算符
3.6.1 重载目的
- 将Vec2D对象转换为double数时,我们可以求该对象的范数,也就是向量长度
3.6.2 重载实例
- 类型转换函数:
1 | Vec2D::operator double() { //操作符名称和要转到的类型同名,类似于构造函数 |
- 调用实例:
1 | Vec2D v1(3, 4); |