主页
文章
分类
系列
标签
简历
【C++ Primer(edition 5) 13】操作重载与类型转换
发布于: 2021-10-19   更新于: 2021-10-19   收录于: Cpp
文章字数: 951   阅读时间: 5 分钟   阅读量:

一、基本概念

1、定义重载运算符

1
return_type operator operator_symbol(...parameter list...);
  • 重载运算符是具有特殊名字的函数:由关键字operator和其后要定义的运算符号共同组成。和其他函数一样,重载的运算符也包含返回类型、参数列表以及函数体。
  • 当一个重载的运算符是成员函数时,this绑定到左侧运算对象。动态运算符符函数的参数数量比运算对象的数量少一个
  • 只能重载大多数的运算符,而不能发明新的运算符号。
  • 重载运算符的优先级和结合律跟对应的内置运算符保持一致。

2、调用重载运算符

方式一 将运算符作用于类型正确的实参,从而以这种间接方式“调用”重 载的运算符函数

1
data1 + data2;

方式二 调用普通函数一样直接调用运算符函数,先指定函数名字,然后传入数量正确、类型适当的实参

1
operator+(data1, data2);

3、哪些运算符不可以重载,那些不建议重载

![[Pasted image 20230122101348.png]]

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

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

4、运算符作为成员函数和非成员函数的问题

有时别无选择,必须作为成员函数,而有些时候作为普通函数更好。 如何做出选择呢,有以下的依据:

  • 赋值(=)、下标([])、调用(())和成员访问箭头(->)运算符必须是成员。
  • 复合赋值运算符一般来说是成员。
  • 改变对象状态的运算符或者和给定类型密切相关的运算符通常是成员,如递增、解引用。
  • 具有对称性的运算符如算术、相等性、关系和位运算符等,通常是非成员函数。
  • 输出运算符<<必须是非成员函数
  • 含有类对象的混合类型的表达式中使用对称性运算符,运算符必须定义成非成员函数
1
2
3
4
5
6
//如果 “+”是string类的成员
string s = "world";
string t = s + "!";     //√  s.operator + ("!")
string u = "hi" + s;    //×  "hi".operator + (s)
//如果“+”是非常成员函数
//“hi”+s 等价于 operator+("hi",s); 每个实参都能够转换为string类型

二、输入和输出运算符

1、重载输出运算符«

  • 第一个形参通常是一个非常量的ostream对象的引用。非常量是因为向流中写入会改变其状态;而引用是因为我们无法复制一个ostream对象。
  • 第二个形参一般来说是一个常量的引用,该常量是我们想要打印的类类型。第二个形参是引用的原因是我们希望避免复制实参,同时不会改变该形参。
  • 输入输出运算符必须是非成员函数。
1
2
3
4
5
ostream &operator<<(ostream &os , const Sales_data &item)
{
	os << item.isbn()<<" "<<item.units_sold<<" ";
	return os;
}

2、重载输入运算符»

  • 第一个形参通常是运算符将要读取的流的引用,第二个形参是将要读取到的(非常量)对象的引用。
  • 输入运算符必须处理输入可能失败的情况,而输出运算符不需要。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
istream &operator>>(istream &is, Sales_data &item)
{
	double price;//不需要初始化,因为我们将先读入数据到price,之后才使用它
	is>>item.bookNo>>item.units_sold>>price;
	if(is)      //检查输入是否成功
		item.revenue = item.units_sold*price;
	else 
		item = Sales_data(); //输入失败:对象被赋予默认的状态
	
	return is;
}

三、算术和关系运算符(+-*/ , ==!=、……)

如果类同时定义了算数运算符和相关的复合赋值运算符,则通常情况下应该使用复合赋值来实现算数运算符。

1
2
3
4
5
6
7
//假设两个对象指向同一本书
Sales_data operator+(const Sales_data &lhs, const Sales_data &rhs)
{
	Sales_data sum = lhs; // 把lhs的数据成员拷贝给sum
	sum += rhs;// 将 rhs 加至sum中 
	return sum;
}

