29. 运算符重载

29.1. 重载规则

  • 不能重载的运算符:成员运算符 . ,条件运算符 ? : ,长度运算 sizeof ,成员指针访问运算符 .* ,域解析运算符 :: 。主要是出于对安全的考虑:如果这些运算符也可以被重载的话,将会造成危害或破坏安全机制,使得事情变得困难或混淆现有的习惯。比如,如果成员运算符 . 被重载,就不能用普通的方法访问成员,只能通过指针和 -> 访问。

  • 必须以成员函数的形式重载的运算符:箭头运算符 -> ,下标运算符 [] ,函数调用运算符 () (用于定义函数对象类),赋值运算符 =

  • 重载不能改变运算符的优先级和结合性。

  • 运算符重载函数不能有默认的参数,否则就改变了运算符操作数的个数。

  • 以全局函数的形式重载,是为了保证该运算符的操作数能够被 对称的处理 。比如, a + bb + a 的行为应该是一样的,如果定义成类成员函数:A operator+(const B b)a + b 被转换成 a.operator+(b) ,而 b + a 被转换成 b.operator+(a) ,它们的行为是不一样的。

  • 如果需要访问非 public 成员,全局函数需要在类内声明为友元(friend)。

运算符重载函数的参数个数取决于:

  • 运算符是一元运算符还是二元运算符。

  • 运算符重载函数是全局函数还是成员函数。对于全局函数,一元运算符有一个参数,二元运算符有两个参数;对于成员函数,一元运算符没有参数,二元运算符有一个参数,类的 this 指针会被绑定到运算符的 左侧 运算对象,成员运算符函数的显式参数一般少一个。new/delete 例外,两种重载形式下参数个数是一样的。

 1// 复数类
 2class Complex
 3{
 4public:  //构造函数
 5    Complex(double real=0.0, double imag=0.0): m_real(real), m_imag(imag){}
 6public:  //运算符重载
 7    //以全局函数的形式重载
 8    friend const Complex operator+(const Complex &c1, const Complex &c2);
 9    friend const Complex operator-(const Complex &c1, const Complex &c2);
10    friend const Complex operator*(const Complex &c1, const Complex &c2);
11    friend const Complex operator/(const Complex &c1, const Complex &c2);
12    friend bool operator==(const Complex &c1, const Complex &c2);
13    friend bool operator!=(const Complex &c1, const Complex &c2);
14    friend istream& operator>>(istream &in, complex &A);
15    friend ostream& operator<<(ostream &out, complex &A);
16    //以成员函数的形式重载
17    Complex& operator=(const Complex &c);
18    Complex& operator+=(const Complex &c);
19    Complex& operator-=(const Complex &c);
20    Complex& operator*=(const Complex &c);
21    Complex& operator/=(const Complex &c);
22public:
23    double real() const{ return m_real; }
24    double imag() const{ return m_imag; }
25private:
26    double m_real;  //实部
27    double m_imag;  //虚部
28};

Note

把 operator + 等四则运算的返回类型定义为 const,是为了防止类似于 a + b = c 之类的赋值操作通过编译。

29.2. 下标运算符 []

为了适应 const 对象,需要重载下面两种函数

1返回值类型& operator[] (参数); // 参数一般为无符号整型
2const 返回值类型& operator[] (参数) const;

因为 const 对象只能调用 const 成员函数。

通过下标访问数组中的元素并不具有检查边界溢出功能,我们可以通过重载实现该功能(抛出异常)。

29.3. 自增和自减

1ClassName& operator++(); // 前缀++,返回的是引用
2const ClassName operator++(int); // 后缀++,返回的是临时变量
3ClassName& operator--(); // 前缀--
4const ClassName operator--(int); // 后缀--

后缀形式有一个 int 类型参数,当函数被调用时,编译器传递一个 0 作为 int 参数的值给该函数,实际上后缀操作符并没有使用它的参数,只是用来区分前缀与后缀函数调用。

后缀操作符最好返回一个 const 对象,用于杜绝产生以下形式的代码

i++++; // same as i.operator++(0).operator++(0);
 1class A
 2{
 3public:
 4    A(int _m=10): m(_m){}
 5
 6    A& operator++();
 7    const A operator++(int);
 8    A& operator--();
 9    const A operator--(int);
10
11    int m;
12};
13
14A& A::operator++()
15{
16    m++;
17    return *this;
18}
19A& A::operator--()
20{
21    m--;
22    return *this;
23}
24const A A::operator++(int)
25{
26    A _a = *this;
27    this->m ++;
28    return _a;
29}
30const A A::operator--(int)
31{
32    A _a = *this;
33    this->m --;
34    return _a;
35}
36
37int main()
38{
39    A a;
40    A b = ++a;
41    A c = a++;
42    cout << a.m << " " << b.m << " " << c.m << endl; // 12 11 11
43    a = c--;
44    c = --b;
45    cout << a.m << " " << b.m << " " << c.m << endl; // 11 10 10
46    return 0;
47}

29.4. >> 和 <<

C++ 的 I/O stream 对象不可拷贝,形参和返回值都是引用。流对象形参不能声明为 const,因为流的缓冲成员(buffer)需要改变。 返回引用有个好处是可以连续输入/输出( cout << a << b; )。

