《C++Primer》第十四章——重载运算与类型转换

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

14.1 基本概念

  • 重载的运算符是具有特殊名字的函数,其名字由关键字 operator 和其后要定义的运算符号共同组成,并且也包含返回类型、参数列表以及函数体
  • 一个重载的运算符,其操作数、优先级和结合律与对应的内置运算符保持一致
  • 当一个重载的运算符是成员函数,则它的第一个(左侧)运算对象绑定到隐式的 this 指针上,因此成员运算符函数的(显式)参数数量比运算符的运算对象总数少一个
  • 对于一个运算符函数来说,至少含有一个类类型的参数,若均为内置类型则为错误的
  • 只能重载已有的运算符,而无权发明新的运算符号
    1.直接调用一个重载的运算符函数
    1)对于非成员运算符函数,可以将运算符作用域类型正确的实参,从而间接调用重载的运算符函数;我们也可以像调用普通函数一样直接调用运算符函数
//一个非成员运算符函数的等价调用
data1 + data2;				//普通的表达式
operator+(data1, data2);	//等价的函数调用

2)对于成员函数,我们也可以像调用其他成员函数一样显式地调用成员运算符函数

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

以上两句都调用了成员函数 operator+=,将 this 绑定到 data1 的地址、将 data2 作为实参传入了函数
2.某些运算符不应该被重载
1)对于逻辑语运算符和逻辑或运算符以及逗号运算符,重载的版本无法保留求值顺序和/或短路求值属性,因此不建议重载它们
2)对于逗号运算符和取地址运算符,C++语言已经定义了这两种算法作用于类类型时的特殊含义,因此不用被重载,否则它们的行为将异于常态,从而导致类的用户无法适应
总结:通常情况下,不应该重载逗号、取地址、逻辑与和逻辑或运算符
3.使用与内置类型一致的含义
当设计一个类时,首先应考虑这个类将提供哪些操作,然后思考应该把每个操作设计成普通函数还是重载运算符,若某些操作在逻辑上与运算符相关,则适合定义为重载的运算符

  • 若类执行 IO 操作,则定义移位运算符使其与内置类型的 IO 保持一致
  • 若类定义了 operator==,则通常也应定义 operator!=
  • 若类定义了 operator<,则也应定义其他关系操作
  • 重载运算符的返回类型应与内置版本的返回类型兼容:逻辑运算符和关系运算符应返回 bool,算数运算符应返回一个类类型的值,赋值运算符和符合赋值运算符应该返回左侧运算对象的一个引用
    4.选择作为成员或非成员
    当定义重载的运算符时,必须首先决定是声明为类的成员函数还是声明为一个普通的非成员函数
  • 赋值(=)、下标([ ])、调用(())和成员访问箭头(->)运算符必须是成员
  • 复合赋值运算符一般来说应该是成员,但并非必须
  • 改变对象状态的运算符或者给定类型密切相关的运算符,如递增、递减和解引用运算符通常是成员
  • 具有对称性的运算符可能转换任意一端的运算对象,例如算数、相等性、关系和位运算符等,通常希望是普通的非成员函数
  • 如果希望提供含有类对象的混合类型表达式,则运算符必须是非成员函数

14.2 输入和输出运算符

1.重载输出运算符 <<

  • 通常情况下,输出运算符的第一个形参是一个非常量 ostream 的引用;非常量是因为向流写入内容会改变其状态,而引用是因为我们无法直接赋值一个 ostream 对象
  • 第二个形参一般来说是一个常量的引用,该常量是我们想要打印的类类型;引用是因为我们希望避免赋值实参。而常量是因为打印对象不会改变对象的内容
  • 为了与其他输出运算符保持一致,operator<< 一般要返回它的 ostream 形参
  • 输出运算符尽量减少格式化操作,其主要负责打印对象的内容而非控制格式,输出运算符不应该打印换行符等
    1)输入输出运算符必须是非成员函数
  • 因为如果是类的成员函数,则它的左侧运算对象将是类的一个对象,假设输入输出运算符是某个类的成员,则它们必须是 istream 或 ostream 成员,而这两个类属于标准库,而我们无法给标准库中的类型添加任何成员,因此我们希望为类自定义 IO 运算符,则必须将其定义为非成员函数
  • IO 运算符通常需要读写非公有数据成员,因此 IO 运算符一般被声明为友元
    2.重载输入运算符 >>
  • 通常情况下,输入运算符的第一个形参是运算符将要读取的流的引用
  • 第二个形参是将要读入到的(非常量)对象的引用,非常量是因为输入运算符本身的目的就是将数据读入该对象
  • 该运算符一般返回某个给定流的引用
    1)输入时的错误
    在执行输入运算对象时,可能发生错误:
  • 当流含有错误类型的数据时读取操作可能失败
  • 当读取操作到达文件末尾或者遇到流的其他错误时也会失败
    输入运算符必须处理输入可能失败的情况,而输出运算符不需要
    当读取操作发生错误时,输入运算符应该负责从错误中恢复