1、相等运算符==

  • 如果定义了operator==,则这个类也应该定义operator!=
  • 相等运算符和不等运算符的一个应该把工作委托给另一个。
  • 相等运算符应该具有传递性。
  • 如果某个类在逻辑上有相等性的含义,则该类应该定义operator==,这样做可以使用户更容易使用标准库算法来处理这个类。
1
2
3
4
5
6
7
8
bool operator==(const Sales_data &lhs, const Sales_data &rhs)
{
	return lhs.isbn() == rhs.isbn() && lhs.units_sold == rhs.units_sold && lhs.revenue == rhs.revenue;
}
bool operator!=(const Sales_data &lhs, const Sales_data &rhs)
{
	return !(lhs == rhs);
}

2、关系运算符

如果存在唯一一种逻辑可靠的<定义,则应该考虑为这个类定义<运算符。如果同时还包含==,则当且晋档<的定义和++产生的结果一直时才定义<运算符。

四、赋值运算符=

  • 我们可以重载赋值运算符。不论形参的类型是什么,赋值运算符都必须定义为成员函数。
  • 赋值运算符必须定义成类的成员,复合赋值运算符通常情况下也应该这么做。这两类运算符都应该返回左侧运算对象的引用。
1
2
3
4
5
6
Sales_data& Sales_data::operator+=(const Sales_data &rhs)
(
	units_sold += rhs.units_sold; 
	revenue += rhs.revenue; 
    return *this;
}

五、下标运算符[]

  • 下标运算符必须是成员函数。
  • 一般会定义两个版本:
    • 1.返回普通引用。
    • 2.类的常量成员,并返回常量引用。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class StrVec {
	public:
		std::strings operator[](std::size_t n) 
		{
			return elements[n]; 
		}
		const std::strings operator[](std::size_t n) const 
		{ 
			return elements[n]; 
		}
	private:
		std::string *elements; //指向数组首元素的指针
}

六、递增和递减运算符(++、–)

  • 定义递增和递减运算符的类应该同时定义前置版本和后置版本,后置版本接受一个额外的(不被使用)int类型的形参以形成重载函数。
  • 通常应该被定义成类的成员。
  • 为了和内置版本保持一致,前置运算符应该返回递增或递减后对象的引用。
  • 同样为了和内置版本保持一致,后置运算符应该返回递增或递减前对象的值,而不是引用。
  • 后置版本接受一个额外的,不被使用的int类型的形参。因为不会用到,所以无需命名。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//前置版本
