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

14.1 基本概念

  1. 重载运算符是具有特殊名字的函数。
    在这里插入图片描述
//一个非成员运算符函数的等价调用
data1 + data2;		 //普通的表达式
operator+ (data1, data2); 	//等价的函数调用

data1 += data2;		//基于“调用”的表达式
data1.operator+=(data2); 	//对成员运算符函数的等价调用
  1. 除了重载的函数调用运算符operator()之外,其他重载运算符不能含有默认实参。
  2. 当一个重载的运算符是成员函数时,this绑定到左侧运算对象。成员运算符函数的(显式)参数数量比运算对象的数量少一个。
  3. 当运算符作用于内置类型的运算对象时,无法改变该运算符的含义。
  4. 只能重载已有的运算符,而无权发明新的运算符号。
  5. 对于一个重载的运算符来说,其优先级和结合律与对应的内置运算符保持一致。
  6. 关于运算对象求值顺序的规则无法应用到重载的运算符上。
  7. &&和||运算符的重载版本无法保留内置运算符的短路求值属性,两个运算对象总是会被求值。
  8. 通常情况下,不应该重载逗号、取地址、逻辑与和逻辑或运算符。
  9. 如果某些操作在逻辑上与运算符相关,则它们适合于定义成重载的运算符。
  10. 如果想提供含有类对象的混合类型表达式,则运算符必须定义成非成员函数。
  11. 当把运算符定义为成员函数时,它的左侧运算对象必须是运算符所属类的一个对象。
  12. 具有对称性的运算符可以转换任意一端的运算对象,通常应该是普通的非成员函数。
//如果operator+是string类的成员:
string s = "world";
string t = s  + "!"; //正确,s.operator+("!")。
string u = "hi" + s; //如果+是string的成员,则产生错误
//等价于"hi".operator+(s)
//const char*是一种内置类型,没有成员函数
//因为string将+定义成了普通的非成员函数,所以"hi"+s等价于operator+("hi",s)
  1. 一个重载的运算符必须是某个类的成员或者至少拥有一个类类型的运算对象。
//错误:不能为int重定义内置的运算符
int operator+(int, int);
  1. 赋值、下标、函数调用和箭头运算符必须作为类的成员。

14.2 输入和输出运算符

14.2.1 重载输出运算符<<

  1. operator<<一般要返回它的ostream形参。
ostream &operator<<(ostream &os, const Sales_data &item)
{
	os << item.isbn() <<" " <<item.units_sold <<" "
		<<item.revenue<<" " <<item.avg_price();
	return os;
}
  1. 令输出运算符尽量减少格式化操作可以使用户有权控制输出的细节。
  2. 通常,输出运算符应该主要负责打印对象的内容而非控制格式,输出运算符不应该打印换行符。
  3. 输入输出运算符必须是非成员函数。
Sales_data data;
data << cout; //如果operator<<是Sales_data的成员
  1. IO运算符通常需要读写类的非公有数据成员,所以IO运算符一般被声明为友元。

14.2.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;
}
  1. 在执行输入运算符时可能发生下列错误:
    当流含有错误类型的数据时读取操作可能失败。
    当读取操作到达文件末尾或者遇到输入流的其他错误时也会失败。
  2. 当读取操作发生错误时,输入运算符应该负责从错误中恢复。

14.3 算术和关系运算符

  1. 通常情况下,把算术和关系运算符定义成非成员函数以允许对左侧或右侧的运算对象进行转换。因为这些运算符一般不需要改变运算对象的状态,所以形参都是常量的引用。
//假设两个对象指向同一本书
Sales_data 
operator+(const Sales_data &lhs, const Sales_data &rhs)
{
	Sales_data sum = lhs; 	//把lhs的数据成员拷贝给sum
	sum += rhs; 		//把rhs加到sum中
	return sum;
}
  1. 如果类同时定义了算术运算符和相关的复合赋值运算符,则通常情况下应该使用复合赋值来实现算术运算符。
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);
}

14.3.1 相等运算符

  1. 如果某个类在逻辑上有相等性的含义,则该类应该定义operator==,这样做可以使得用户更容易使用标准库算法来处理这个类。

