《C++11Primer》阅读随记 -- 十四、重载运算与类型转换

第十四章 重载运算与类型转换

基本概念

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

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

// 错误:不能为 int 重定义内置的运算符
int operator+(int, int);

在这里插入图片描述

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

可以被重载的运算符
+-**/%^
&|~!=
<><=>=++
<<>>==!=&&||
+=-=/=%=^=&=
|=*=<<=>>=[]()
->->*newnew[]deletedelete[]
不能被重载的运算符
::.*.? :

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

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

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

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

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

data1 += data2;			// 基于 “调用” 地表达式
data1.operator+=(data2);	// 对成员运算符函数的等价调用

选择作为成员或者非成员

当我们定义重载的运算符时,必须首先决定将其声明为类的成员函数还是声明为一个普通的非成员函数。

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

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

string s = "world";
string t = s + "!";		// 正确: 我们能把一个 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

输入和输出运算符

重载输出运算符 <<

通常情况下,输出运算符的第一个形参是一个非常量 ostream 的引用。
第二个形参一般来说是一个常量引用,该常量是我们想要打印的类类型。

ostream& operator<<(ostream& os, const Sales_data& item){
	os << item.isbn() << " " << item.units_sold << " "
	   << item.revenue << " " << item.avg_price();
	return os;
}

输出运算符尽量减少格式化操作

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

通常,输出运算符应该主要负责打印对象的内容而非控制格式,输出运算符不应该打印换行符

输入输出运算符是非成员函数

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

Sales_data data;
data << cout; 	// 如果 operator<< 是 Sales_data 的成员

以前的笔记:
在这里插入图片描述
如果我们希望为类自定义 IO 运算符,则必须将其定义成非成员函数。当然,IO 运算符通常需要读写类的非公有数据成员,所以 IO 运算符一般被声明为友元

重载输入运算符 >>

Sales_data 的输入运算符

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;
}

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

输入时的错误

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

  • 当流含有错误类型的数据时读取操作可能失败。例如在读取完 bookNo 后,输入运算符假定接下来读入的是两个数字数据,一旦输入的不是数字数据,则读取操作及后续对流的其他使用都将失败
  • 当读取操作到达文件末尾或者遇到输入流的其他错误时也会失败

当读取操作发生错误时,输入运算符应该负责从错误中恢复

算术和关系运算符

如果类定义了算术运算符,则它一般也会顶一个一个对应的符合赋值运算符。此时最有效的方式是使用符合赋值来定义算数运算符

Sales_data
operator+(const Sales_data& lhs, const Sales_data& rhs){
	Sales_data sum = lhs;
	sum += rhs;
	return sum;
}

相等运算符

bool operator==(const Sales_data& lhs, const Sales_data& rhs){
	return lhs.isbn() == rhs.isbn() &&
		   lsh.units_sold == rhs.units_sold &&
		   lhs.revenue == rhs.revenue;
}
bool operator!=(const Sales_data& lhs, const Sales_data& rhs){
	return !(rhs == lhs);
}

这些函数体现了设计准则:

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

关系运算符

如果存在唯一一种逻辑可靠的 < 定义,则应该考虑为这个类去定义 < 运算符。如果类同时还包含 ==,则当且仅当 < 的定义和 == 产生的结果一致时菜定义 < 运算符。比如,对于 Sales_data 类,因为可比较的类成员多,所以不适合做 <

赋值运算符

在拷贝赋值和移动赋值运算符之外,标准库 vector 类还定义了第三种赋值运算符,该运算符接受花括号内的元素列表作为参数

vector<string> v;
v = {"a", "an", "the"};

同样,也可以把这个运算符添加到 StrVec 类中

class StrVec{
public:
	StrVec& operator=(std::initializer_list<std::string>);
	// ...
};
StrVec& StrVec::operator=(std::initializer_list<std::string> il){
	// alloc_n_copy 分配内存空间并从给定范围内拷贝元素
	auto data = alloc_n_copy(il.begin(), il.end());
	free();						// 销毁对象中的元素并释放空间
	elements = data.first;		// 更新苏韩剧成员时期指向新空间
	first_free = cap = data.second;
	return *this;
}

复合赋值运算符

// 作为成员的二元运算符: 左侧运算对象绑定到隐式的 this 指针
// 假定两个对象表示的是同一本书
Sales_data& Sales_data::operator+=(const Sales_data& rhs){
	units_sold += rhs.units_sold;
	revenue += rhs.revenue;
	return *this;
}