class StrBlobPtr 
{ 
	public:
	//递增和递减运算符
	StrBlobPtr& operator++() ; // 前置运算符 
	StrBlobPtr& operator--() ;
	//其他成员和之前的版本一致
};
StrBlobPtr& StrBlobPtr::operator++()
{
	// 如果curr已经指向了容器的尾后位置,则无法递增它
	check(curr, "increment past end of StrBlobPtr*');
	++curr; //将 curr在当前状态下向前移动一个元素
	return *this;
}

//后置版本
class StrBlobPtr 
{ 
	public:
	//递增和递减运算符
	StrBlobPtr& operator++(int) ; // 后置运算符 
	StrBlobPtr& operator--(int) ;
	//其他成员和之前的版本一致
};
StrBlobPtr StrBlobPtr::operator++(int)
{
	//此处无须检查有效性,调用前置递增运算时才需要检查
	StrBlobPtr ret = *this; // 记录当前的值
	++*this; //向前移动一个元素,前置++需要检查递增的有效性
	return ret; //返回之前记录的状态
}
StrBlobPtr StrBlobPtr::operator--(int)
{
	//此处无须检查有效性,调用前置递减运算时才需要检查
	StrBlobPtr ret = *this; //记录当前的值
	--*this; / / 向后移动一个元素,前置--需要检查递减的有效性
	return ret; / / 返回之前记录的状态
}
		  

七、成员访问运算符(*、->)

  • 箭头运算符必须是类的成员。解引用运算符通常也是类的成员,尽管并非必须如此。
  • 重载的箭头运算符必须返回类的指针或者自定义了箭头运算符的某个类的对象。
  • 解引用和乘法的区别是一个是一元运算符,一个是二元运算符。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class StrBlobPtr 
{
public:
	std::strings operator*() const 
	{
		auto p = check(curr, "dereference past end"); 
		return (*p) [curr] ; // (*p)是对象所指的 vector
	}

	std::string* operator-> () const
	{
		//将实际工作委托给解引用运算符
		return & this->operator*();
	}
	//其他成员与之前的版本一致
}

八、函数调用运算符

如果类重载了函数调用运算符,则我们可以像使用函数一样使用该类的对象

  • 可以像使用函数一样,调用该类的对象。因为这样对待类同时也能存储状态,所以与普通函数相比更加灵活。
  • 函数调用运算符必须是成员函数。
  • 一个类可以定义多个不同版本的调用运算符,相互之间应该在参数数量或类型上有所区别。
  • 如果累定义了调用运算符,则该类的对象称作函数对象
1
2
3
4
5
6
7
8
9
struct abslnt 
{ 
	int operator()(int val) const 
		{return val < 0 ? -val : val}
};

int i = -42;
abslnt absObj;
int ui = absObj(i);

1、lambda是函数对象

  • lambda捕获变量:lambda产生的类必须为每个值捕获的变量建立对应的数据成员,同时创建构造函数。

2、标准库定义的函数对象

标准库函数对象 标准库定义了一组表示算术运算符、关系运算符和逻辑运算符的类,每个类分别定义了一个执行命名操作的调用运算符。例如,plus类定义了一个函数调用运算符用于对一 对运算对象执行+的操作;modulus类定义了一个调用运算符执行二元的号操作; equal_to类执行== , 等等。 这些类都被定义成模板的形式,我们可以为其指定具体的应用类型,这里的类型即调 用运算符的形参类型。例如,plus<string>令 string加法运算符作用于string对象; plus<int>的运算对象是 int; plus<Sales_data>对 Sales_data 对象执行加 法运算,以此类推

1
2
3
4
5
6
7
8
plus<int> intAdd; //可执行int加法的函数对
negate<int> intNegate; //可对int值取反的函数对象
// intAdd::operator (int, int)求 10 和 20 的和 
int sum = intAdd (10, 20) ; // 等价于 sum = 30
sum = intNegate (intAdd (10, 20) ) ; // 等价于 sum = 30
// 使用 intNegate: :operator (int) 
// 然后将-10作为 intAdd::operator (int, int)的第二个参数
sum = intAdd(10, intNegate(10)); // sum = 0
算术 关系 逻辑
plus<Type> equal_to<Type> logical_and<Type>
minus<Type> not_equal_to<Type> logical_or<Type>
multiplies<Type> greater<Type> logical_not<Type>
divides<Type> greater_equal<Type>
modulus<Type> less<Type>
negate<Type> less_equal<Type>

3、可调用对象与function

C++语言中有几种可调用的对象:函数、函数指针、lambda表达式、bind创建的对象以及重载了函数调用运算符的类。

调用对象本身也是有类型的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
//普通函数类型
int add(int i , int j);
//未命名的函数对象类
auto mod = [](int i, int j){return i%j;};
//函数对象类
struct divide{
	int operator()(int denominator , int divisor)
	{
		return denominator / divisor ;
	}
}

想象一种场景,我们要设计一个计算,希望通过用户键入的符号来判断进行那种函数,可以用map来实现这样的函数表,细看上述的三个函数,似乎都是返回值为int,需要两个参数的函数形式int (int , int) 故可以声明函数表:map<string , int(*)(int , int)> 但是,这样就有一个问题,因为调用对象本身也有类型,而int (*)(int , int)的类型是一个函数指针,不能指向函数类类型,所以不能将mod和divide类传入

为了解决这个问题,我们可以使用function标准库来解决这个问题

标准库function类型: 我们可以使用一个名为function的新的标准库类型解决上述问题,function 定义在functional头文件中。function是一个模板,和其他模板一样,当创建一个具体的 function 类型时我们必须提供额外的信息。在此例中,所谓额外的信息是指该 function 类型能够表示的对象的调用形式。

操作 解释
function<T> f; f是一个用来存储可调用对象的空function,这些可调用对象的调用形式应该与类型T相同。
function<T> f(nullptr); 显式地构造一个空function
function<T> f(obj) f中存储可调用对象obj的副本
f f作为条件:当f含有一个可调用对象时为真;否则为假。
定义为function<T>的成员的类型
result_type function类型的可调用对象返回的类型
argument_type T有一个或两个实参时定义的类型。如果T只有一个实参,则argument_type
first_argument_type 第一个实参的类型
second_argument_type 第二个实参的类型
1
2
function<int(int,int)> f1 = [](int i,int j){return i*j;}
cout << f1(4,2) << endl;

这样就可以重新定义函数表

1
2
3
4
5
6
7
map<string , function<int(int,int)>> binops = {
	{"+" , add},
	{"-" , std::minus<int>()},
	{"/" , divide()},
	{"*" , [](int i, int j){return i*j;}},
	{"%" , mod}
};

重载函数与function 不能将重载函数的名字存入function类型的对象,但是可以两个方法解决

1
2
3
4
int add(int i , int j){return i+j;}
Sales_data add(const Sales_data& , const Sales_data&);
map<string , function<int(int , int)>> binops;
binops.insert({"+" , add});   //× ,which add
  1. 存储函数指针
1
2
int (*fp)(int , int) = add;
binops.insert({"+",fp});
  1. lamba
1
binops.insert({"+",[](int a , int b){return add(a,b);}});

九、重载、类型转换、运算符

1、类型转换运算符

[[类#2.4 转换构造函数]]

  • 类型转换运算符是类的一种特殊成员函数,它负责将一个类类型的值转换成其他类型。类型转换函数的一般形式如下:
1
operator type() const;
  • 一个类型转换函数必须是类的成员函数;它不能声明返回类型,形参列表也必须为空。类型转换函数通常应该是const
  • 避免过度使用类型转换函数。
  • C++11引入了显式的类型转换运算符。
  • bool的类型转换通常用在条件部分,因此operator bool一般定义成explicit的。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class SmallInt{
public:
	SmallInt(int i = 0):val(i)
	{
		if(i<0 || i > 255)
		  throw std::out_of_range("Bad SmallInt value");
	}
	 operator int() const{return val;}
private:
	std::size_t val;
}

SmallInt si;
si = 4 ;  
si + 3 ;

SmallInt sj ;
sj = 3,14 ; 
sj + 3.14 ;

2、避免有二义性的类型转换

  • 通常,不要为类定义相同的类型转换,也不要在类中定义两个及以上转换源或转换目标是算术类型的转换。
  • 在调用重载函数时,如果需要额外的标准类型转换,则该转换的级别只有当所有可行函数都请求同一个用户定义的类型转换时才有用。如果所需的用户定义的类型转换不止一个,则该调用具有二义性。

3、函数匹配与重载运算符

  • 如果a是一种类型,则表达式a sym b可能是:
    • a.operatorsym(b);
    • operatorsym(a,b);
  • 如果我们队同一个类既提供了转换目标是算术类型的类型转换,也提供了重载的运算符,则将会遇到重载运算符与内置运算符的二义性问题。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class SmallInt{
	friend
	SmallInt operator+(const SmallInt& , const SmallInt&);
public:
	SmallInt(int = 0);
	operator int() const {return val;}
private:
	std::size_t val;
}

SmallInt s1, s2 ;
SmallInt s3 = s1 + s2 ;
int i = s3 + 0 ;  //二义性