前言

本文引用于“C语言中文网”,我整理出来放在博客,方便大家共同学习。所有知识点和代码均已亲测可用,如有疑问,可提出,一起讨论学习。

本章内容:

1. C++运算符重载的概念和原理
2. C++重载=(C++重载赋值运算符)
3. C++深拷贝和浅拷贝(C++深复制和浅复制)
4. C++运算符重载为友元函数
5. C++实现可变长度的动态数组
6. C++重载<<和>>(C++重载输出运算符和输入运算符)
7. C++重载()(强制类型转换运算符)
8. C++重载++和--(自增和自减运算符)
9. C++运算符重载注意事项以及汇总

1 C++运算符重载的概念和原理 

如果不做特殊处理,C++ 的 +、-、*、/ 等运算符只能用于对基本类型的常量或变量进行运算,不能用于对象之间的运算。

有时希望对象之间也能用这些运算符进行运算,以达到使程序更简洁、易懂的目的。例如,复数是可以进行四则运算的,两个复数对象相加如果能直接用+运算符完成,不是很直观和简洁吗?

利用 C++ 提供的“运算符重载”机制,赋予运算符新的功能,就能解决用+将两个复数对象相加这样的问题。

运算符重载,就是对已有的运算符赋予多重含义,使同一运算符作用于不同类型的数据时产生不同的行为。运算符重载的目的是使得 C++ 中的运算符也能够用来操作对象。

运算符重载的实质是编写以运算符作为名称的函数。不妨把这样的函数称为运算符函数。运算符函数的格式如下:

返回值类型  operator  运算符(形参表)
{
    ....
}

包含被重载的运算符的表达式会被编译成对运算符函数的调用,运算符的操作数成为函数调用时的实参,运算的结果就是函数的返回值。运算符可以被多次重载。

运算符可以被重载为全局函数,也可以被重载为成员函数。一般来说,倾向于将运算符重载为成员函数,这样能够较好地体现运算符和类的关系。来看下面的例子:

#include <iostream>
using namespace std;
class Complex
{
    public:
    double real, imag;
    Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) { }
    Complex operator - (const Complex & c);
};
Complex operator + (const Complex & a, const Complex & b)
{
    return Complex(a.real + b.real, a.imag + b.imag); //返回一个临时对象
}
Complex Complex::operator - (const Complex & c)
{
    return Complex(real - c.real, imag - c.imag); //返回一个临时对象
}
int main()
{
    Complex a(4, 4), b(1, 1), c;
    c = a + b; //等价于 c = operator + (a,b);
    cout << c.real << "," << c.imag << endl;
    cout << (a - b).real << "," << (a - b).imag << endl; //a-b等价于a.operator - (b)
    return 0;
}

程序的输出结果是:

5,5
3,3

程序将+重载为一个全局函数(只是为了演示这种做法,否则重载为成员函数更好),将-重载为一个成员函数。

运算符重载为全局函数时,参数的个数等于运算符的目数(即操作数的个数);运算符重载为成员函数时,参数的个数等于运算符的目数减一。

如果+没有被重载,第 21 行会编译出错,因为编译器不知道如何对两个 Complex 对象进行+运算。有了对+的重载,编译器就将a+b理解为对运算符函数的调用,即operator+(a,b),因此第 21 行就等价于:

c = operator+(a, b);

即以两个操作数 a、b 作为参数调用名为operator+的函数,并将返回值赋值给 c。

第 12 行,在 C++ 中,“类名(构造函数实参表)”这种写法表示生成一个临时对象。该临时对象没有名字,生存期就到包含它的语句执行完为止。因此,第 12 行实际上生成了一个临时的 Complex 对象作为 return 语句的返回值,该临时对象被初始化为 a、b 之和。第 16 行与第 12 行类似。

由于-被重载为 Complex 类的成员函数,因此,第 23 行中的a-b就被编译器处理成:

a.operator-(b);

由此就能看出,为什么运算符重载为成员函数时,参数个数要比运算符目数少 1 了。