赋值运算符必须定义成类的成员,复合赋值运算符通常情况下也应该这样做。这两类运算符都应该返回左侧运算对象的引用

下标运算符

class StrVec{
public:
	std::string& operator[](std::size_t n)
		{ return elements[n]; }
	const std::string& operator[](std::size_t n) const
		{ return elements[n]; }
	// ...
private:
	std::string* elements;	// 指向数组首元素的指针
}:
// 假设 svec 是一个 StrVec 对象
const StrVec cvec = svec;		// 把 svec 的元素拷贝到 cvec 中
// 如果 svec 中含有元素,对第一个元素运行 string 的 empty 函数
if( svec.size() && svec[0].empty() ){
	svec[0] = "zero";					// 正确:下标运算符返回 string 的引用
	cvec[0] = "zip";					// 错误: 对 cvec 取下标返回的是常量引用
}

递增和递减运算符

定义前置递增/递减运算符

class StrBlobPtr{
public:
	// 递增和递减运算符
	StrBlobPtr& operator++();
	StrBlobPtr& operator--();
	// ...
};

递增和递减运算符的工作机制非常相似: 首先调用 check 函数检验 StrBlobPtr 是否有效,如果是,接着检查给定的索引值是否有效。如果 check 函数没有抛出异常,则运算符返回对象的引用。

在递增运算符的例子中,我们把 curr 的当前值传递给 check 函数。如果这个值小于 vector 的大小,则 check 正常返回;否则,如果 curr 已经到达了 vector 的末尾,check 抛出异常

StrBlobPtr& operator++(){
	// 如果 curr 已经指向了容器的尾后位置,则无法递增它
	check(curr, "increment past end of StrBlobPtr");
	++curr;
	return *this;
}
StrBlobPtr& operator--(){
	// 如果 curr 已经指向了容器的尾后位置,则无法递增它
	check(curr, "decrement past begin of StrBlobPtr");
	--curr;
	return *this;
}

区分前置和后置运算符

要想同时定义前置和后置运算符,必须首先解决普通的重载形式无法区分这两种情况的问题。

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

class StrBlobPtr{
public:
	// 递增和递减运算符
	StrBlobPtr operator++(int);
	StrBlobPtr operator--(int);
	// ...
};

为了和内置版本保持一致,后置运算符应该返回对象的原值( 递增和递减之前的值 ),返回的形式是一个值而非引用

对于后置版本来说,在递增对象之前需要首先记录对象的状态

StrBlobPtr operator++(int){
	// 此处无须家产有效性,调用前置递增运算符时才需要检查
	StrBlobPtr ret = *this; 				// 记录当前值
	++*this;								// 向前移动一个元素,前置 ++ 需要检查
											// 递增的有效性
	return ret;								// 返回之前记录的状态
}

StrBlobPtr operator--(int){
	// 此处无须家产有效性,调用前置递减运算符时才需要检查
	StrBlobPtr ret = *this; 				// 记录当前值
	--*this;								// 向后移动一个元素,前置 -- 需要检查
											// 递增的有效性
	return ret;								// 返回之前记录的状态
}

由上可知,后置运算符调用各自的前置版本来完成实际的工作。

因为我们不会用到 int 形参,所以无须为其命名

显示地调用后置运算符

StrBlobPtr p(a1);			// p 指向 a1 中的 vector
p.operator++(0);			// 调用后置版本的 operator++
p.operator++();				// 调用前置版本的 operator++

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

成员访问运算符

class StrBlobPtr{
public:
	std::string& operator*() const{
		auto p = check(curr, "dereference past end");
		return (*p)[curr];			// (*p) 是对象所指的 vector
	}
	std::string* operator->() const{
		// 将实际工作委托给解引用运算符
		return & this->operator*();
	}
	// ...
};

解引用运算符首先检查 curr 是否仍在作用范围内,如果是,则放回 curr 所指元素的一个引用。箭头运算符不执行任何自己的操作,而是调用解引用运算符并返回解引用结果元素的地址

箭头运算符必须是类的成员。解引用运算符通常也是类的成员。

函数调用运算符

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

struct absInt{
	int operator()(int val) const{
		return val < 0 ? -val : val;
	}
};

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

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

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

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