14.3.2 关系运算符

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

14.4 赋值运算符

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

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& Sales_data::operator+=(const Sales_data &rhs)
{
	units_sold += rhs.units_sold;
	revenue += rhs.revenue;
	return *this;
}

14.5 下标运算符

  1. 下标运算符必须是成员函数。
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];}
	//...
};
  1. 如果一个类包含下标运算符,则它通常会定义两个版本:一个返回普通引用,另一个是类的常量成员并且返回常量引用。
// 通过this是否指向常量进行匹配
//假设svec是一个StrVec对象
const StrVec cvec = svec; //把svec的元素拷贝到cvec中
//如果svec中含有元素,对第一个元素运行string的empty函数
if(svec.size() && svec[0].empty()){
	svec[0]="zero"; //正确
	cvec[0]="Zip"; //错误
}

14.6 递增和递减运算符

  1. 定义递增和递减运算符的类应该同时定义前置版本和后置版本。这些运算符通常应该被定义成类的成员(因为它们改变的正好是所操作对象的状态)。
class StrBlobPtr{
public:
	//递增和递减运算符
	StrBlobPtr& operator++(); //前置运算符
	StrBlobPtr& operator--(); 
	//其他...
};
StrBlobPtr& StrBlobPtr::operator++(){//前置版本:返回递增/递减对象的引用
	//如果curr已经指向了容器的尾后位置,则无法递增它
	check(curr,"increment past end of StrBlobPtr");
	++curr; //将curr在当前状态下向前移动一个元素
	return *this;
}
StrBlobPtr& StrBlobPtr::operator--(){
	//如果curr是0,则继续递减它将参数一个无效下标
	--curr; //将curr在当前状态下向后移动一个元素
	check(curr,"decrement past begin of StrBlobPtr");
	return *this;
}
  1. 为了与内置版本保持一致,前置运算符应该返回递增或递减后对象的引用。后置运算符应该返回对象的原值(递增或递减之前的值),返回的形式是一个值而非引用。
  2. 为了区分前置和后置,后置版本接受一个额外的(不被使用)int类型的形参,编译器会为这个形参提供一个值为0的实参。
p++;
p.operator++(0);
++p;
p.operator++();
  1. 如果想通过函数调用的方式调用后置版,则必须为它的整形参数传递一个值,尽管传入的值通常会被运算符函数忽略,但却必不可少,因为编译器只有通过它才能知道应该使用后置版本。
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; //返回之前记录的状态
}

14.7 成员访问运算符

  1. 箭头运算符必须是类的成员。解引用运算符通常也是类的成员,尽管并非必须如此。
class StrBlobPtr{
public:
	std::string& operator*() const
	{
		auto p = check(curr,"deference past end");
		return (*p)[curr]; //(*p)是对象所指的vector
}

std::string* operator->() const
{
	//实际工作委托给解引用运算符
	return & this->operator*();
}
//...
}
  1. 这两个运算符的用法与指针或者vector迭代器的对应操作完全一致。
StrBlob a1 = {"hi","bye","now"};
StrBlobPtr p(a1); //p指向a1中的vector
*p = "okay"; //给a1的首元素赋值
cout<<p->size()<<endl; //打印4,这是a1首元素的大小
cout<<(*p).size()<<endl; //等价于p->size()
  1. 对箭头运算符返回值的限定(永远不能丢弃成员访问的含义)。
//根据类型的不同,point->mem分别等价于
(*point).mem; //point是一个内置的指针类型
point.operator->()->mem; //point是类的一个对象

14.8 函数调用运算符

  1. 像使用函数一样使用类的对象。
struct absInt {
	int operator()(int val) const { 
		return val < 0 ? -val : val;
	}
};

int i = -42;
absInt absObj; 	//含有函数调用运算符的对象
int ui = absObj(i); 	//将i传递给absObj.operator()
  1. 函数调用运算符必须是成员函数。一个类可以定义多个不同版本的调用运算符,相互之间应该在参数数量或类型上有所区别。
  2. 如果类定义了调用运算符,则该类的对象称作函数对象。函数对象常常作为泛型算法的实参,例如for_each算法的第三个参数。
  3. 函数对象类通常含有一些数据成员,用于定制调用运算符中的操作。
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; 		//用于将不同输出隔开的字符
};

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