istream &operator>>(istream &is, Sales_data &item)
{
	double price;
	is >> item.bookNo >> item.units_sold >> price;
	if(is)		//检查输入是否成功
		item.revenue = item.units_sold * price;
	else
		item = Sales_data();	//输入失败,对象被赋予默认的状态
	return is;
}

14.3 算术和关系运算符

  • 通常情况下,把算数和关系运算符定义为非成员函数以允许对左侧或右侧的运算对象进行转换
  • 由于这些运算符一般不需要改变运算对象的状态,所以形参都是常量的引用
    1.相等运算符
  • 通常情况下,C++中的类通过定义相等运算符来检验两个对象是否相等,它们会比较对象的每一个数据成员,只有当所有对应的成员都相等时才认为两个对象相等
  • 如果某个类在逻辑上有相等性的含义,则该类应该定义 operator== ,这样可以使得用户更容易使用标准库算法来处理这个类
  • 相等运算符和不相等运算符中的一个应该把工作委托给另外一个,意味着其中一个运算符应该负责实际比较对象的工作,而另一个运算符则只是调用哪个真正工作的运算符
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.不相等运算符
如果存在唯一一种逻辑可靠的 < 定义,则应该考虑为这个类定义 < 运算符,如果类同时还包含 ==,则当且仅当小于运算符的定义和相等运算符产生的结果一致时才定义小于运算符

14.4 赋值运算符

  • 我们可以重载赋值运算符,不论形参是什么,赋值运算符都必须定义为成员函数
  • 为了与内置类型的赋值运算符保持一致,重载的赋值运算符应返回其左侧对象的引用
  • 类除了可以定义拷贝赋值和移动赋值运算符之外,还可以定义其他赋值运算符以使用别的类型作为右侧运算对象;比如说标准库 vector 类还定义了第三种赋值运算符,该运算符接受花括号内的元素列表作为参数
vector<string> v;
v = {"a", "an", "the"};

1.复合赋值运算符

  • 复合赋值运算符不非得是类的成员函数,但我们还是倾向于把包括复合赋值在内的所有赋值运算都定义在类的内部
  • 为了与内置类型的复合赋值保持一致,类中的复合赋值运算符也要返回其左侧运算对象的引用
//作为成员的二元运算符,左侧运算对象绑定到隐式的 this 指针
//假定两个对象表示同一本书
Sales_data& Sales_data::operator+=(const Sales_data &rhs)
{
	units_sold += rhs.units_sold;
	revenue += rhs.revenue;
	return *this;
}

14.5 下标运算符

  • 表示容器的类通常可以通过元素在容器中的位置访问元素,这些类一般会定义下标运算符 operator[ ]
  • 下标运算符必须是成员函数
  • 为了与下标的原始定义兼容,下标运算符通常以所访问元素的引用作为返回值,从而使下标可以出现在赋值运算符的任意一端
  • 如果一个类包含下标运算符,则它通常会定义两个版本:一个返回普通引用,另一个则是类的常量成员并返回常量引用,当作用于一个常量对象时,下标运算符返回常量引用以确保不会给返回的对象赋值
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;		//指向数组首元素的指针
}


const StrVec cvec = svec;
if(svec.size() && svec[0].empt()){
	svec[0] = "zero";		//正确,下标运算符返回 string 的引用
	cvec[0] = "zip";		//错误,对 cvec 取下标返回的是常量引用
}