2 C++重载=(C++重载赋值运算符)

赋值运算符=要求左右两个操作数的类型是匹配的,或至少是兼容的。有时希望=两边的操作数的类型即使不兼容也能够成立,这就需要对=进行重载。C++ 规定,=只能重载为成员函数。来看下面的例子。

要编写一个长度可变的字符串类 String,该类有一个 char* 类型的成员变量,用以指向动态分配的存储空间,该存储空间用来存放以\0结尾的字符串。String 类可以如下编写:

#include <iostream>
#include <cstring>
using namespace std;
class String {
private:
    char * str;
public:
    String() :str(NULL) { }
    const char * c_str() const { return str; };
    String & operator = (const char * s);
    ~String();
};
String & String::operator = (const char * s)
//重载"="以使得 obj = "hello"能够成立
{
    if (str)
        delete[] str;
    if (s) { //s不为NULL才会执行拷贝
        str = new char[strlen(s) + 1];
        strcpy(str, s);
    }
    else
        str = NULL;
    return *this;
}
String::~String()
{
    if (str)
        delete[] str;
};
int main()
{
    String s;
    s = "Good Luck,"; //等价于 s.operator=("Good Luck,");
    cout << s.c_str() << endl;
    // String s2 = "hello!"; //这条语句要是不注释掉就会出错
    s = "Shenzhou 8!"; //等价于 s.operator=("Shenzhou 8!");
    cout << s.c_str() << endl;
    return 0;
}

程序的运行结果:

Good Luck,
Shenzhou 8!

第 8 行的构造函数将 str 初始化为 NULL,仅当执行了 operator= 成员函数后,str 才会指向动态分配的存储空间,并且从此后其值不可能再为 NULL。在 String 对象的生存期内,有可能从未执行过 operator= 成员函数,所以在析构函数中,在执行delete[] str之前,要先判断 str 是否为 NULL。

第 9 行的函数返回了指向 String 对象内部动态分配的存储空间的指针,但是不希望外部得到这个指针后修改其指向的字符串的内容,因此将返回值设为 const char*。这样,假定 s 是 String 对象,那么下面两条语句编译时都会报错,s 对象内部的字符串就不会轻易地从外部被修改了 :

char* p = s.c_str ();
strcpy(s.c_str(), "Tiangong1");

第一条语句出错是因为=左边是 char* 类型,右边是 const char * 类型,两边类型不匹配;第二条语句出错是因为 strcpy 函数的第一个形参是 char* 类型,而这里实参给出的却是 const char * 类型,同样类型不匹配。

如果没有第 13 行对=的重载,第 34 行的s = "Good Luck,"肯定会因为类型不匹配而编译出错。经过重载后,第 34 行等价s.operator=("Good Luck,");,就没有问题了。

在 operator= 函数中,要先判断 str 是否已经指向动态分配的存储空间,如果是,则要先释放那片空间,然后重新分配一片空间,再将参数 s 指向的内容复制过去。这样,对象中存放的字符串就和 s 指向的字符串一样了。分配空间时,要考虑到字符串结尾的\0,因此分配的字节数要比 strlen(s) 多 1。

需要注意一点,即使对=做了重载,第 36 行的String s2 = "hello!";还是会编译出错,因为这是一条初始化语句,要用到构造函数,而不是赋值运算符=。String 类没有编写参数类型为 char * 的构造函数,因此编译不能通过。

就上面的程序而言,对 operator= 函数的返回值类型没有什么特别要求,void 也可以。但是在对运算符进行重载时,好的风格是应该尽量保留运算符原本的特性,这样其他人在使用这个运算符时才不容易产生困惑。赋值运算符是可以连用的,这个特性在重载后也应该保持。即下面的写法应该合法:

a = b = c;

假定 a、b、c 都是 String 对象,则上面的语句等价于下面的嵌套函数调用:

a.operator=( b.operator=(c) );