//函数对象常常作为泛型算法的实参
for_each(vs.begin(), vs.end(), PrintString(cerr,'\n'));

14.8.1 lambda是函数对象

  1. 当编写了一个lambda后,编译器将该表达式翻译成一个未命名类的未命名对象,在lambda表达式产生的类中含有一个重载的函数调用运算符。
    //根据单词长度进行排序
stable_sort(words.begin(),words.end(),
	[](const string &a,const string &b){return a.size() < b.size();} );
//其行为类似于下面这个类的一个未命名对象
class ShorterString{
public:
	bool operator()(const &s1,const string &s2) const
		{return s1.size() < s2.size();}
};

stable_sort(words.begin(), words.end(), ShorterString() );
  1. 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; //该数据成员对应通过值捕获的变量
};

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

14.8.2 标准库定义的函数对象

  1. 标准库定义了一组表示算术运算符、关系运算符和逻辑运算符的类,每个类分别定义了一个执行命名操作的调用运算符。
    在这里插入图片描述
plus<int> intAdd; 	//可执行int加法的函数对象
negate<int> intNegate; 	//可执行int值取反的函数对象

int sum = intAdd(10,20); //使用intAdd::operator()(int,int)求10和20的和
sum = intNegate(intAdd(10,20)); //使用intNegate::operator()(int),-30

sum = intAdd(10, intNegate(10)); //sum=0;
  1. 表示运算符的函数对象类常用来替换算法中的默认运算符。
  2. 标准库规定其函数对象对于指针同样适用。
//传入一个临时的函数对象用于执行两个string对象的>比较运算
//默认是使用<
sort(svec.begin(),svec.end(),greater<string>());

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

14.8.3 可调用对象与function

  1. C++中有几种可调用的对象:函数、函数指针、lambda表达式、bind创建的对象以及重载了函数调用运算符的类。
  2. 不同类型可能具有相同的调用形式(调用签名一样)。调用形式指明了调用返回的类型以及传递给调用的实参类型。
//普通函数
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;
	}
};
//调用形式(call signature)都是int(int,int)

在这里插入图片描述

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

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); //调用add(10,5)
binops["-"](10,5); 
binops["/"](10,5); 
binops["*"](10,5); 
binops["%"](10,5); 
  1. 不能(直接)将重载函数的名字存入function类型的对象中,可能会产生二义性。解决办法为存储函数指针而非函数名字,或者使用lambda。
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}); //错误:哪个add?

//解决二义性问题的一个途径是存储函数指针
int (*fp)(int, int) = add;
binops.insert({"+", fp});
//也可以使用lamdba来消除二义性
binops.insert({"+",[](int a, int b){return add(a,b);}});

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

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

14.9.1 类型转换运算符

  1. 类型转换运算符是类的一种特殊成员函数,它负责将一个类类型的值转换成其他类型。
//定义一个比较简单的类,令其表示0到255之间的一个整数
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; //首先将4隐式地转换成SmallInt,然后调用SmallInt::operator=
si + 3; //首先将si隐式地转换成int,然后执行整数的加法

//内置类型转换将double实参转成int
SmallInt si = 3.14; //调用SmallInt(int)构造函数
//SmallInt的类型转换运算符将si转换成int
si + 3.14; //内置类型转换将所得的int继续转换成double
  1. 一个类型转换函数必须是类的成员函数;它不能声明返回类型,形参列表也必须为空。类型转换函数通常应该是const,一般形式为:operator type() const。
class SmallInt; 
operator int(SmallInt&); //错误:不是成员函数

class SmallInt{
public:
	int operator int() const; 	//错误:指定了返回类型
	operator int(int = 0) const; 	//错误:参数列表不为空
	operator int*() const { return 42; } //错误:42不是一个指针
};
  1. 避免过度使用类型转换函数。
  2. 类型转换运算符可能产生意外结果。
