高级程序设计

10-14 操作符重载

2020-03-12 14:00 CST
2020-03-21 23:28 CST
CC BY-NC 4.0

C++中的操作符

  • 算术操作符
  • 关系与逻辑操作符
  • 位操作符
  • 赋值操作符
  • 其他操作符:? : , :: sizeof new delete

操作符重载的重要性

  • 使用函数调用不符合习惯
  • C++允许对已有的操作符进行重载,使得它们能够对自定义类型(类)的对象进行操作
  • 与函数名重载一样,操作符重载也是实现多样性的一种语言机制

操作符重载的基本原则

  • 只能重载C++语言中的已有的操作符,不能创造新的操作符
  • 可以重载除. .* ?: :: sizeof以外的所有操作符
  • 遵循已有操作符的语法:
    • 不能改变操作数个数
    • 不改变原操作符的优先级和结合性
  • 尽量遵循已有操作符原来的语义

操作符重载的实现途径

  • 作为一个类的非静态的成员函数(newdelete除外)
  • 作为一个全局(友元)函数,至少有一个参数是类、结构、枚举或它们的引用类型

双目操作符重载

作为成员函数重载:

class Complex {
  double real, imag;

public:
  bool operator==(const Conplex &x) const {
    return (real == x.real) and (imag == x.imag);
  }
};

Complex c1, c2;
c1 == c2;          // OK
c1.operator==(c2); // OK

作为全局(友元)函数重载:

class Complex {
  double real, imag;

public:
  // Constructors...
  friend Complex operator+(const Complex &c1, const Complex &c2);
  friend Complex operator+(const Complex &c, double d);
  friend Complex operator+(double d, const Complex &c);
};

Complex operator+(const Complex &c1, const Complex &c2) {
  return Complex(c1.real + c2.real, c1.imag + c2.imag);
}
Complex operator+(const Complex &c, double d) {
  return Complex(c.real + d, c.imag);
}
// 注意:实数+复数只能作为全局函数重载
Complex operator+(double d, const Complex &c) {
  return Complex(d + c.real, c.imag);
}

double d;
Complex c1, c2;
c1 + c2;           // OK
c1 + d, d + c2;    // OK
operator+(c1, c2); // OK

单目操作符重载

作为成员函数重载:

class Complex {
  double real, imag;

public:
  Complex operator-() const {
    Complex temp;
    temp.real = -real;
    temp.imag = -imag;
    return temp;
  }
}

Complex a, b;
b = -a;            // OK
b = a.operator-(); // OK

作为全局(友元)函数重载:

class Complex {
  double real, imag;

public:
  friend bool operator!(const Complex &c);
}

bool operator!(const Complex &c) {
  return (c.real == 0.0) and (c.imag == 0.0);
}

Complex a;
!a;           // OK
operator!(a); // OK

操作符++--的重载

  • 单目操作符++--有前置和后置两种用法
  • 重载++--时,如果没有特殊处理,它们的后置用法和前置用法共用同一个重载函数
  • 为了能够区分前置和后置用法,可以为它们再写一个重载函数用于实现它们的后置用法,该重载函数应有一个形式上的int型参数
class Counter {
  int value;

public:
  // Constructors...
  // 前置的++重载函数
  Counter &operator++() {
    value++;
    return *this;
  }
  // 后置的++重载函数,返回const对象
  const Counter operator++(int) {
    Counter temp = *this; // 保存原来的对象
    value++;              // 等价于++(*this);
    return temp;          // 返回原来的对象
  }
}

Counter a;
++(++a); // OK
(++a)++; // OK
++(a++); /* ERROR */
(a++)++; /* ERROR */

赋值操作符=的重载

C++编译程序会为每个类定义一个隐式的赋值操作符重载函数,其行为是逐个成员进行赋值操作。

  • 对于普通成员采用常规的赋值操作;
  • 对于成员对象调用该成员对象类的赋值操作符重载函数进行赋值操作。

例如对字符串类型重载赋值操作符:

class String {
  char *p;

public:
  String &operator=(const String &s) {
    if (&s == this) return *this; // 防止自身赋值
    delete []p;
    p = new char[strlen(s.p) + 1];
    strcpy(p, a.p);
    return *this;
  }
};

赋值操作符函数的返回类型是对类的引用,因此可以进行(a = b) = c这样的操作。

赋值操作符只能作为非静态的成员函数来重载,不能被继承。

一般来说,需要自定义拷贝构造函数的类通常也需要自定义赋值操作符重载函数。

需要注意=的不同含义:

  • myClass foo = bar;:初始化,调用拷贝构造函数,相当于myClass foo(bar);
  • myClass foo; foo = bar;:赋值,调用赋值操作符重载函数

访问数组元素操作符[]的重载

对于具有线性关系的元素所构成的对象,可通过重载[]实现对其元素的访问。

class Vector {
  int *data;

public:
  // Constructors...
  int &operator[](int i) {
    return data[i];
  }
  // Or, if data is const:
  int operator[](int i) const {
    return data[i];
  }
}

操作符newdelete的重载

这两个操作符用于创建和撤销动态对象。

系统提供的newdelete操作所涉及的空间分配和释放是通过系统的堆区管理系统来进行的,效率常常不高。可以通过对这两个操作符进行重载,使得程序能以自己的方式来实现动态对象空间的分配和释放功能。

操作符new有两个功能:

  • 为动态对象分配空间;
  • 调用对象类的构造函数。

操作符delete有两个功能:

  • 调用对象类的析构函数;
  • 释放动态对象的空间。

重载操作符newdelete时,重载的是它们的分配空间和释放空间的功能,不影响对构造函数和析构函数的调用。

重载操作符new