函数调用运算符必须是成员函数。一个类可以定义多个不同版本的调用运算符,但参数上应该有所区别

如果类定义了调用运算符,则该类的对象称作函数对象(function object)。函数对象通常含有一些数据成员,这些成员被用于定制调用运算符中的操作。

class PrintString{
public:
	PrintString(ostream& o = cout, char c = ' '):os(o), sep(c){}
	void operator()(const string& s) const { os << s << sep; }
private:
	ostream& os;		// 用于写入的目的流
	char sep;			// 用于将不同输出隔开的字符
};

类有一个构造函数,接收一个输出流的引用以及一个用于分隔的字符,这两个形参的默认实参分别是 cout 和空格。之后的函数调用运算符使用这些成员协助其打印给定的 string

PrintString printer;			// 使用默认值,打印到 cout
printer(s);						// 在 cout 中打印 s,后面跟一个空格
PrintString errors(cerr, '\n');	
errors(s);						// 在 cerr 中打印 s, 后面跟一个换行符

函数对象常常作为泛型算法的实参。例如,可以使用标准库 for_each 算法和我们自己的 PritntString 类来打印容器的内容

for_each(vs.begin(), vs.end(), PrintString(cerr, '\n'));

for_each 的第三个实参是类型 PrintString 的一个临时对象,其中我们用 cerr 和 换行符初始化了该对象。当程序调用 for_each 时,将会把 vs 中的每个元素一次打印到 cerr 中,元素之间以换行符分隔

在这里插入图片描述

lambda 是函数对象

PrintString 对象作为调用 for_each 的实参,这一用法类似于使用 lambda 表达式的程序。当我们编写了一个 lambda 后,编译器将该表达式作为一个未命名类的未命名对象。在 lambda 表达似乎产生的类中含有一个重载的函数调用运算符,例如,对于我们传递给 stable_sort 作为其最后一个实参的 lambda 表示来说:

// 根据单词的长度对其进行排序,对于长度相同的单词按照字母表顺序排序
stable_sort(words.begin(), words.end(),
			[](const string* a, const string& b)
			{ return a.size() < b.size(); });

其行为类似于下面这个类的一个未命名对象

class ShorterString{
public:
	bool operator()(const string& s1, const string& s2) const
	{ return s1.size() < s2.szie(); }
};

产生的类只有一个函数调用运算符成员,它负责接收两个 string 并比较它们的长度,它的形参列表和函数体与 lambda 表达式完全一样。

用这个类替代 lambda 表达式后,可以重写并重新调用 stable_sort

stable_sort(words.begin(), words.end(), ShorterString());

// 之前的几种写法
// 比较函数,用来按长度排序单词
bool isShorter(const string& s1, const string& s2){
	return s1.size() < s2.size();
}

// 按长度由短到长排序 words
sort(words.begin(), words.end(), isShorter);

第三个实参是新构建的 ShorterString 对象,当 stable_sort 内部的代码每次比较两个 string 时就会调用这一对象,此时该对象将调用运算符的函数体,判断第一个 string 的大小小于第二个时返回 true

表示 lambda 及相应捕获行为的类

当一个 lambda 表达式通过引用捕获变量时,将由程序负责确保 lambda 执行时所引的对象确实存在。因此,编译器可以直接使用该引用而无须在 lambda 产生的类中将其存储为数据成员

相反,通过值捕获的变量被拷贝到 lambda 中。因此,这种方式产生的类必须为每个值捕获的变量建立对应的数据成员,同时创建构造函数,令其使用捕获的变量的值来初始化数据成员

// 获得第一个指向满足条件元素的迭代器,该元素满足 size() is >= sz
auto wc = find_if(word.begin(), word.end(), [sz](const string& a)
											{ return a.szie() >= sz; })

lambda 表达式产生的类将形如:

class SizeComp{
public:
	SizeComp(size_t n):sz(n){}	// 该形参对应捕获的变量
	// 该调用运算符的返回类型、形参和函数体都与 lambda 一致
	bool operator()(const string& s) const
		{ return s.size() >= sz; }
private: 
	size_t sz;
};

ShorterString 类不同,上面这个类含有一个数据成员以及一个用于初始化该成员的构造函数。这个合成的类不含有默认构造函数,因此想要使用这个类必须提供一个实参。

auto wc = find_if(word.begin(), word.end(), SizeComp(sz));