14.6 递增和递减运算符

  • 在迭代器类中通常会实现递增运算符(++)和递减运算符(–)
  • C++语言并不要求递增和递减运算符必须是类的成员,但是因为它们改变的是所操作对象的状态,因此建议将其设定为成员函数
  • 定义递增和递减运算符的类应该同时定义前置版本和后置版本
    1.定义前置递增/递减运算符
  • 为了与内置版本保持一致,前置运算符应该返回递增或递减后对象的引用
    2.区分前置和后置运算符
  • 普通的重载形式无法区分这两种情况,为了解决这个问题,后置版本接受一个额外的(不被使用)的 int 类型的形参,当使用后置运算符时,编译器为这个形参提供一个值为 0 的实参,这个形参的唯一作用就是区分前置版本和后置版本的函数,并不参与运算
  • 后置运算符调用各自的前置版本来完成实际的工作
  • 为了与内置版本保持一致,后置运算符应该返回对象的原值(即递增或递减之前的值),返回的形式是一个值而非引用
    3.显式地调用后置运算符
    我们可以显式地调用一个重载的运算符,但如果想要通过函数调用的方式调用和后置版本,则必须为它的整型参数传递一个值
StrBlobPtr p(a1);	
p.operator++(0);	//调用后置版本的 operator++
p.operator++();		//调用前置版本的 operator++

14.7 成员访问运算符

  • 迭代器及智能指针类中常常用到解引用运算符(*)和箭头运算符(->)
  • 箭头运算符必须是类的成员,解引用运算符通常也是类的成员
    1)对箭头运算符返回值的限定(重点理解)
    重载的箭头运算符必须返回类的指针或者自定义了箭头运算符的某个类的对象
    对于形如 point->mem 的表达式,point 必须是指向类对象的指针或者是一个重载了 operator-> 的类的对象,根据 point->mem 中 point 的类型不同,point->mem 分别等价于
    (*point).mem; //point 是一个指针类型
    point.operator()->mem; //point 是类的一个对象
  • 若 point 是指针,则我们应用内置的箭头运算符,表达式等价于 (*point).mem,首先解引用该指针,然后从所得的对象中获取指定的成员,若 point 所指类型没有名为 mem 的成员,程序会发生错误
  • 若 point 是定义了 operator-> 的类的一个对象,则使用 point.operator->() 的结果来获取 mem。其中,若该结果是一个指针,则执行上面所述的步骤,若该结果本身含有重载的 operator->(),则重复调用当前步骤。最终,当这一过程结束时程序或者返回来所需的内容,或者返回一些表示当前程序错误的信息

14.8 函数调用运算符

  • 如果类重载了函数调用运算符,则我们可以像使用函数一样使用该类的对象
  • 函数调用运算符必须是成员函数,一个类可以定义多个不同版本的调用运算符,相互之间应该在参数数量或者类型上有所区别
  • 如果类定义了调用运算符,则该类的对象称作函数对象,因为可以调用这种对象,所以我们说这些对象的行为像函数一样
    回顾:可调用对象有五种,分别是函数、函数指针、bind创建的对象、lambda 表达式、重载了函数调用运算符的类
    1. lambda 是函数对象
    当我们编写了一个 lambda 后,编译器将该表达式翻译成一个未命名类的未命名对象;在 lambda 表达式产生的类中含有一个重载的函数调用运算符,仔细观察下面的例子
stable_sort(words.begin(), words.end(), 
			[](const string &a, const string &b){ return a.size() < b.size(); });


//用类代替 lambda 表达式
class ShorterString{
public:
	bool operator()(const string &s1, const string &s2) const
	{ return s1.size() < s2.size(); }
};
stable_sort(words.begin(), words.end(), ShorterString());

注:默认情况下 lambda 不能改变它捕获的变量,因此默认情况下,由 lambda 产生的类当中的函数调用运算符是一个 const 成员函数
1)表示 lambda 及相应捕获行为的类
当一个 lambda 表达式通过引用捕获变量时,将由程序负责确保 lambda 执行时引用所引对象确实存在,编译器可以直接使用而该引用无须在 lambda 产生的类中将其存储为数据成员
而当 lambda 表达式通过值捕获变量时,这种 lambda 产生的类必须为每个值捕获的变量建立对应的数据成员,同时创建构造函数,令其使用捕获的变量的值来初始化数据成员

auto wc = find_if(words.begin(), words.end(),
				[sz](const string &a){return a.size() >= sz;});

//该 lambda 表达式产生的类将形如
class SizeComp{
	SizeComp(size_t n) : sz(n){ }	//该形参对应捕获的变量
	//该调用运算符的返回类型、形参、函数体斗鱼 lambda 一致
	bool operator()(const string &s) const
	{ return s.size() >= sz;}
private:
	size_t sz;	//该数据成员对应通过值捕获的变量
}

