C++程序设计08——运算符重载

崔毅东C++程序设计——运算符重载的笔记。

1 引入:平面向量类 2D Vector Class

1.1 在C++中描述平面向量

  1. C++ STL vector: 变长数组

  2. 向量数据成员:

    double x, double y

    或者 std::array v_;

  3. 运算

    a. 数乘、点积

    b. 求长度和方向

    | (1, 2)| : √ ( 11 + 22)

    dir (1, 2) : arctan ( 1/2 )

  4. ==, !=, <, <=, >, >=

  5. 类型转换:

    1. 转为double,即求向量长度;
    2. 转为string
  6. 负值

  7. 自加1,自减1

1.2 TDD开发设计方法

  1. Test-Driven Development (TDD),测试驱动开发

    一种开发设计方法,值得一看。Kent Beck 《测试驱动开发》

  2. 步骤

    (1) 先编写测试代码,而不是功能代码

    (2) 编译运行测试代码,发现不能通过

    (3) 做一些小小的改动(编写功能代码),尽快地让测试程序可运行

    (4) 重构代码,优化设计

1.3 Vector 2D 类成员函数

1
2
3
double atan(double x); //返回x的反正切值,以弧度为单位。有其他类型重载
double sqrt(double x); //返回x的平方根。有其它类型重载
double pow (double b, double exp); // 返回b的exp次方。有其它类型重载

2 重载运算符基本介绍

2.1 运算符与函数的异同

  1. 运算符可以看做是函数

  2. 不同之处

    (1) 语法有区别

    (2) 不能自定义新的运算符,只能重载已经存在的运算符

    (3) 函数可overload, override产生任何想要的结果,但运算符作用于内置类型的行为不能修改

  3. 函数式编程语言的观念——一切皆是函数【Haskell】

  4. Emacs calculator软件

2.2 C++运算符函数

2.2.1 可重载的运算符

  1. 类型转换运算符:double, int, char, ……

  2. new/delete, new []/delete[]

  3. “”_suffix 用户自定义字面量运算符(自C++11起)

  4. 一般运算符:

    QQ截图20210506113235

2.2.2 不可重载的运算符

Operator Name
. 类属关系运算符
.* 成员指针运算符
:: 作用域运算符
?: 条件运算符
# 预编译符号

2.2.3 运算符重载的限制

  1. 优先级和结合性不变
  2. 不可创造新的运算符

2.2.4 运算符函数

QQ截图20210506113253

注意

  1. v2作为参数传递给v1的operator函数
  2. this指针的几个要点(见上图),调用v1 < v2 时,this指向v1
  3. 传参的时候可以不用引用吗——可以
  4. 使用引用的好处——提高调用效率,不需要调用拷贝构造函数!

2.2.5 确定运算符的调用形式

image-20210426215559462

2.3 左值、纯右值与将亡值

2.3.1 C++03 的左值和右值

(1) 能放在等号左边的是lvalue

(2) 只能放在等号右边的是rvalue

(3) lvalue可以作为rvalue使用

2.3.2 C++11 的左值和右值

  1. 左值

    • 指定了一个函数或者对象,它是一个可以取地址的表达式

    • 举例:

      ​ (1) 解引用表达式p:&(\p)

      ​ (2) 字符串字面量”abc”:字符串存储在静态区,是有地址的,首地址是一个指针。

      ​ (3) 前置自增/自减表达式 ++i / —i:前置表达式操作过程是先对i进行自增自减运算,再取地址,&(++i)等价于&i。后置自增/自减表达式不能进行取地址操作

      ​ (4) 赋值或复合运算符表达式(x=y或m*=n等)

  2. 纯右值

    • 纯右值:不和对象相关联的值(字面量),或者其求值结果是字面量或者一个匿名的临时对象

    • 举例:

      (1) 除字符串字面量以外的字面量,比如 32, ‘a’

      (2) 返回非引用类型的函数调用 int f() { return 1;}

      (3) 后置自增/自减表达式 i++/i—

      (4) 算术/逻辑/关系表达式(a+b、a&b、a<=b、a<b)

      (5) 取地址(&x)

  3. 左值可以当成右值使用