如果 operator= 函数的返回值类型为 void,显然上面这个嵌套函数调用就不能成立。将返回值类型改为 String 并且返回 *this 可以解决问题,但是还不够好。因为,假设 a、b、c 是基本类型的变量,则

(a =b) = c;

这条语句执行的效果会使得 a 的值和 c 相等,即a = b这个表达式的值其实是 a 的引用。为了保持=的这个特性,operator= 函数也应该返回其所作用的对象的引用。因此,返回值类型为 String & 才是风格最好的写法。在 a、b、c 都是 String 对象时,(a=b)=c;等价于

( a.operator=(b) ).operator=(c);

a.operator=(b) 返回对 a 的引用后,通过该引用继续调用 operator=(c),就会改变 a 的值。

3 C++深拷贝和浅拷贝(C++深复制和浅复制) 

同类对象之间可以通过赋值运算符=互相赋值。如果没有经过重载,=的作用就是把左边的对象的每个成员变量都变得和右边的对象相等,即执行逐个字节拷贝的工作,这种拷贝叫作“浅拷贝”。

有的时候,两个对象相等,从实际应用的含义上来讲,指的并不应该是两个对象的每个字节都相同,而是有其他解释,这时就需要对=进行重载。

上节我们定义了 String 类,并重载了=运算符,使得 char * 类型的字符串可以赋值给 String 类的对象。完整代码如下:

#include <iostream>
#include <cstring>
using namespace std;
class String {
private:
    char * str;
public:
    String() :str(NULL) { }
    const char * c_str() const { return str; };
    String & operator = (const char * s);
    ~String();
};
String & String::operator = (const char * s)
//重载"="以使得 obj = "hello"能够成立
{
    if (str)
        delete[] str;
    if (s) { //s不为NULL才会执行拷贝
        str = new char[strlen(s) + 1];
        strcpy(str, s);
    }
    else
        str = NULL;
    return *this;
}
String::~String()
{
    if (str)
        delete[] str;
};
int main()
{
    String s;
    s = "Good Luck,"; //等价于 s.operator=("Good Luck,");
    cout << s.c_str() << endl;
    // String s2 = "hello!"; //这条语句要是不注释掉就会出错
    s = "Shenzhou 8!"; //等价于 s.operator=("Shenzhou 8!");
    cout << s.c_str() << endl;
    return 0;
}

对于上面的代码,如果让两个 String 对象相等(把一个对象赋值给另一个对象),其意义到底应该是什么呢?是两个对象的 str 成员变量都指向同一个地方,还是两个对象的 str 成员变量指向的内存空间中存放的内容相同?如果把 String 对象理解为存放字符串的对象,那应该是后者比较合理和符合习惯,而前者不但不符合习惯,还会导致程序漏洞。

按照上面代码中 String 类的写法,下面的程序片段会引发问题:

String s1, s2;
s1 = "this";
s2 = "that";
s2 = s1;

执行完上面的第 3 行后,s1 和 s2 的状态如图 1 (a) 所示,它们的 str 成员变量指向不同的存储空间。
 


                                                                               图1:浅拷贝导致的错误

s2=s1;执行的是浅拷贝。执行完s2=s1;后,s2.str s1.str 指向同一个地方, 如图 1 (b) 所示。这导致 s2.str 原来指向的那片动态分配的存储空间再也不会被释放,变成内存垃圾。

此外,s1 和 s2 消亡时都会执行delete[] str;,这就使得同一片存储空间被释放两次,会导致严重的内存错误,可能引发程序意外中止。

而且,如果执行完s1=s2;后 又执行s1 = "some";,则会导致 s2.str 也被释放。

为解决上述问题,需要对做=再次重载。重载后的的逻辑,应该是使得执行s2=s1;后,s2.str 和 s1.str 依然指向不同的地方,但是这两处地方所存储的字符串是一样的。再次重载=的写法如下:

String & String::operator = (const String & s)
{
    if(str == s.str)
        return * this;
    if(str)
        delete[] str;
    if(s.str){ //s. str不为NULL才执行复制操作
        str = new char[ strlen(s.str) + 1 ];
        strcpy(str, s.str);
    }
    else
        str = NULL;
    return * this;
}

经过重载,赋值号=的功能不再是浅拷贝,而是将一个对象中指针成员变量指向的内容复制到另一个对象中指针成员变量指向的地方。这样的拷贝就叫“深拷贝”。

程序第 3 行要判断 str==s.str,是因为要应付如下的语句:

s1 = s1;

这条语句本该不改变s1的值才对。s1=s1;等价于s.operator=(s1);,如果没有第 3 行和第 4 行,就会导致函数执行中的 str 和 s.str 完全是同一个指针(因为形参 s 引用了实参 s1,因此可以说 s 就是 s1)。第 8 行为 str 新分配一片存储空间,第 9 行从自己复制到自己,那么 str 指向的内容就不知道变成什么了。

当然,程序员可能不会写s1=s1;这样莫名奇妙的语句,但是可能会写rs1=rs2;,如果 rs1 和 rs2 都是 String 类的引用,而且它们正好引用了同一个 String 对象,那么就等于发生了s1=s1;这样的情况。

思考题:上面的两个 operator= 函数有什么可以改进以提高执行效率的地方?

重载了两次=的 String 类依然可能导致问题。因为没有编写复制构造函数,所以一旦出现使用复制构造函数初始化的 String 对象(例如,String 对象作为函数形参,或 String 对象作为函数返回值),就可能导致问题。最简单的可能出现问题的情况如下:

String s2;
s2 = "Transformers";
String s1(s2);

s1 是以 s2 作为实参,调用默认复制构造函数来初始化的。默认复制构造函数使得 s1.str 和 s2.str 指向同一个地方,即执行的是浅拷贝,这就导致了前面提到的没有对=进行第二次重载时产生的问题。因此还应该为 String 类编写如下复制构造函数,以完成深拷贝:

String::String(String & s)
{
    if(s.str){
        str = new char[ strlen(s.str) + 1 ];
        strcpy(str, s.str);
    }
    else
        str = NULL;
}
最后,给出 String 类的完整代码:
class String {
private:
    char * str;
public:
    String() :str(NULL) { }
    String(String & s);
    const char * c_str() const { return str; };
    String & operator = (const char * s);
    String & operator = (const String & s);
    ~String();
};
String::String(String & s)
{
    if (s.str) {
        str = new char[strlen(s.str) + 1];
        strcpy(str, s.str);
    }
    else
        str = NULL;
}
String & String::operator = (const String & s)
{
    if (str == s.str)
        return *this;
    if (str)
        delete[] str;
    if (s.str) { //s. str不为NULL才执行复制操作
        str = new char[strlen(s.str) + 1];
        strcpy(str, s.str);
    }
    else
        str = NULL;
    return *this;
}
String & String::operator = (const char * s)
//重载"="以使得 obj = "hello"能够成立
{
    if (str)
        delete[] str;
    if (s) { //s不为NULL才会执行拷贝
        str = new char[strlen(s) + 1];
        strcpy(str, s);
    }
    else
        str = NULL;
    return *this;
}
String::~String()
{
    if (str)
        delete[] str;
};

4 C++运算符重载为友元函数

一般情况下,将运算符重载为类的成员函数是较好的选择。但有时,重载为成员函数不能满足使用要求,重载为全局函数又不能访问类的私有成员,因此需要将运算符重载为友元。

例如,对于复数类 Complex 的对象,希望它能够和整型以及实数型数据做四则运算,假设 c 是 Complex 对象,希望c+55+c这两个表达式都能解释得通。

将+重载为 Complex 类的成员函数能解释c+5,但是无法解释5+c。要让5+c有意义,则应对+进行再次重载,将其重载为一个全局函数。为了使该全局函数能访问 Complex 对象的私有成员,就应该将其声明为 Complex 类的友元。具体写法如下:

class Complex
{
    double real, imag;
public:
    Complex(double r, double i):real(r), imag(i){};
    Complex operator + (double r);
    friend Complex operator + (double r, const Complex & c);
};
Complex Complex::operator + (double r)
{ //能解释c+5
    return Complex(real+r, imag);
}
Complex operator + (double r, const Complex & c)
{ //能解释5+c
    return Complex (c.real+r, c.imag);
}

 5 C++实现可变长度的动态数组

实践中经常碰到程序需要定义一个数组,但不知道定义多大合适的问题。按照最大的可能性定义,会造成空间浪费;定义小了则无法满足需要。

如果用动态内存分配的方式解决,需要多少空间就动态分配多少,固然可以解决这个问题,但是要确保动态分配的内存在每一条执行路径上都能够被释放,也是一件头疼的事情。

因此需要编写一个长度可变的数组类,该类的对象就能存放一个可变长数组。该数组类应该有以下特点:

  • 数组的元素个数可以在初始化该对象时指定。
  • 可以动态往数组中添加元素。
  • 使用该类时不用担心动态内存分配和释放问题。
  • 能够像使用数组那样使用动态数组类对象,如可以通过下标访问其元素。

程序代码如下:

#include <iostream>
#include <cstring>
using namespace std;
class CArray
{
	int size; //数组元素的个数
	int* ptr; //指向动态分配的数组
public:
	CArray(int s = 0); //s代表数组元素的个数
	CArray(CArray & a);
	~CArray();
	void push_back(int v); //用于在数组尾部添加一个元素 v
	CArray & operator = (const CArray & a); //用于数组对象间的赋值
	int length() const { return size; } //返回数组元素个数
	int & operator[](int i)
	{ //用以支持根据下标访问数组元素,如“a[i]=4;”和“n=a[i];”这样的语句
		return ptr[i];
	};
};
CArray::CArray(int s) : size(s)
{
	if (s == 0)
		ptr = NULL;
	else
		ptr = new int[s];
}
CArray::CArray(CArray & a)
{
	if (!a.ptr) {
		ptr = NULL;
		size = 0;
		return;
	}
	ptr = new int[a.size];
	memcpy(ptr, a.ptr, sizeof(int) * a.size);
	size = a.size;
}
CArray::~CArray()
{
	if (ptr) delete[] ptr;
}
CArray & CArray::operator=(const CArray & a)
{ //赋值号的作用是使 = 左边对象中存放的数组的大小和内容都与右边的对象一样
	if (ptr == a.ptr) //防止 a=a 这样的赋值导致出错
		return *this;
	if (a.ptr == NULL) { //如果a里面的数组是空的
		if (ptr)
		delete[] ptr;
		ptr = NULL;
		size = 0;
		return *this;
	}
	if (size < a.size) { //如果原有空间够大,就不用分配新的空间
		if (ptr)
			delete[] ptr;
		ptr = new int[a.size];
	}
	memcpy(ptr, a.ptr, sizeof(int)*a.size);
	size = a.size;
	return *this;
}
void CArray::push_back(int v)
{ //在数组尾部添加一个元素
	if (ptr) {
		int* tmpPtr = new int[size + 1]; //重新分配空间
		memcpy(tmpPtr, ptr, sizeof(int) * size); //复制原数组内容
		delete[] ptr;
		ptr = tmpPtr;
	}
	else //数组本来是空的
		ptr = new int[1];
	ptr[size++] = v; //加入新的数组元素
}
int main()
{
	CArray a; //开始的数组是空的
	for (int i = 0; i<5; ++i)
		a.push_back(i);
	CArray a2, a3;
	a2 = a;
	for (int i = 0; i<a.length(); ++i)
		cout << a2[i] << " ";
	a2 = a3; //a2 是空的
	for (int i = 0; i<a2.length(); ++i) //a2.length()返回 0
		cout << a2[i] << " ";
	cout << endl;
	a[3] = 100;
	CArray a4(a);
	for (int i = 0; i<a4.length(); ++i)
		cout << a4[i] << " ";
	return 0;
}

