C++Primer 第十四章重载运算与类型转换
第十四章 重载运算与类型转换
14.1 基本概念
重载运算符函数的参数数量与运算符作用的运算对象一样多。一元运算符有一个参数,二元运算符有两个参数。对于二元运算符,左侧运算对象传递给第一个参数,右侧运算对象传递给第二个参数。如果运算符函数是成员函数,它的第一个左侧运算对象绑定到隐式的this上。
直接调用一个重载的运算符函数
//一个非成员运算符函数的等价调用
data1 + data2; //普通的表达式
operator+(data1, data2); //等价的函数调用
以上两种的调用是等价的。
//成员运算符函数的等价调用
data1 += data2;
data1.operator += (data2); //成员运算符函数的等价调用
使用与内置类型一致的含义
逻辑和关系运算符返回bool, 算术运算符返回类类型的值,赋值运算符和复合赋值运算符返回左侧运算对象的引用。
14.2 输入和输出运算符
14.2.1 重载输出运算符<<
输出运算符的第一个形参是一个非常量ostream
的引用,第二形参是一个常量对象的引用,该常量是想要打印的类型。
输入输出运算符必须是非成员函数
Sales_data data;
data << out; //如果<<是sales_data的成员
如果是成员函数,左侧运算对象是一个类的对象,但是istream
和ostream
是标准库中的对象,无法向标准库中添加对象。
14.2.2 重载输入运算符>>
输入运算符的第一个形参是一个非常量istream
的引用,第二形参是一个常量对象的引用,该常量是想要将要读入的对象的引用,并且该运算符会返回某个给定流的引用。
输入运算符必须处理输入可能失败的情况,而输出运算符不需要,当读取操作发生错误时,输入运算符应该负责从错误中恢复
14.3 算术和关系运算符
14.3.1 相等运算符
定义了相等运算符,一定要定义不等运算符,相等运算符一定要所有成员都相等才能成立。
14.4 赋值运算符
除了拷贝赋值和移动赋值运算符,还可以定义赋值运算符以使用别的类型作为右侧运算对象。
vector v;
v = {"a", "b", "the"};
接受话括号内的元素列表作为参数。
class StrVec {
public:
StrVec &operator=(std::initializer_list);
};
StrVec &StrVec::operator=(std::initializer_list il) {
auto data = alloc_n_copy(il.begin(), il.end());
free();
elements = data.first;
first_free = cap = data.second;
return *this;
}
和拷贝赋值及移动赋值运算符一样,其他重载的赋值运算符也必须先释放当前的内存空间,在创造新空间。
无论形参是什么,赋值运算符都必须定义为成员函数
复合赋值运算符
复合赋值运算符也要返回左侧对象的引用。
Sales_data& Sales_data::operator+=(const Sales_data &rhs) {
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}
14.5 下标运算符
表示容器的类通常可以通过元素在容器中的位置访问元素,下标运算符必须是成员函数。
下标运算符通常以访问元素的引用作为返回值,这样可以保证可以出现在赋值运算符的任意一端。
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];
}
};
const StrVec cvec = svec;
if (svec.size() && svec[0].empty()) {
svec[0] = "zero";
cvec[0] = "Zip"; //error,cvec是一个常量对象,无法对常量复制
}
14.6 递增和递减运算符
定义递增运算符和递增运算符要有前置版本和后置版本,最好定义为成员函数。
class StrBlobPtr {
public:
StrBlobStr& operator++();
StrBlobStr& operaotr--();
};
前置运算符应该返回递增或递减后对象的引用。
StrBlobPtr& operator++() {
check(curr, "increment past end of StrBlobPtr");
++curr;
return *this;
}
StrBlobPtr& operaotr--() {
--curr;
check(curr, "decrement past begin of StrBlobPtr");
return *this;
}
使用的时候先检查下表是否越界,然后再进行递增或递减
区分前置和后置运算符
为了区分前置和后置运算符,后置版本接受一个额外的int类型的形参,这个int形参不参与运算,只用来区分前置和后置运算符。
class StrBlobPtr {
public:
StrBlobPtr operator++(int);
StrBlobPtr operator--(int);
};
后置运算符应该返回对象的原值。
后置版本调用前需要记录对象的状态。
StrBlobPtr StrBlobPtr::operator++(int) {
StrBlobPtr ret = *this; //记录当前的值
++*this; //向前移动一个元素,此处调用前置递增运算符,需要检查递增的有效性
return ret;
}
StrBlobPtr StrBlobPtr::operaotr--(int) {
StrBlobPtr ret = *this;
--*this; //向后移动一个元素,检查递减的有效性
return ret;
}
显式地调用后置递增运算符
StrBlobPtr p(a1);
p.operator++(0); //调用后置版本的++,编译器通过传入的值来区分前置和后置
p.operator++(); //调用前置版本的++
14.7 成员访问运算符
class StrBlobPtr {
public:
std::string& operator*() const {
auto p = check(curr, "deference past end");
return (*p)[curr];
}
std::string* operator->() const {
return & this->operator*();
}
}
解引用运算符返回指向元素的引用,箭头运算符调用解引用运算符返回解引用结果元素的地址。
箭头运算符必须是类的成员,解引用运算符通常是类的成员
重载箭头运算符时,可以改变箭头从哪个对象当中获取成员,而箭头获取成员这一事实无法改变。重载的箭头运算符必须返回类的指针或者自定义箭头运算符的某个类的对象。
14.8 函数调用运算符
类重载了函数调用运算符,可以像使用函数一样使用该类的对象。
struct absInt {
int operator() (int val) const {
return val < 0 ? -val : val;
}
}
int i = -42;
absInt absObj;
int ui = absObj(i); //将i传递给absObj.operator
函数调用运算符必须是成员函数
类定义了调用运算符,该类的对象称作函数对象。
含有状态的函数对象类
class PrintString {
public:
PrintString (std::ostream &o = std::cout, char c = ' ') : os(o), sep(c) {}
void operator() (const std::string &s) const { os << s << sep; }
private:
std::ostream &os;
char sep;
};
std::for_each(vs.begin(), vs.end(), PrintString(std::cerr, '\n'));
函数对象常作为泛型算法的实参。
14.8.1 lambda是函数对象
lambda表达式被编译器翻译为一个未命名类的未命名对象。
auto wc = find_if(words.begin(), words.end(), [sz](const string &a) { return a.size() >= sz; });
//相应的具有捕获行为的类
class SizeComp {
public:
SizeComp(size_t n) : sz(n) {}; //形参对应捕获的变量
bool operator() (const string &s) const {
return s.size() >= sz;
}
private:
size_t sz;
}
auto wc = find_if(words.begin(), word.end(), SizeComp(sz));
14.8.2 标准库定义的函数对象
标准库定义的函数对象定义在functional头文件中
plus intAdd; //可执行int加法的函数对
negate intNegate; //可对int值取反的函数对象
int sum = intAdd(10, 20); //等价于sum = 30;
sum = intNegate(intAdd(10, 20)); //sum = -30;
sort(svec.begin(), svec.end(), greater());
vector tmp;
sort(tmp.begin(), tmp.end(), less());
14.8.3 可调用对象与function
C++中的可调用对象:函数、函数指针、lambda表达式、bind创建的对象以及重载了函数调用运算符的类。不同类型有不同的调用方式。
int add(int i, int j) { return i + j; }
auto mod = [] (int i, int j) { return i % j; }
struct divide {
int operator() (int denominator, int divisor) { return denominator / divisor; }
}
上述三个函数都是int (int, int)
类型的,如果定义map
, binops.insert({"+", add})
只能调用add
无法调用其他两个,因为类型不同,mod是lambda类型,不是函数指针。
使用funtion
解决上述问题。
function
创建一个存储可调用对象的空function,调用类型因该与T相同。
function
显式地创建空function
function
存储可调用对象obj的副本
function f1 = add; //函数指针
function f2 = divide(); //函数对象类的对象
function f3 = [](int i, int j) { return i * j; } //lambda
map> mp; //将所有类型的都包括了
binops["+"](10, 5); //调用add(10, 5);
binops["*"](10, 5); //调用lambda函数对象
重载的函数与function
不能将重载的函数名字存入function中
int add(int i, int j) { return i * j; }
Sales_data add(const Sales_data&, const Sales_data&);
binops.insert({"+", add}); //error,编译器无法区分
可以使用函数指针或者lambda来消除二义性。
int (*fp)(int, int) = add;
binops.insert({"+", fp}); //fp指向正确的add
binops.insert({"+", [](int a, int b) { return add(a, b); }});
14.9 重载、类型转换与运算符
14.9.1 类型转换运算符
类型转换运算符必须定义为成员函数,一般形式为:operator type() const
。
类型转换运算符不能声明返回类型,形参列表必须为空,类型转换函数为const(不能改变转换的内容)
class SmallInt {
public:
SmallInt(int i = 0) : val(i) {}
operator int () const { return val; }
private:
std::size_t val;
};
SmallInt si;
si = 4; //4隐式的转化为smallint, 然后使用operator=
si + 3; //si隐式的转化为int
类型转换运算符可能会出现意外结果
int i = 42;
cin << i;
istream
的bool类型转换运算符将cin转换为bool类型,然后进行移位操作,会出现错误。
显式地类型转换运算符
class SmallInt {
public:
explicit operator int () const { return val;}
};
SmallInt si = 3; //正确,SmallInt的构造函数为显式的
si + 3; //error, 需要隐式的类型转换,但是类的运算符为显式的
static_cast(si) + 3; //right,显式的请求类型转换。
但是如果表达式用于条件,编译器会隐式的执行显式的类型转换。
14.9.2 避免有二义性的类型转换
struct B;
struct A {
A () = default;
A (const B&); //把一个B转换成A
};
struct B {
operator A() const;
};
A f(const A&);
B b;
A a = f(b); //出现二义性,是调用B::operaotrA() 还是调用A::A(const B&)
struct A {
A (int = 0); //最好不要创建两个转换源都是算术类型的类型转换
A (double);
operator int() const;
operator double() const;
};
void f2(long double);
A a;
f2(a); //二义性错误
long lg;
A a2(lg); //二义性错误
上述的两种类型转换都无法精确匹配long double,因此会出现二义性。如果转换过程包含标准类型转换,按照标准类型转换级别进行转换。
short s = 42;
A a3(s); //right,shor有限转换为int
避免二义性的经验规则:
- 不要令两个类执行相同的类型转换:Foo类接受一个Bar类对象的构造函数,不要在Bar类中再定义转换目标是Foo类的类型转换运算符。
- 避免目标是内置算术类型的类型转换。
重载函数与转换构造函数
struct C{
C(int);
};
struct D {
D(int);
};
void manip(const C&);
void manip(const D&);
manip(10); //出现二义性,无法识别是manip(C(10))还是manip(D(10));
14.9.3 函数匹配与重载运算符
class SmallInt {
friend SmallInt operator+(const SmallInt&, const SmallInt &);
public:
SmallInt(int i = 0);
operator int () const { return val; }
private:
size_t val;
}
SmallInt s1, s2;
SmallInt s3 = s1 + s2; //使用重载的operator+
int i = s3 + 0; //出现二义性,可以把0转换成SmallInt,然后使用SmallInt+,也可以把s3转换成int
如果对同一个类既提供了转换目标是算术类型的类型转换,也提供了重载运算符,会遇到重载运算符与内置运算符的二义性问题