29. 运算符重载
29.1. 重载规则
不能重载的运算符:成员运算符
.
,条件运算符? :
,长度运算sizeof
,成员指针访问运算符.*
,域解析运算符::
。主要是出于对安全的考虑:如果这些运算符也可以被重载的话,将会造成危害或破坏安全机制,使得事情变得困难或混淆现有的习惯。比如,如果成员运算符.
被重载,就不能用普通的方法访问成员,只能通过指针和->
访问。必须以成员函数的形式重载的运算符:箭头运算符
->
,下标运算符[]
,函数调用运算符()
(用于定义函数对象类),赋值运算符=
。重载不能改变运算符的优先级和结合性。
运算符重载函数不能有默认的参数,否则就改变了运算符操作数的个数。
以全局函数的形式重载,是为了保证该运算符的操作数能够被 对称的处理 。比如,
a + b
和b + 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. 参考资料
C++运算符重载
重载new和delete运算符
C++ new的三种面貌