程序的输出结果为:

0 1 2 3 4
0 1 2 100 4

[]是双目运算符,有两个操作数,一个在里面,一个在外面。表达式 a[i] 等价于 a.operator[](i)。按照[]原有的特性,a[i]应该能够作为左值使用,因此 operator[] 函数应该返回引用。

思考题:每次在数组尾部添加一个元素都要重新分配内存并且复制原有内容,显然效率是低下的。有什么办法能够加快添加元素的速度呢?

6 C++重载<<和>>(C++重载输出运算符和输入运算符)

在 C++ 中,左移运算符<<可以和 cout 一起用于输出,因此也常被称为“流插入运算符”或者“输出运算符”。实际上,<<本来没有这样的功能,之所以能和 cout 一起使用,是因为被重载了。

cout 是 ostream 类的对象。ostream 类和 cout 都是在头文件 <iostream> 中声明的。ostream 类将<<重载为成员函数,而且重载了多次。为了使cout<<"Star War"能够成立,ostream 类需要将<<进行如下重载:

ostream & ostream::operator << (const char* s)
{
    //输出s的代码
    return * this;
}

为了使cout<<5;能够成立,ostream 类还需要将<<进行如下重载:

ostream & ostream::operator << (int n)
{
    //输出n的代码
    return *this;
}

重载函数的返回值类型为 ostream 的引用,并且函数返回 *this,就使得cout<<"Star War"<<5能够成立。有了上面的重载,cout<<"Star War"<<5;就等价于:

( cout.operator<<("Star War") ).operator<<(5);

重载函数返回 *this,使得cout<<"Star War"这个表达式的值依然是 cout(说得更准确一点就是 cout 的引用,等价于 cout),所以能够和<<5继续进行运算。

cin 是 istream 类的对象,是在头文件 <iostream> 中声明的。istream 类将>>重载为成员函数,因此 cin 才能和>>连用以输入数据。一般也将>>称为“流提取运算符”或者“输入运算符”。

例题:假定 c 是 Complex 复数类的对象,现在希望写cout<<c;就能以 a+bi 的形式输出 c 的值;写cin>>c;就能从键盘接受 a+bi 形式的输入,并且使得 c.real = a, c.imag = b。

显然,要对<<>>进行重载,程序如下:

#include <iostream>
#include <string>
#include <cstdlib>
using namespace std;
class Complex
{
	double real,imag;
public:
	Complex( double r=0, double i=0):real(r),imag(i){ };
	friend ostream & operator<<( ostream & os,const Complex & c);
	friend istream & operator>>( istream & is,Complex & c);
};
ostream & operator<<( ostream & os,const Complex & c)
{
	os << c.real << "+" << c.imag << "i"; //以"a+bi"的形式输出
	return os;
}
istream & operator>>( istream & is,Complex & c)
{
	string s;
	is >> s; //将"a+bi"作为字符串读入, "a+bi" 中间不能有空格
	int pos = s.find("+",0);
	string sTmp = s.substr(0,pos); //分离出代表实部的字符串
	c.real = atof(sTmp.c_str());//atof库函数能将const char*指针指向的内容转换成 float
	sTmp = s.substr(pos+1, s.length()-pos-2); //分离出代表虚部的字符串
	c.imag = atof(sTmp.c_str());
	return is;
}
int main()
{
	Complex c;
	int n;
	cin >> c >> n;
	cout << c << "," << n;
	return 0;
}

程序的运行结果:

13.2+133i 87
13.2+133i,87

因为没有办法修改 ostream 类和 istream 类,所以只能将<<>>重载为全局函数的形式。由于这两个函数需要访问 Complex 类的私有成员,因此在 Complex 类定义中将它们声明为友元。

cout<<c会被解释成operator<<(cout, c),因此编写 operator<< 函数时,它的两个参数就不难确定了。

