【C++】运算符重载

目录

[1. 基本概念](#1. 基本概念)

[1.1 直接调用一个重载的运算符函数](#1.1 直接调用一个重载的运算符函数)

[1.2 某些运算符不应该被重载](#1.2 某些运算符不应该被重载)

[1.3 使用与内置类型一致的含义](#1.3 使用与内置类型一致的含义)

[1.4 赋值和复合赋值运算符](#1.4 赋值和复合赋值运算符)

[1.5 选择作为成员或者非成员](#1.5 选择作为成员或者非成员)

[2. 输入和输出运算符](#2. 输入和输出运算符)

[2.1 <<输出运算符重载](#2.1 <<输出运算符重载)

[2.2 >>输入运算符重载](#2.2 >>输入运算符重载)

[3. 算术和关系运算符](#3. 算术和关系运算符)

[3.1 算数运算符重载](#3.1 算数运算符重载)

[3.2 ==相等运算符重载](#3.2 ==相等运算符重载)

[3.3 关系运算符重载](#3.3 关系运算符重载)

[4. 赋值运算符](#4. 赋值运算符)

[4.1 =赋值运算符重载](#4.1 =赋值运算符重载)

[4.2 复合赋值运算符重载](#4.2 复合赋值运算符重载)

[5. 递增和递减运算符](#5. 递增和递减运算符)

[5.1 前置递增/递减运算符重载](#5.1 前置递增/递减运算符重载)

[5.2 后置递增/递减运算符重载](#5.2 后置递增/递减运算符重载)

[6. 函数调用运算符](#6. 函数调用运算符)


1. 基本概念

重载的运算符是具有特殊名字的函数:它们的名字由关键字operator和其后要定义的运算符号共同组成。和其他函数一样,重载的运算符也包含返回类型、参数列表以及函数体。

重载运算符函数的参数数量与该运算符作用的运算对象数量一样多。一元运算符有一个参数,二元运算符有两个。对于二元运算符来说,左侧运算对象传递给第一个参数,而右侧运算对象传递给第二个参数。除了重载的函数调用运算符operator()之外,其他重载运算符不能含有默认实参。

如果一个运算符函数是成员函数,则它的第一个(左侧)运算对象绑定到隐式的this指针上,因此,成员运算符函数的(显式)参数数量比运算符的运算对象总数少一个。

对于一个运算符函数来说,它或者是类的成员,或者至少含有一个类类型的参数:

cpp 复制代码
int operator+(int,int); // err 不能为int重定义内置的运算符

这一约定意味着当运算符作用于内置类型的运算对象时,我们无法改变该运算符的含义。

不能重载的运算符:. .* :: ?: sizeof

我们只能重载已有的运算符,而无权发明新的运算符号。例如,我们不能提供operator**来执行幂操作。

有四个符号(+、-、*、&)既是一元运算符也是二元运算符,所有这些运算符都能被重载,从参数的数量我们可以推断到底定义的是哪种运算符。

对于一个重载的运算符来说,其优先级和结合律与对应的内置运算符保持一致。不考虑运算对象类型的话,

cpp 复制代码
x == y + z;

永远等价于

cpp 复制代码
x == (y + z);

1.1 直接调用一个重载的运算符函数

通常情况下,我们将运算符作用于类型正确的实参,从而以这种间接方式"调用"重载的运算符函数。然而,我们也能像调用普通函数一样直接调用运算符函数,先指定函数名字,然后传入数量正确、类型适当的实参:

cpp 复制代码
// 一个非成员运算符函数的等价调用
data1 + data2;           // 普通的表达式
operator+(data1, data2); // 等价的函数调用

这两次调用是等价的,它们都调用了非成员函数operator+,传入data1作为第一个实参、传入data2作为第二个实参。

我们像调用其他成员函数一样显式地调用成员运算符函数。具体做法是,首先指定运行函数的对象(或指针)的名字,然后使用点运算符(或箭头运算符)访问希望调用的函数:

cpp 复制代码
data1 += data2;          // 基于"调用"的表达式
data1.operator+=(data2); // 对成员运算符函数的等价调用

这两条语句都调用了成员函数operator+=,将this绑定到data1的地址、将data2作为实参传入了函数。

1.2 某些运算符不应该被重载

某些运算符指定了运算对象求值的顺序。因为使用重载的运算符本质上是一次函数调用,所以这些关于运算对象求值顺序的规则无法应用到重载的运算符上。特别是,逻辑与运算符、逻辑或运算符和逗号运算符的运算对象求值顺序规则无法保留下来。除此之外,&&和||运算符的重载版本也无法保留内置运算符的短路求值属性,两个运算对象总是会被求值。

因为上述运算符的重载版本无法保留求值顺序和/或短路求值属性,因此不建议重载它们。当代码使用了这些运算符的重载版本时,用户可能会突然发现他们一直习惯的求值规则不再适用了。

还有一个原因使得我们一般不重载逗号运算符和取地址运算符:C++语言已经定义了这两种运算符用于类类型对象时的特殊含义,这一点与大多数运算符都不相同。因为这两种运算符已经有了内置的含义,所以一般来说它们不应该被重载,否则它们的行为将异于常态,从而导致类的用户无法适应。

1.3 使用与内置类型一致的含义

当你开始设计一个类时,首先应该考虑的是这个类将提供哪些操作。在确定类需要哪些操作之后,才能思考到底应该把每个类操作设成普通函数还是重载的运算符。如果某些操作在逻辑上与运算符相关,则它们适合于定义成重载的运算符:

  • 如果类执行IO操作,则定义移位运算符使其与内置类型的IO保持一致。

  • 如果类的某个操作是检查相等性,则定义operator==;如果类有了operator==,意味着它通常也应该有operator!=。

  • 如果类包含一个内在的单序比较操作,则定义operator<;如果类有了operator<,则它也应该含有其他关系操作。

  • 重载运算符的返回类型通常情况下应该与其内置版本的返回类型兼容:逻辑运算符和关系运算符应该返回bool,算术运算符应该返回一个类类型的值,赋值运算符和复合赋值运算符则应该返回左侧运算对象的一个引用。

1.4 赋值和复合赋值运算符

赋值运算符的行为与复合版本的类似:赋值之后,左侧运算对象和右侧运算对象的值相等,并且运算符应该返回它左侧运算对象的一个引用。重载的赋值运算应该继承而非违背其内置版本的含义。

如果类含有算术运算符或者位运算符,则最好也提供对应的复合赋值运算符。无须赘言,+=运算符的行为显然应该与其内置版本一致,即先执行+,再执行=。

1.5 选择作为成员或者非成员

当我们定义重载的运算符时,必须首先决定是将其声明为类的成员函数还是声明为一个普通的非成员函数。在某些时候我们别无选择,因为有的运算符必须作为成员;另一些情况下,运算符作为普通函数比作为成员更好。

下面的准则有助于我们在将运算符定义为成员函数还是普通的非成员函数做出抉择:

  • 赋值(=)、下标([])、调用(())和成员访问箭头(->)运算符必须是成员。
  • 复合赋值运算符一般来说应该是成员,但并非必须,这一点与赋值运算符略有不同。
  • 改变对象状态的运算符或者与给定类型密切相关的运算符,如递增、递减和解引用运算符,通常应该是成员。
  • 具有对称性的运算符可能转换任意一端的运算对象,例如算术、相等性、关系和位运算符等,因此它们通常应该是普通的非成员函数。

程序员希望能在含有混合类型的表达式中使用对称性运算符。例如,我们能求一个int和一个double的和,因为它们中的任意一个都可以是左侧运算对象或右侧运算对象,所以加法是对称的。如果我们想提供含有类对象的混合类型表达式,则运算符必须定义成非成员函数。

当我们把运算符定义成成员函数时,它的左侧运算对象必须是运算符所属类的一个对象。例如:

cpp 复制代码
string s = "world";
string t = s + "!";  // ok 我们能把一个const char*加到一个string对象中
string u = "hi" + s; // 如果+是string的成员,则产生错误

如果operator+是string类的成员,则上面的第一个加法等价于s.operator+("!")。同样的,"hi"+s等价于"hi".operator+(s)。显然"hi"的类型是const char*,这是一种内置类型,根本就没有成员函数。

因为string将+定义成了普通的非成员函数,所以"hi"+s等价于operator+("hi",s)。和任何其他函数调用一样,每个实参都能被转换成形参类型。唯一的要求是至少有一个运算对象是类类型,并且两个运算对象都能准确无误地转换成string。


有一个Date类:

cpp 复制代码
class Date
{
public:
	// 获取某年某月的天数
	int GetMonthDay(int year, int month) const
	{
		assert(month > 0 && month < 13);
		int monthArray[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
		if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400) == 0))
		{
			return 29;
		}
		else
		{
			return monthArray[month];
		}
	}
	// 构造函数
	Date(int year = 1900, int month = 1, int day = 1)
	{
		if (month > 0 && month < 13 && (day > 0 && day <= GetMonthDay(year, month)))
		{
			_year = year;
			_month = month;
			_day = day;
		}
		else
		{
			cout << "日期非法" << endl;
		}
	}
private:
	int _year;
	int _month;
	int _day;
};

2. 输入和输出运算符

如我们所知,IO标准库分别使用>>和<<执行输入和输出操作。对于这两个运算符来说,IO库定义了用其读写内置类型的版本,而类则需要自定义适合其对象的新版本以支持IO操作。

2.1 <<输出运算符重载

通常情况下,输出运算符的第一个形参是一个非常量ostream对象的引用。之所以ostream是非常量是因为向流写入内容会改变其状态;而该形参是引用是因为我们无法直接复制一个ostream对象。

第二个形参一般来说是一个常量的引用,该常量是我们想要打印的类类型。第二个形参是引用的原因是我们希望避免复制实参;而之所以该形参可以是常量是因为(通常情况下)打印对象不会改变对象的内容。

为了与其他输出运算符保持一致,operator<<一般要返回它的ostream形参。

cpp 复制代码
class Date
{
    // <<输出运算符重载
    friend ostream& operator<<(ostream& out, const Date& d);
public:
	int GetMonthDay(int year, int month) const
	{
		// ...
	}
	Date(int year = 1900, int month = 1, int day = 1)
	{
		// ...
	}
private:
	int _year;
	int _month;
	int _day;
};

// <<输出运算符重载
ostream& operator<<(ostream& out, const Date& d)
{
	out << d._year << "年" << d._month << "月" << d._day << "日";
	return out;
}

用于内置类型的输出运算符不太考虑格式化操作,尤其不会打印换行符,用户希望类的输出运算符也像如此行事。如果运算符打印了换行符,则用户就无法在对象的同一行内接着打印一些描述性的文本了。相反,令输出运算符尽量减少格式化操作可以使用户有权控制输出的细节。

与iostream标准库兼容的输入输出运算符必须是普通的非成员函数,而不能是类的成员函数。否则,它们的左侧运算对象将是我们的类的一个对象:

cpp 复制代码
Date d;
d << cout; // 如果operator<<是Date的成员

假设输入输出运算符是某个类的成员,则它们也必须是istream或ostream的成员。然而,这两个类属于标准库,并且我们无法给标准库中的类添加任何成员。

因此,如果我们希望为类自定义IO运算符,则必须将其定义成非成员函数。当然,IO运算符通常需要读写类的非公有数据成员,所以IO运算符一般被声明为友元。

2.2 >>输入运算符重载

通常情况下,输入运算符的第一个形参是运算符将要读取的流的引用,第二个形参是将要读入到的(非常量)对象的引用。该运算符通常会返回某个给定流的引用。第二个形参之所以必须是个非常量是因为输入运算符本身的目的就是将数据读入到这个对象中。

cpp 复制代码
class Date
{
    // >>输入运算符重载
    friend istream& operator>>(istream& in, Date& d);
public:
	int GetMonthDay(int year, int month) const
	{
		// ...
	}
	Date(int year = 1900, int month = 1, int day = 1)
	{
		// ...
	}
private:
	int _year;
	int _month;
	int _day;
};

// >>输入运算符重载
istream& operator>>(istream& in, Date& d)
{
	in >> d._year >> d._month >> d._day;
	if (!in)
	{
		d = Date(); // 输入失败:对象被赋予默认的状态
	}
	return in;
}

if语句检查读取操作是否成功,如果发生了IO错误,则运算符将给定的对象重置为默认的Date,这样可以确保对象处于正确的状态。

输入运算符必须处理输入可能失败的情况,而输出运算符不需要。

在执行输入运算符时可能发生下列错误:

  • 当流含有错误类型的数据时读取操作可能失败。
  • 当读取操作到达文件末尾或者遇到输入流的其他错误时也会失败。

3. 算术和关系运算符

通常情况下,我们把算术和关系运算符定义成非成员函数以允许对左侧或右侧的运算对象进行转换。因为这些运算符一般不需要改变运算对象的状态,所以形参都是常量的引用。

3.1 算数运算符重载

算术运算符通常会计算它的两个运算对象并得到一个新值,这个值有别于任意一个运算对象,常常位于一个局部变量之内,操作完成后返回该局部变量的副本作为其结果。如果类定义了算术运算符,则它一般也会定义一个对应的复合赋值运算符。此时,最有效的方式是使用复合赋值来定义算术运算符:

定义为非成员函数(最好):

cpp 复制代码
class Date
{
public:
	int GetMonthDay(int year, int month) const
	{
		// ...
	}
	Date(int year = 1900, int month = 1, int day = 1)
	{
		// ...
	}
	// +=复合赋值运算符重载:日期+=天数
	Date& operator+=(int day)
	{
		if (day < 0)
		{
			*this -= -day;
			return *this;
		}
		_day += day;
		while (_day > GetMonthDay(_year, _month))
		{
			_day -= GetMonthDay(_year, _month);
			_month++;
			if (_month == 13)
			{
				++_year;
				_month = 1;
			}
		}
		return *this;
	}
	// -=复合赋值运算符重载:日期-=天数
	Date& operator-=(int day)
	{
		if (day < 0)
		{
			*this += -day;
			return *this;
		}
		_day -= day;
		while (_day <= 0)
		{
			--_month;
			if (_month == 0)
			{
				--_year;
				_month = 12;
			}
			_day += GetMonthDay(_year, _month);
		}
		return *this;
	}
private:
	int _year;
	int _month;
	int _day;
};

// +加号运算符重载:日期+天数
Date operator+(Date& d, int day)
{
	Date tmp = d;
	tmp += day;
	return tmp;
}
// -减号运算符重载:日期-天数
Date operator-(Date& d, int day)
{
	Date tmp = d;
	tmp -= day;
	return tmp;
}

定义为成员函数:

cpp 复制代码
class Date
{
public:
	int GetMonthDay(int year, int month) const
	{
		// ...
	}
	Date(int year = 1900, int month = 1, int day = 1)
	{
		// ...
	}
	// +=复合赋值运算符重载:日期+=天数
	Date& operator+=(int day)
	{
		if (day < 0)
		{
			*this -= -day;
			return *this;
		}
		_day += day;
		while (_day > GetMonthDay(_year, _month))
		{
			_day -= GetMonthDay(_year, _month);
			_month++;
			if (_month == 13)
			{
				++_year;
				_month = 1;
			}
		}
		return *this;
	}
	// -=复合赋值运算符重载:日期-=天数
	Date& operator-=(int day)
	{
		if (day < 0)
		{
			*this += -day;
			return *this;
		}
		_day -= day;
		while (_day <= 0)
		{
			--_month;
			if (_month == 0)
			{
				--_year;
				_month = 12;
			}
			_day += GetMonthDay(_year, _month);
		}
		return *this;
	}
	// +加号运算符重载:日期+天数
	Date operator+(int day) const
	{
		Date tmp(*this);
		tmp += day;
		return tmp;
	}
	// -减号运算符重载:日期-天数
	Date operator-(int day) const
	{
		Date tmp(*this);
		tmp -= day;
		return tmp;
	}
private:
	int _year;
	int _month;
	int _day;
};

3.2 ==相等运算符重载

通常情况下,C++中的类通过定义相等运算符来检验两个对象是否相等。也就是说,它们会比较对象的每一个数据成员,只有当所有对应的成员都相等时才认为两个对象相等。

定义为非成员函数(最好):

cpp 复制代码
class Date
{
	// ==相等运算符重载
	friend bool operator==(const Date& d1, const Date& d2);
	// !=不相等运算符重载
	friend bool operator!=(const Date& d1, const Date& d2);
public:
	int GetMonthDay(int year, int month) const
	{
		// ...
	}
	Date(int year = 1900, int month = 1, int day = 1)
	{
		// ...
	}
private:
	int _year;
	int _month;
	int _day;
};

// ==相等运算符重载
bool operator==(const Date& d1, const Date& d2)
{
	return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;
}
// !=不相等运算符重载
bool operator!=(const Date& d1, const Date& d2)
{
	return !(d1 == d2);
}

定义为成员函数:

cpp 复制代码
class Date
{
public:
	int GetMonthDay(int year, int month) const
	{
		// ...
	}
	Date(int year = 1900, int month = 1, int day = 1)
	{
		// ...
	}
	// ==相等运算符重载
	bool operator==(const Date& d) const
	{
		return _year == d._year && _month == d._month && _day == d._day;
	}
	// !=不相等运算符重载
	bool operator!=(const Date& d) const
	{
		return !(*this == d);
	}
private:
	int _year;
	int _month;
	int _day;
};

就上面这些函数的定义本身而言,它们似乎比较简单,也没什么价值,对于我们来说重要的是从这些函数中体现出来的设计准则:

  • 如果一个类含有判断两个对象是否相等的操作,则它显然应该把函数定义成operator==而非一个普通的命名函数:因为用户肯定希望能使用==比较对象,所以提供了==就意味着用户无须再费时费力地学习并记忆一个全新的函数名字。此外,类定义了==运算符之后也更容易使用标准库容器和算法。
  • 如果类定义了operator==,则该运算符应该能判断一组给定的对象中是否含有重复数据。
  • 通常情况下,相等运算符应该具有传递性,换句话说,如果a==b和b==c都为真,则a==c也应该为真。
  • 如果类定义了operator==,则这个类也应该定义operator!=。对于用户来说,当他们能使用==时肯定也希望能使用!=,反之亦然。
  • 相等运算符和不相等运算符中的一个应该把工作委托给另外一个,这意味着其中一个运算符应该负责实际比较对象的工作,而另一个运算符则只是调用那个真正工作的运算符。

3.3 关系运算符重载

定义为非成员函数(最好):

cpp 复制代码
class Date
{
	friend bool operator==(const Date& d1, const Date& d2);
	// <关系运算符重载
	friend bool operator<(const Date& d1, const Date& d2);
	// <=关系运算符重载
	friend bool operator<=(const Date& d1, const Date& d2);
	// >关系运算符重载
	friend bool operator>(const Date& d1, const Date& d2);
	// >=关系运算符重载
	friend bool operator>=(const Date& d1, const Date& d2);
public:
	int GetMonthDay(int year, int month) const
	{
		// ...
	}
	Date(int year = 1900, int month = 1, int day = 1)
	{
		// ...
	}	
private:
	int _year;
	int _month;
	int _day;
};

bool operator==(const Date& d1, const Date& d2)
{
	// ...
}
// <关系运算符重载
bool operator<(const Date& d1, const Date& d2)
{
	return d1._year < d2._year
		|| (d1._year == d2._year && d1._month < d2._month)
		|| (d1._year == d2._year && d1._month == d2._month && d1._day < d2._day);
}
// <=关系运算符重载
bool operator<=(const Date& d1, const Date& d2)
{
	return d1 < d2 || d1 == d2;
}
// >关系运算符重载
bool operator>(const Date& d1, const Date& d2)
{
	return !(d1 <= d2);
}
// >=关系运算符重载
bool operator>=(const Date& d1, const Date& d2)
{
	return !(d1 < d2);
}

定义为成员函数:

cpp 复制代码
class Date
{
public:
	int GetMonthDay(int year, int month) const
	{
		// ...
	}
	Date(int year = 1900, int month = 1, int day = 1)
	{
		// ...
	}
	bool operator==(const Date& d) const
	{
		// ...
	}
	// <关系运算符重载
	bool operator<(const Date& d) const
	{
		return _year < d._year
			|| (_year == d._year && _month < d._month)
			|| (_year == d._year && _month == d._month && _day < d._day);
	}
	// <=关系运算符重载
	bool operator<=(const Date& d) const
	{
		return *this < d || *this == d;
	}
	// >关系运算符重载
	bool operator>(const Date& d) const
	{
		return !(*this <= d);
	}
	// >=关系运算符重载
	bool operator>=(const Date& d) const
	{
		return !(*this < d);
	}
private:
	int _year;
	int _month;
	int _day;
};

4. 赋值运算符

4.1 =赋值运算符重载

我们可以重载赋值运算符。不论形参的类型是什么,赋值运算符都必须定义为成员函数。

cpp 复制代码
class Date
{
public:
	int GetMonthDay(int year, int month) const
	{
		// ...
	}
	Date(int year = 1900, int month = 1, int day = 1)
	{
		// ...
	}
	// =赋值运算符重载
	Date& operator=(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};

4.2 复合赋值运算符重载

复合赋值运算符不非得是类的成员,不过我们还是倾向于把包括复合赋值在内的所有赋值运算都定义在类的内部。为了与内置类型的复合赋值保持一致,类中的复合赋值运算符也要返回其左侧运算对象的引用。

cpp 复制代码
class Date
{
public:
	int GetMonthDay(int year, int month) const
	{
		// ...
	}
	Date(int year = 1900, int month = 1, int day = 1)
	{
		// ...
	}
	// +=复合赋值运算符重载:日期+=天数
	Date& operator+=(int day)
	{
		if (day < 0)
		{
			*this -= -day;
			return *this;
		}
		_day += day;
		while (_day > GetMonthDay(_year, _month))
		{
			_day -= GetMonthDay(_year, _month);
			_month++;
			if (_month == 13)
			{
				++_year;
				_month = 1;
			}
		}
		return *this;
	}
	// -=复合赋值运算符重载:日期-=天数
	Date& operator-=(int day)
	{
		if (day < 0)
		{
			*this += -day;
			return *this;
		}
		_day -= day;
		while (_day <= 0)
		{
			--_month;
			if (_month == 0)
			{
				--_year;
				_month = 12;
			}
			_day += GetMonthDay(_year, _month);
		}
		return *this;
	}
private:
	int _year;
	int _month;
	int _day;
};

5. 递增和递减运算符

在迭代器类中通常会实现递增运算符(++)和递减运算符(--),这两种运算符使得类可以在元素的序列中前后移动。C++语言并不要求递增和递减运算符必须是类的成员,但是因为它们改变的正好是所操作对象的状态,所以建议将其设定为成员函数。

对于内置类型来说,递增和递减运算符既有前置版本也有后置版本。同样,我们也应该为类定义两个版本的递增和递减运算符。接下来我们首先介绍前置版本,然后实现后置版本。

5.1 前置递增/递减运算符重载

cpp 复制代码
class Date
{
public:
	int GetMonthDay(int year, int month) const
	{
		// ...
	}
	Date(int year = 1900, int month = 1, int day = 1)
	{
		// ...
	}
	Date& operator+=(int day)
	{
		// ...
	}
	Date& operator-=(int day)
	{
		// ...
	}
	// 前置递增运算符重载
	Date& operator++()
	{
		*this += 1;
		return *this;
	}
	// 前置递减运算符重载
	Date& operator--()
	{
		*this -= 1;
		return *this;
	}
private:
	int _year;
	int _month;
	int _day;
};

5.2 后置递增/递减运算符重载

要想同时定义前置和后置运算符,必须首先解决一个问题,即普通的重载形式无法区分这两种情况。前置和后置版本使用的是同一个符号,意味着其重载版本所用的名字将是相同的,并且运算对象的数量和类型也相同。

为了解决这个问题,后置版本接受一个额外的(不被使用)int类型的形参。当我们使用后置运算符时,编译器为这个形参提供一个值为0的实参。尽管从语法上来说后置函数可以使用这个额外的形参,但是在实际过程中通常不会这么做。这个形参的唯一作用就是区分前置版本和后置版本的函数,而不是真的要在实现后置版本时参与运算。

cpp 复制代码
class Date
{
public:
	int GetMonthDay(int year, int month) const
	{
		// ...
	}
	Date(int year = 1900, int month = 1, int day = 1)
	{
		// ...
	}
	Date& operator+=(int day)
	{
		// ...
	}
	Date& operator-=(int day)
	{
		// ...
	}
	// 后置递增运算符重载
	Date operator++(int)
	{
		Date tmp(*this);
		*this += 1;
		return tmp;
	}
	// 后置递减运算符重载
	Date operator--(int)
	{
		Date tmp(*this);
		*this -= 1;
		return tmp;
	}
private:
	int _year;
	int _month;
	int _day;
};

可以显式地调用一个重载的运算符,其效果与在表达式中以运算符号的形式使用它完全一样。如果我们想通过函数调用的方式调用后置版本,则必须为它的整型参数传递一个值:

cpp 复制代码
d.operator++(0) ; // 调用后置版本的operator++
d.operator++();   // 调用前置版本的operator++

尽管传入的值通常会被运算符函数忽略,但却必不可少,因为编译器只有通过它才能知道应该使用后置版本。

6. 函数调用运算符

如果类重载了函数调用运算符,则我们可以像使用函数一样使用该类的对象。因为这样的类同时也能存储状态,所以与普通函数相比它们更加灵活。

举个简单的例子,下面这个名为absInt的struct含有一个调用运算符,该运算符负责返回其参数的绝对值:

cpp 复制代码
struct absInt
{
	int operator()(int val) const
	{
		return val < 0 ? -val : val;
	}
};

这个类只定义了一种操作:函数调用运算符,它负责接受一个int类型的实参,然后返回该实参的绝对值。

我们使用调用运算符的方式是令一个absInt对象作用于一个实参列表,这一过程看起来非常像调用函数的过程:

cpp 复制代码
int i = -42;
absInt absObj;     // 含有函数调用运算符的对象
int ui = absObj(i);// 将i传递给absObj.operator()

即使absObj只是一个对象而非函数,我们也能"调用"该对象。调用对象实际上是在运行重载的调用运算符。在此例中,该运算符接受一个int值并返回其绝对值。

函数调用运算符必须是成员函数。一个类可以定义多个不同版本的调用运算符,相互之间应该在参数数量或类型上有所区别。

如果类定义了调用运算符,则该类的对象称作函数对象(function object)。因为可以调用这种对象,所以我们说这些对象的"行为像函数一样"。

相关推荐
小芒果_013 分钟前
P11229 [CSP-J 2024] 小木棍
c++·算法·信息学奥赛
gkdpjj9 分钟前
C++优选算法十 哈希表
c++·算法·散列表
王俊山IT10 分钟前
C++学习笔记----10、模块、头文件及各种主题(一)---- 模块(5)
开发语言·c++·笔记·学习
-Even-13 分钟前
【第六章】分支语句和逻辑运算符
c++·c++ primer plus
我是谁??1 小时前
C/C++使用AddressSanitizer检测内存错误
c语言·c++
发霉的闲鱼1 小时前
MFC 重写了listControl类(类名为A),并把双击事件的处理函数定义在A中,主窗口如何接收表格是否被双击
c++·mfc
小c君tt1 小时前
MFC中Excel的导入以及使用步骤
c++·excel·mfc
xiaoxiao涛2 小时前
协程6 --- HOOK
c++·协程
羊小猪~~4 小时前
数据结构C语言描述2(图文结合)--有头单链表,无头单链表(两种方法),链表反转、有序链表构建、排序等操作,考研可看
c语言·数据结构·c++·考研·算法·链表·visual studio
脉牛杂德4 小时前
多项式加法——C语言
数据结构·c++·算法