注:lambda 表达式产生的类不含默认构造函数、赋值运算符及默认析构函数,是否含有默认的拷贝/移动构造函数则通常要视捕获的数据成员类型而定
2.标准库定义的函数对象

  • 标准库定义了一组表示算数运算符、关系运算符和逻辑运算符的类,每个类分别定义了一个执行命名操作的调用运算符
  • 这些类都被定义成模板的形式,我们可以为其指定具体的应用类型,即调用运算符的形参类型,这些类型都被定义在 functional 头文件中
    1)在算法中使用标准库函数对象
    表示运算符的函数对象类常用来替换算法中的默认运算符
    注:标准库规定其函数对象对于指针同样适用,可见下面的例子
vector<string *> nameTable;
//错误:nameTable 中的指针之间没有关系,所以 < 将产生未定义的行为
sort(nameTable.begin(), nameTable.end(),
	[](stirng *a, string *b){return a < b;});
//正确:标准库规定指针的 less 是定义良好的
sort(nameTable.begin(), nameTable.end(), less<string*>());

3.可调用对象与 function

  • 和其他对象一样,可调用对象也有类型。例如,每个 lambda 有它自己唯一的(未命名的)类类型;函数及函数指针的类型则由其返回值类型和实参类型决定
  • 但是,不同类型的可调用对象可能共享同一种调用形式调用形式指明了调用返回的类型以及传递给调用的实参类型一种调用形式对应一个函数类型,eg: int(int, int)是一个函数类型,接受两个 int,返回一个 int
    1)不同类型可能具有相同的调用形式
//普通函数
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)
2)标准库 function 类型(p 512)
function 是一个模板,和我们使用过的其他模板一样,当创建一个具体的 function 类型时,我们必须提供额外的信息

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

3)重载的函数与 function
我们不能直接将重载函数的名字存入 function 类型的对象中,解决二义性的方法有以下两种

  • 通过存储函数指针代替函数的名字
  • 使用 lambda 消除二义性

14.9 重载、类型转换与运算符

转换构造函数类型转换运算符共同定义了类类型转换,这样的转换有时也称为用户定义的类型转换
1.类型转换运算符

  • 类型转换运算符是类的一种特殊成员函数,负责将一个类类型的值转换成其他类型
    operator type() const;
  • 一个类型转换运算符必须是类的成员函数;它不能声明返回类型,形参列表也必须为空;类型转换函数通常应该是 const
    1)显式的类型转换运算符
    C++11新标准引入了显式的类型转换运算符,当类型转换运算符是显式的时,必须通过显式的强制类型转换才可以
class SmallInt{
public:
	//编译器不会自动执行这一类型转换
	explicit operator int() const { return val; }
}

si + 3;						//错误:此处需要隐式的类型转换,但类的运算符是显式的
static_cast<int>(si) + 3;	//正确:显式地请求类型转换

当该规定存在一个例外,如果表达式被用作条件,则编译器会将显式的类型转换自动应用于它,即当表达式出现在下列位置时,显式的类型转换将被隐式的执行

  • if、while 及 do 语句的条件部分
  • for 语句头的条件表达式
  • 逻辑非运算符(!)、逻辑或运算符(||)、逻辑与(&&)运算符的运算对象
  • 条件运算符( ? : )的条件表达式
    :向 bool 的类型转换通常用在条件部分,因此 operator bool 一般定义为 explicit 的
//为了对条件求值,cin 被 istream operator bool 类型转换函数隐式的执行了转换
//若 cin 的条件状态为 good,则干函数返回为真
//否则该函数返回为假
while(std::cin >> value)

2.避免有二义性的类型转换
通常情况下,不要为类定义相同的类型转换也不要在类中定义两个及两个以上的转换源或转换目标是算数类型的转换
以下两种情况可能产生多重转换路径(详细例子见p 517)
1)两个类提供相同的类型转换:比如,A类定义了一个接受B类对象的转换构造函数,而同时B类定义了一个转换目标是A类的类型转换运算符时,我们称它们提供了相同的类型转换
2)类定义了多个转换规则,而这些转换设计的类型本身可以通过其他类型转换联系在一起
3.函数匹配与重载运算符(p 521)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值