2.3.3 C++11 将亡值

  1. 将亡值:将亡值也指定了一个对象,是一个将纯右值转换为右值引用的表达式

    • 右值引用:int&& rvr1{ 22 };
    • 举例:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      int 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
2
3
4
Vec2D a{1,2}, b{3, 6}; double z {1.3};
Vec2D c = a + b; // a.operator+(b); à Vec2D Vec2D::operator+(Vec2D);
Vec2D d = a + z; // a.operator+(z); à Vec2D Vec2D::operator+(double);
Vec2D e = z + b; // z.operator+(b); à Vec2D double::operator+(Vec2D);错误!

【注意】最后一行错误原因:double 类型是内建类型,其运算符不能被重载

3.1.2 函数原型

1
2
3
4
5
6
7
8
struct Vec2D {
Vec2D operator +(Vec2D); //成员函数
Vec2D operator +(double); //成员函数
friend Vec2D operator +(double, Vec2D); //非成员函数,友元函数
};

Vec2D operator +(double, Vec2D) { // Do something
}

【注意】友元函数需要两个参数,不能像成员函数一样默认本对象为调用者

3.1.3 关于返回值

1
2
3
4
5
6
7
8
9
10
Matrice Matrice::operator+(const Matrice& second_m) const {
Matrice resultMatrice{ this->lines, this->rows };
for (int i = 0; i < (this->lines); i++) {
for (int j = 0; j < (this->rows); j++) {
(resultMatrice.matricePointer)[i][j] =
(this->matricePointer)[i][j] + (second_m.matricePointer)[i][j];
}
}
return resultMatrice; //调用拷贝构造函数
}

【注意】类内如果有指针成员,在返回的时候一定要格外小心深拷贝、浅拷贝。

如果要用上面代码实现矩阵加法的重载,一定要先重载拷贝构造函数,将默认的浅拷贝重载成深拷贝的方式,不然将会出现内存泄漏——也就是直接返回了 resultMatrice 这个局部对象,而+运算的函数体结束后这个对象会立刻被析构,其中的矩阵指针也会立刻被释放,这时继续使用返回值里的矩阵指针,显然是内存泄漏!!

3.1.4 举例

1
2
3
4
Vec2D v1(2, 4);
Vec2D v2 = v1 + Vec2D(2, 3)
cout << "v1 is " << v1.toString() << endl;
cout << "v2 is " << v2.toString() << endl;

3.2 重载复合二元算术运算符(+=, -=, *=, and /=)

3.2.1 复合运算符操作的特殊之处

1
2
v1 += v2; // 语句执行后,v1的值被改变了(这非常重要!!)
v1 = v1 + v2;

3.2.2 复合运算符的重载

1
2
3
4
5
6
7
8
9
10
Vec2D Vec2D::operator +=(const Vec2D& secondVec2D ) {
*this = this->add(secondVec2D); //add函数返回临时的匿名对象,这个对象被赋给this指针
return (*this); //返回解引用的this指针,也就是返回了this指向的对象
}

Vec2D Vec2D::add(const Vec2D& secondVec2D) { //prvalue
double m = x_ + secondVec2D.getX()
double n = y_ + secondVec2D.y_;
return Vec2D(m, n); //返回临时的匿名对象
}
  • 如果将以上代码对应v1+=v2; 则有:

    • secondVec2D 是 v2
    • (*this) 是改变后的 v1
  • 如果将

    1
    2
    *this = this->add(secondVec2D);
    return (*this);

    修改为:

    1
    return this->add(secondVec2D);

    这个重载是否还有效?

3.2.3 举例

1
2
3
Vec2D v2(2, 4);
v2 += Vec2D(2, 3);
cout << "v2 is " << v2.toString() << endl;