操作符new必须作为静态的成员函数(static可以不写)来重载。

class Foo {
  int x, y;
  
public:
  void *operator new(size_t size) {
    void *p = malloc(size);
    memset(p, 0, size); // set to 0
    return p;
  }
}

重载的new操作符在创建动态对象时会自动计算大小并将其作为参数去调用重载函数。

重载new时也可以带有其他参数,此时调用方式变为

class Foo {
  int x, y;

public:
  Foo(int i) { /* ... */ }
  void *operator new(size_t size, void *p) {
    return p;
  }
};

char buf[sizeof(Foo)];
Foo *p = new (buf) Foo(0);

重载操作符delete

一般来说,如果对某个类重载了操作符new,则相应地也要重载操作符delete。操作符delete也必须作为静态成员的函数来重载。

针对某个类的动态对象,程序可以自己管理该类对象的空间分配和释放,以提高效率。例如:

  • 重载new
    • 第一次创建该雷的动态对象时,先从系统管理的堆区中申请一块大的空间;
    • 将上述大空间分成若干小块,每个小块的大小为该类一个对象的大小,然后用链表来管理这些小块;
    • 在该链表上为该类对象分配空间。
  • 重载delete
    • 该类的一个对象消亡时,该对象的空间归还到链表中,而不是归还到系统的堆区中。

函数调用操作符()

函数调用操作符重载主要用于只有一个操作的对象(函数对象functor),该对象除了具有一般函数的行为外,它还可以拥有状态。

class Random {
  int seed;
public:
  int operator()() {
    seed = ...;
    return seed;
  }
};

$\lambda$表达式

在C++中,$\lambda$表达式是通过函数对象实现的。C++11提供的匿名函数机制可以把函数的定义和使用合二为一。

$\lambda$表达式的定义格式:[<环境变量使用说明>]<形式参数><返回值类型指定><函数体>

  • 环境变量使用说明:
    • 空:不能使用外层作用域中的自动变量;
    • &:按引用方式使用外层作用域中的自动变量(可以改变这些变量的值);
    • =:按值方式使用使用外层作用域中的自动变量(不能改变这些变量的值)。
    • &=可以统一指定外部变量,也可以对某个变量单独指定。
  • 形式参数:指出函数的参数及类型,如果没有参数可以省略。
  • 返回值类型制定:指出函数的返回值类型,如-> int,可以省略而根据return语句隐式确定。
  • 函数体:符合语句。

$\lambda$表达式通常用于把一个匿名函数作为参数传给另一个函数的场合。$\lambda$表达式分为以下几种类型:

  • 闭包类型(Closure Type):包含环境变量捕获、参数、返回值、函数体等成员;
  • 不使用环境变量的$\lambda$表达式可隐式转换成函数指针;
  • 可转换为std::function的一个实例。

在C++中$\lambda$表达式的具体实现:

  • 首先隐式定义一个类,数据成员对应用到的环境变量,用构造函数对其初始化。
  • 重载了函数调用操作符,重载函数按相应$\lambda$表达式的功能来实现。
  • 创建上述类的一个临时对象,在使用上述$\lambda$表达式的地方用该对象来替代。

类成员访问操作符->:智能指针

->是一个双目运算符,第一个操作数是指向类或结构体的指针,第二个操作数是第一个操作数指向的类或结构体的成员。

通过对->进行重载,可以实现一种智能指针(Smart Pointer):

  • 一个具有指针功能的对象,通过该对象能访问所“指向”的另一个对象。
  • 通过该指针对象去访问所指向的对象的成员前能做一些额外的事情。
class B {
private:
  A *ptr;
  int count;
public:
  A *operator->() {
    ++count;
    return ptr;
  }
}

为了完全模拟普通的指针功能,智能指针类还可以重载*(间接对象访问)等操作符。在C++11中,智能指针类shared_ptr实现资源的计数访问和自动回收。

类成员指针.*->*

  • a.*b:返回对象a的成员b所指向的对象;
  • a->*b:返回指针a所指向的对象的成员b所指向的对象。
class A {
public:
  int x;
  void f();
};

int *p_x = &A::x;          // Error, A is not an instance
void (*p_f)() = &A::f;     // Error, A::f has an implict argument this
int A::*pm_x = &A::x;      // OK, pm_x is a pointer to int member
void (A::*pm_f)() = &A::f; // OK, pm_f is a pointer to void function member

A a;
a.*pm_x = 0;  // OK, equivalent to a.x = 0;
a.*pm_f();    // OK, equivalent to a.f();

A *p = new A;
p->*pm_x = 0; // OK, equivalent to p->x = 0;
p->*pm_f();   // OK, equivalent to p->f();

类成员指针的应用:将相同类型、相同签名、不同名称的成员函数用同种指针构造成一个数组,然后依次调用。

操作符..*无法重载,但->->*可以重载,使得第一个操作数可以是类(结构)的对象,而不是指针。(此操作符重载时对第二操作数无限制。)

类型转换操作符

类中带一个参数的构造函数可以用作从其他类型到该类的转换。也可以自定义从一个类转换成其他类型的转换。

class Complex {
public:
  Complex(double r); // constructor with one argument
  operator int();    // transform Complex to int
};

Complex c;
c = c + 1.0; // OK, 1.0 becomes Complex implictly
int r = c;   // OK, c transformed to int implictly

需要注意的是自定义类型转换操作符可能会导致歧义问题:如double r = c + 1.0可以是c转换为double类型,也可以是1.0转换为Complex类型。

要解决歧义问题,有两种方式:

  • 使用显式类型转换:double r = (double)c + 1.0
  • 增加修饰符,禁止编译器将构造函数当作隐式类型转换符:explicit Complex(double r);