由于 >><< 左侧对象是流对象(cin、cout等),而不是自定义的类对象本身,因此只能重载为全局函数。

 1istream& operator>>(istream &in, complex &A)
 2{
 3    in >> A.m_real >> A.m_imag;
 4    return in;
 5}
 6
 7ostream& operator<<(ostream &out, complex &A)
 8{
 9    out << A.m_real <<" + "<< A.m_imag <<" i ";
10    return out;
11}

29.5. new 和 delete

内存管理运算符 new、new[]、delete 和 delete[] 也可以进行重载,其重载形式既可以是类的成员函数,也可以是全局函数。一般情况下,内建的内存管理运算符就够用了,只有在需要自己管理内存时才会重载。

new 表达式实际完成了三件事:

  • 调用 operator new 或 operator new[],作用是分配一块足够大的内存空间以便存储特定类型的对象。

  • 执行构造函数,在这块内存上构造对象。

  • 返回一个带类型的指针,指向这块内存。

delete 表达式完成了两件事:

  • 调用指针所指对象的析构函数。

  • 调用 operator delete 或 operator delete[] 释放内存。

在重载 new 或 new[] 时,无论是作为成员函数还是作为全局函数,它的第一个参数必须是 size_t 类型,表示的是要分配空间的大小;对于 new[] 的重载函数而言,表示所需要分配空间的总和。这个参数由编译器产生并传递给我们。

注意,new 的返回值是类型 void* ,而不是指向任何特定类型的指针。该操作符本身做的是分配内存,而不是完成一个对象构造。

为一个类重载 new 和 delete 的时候,尽管不必显式使用 static ,但是实际上仍是在创建 static 成员函数。

如果类中没有定义 new 和 delete 的重载函数,那么会自动调用内建的 new 和 delete 运算符。

 1class A
 2{
 3public:
 4    A(){cout << "+A" << endl;}
 5    ~A(){cout << "~A" << endl;}
 6
 7    void* operator new(size_t sz)
 8    {
 9        cout << "A::new " << sz << " bytes" << endl;
10        void* m = malloc(sz);
11        if(!m) cout << "out of memory" << endl;
12        return m;
13    }
14    void operator delete(void* m)
15    {
16        cout << "A::delete" << endl;
17        free(m);
18    }
19    void* operator new[](size_t sz)
20    {
21        cout << "A::new[] " << sz << " bytes" << endl;
22        void* m = malloc(sz);
23        if(!m) cout << "out of memory" << endl;
24        return m;
25    }
26    void operator delete[](void* m)
27    {
28        cout << "A::delete[]" << endl;
29        free(m);
30    }
31private:
32    int a[10];
33};
34int main()
35{
36    A* a = new A();
37    delete a;
38
39    A* arr = new A[3];
40    delete[] arr;
41
42    return 0;
43}

输出:

 1A::new 40 bytes
 2+A
 3~A
 4A::delete
 5A::new[] 128 bytes
 6+A
 7+A
 8+A
 9~A
10~A
11~A
12A::delete[]

new 的三种形态

new 的三种形态分别是:new operator、operator new()、 placement new()。

new operator

new operator 就是上文提到的 new 表达式,它完成三件事:申请内存、构造对象、令指针指向该块内存。这个过程中调用了 operator new() 和 placement new()。

string* p = new string("hello world"); 等价于:

1void* m = operator new(strlen("hello world")); // operator new()
2new(m) string("hello world"); // placement new()
3string* p = static_cast<string*>(m);

delete p; 等价于:

1p->~string();
2operator delete(p);

operator new()

operator new() 用于申请堆空间,功能类似于 C 语言的库函数 malloc() 。如果申请成功则直接返回,如果失败则抛出一个 bad_alloc 异常。

void* operator new(std::size_t size) throw (std::bad_alloc);

正如 new 与 delete 相互对应,operator new() 与 operator delete() 也是一一对应,如果重载了 operator new(),那么理应重载 operator delete()。

placement new()

使用 new 申请空间时,是从系统的堆中分配空间,申请所得空间的 位置 是根据当时内存实际使用情况决定。但是,在某些特殊情况下,可能需要在指定的内存位置去创建对象。

placement new() 的作用是在已经获得的堆空间上调用构造函数来初始化对象,也就是定位构造对象。placement new() 是 C++ 标准库的一部分,被申明在头文件 <new> 中,其函数原型是:

1void* operator new(std::size_t, void* __p) throw()
2{
3    return __p;
4}

placement new() 只是 operator new() 的一个重载,多了一个已经申请好的空间,由 void* __p 指定。用法是 new(addr) constructor() ,在 addr 指向的内存空间调用构造函数进行初始化。

placement new() 既可以在栈上构造对象,也可以在堆上构造对象,取决于参数 __p 所指的空间位置。

正如 new 与 delete 相互对应,operator new() 需要对应一个析构函数来清理所在内存中的内容(不是直接释放内存)。

1string* p = new string(""); // 堆
2new(p) string("hello world");
3p->~string();
4string* q = new(p) string("goodbye");
5assert(p == q);
6q->~string();
7operator delete(q);
1string mem = "abcd"; // 栈
2string* p = new(&mem) string("hello world");
3assert(&mem == p);
4p->~string();

29.6. 参考资料

  1. C++运算符重载

  1. 重载new和delete运算符

  1. C++ new的三种面貌