3.3 重载数组下标运算符

3.3.1 重载[]运算符

  • 为什么重载[]运算符

  • []重载后可用类似数组的语法格式访问对象内容

  • 重载代码示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    Vec2D 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 数组下标运算符作为访问器和修改器

  1. 访问器和修改器:作为访问器就是用[]运算符读取数据,修改器就是用[]运算符修改数据

  2. 使r2[]成为左值的方法:使[]返回一个引用

    1
    2
    3
    4
    5
    double& 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 单目运算符

  1. 主要的单目运算符:—, ++, -(负号), *(解引用)

  2. 编译器执行过程:

    • 若operator @是在obj的类的成员函数,则调用obj.operator @()【无参数】

    • 若operator @是obj的类的友元函数,则调用operator @(obj)

3.4.2 重载负号运算符

3.4.2.1 调用

1
2
3
Vec2D v1(2, 3);
Vec2D v2 = -v1; // 向量v1求负值;v1的值不变
cout << v1.toString();

3.4.2.2 重载负号运算符

1
2
3
Vec2D* Vec2D::operator-(){
return *Vec2D*(-this->x_**,** **-**this->y_); // 返回匿名临时对象
}

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
2
3
4
5
6
7
8
Vec2D v1(2, 3);
cout << "v1: " << v1.toString() << endl; //v1: (3, 4)
Vec2D v2 = ++v1;
cout << "v2: " << v2.toString() << endl; // v2: (3, 4)
Vec2D v3(2, 3);
Vec2D v4 = v3++;
cout << "v3: " << v3.toString() << endl; // v3: (3, 4)
cout << "v4: " << v4.toString() << endl; // v4: (2, 3)

3.4.3.2 前置与后置在函数定义中的区别

image-20210426215559462

  • 前置++/—重载无参数,返回引用类型

  • 后置++/—重载带参数——“dummy”

    • 这个参数用于表示这是一个后置运算符,实际调用的时候并不会传参
  • 若在类外定义,则不论前置后置都需要参数

3.4.3.3 重载实例

  • 前置++运算符
1
2
3
4
5
Vec2D& Vec2D::operator++(){
x_ += 1;
y_ += 1;
return *this; //返回对象本身
}
  • 后置++运算符
1
2
3
4
5
6
Vec2D Vec2D::operator++(int dummy){
Vec2D temp(this->x_, this->y_);
x_ += 1;
y_ += 1;
return temp; //返回的是未自增的对象,是一个纯右值
}
  • 两条返回语句的不同导致了前置++和后置++的区别

3.5 重载流插入(<<)/提取(>>)运算符

3.5.1 重载<</>>的目的

能够把对象信息直接输出,比如:

1
2
cout << vec2d;
cin >> vec2d;

3.5.2 重载为友元函数

  1. 为什么不能重载为成员函数

    运算符重载为类成员函数后,当调用该运算符时,左操作数必须是该类的实例。若<<和>>重载为成员函数,则只能用 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; //更符合编程习惯
  2. 为什么友元函数的返回值是ostream&类型

    要实现cout<<x<<y; 这类连续输出的代码,想要输出y,cout<<x这一部分应该返回一个ostream类型,所以使用ostream&作为返回值

    返回ostream类型与&有什么联系?为什么一定是&呢

3.6 重载对象转换运算符

3.6.1 重载目的

  • 将Vec2D对象转换为double数时,我们可以求该对象的范数,也就是向量长度

3.6.2 重载实例

  • 类型转换函数:
1
2
3
Vec2D::operator double() { //操作符名称和要转到的类型同名,类似于构造函数
return magnitude();
}
  • 调用实例:
1
2
3
Vec2D v1(3, 4);
double d = v1 + 5.1; // d: 10.1
double e = static_cast<double>(v1); // e: 5.0

3.7 重载赋值运算符

C++程序设计(面向对象进阶)_中国大学MOOC(慕课) (icourse163.org)