第 13 行,参数 os 只能是 ostream 的引用,而不能是 ostream 对象,因为 ostream 的复制构造函数是私有的,没有办法生成 ostream 参数对象。operator<< 函数的返回值类型设为 ostream &,并且返回 os,就能够实现<<的连续使用,如cout<<c<<5。在本程序中,执行第 34 行的cout<<c进入 operator<< 后,os 引用的就是 cout,因此第 34 行就能产生输出。

用 cin 读入复数时,对应的输入必须是 a+bi 的格式,而且中间不能有空格,如输入 13.2+33.4i。第 21 行的is>>s;读入一个字符串。假定输入的格式没有错误,那么被读入 s  的就是 a+bi 格式的字符串。

读入后需要将字符串中的实部 a 和虚部 b 分离出来,分离的办法就是找出被+隔开的两个子串,然后将两个字符串转换成浮点数。第 24 行调用了标准库函数 atof 来将字符串转换为浮点数。该函数的原型是float atof(const char *),它在 <cstdlib> 头文件中声明。

7 C++重载()(强制类型转换运算符)

在 C++ 中,类型的名字(包括类的名字)本身也是一种运算符,即类型强制转换运算符。

类型强制转换运算符是单目运算符,也可以被重载,但只能重载为成员函数,不能重载为全局函数。经过适当重载后,(类型名)对象这个对对象进行强制类型转换的表达式就等价于对象.operator 类型名(),即变成对运算符函数的调用。

下面的程序对 double 类型强制转换运算符进行了重载。

#include <iostream>
using namespace std;
class Complex
{
    double real, imag;
public:
    Complex(double r = 0, double i = 0) :real(r), imag(i) {};
    operator double() { return real; } //重载强制类型转换运算符 double
};
int main()
{
    Complex c(1.2, 3.4);
    cout << (double)c << endl; //输出 1.2
    double n = 2 + c; //等价于 double n = 2 + c. operator double()
    cout << n; //输出 3.2
}

程序的输出结果是:

1.2
3.2

第 8 行对 double 运算符进行了重载。重载强制类型转换运算符时,不需要指定返回值类型,因为返回值类型是确定的,就是运算符本身代表的类型,在这里就是 double。

重载后的效果是,第 13 行的(double)c等价于c.operator double()

有了对 double 运算符的重载,在本该出现 double 类型的变量或常量的地方,如果出现了一个 Complex 类型的对象,那么该对象的 operator double 成员函数就会被调用,然后取其返回值使用。

例如第 14 行,编译器认为本行中c这个位置如果出现的是 double 类型的数据,就能够解释得通,而 Complex 类正好重载了 double 运算符,因而本行就等价于:

double n = 2 + c.operator double();

8 C++重载++和--(自增和自减运算符)

自增运算符++、自减运算符--都可以被重载,但是它们有前置、后置之分。

++为例,假设 obj 是一个 CDemo 类的对象,++objobj++本应该是不一样的,前者的返回值应该是 obj 被修改后的值,而后者的返回值应该是 obj 被修改前的值。如果如下重载++运算符:

CDemo & CDemo::operator ++ ()
{
    //...
    return * this;
}

那么不论obj++还是++obj,都等价于obj.operator++()无法体现出差别。

为了解决这个问题,C++ 规定,在重载++--时,允许写一个增加了无用 int 类型形参的版本,编译器处理++--前置的表达式时,调用参数个数正常的重载函数;处理后置表达式时,调用多出一个参数的重载函数。来看下面的例子:

#include <iostream>
using namespace std;
class CDemo {
private:
	int n;
public:
	CDemo(int i=0):n(i) { }
	CDemo & operator++(); //用于前置形式
	CDemo operator++( int ); //用于后置形式
	operator int ( ) { return n; }
	friend CDemo & operator--(CDemo & );
	friend CDemo operator--(CDemo & ,int);
};
CDemo & CDemo::operator++()
{//前置 ++
	n ++;
	return * this;
}
CDemo CDemo::operator++(int k )
{ //后置 ++
	CDemo tmp(*this); //记录修改前的对象
	n++;
	return tmp; //返回修改前的对象
}
CDemo & operator--(CDemo & d)
{//前置--
	d.n--;
	return d;
}
CDemo operator--(CDemo & d,int)
{//后置--
	CDemo tmp(d);
	d.n --;
	return tmp;
}
int main()
{
	CDemo d(5);
	cout << (d++ ) << ","; //等价于 d.operator++(0);
	cout << d << ",";
	cout << (++d) << ","; //等价于 d.operator++();
	cout << d << endl;
	cout << (d-- ) << ","; //等价于 operator-(d,0);
	cout << d << ",";
	cout << (--d) << ","; //等价于 operator-(d);
	cout << d << endl;
	return 0;
}

程序运行结果:

5,6,7,7
7,6,5,5

本程序将++重载为成员函数,将--重载为全局函数。其实都重载为成员函数更好,这里将--重载为全局函数只是为了说明可以这么做而已。

调用后置形式的重载函数时,对于那个没用的 int 类型形参,编译器自动以 0 作为实参。 如第 39 行,d++等价于d.operator++(0)

对比前置++和后置++运算符的重载可以发现,后置++运算符的执行效率比前置的低。因为后置方式的重载函数中要多生成一个局部对象 tmp(第21行),而对象的生成会引发构造函数调用,需要耗费时间。同理,后置--运算符的执行效率也比前置的低。

前置++运算符的返回值类型是 CDemo &,而后置++运算符的返回值类型是 CDemo,这是因为运算符重载最好保持原运算符的用法。C++ 固有的前置++运算符的返回值本来就是操作数的引用,而后置++运算符的返回值则是操作数值修改前的复制品。例如:

int a = 5;
(++a) = 2;

上面两条语句执行后,a 的值是 2,因为 ++a 的返回值是 a 的引用。而

(a++) = 2;

这条语句是非法的,因为 a++ 的返回值不是引用,不能作为左值。

--运算符的返回值类型的设定和++运算符一样。

在有的编译器(如Visual Studio)中,如果没有后置形式的重载,则后置形式的自增或自减表达式也被当作前置形式处理。而在有的编译器(如Dev C++)中,不进行后置形式的重载,则后置形式的表达式就会编译出错。

9 C++运算符重载注意事项以及汇总

在 C++ 中进行运算符重载时,有以下问题需要注意:

  • 重载后运算符的含义应该符合原有用法习惯。例如重载+运算符,完成的功能就应该类似于做加法,在重载的+运算符中做减法是不合适的。此外,重载应尽量保留运算符原有的特性。
  • C++ 规定,运算符重载不改变运算符的优先级。
  • 以下运算符不能被重载:..*::? :sizeof
  • 重载运算符()[]->、或者赋值运算符=时,只能将它们重载为成员函数,不能重载为全局函数。

运算符重载的实质是将运算符重载为一个函数,使用运算符的表达式就被解释为对重载函数的调用。

运算符可以重载为全局函数。此时函数的参数个数就是运算符的操作数个数,运算符的操作数就成为函数的实参。

运算符也可以重载为成员函数。此时函数的参数个数就是运算符的操作数个数减一,运算符的操作数有一个成为函数作用的对象,其余的成为函数的实参。

必要时需要重载赋值运算符=,以避免两个对象内部的指针指向同一片存储空间。

运算符可以重载为全局函数,然后声明为类的友元。

<<和>>是在 iostream 中被重载,才成为所谓的“流插入运算符”和“流提取运算符”的。

类型的名字可以作为强制类型转换运算符,也可以被重载为类的成员函数。它能使得对象被自动转换为某种类型。

自增、自减运算符各有两种重载方式,用于区别前置用法和后置用法。

运算符重载不改变运算符的优先级。重载运算符时,应该尽量保留运算符原本的特性。

Logo

助力合肥开发者学习交流的技术社区,不定期举办线上线下活动,欢迎大家的加入

更多推荐