lambda 表达式产生的类不含默认构造函数、赋值运算符以及默认析构函数: 它是否含有默认的拷贝/移动构造函数通常要视捕获的数据成员类型而定

标准库定义的函数对象

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
// 然后将 -10 作为 intAdd::operator(int, int) 的第二个参数
sum = intAdd(10, intNegate(10));	// sum = 0

下表类型定义在 functional 头文件中

算术关系逻辑
plus<Type>equal_to<Type>logical_and<Type>
minus<Type>not_equal_to<Type>logical_or<Type>
multiplies<Type>greater<Type>logicalnot<Type>
divides<Type>greater_equal<Type>
modulus<Type>less<Type>
negate<Type>less_equal<Type>

在算法中使用标准库函数对象

表示运算符的函数对象类常用来替换算法中的默认运算符。比如,在默认情况下使用排序算法使用 operator<将序列按照升序排序>。如果要执行降序排列的话,我们可以传入一个 greater 类型的对象。该类将产生一个调用运算符并负责执行待排序类型的大于运算。例如,如果 svec 是一个 vector<string>

// 传入一个临时的函数对象用于执行两个 string 对象的 > 比较运算
sort(svec.begin(), svec.end(), greater<string>());

需要特别注意的时,标准库规定其函数对象对于指针同样适用。之前介绍比较两个无关指针将产生未定义的行为,然而可能会希望通过比较指针的内存地址来 sort 指针的 vector。直接这么做将产生未定义的行为,因此可以使用一个标准库函数对象来实现该目的

vector<string*> nameTable;
// 错误: nameTable 中的指针彼此之间没有关系,所以 < 将产生未定义的行为
sort(nameTable.begin(), nameTable.end(),
					[](string* a, string* b){ return a < b; })

// 正确: 便准库规定的指针的 less 是定义良好的
sort(nameTable.begin(), nameTable.end(), less<string*>());

关联容器使用 less<ket_type> 对元素排序,因此我们可以定义一个指针的 set 或者在 map 中使用指针作为关键值而无须直接声明 less

可调用对象与 function

和其他对象一样,可调用的对象也有类型。例如,每个 lambda 有它自己唯一的( 未命名 )类类型;函数及函数指针的类型则由其返回值类型和实参类型决定,等等。

然而,两个不同类型的可调用对象却可能共享同一种调用形式( call signature)。调用形式指明了调用返回的类型以及传递给调用的实参类型。一种调用形式对应一个函数类型,例如:

int(int, int)

是一个函数类型,它接受两个 int、返回一个 int

不同类型可能具有相同的调用形式
对于几个可调用对象共享同一种调用形式的情况,有时我们会希望把它们看成具有相同的类型。例如:

// 普通函数
int add(int i, int j) { return i + j; }
// lambda, 其产生一个未命名的对象类
auto mod = [](int i, int j){ return i % j; }
// 函数对象类
struct divide{
	int operator()(int denominator, int divisor){
		return denominator / divisor;
	}
};

上诉可调用对象共享同一种调用形式

int(int, int)

我们可能希望使用这些可调用对象构建一个简单的桌面计算器。为了实现这一目的,需要定义一个函数表( function table ) 用于存储指向这些可调用对象的“指针”。当程序需要执行某个特定的操作时,从表中查找该调用的函数。

// 构建从运算符到函数指针的映射关系,其中函数接收两个 int、返回一个 int
map<string, int(*)(int, int)> binops;

我们可以按照下面的形式将 add 指针添加到 binops

// 正确: add 是一个指向正确类型函数的指针
binops.insert({"+", add});		// {"+", add} 是一个 pair

但是我们不能将 mod 或者 divide 存储 binops

binops.insert({"%", mod});	// 错误: mod 不是一个函数指针

问题在于 mod 是个 lambda 表达式,而每个 lambda 有它自己的类类型,该类型与存储在 binops 中的值的类型不匹配

标准库 function 类型

可以使用一个名为 function 的新标准库类型解决上述问题。

操作解释
function<T> ff 是一个用来存储可调用对象的空 function,这些可调用对象的调用形式应该与函数类型 T 相同
function<T> f(nullptr)显式地构造一个空 function
function<T> f(obj)f 中存储可调用对象 obj 的副本
ff 作为条件:当 f 含有一个可调用对象时为真,否则假
f(args)调用 f 中的对象,参数 args
定义为 function 的成员的类型
result_typefunction 类型的可调用对象返回的类型
argument_type first_argument_type second_argument_typeT 有一个或两个实参时定义的类型。如果 T 只有一个实参,则 argument_type 是该类型的同义词; 如果 T 有两个实参,则 first_argument_typesecond_argument_type 分别代表两个实参的类型