//当istream含有向bool的类中转换时
int i = 42;
cin << i; //提升后的bool值(1或0)最终被左移42个位置
  1. 为了防止转换产生的异常,C++ 11引入了显式的类型转换运算符。
class SmallInt{
public:
	//编译器不会自动指向这一类型转换
	explicit operator int() const { return val; }
	//其他成员与之前的版本一致
};

SmallInt si = 3;//正确:SmallInt的构造函数不是显式的
si + 3;//错误:此处需要隐式的类型转换,但类的运算符是显式的
static_cast<int>(si) + 3; //正确:显式地请求类型转换
  1. 显式的类型转换运算符,当表达式出现在下列位置时,显式的类型转换将被隐式地执行:
    if、while及do语句的条件部分。
    for语句头的条件表达式。
    逻辑非、逻辑或、逻辑与运算符的运算对象。
    条件运算符的条件表达式。
  2. 向bool的类型转换通常用在条件部分,因此operator bool 一般定义成explicit的。

14.9.2 避免有二义性的类型转换

  1. 通常情况下,不要为类定义相同的类型转换,也不要在类中定义两个及两个以上转换源或转换目标是算术类型的转换。
//最好不要在两个类之间构建相同的类型转换
struct B;
struct A {
	A() = default;
	A(const B&); //把一个B转换成A
	//其他数据成员
};
struct B{
	operator A() const; //也是把一个B转换成A
	//其他数据成员
};
A f(const A&);
B b;
A a = f(b); //二义性错误:含义是f(B::operator A()),还是f(A::A(const B&))?
A a1 = f(b.operator A()); 	//正确
A a2 = f(A(b)); 		//正确
struct A {
	A(int = 0); //最好不要创建两个转换源都是算术类型的类型转换
	A(double);
	operator int() const; //最好不要创建两个转换对象都是算术类型的类型转换
	operator double() const; 
	//其他成员
};
void f2(long double);
A a;
f2(a); //二义性错误:含义是f(A::operator int())还是f(A::operator double())?
long lg;
A a2(lg); //二义性错误:含义是A::A(int)还是A::A(double)?

short s = 42;
//把short提升成int优于把short转换成double
A a3(s); //使用A::A(int)
  1. 无法使用强制类型转换来解决二义性问题,因为强制类型转换本身也面临二义性。
  2. 当使用两个用户定义的类型转换时,如果转换函数之前或之后存在标准类型转换,则标准类型转换将决定最佳匹配到底是哪个。
  3. 除了显式地向bool类型的转换之外,应该尽量避免定义类型转换函数并尽可能地限制那些“显然正确”的非显式构造函数。
  4. 如果在调用重载函数时需要使用构造函数或者强制类型转换来改变实参的类型,则通常意味着程序的设计存在不足。
struct C{
	C(int);
	//其他成员
};
struct D{
	D(int);
	//其他成员
};
void manip(const C&);
void manip(const D&);
manip(10); //二义性错误
manip(C(10)); //正确:调用manip(const C&)
  1. 在调用重载函数时,如果需要额外的标准类型转换,则该转换的级别只有当所有可行函数都请求同一个用户定义的类型转换时才有用。如果所需的用户定义的类型转换不止一个,则该调用具有二义性。
struct E{
	E(double);
	//其他成员
};
void manip2(const C&);
void manip2(const E&);
//二义性错误:两个不同的用户定义的类型转换都能用于此处
manip2(10); //C(10)还是E(double(10))

14.9.3 函数匹配与重载运算符

  1. 表达式中运算符的候选函数集既应该包括成员函数,也应该包括非成员函数。
  2. 如果对同一个类既提供了转换目标是算术类型的类型转换,也提供了重载的运算符,则将会遇到重载运算符与内置运算符的二义性问题。
class SmallInt {
	friend SmallInt operator+(const SmallInt&,const SmallInt&); //非成员函数
public:
	SmallInt(int = 0); 			//转换源为int的类型转换
	operator int() const { return val; } 	//转换目标为int的类型转换
private:
	std::size_t val;
};

SmallInt s1, s2;
SmallInt s3 = s1 + s2; 	//使用重载的operator+
int i = s3 + 0; 		//二义性错误
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值