function<int(int, int)>

在此,我们声明了一个 function 类型,它可以表示接收两个 int、返回一个 int 的可调用对象。因此,我们可以用这个新声明的类型表示任意一种桌面计算器用到的类型

function<int(int, int)> f1 = add;				// 函数指针
function<int(int, int)> f2 = divide();			// 函数对象类的对象
function<int(int, int)> f3 = [](int i, int j)	// lambda
							 { return i * j; };
				
cout << f1(4, 2) << endl;						// 6
cout << f2(4, 2) << endl;						// 2
cout << f3(4, 2) << endl;						// 8

使用这个 function 类型我们可以重新定义 map

// 列举了可调用对象与二元运算符对应关系的表格
// 所有可调用对象都必须接收两个 int、返回一个 int
// 其中的元素可以是函数指针、函数对象或者 lambda
map<string, function<int(int, int)>> binops;

map<string, function<int(int, int)>> binops = {
	{"+", add},									// 函数指针
	{"-", std::minus<int>()},					// 标准库函数对象
	{"/", divide()},							// 用户定义的函数对象
	{"*", [](int i, int j){ return i * j; }}	// 未命名的 lambda
	{"%", mod}									// 命名了的 lambda 对象
};


binops["+"](10, 5);				
binops["-"](10, 5);				
binops["/"](10, 5);				
binops["*"](10, 5);				
binops["%"](10, 5);

重载的函数与 function

我们不能(直接)将重载函数的名字存入 function 类型的对象中

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});		// 错误

解决上述二义性问题的一条途径是存储函数指针而非函数的名字:

int(*fp)(int, int) = add;	// 指针所指的 add 是接收两个 int 的版本
binops.insert({"+", fp});	// 正确:fp 指向一个正确的 add 版本

同样,使用 lambda 也可以消除二义性

// 正确:使用 lambda 来指定我们希望使用的 add 版本
binops.insert({"+", [](int a, int b){ return add(a, b);}});

lambda 内部的函数调用传入了两个 int, 因此该调用只能匹配接收两个 intadd 版本,而这也正是执行 lambda 时真正调用的函数。

重载、类型转换与运算符

转换构造函数和类型转换运算符共同定义了类类型转换运算符(class-type conversions), 这样的转换有时也被称作用户定义的类型转换(user-defined conversions)

类型转换运算符

类型转换运算符(conversion operator) 是类的一种特殊成员函数,它负责将一个类类型的值转换成其他类型。类型转换函数的一般形式如下所示:

operator type() const;

其中 type 表示某种类型。类型转换运算符可以面向任意类型(除了 void 以外)进行定义,只要该类型能作为函数的返回类型。因此,不允许转换成数组或者函数类型,但允许转换成指针或者引用类型。

一个类型转换函数必须是类的成员函数;它不能声明返回类型,形参列表页必须为空。类型转换函数通常应该是 const

定义含有类型转换运算符的类

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 类及定义了向类类型的转换,页定义了从类类型向其他类型的转换。其中,构造函数将算数类型的值转换成 SmallInt 对象,而类型转换运算符将 SmallInt 对象转换成 int

SmallInt si;
si = 4;	// 首先将 4 隐式地转换成 SmallInt, 然后调用 SmallInt::operator=
si + 3;	// 首先将 si 隐式地转换成 int,然后执行整数的加法
class SmallInt;
operator int(SmallInt&);				// 错误:不是成员函数
class SmallInt{
public:
	int operator int() const;			// 错误:指定了返回类型
	operator int(int = 0) const;		// 错误:参数列表不为空
	operator int*() const { return 42; }	// 错误:42 不是一个指针
};

显式的类型转换运算符

class SmallInt{
public:
	// 编译器不会自动执行这一类型转换
	explcit operator int() const { return val; }
	// ...
}:
SmallInt si = 3;		// 正确:SmallInt 的构造函数不是显式的
si + 3;					// 错误:此处需要隐式的类型转换,但类的运算符是显式的
static_cast<int>(si) + 3;	// 正确:显式地请求类型转换
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Artintel

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值