文章目录
- 基本概念
- 调用
- 选择作为成员还是非成员
- 输入和输出运算符
- 算术和关系运算符
- 相等和不等运算符
- 赋值运算符
- 下标运算符
- 递增和递减运算符
- 成员访问运算符
- 函数调用运算符
- lambda是函数对象
- 标准库定义的函数对象
- 可调用对象与function
- 重载、类型转换与运算符
- 类型转换运算符
- 避免有二义性的类型转换
基本概念
重载的运算符是具有特殊名字的函数:它们的名字由关键字operator
和其后要定义的运算符号共同组成。其参数数量和该运算符的运算数量一样多。除了重载的函数调用运算符operator()
之外,其他重载运算符不能含有默认参数。
如果一个运算符函数是成员函数,则它的第一个运算对象绑定到this指针。对于一个运算符函数来说,它或者是类的成员,或者至少含有一个类类型的参数:
int operator+(int, int); //err
我们可以重载大多数已有的运算符,而无权发明新的运算符号。重载后运算符和内置运算符的优先级和结合律保持一致。
一般不建议重载逗号、取地址、逻辑与和逻辑或运算符。
if (a && b) { ... }
如果 a 为 false,则 b 根本不会计算。
但一旦重载了 operator&&, operator||
,这个短路规则 完全丢失,编译器会强制先算两个操作数,再传给重载函数。
int x = (f1(), f2()); // 保证先执行 f1,再执行 f2
如果重载了 operator,,它就变成了你自定义的逻辑,而失去了“顺序保证”这个语言特性。
取地址则是对于类类型已经有了内置的含义。
调用
//直接调用
data1 + data2;//间接调用
operator+(data1, data2); //普通运算符
data1.operator+(data2); //成员运算符
选择作为成员还是非成员
- 赋值(=),下表([]),调用(()),成员访问符(->)等必须是成员;
- 复合运算符一般是成员;
- 改变对象状态的运算符或者给定类型密切相关的运算符,如递增、递减和解引用运算符,通常是成员;
- 具有对称性的运算符,例如算数、相等性、关系和位运算符等,通常是非成员;
- 输入输出运算符必须是非成员(如果是成员的话,不符合第一个操作对象是输入输出)。
输入和输出运算符
//输出
ostream &operator<<(ostream &os, const Sales_data &item);
Sales_data data;
cout << data;//输入
istream &operator>>(istream &is, Sales_data &item)
{double price;is >> item.bookNo >> item.units_sold >> price;if (is)item.revenue = item.units_sold * price;elseitem = Sales_data();return is;
}
cin >> data;
输入运算符通常会在还会进行错误处理。
算术和关系运算符
如果定义了算术运算符,一般也会定义一个对应的复合运算符:
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() && lhs.units_sold == rhs.units_sold &&lhs.revenue == rhs.revenue;
}bool operator!=(const Sales_data &lhs, const Sales_data &rhs)
{return !(lhs == rhs);
}
赋值运算符
拷贝赋值和移动赋值可以把类的一个对象赋值给该类的另外一个对象。此外,类还可以定义其他赋值运算符以使得别的类型作为右侧运算对象:
//标准库vector类还定义了第三种赋值运算符,该运算符接受花括号元素列表作为参数
vector<string> v;
v = {"a", "an", "the"};
下标运算符
如果一个类包含下标运算符,则通常会定义两个版本:一个返回普通的引用,另一个则是类的常量成员返回常量引用:
class StrVec
{
public:std::string& operator[](std::size_t n){return element[n];}const std::string& operator[](std::size_t n) const{ return element[n];}
private:std::string *element;
}
递增和递减运算符
定义递增和递减运算符的类应该同时定义前置和后置版本。这些运算符通常被定义为成员。
后置版本接受一个额外的int形参,该形参一般不被使用:
class StrBlobPtr
{
public://前置StrBlobPtr& operator++();StrBlobPtr& operator--();//后置StrBlobPtr operator++(int);StrBlobPtr operator--(int);
}//显示调用
StrBlobPtr p(a1);
p.operator++(0); //后置
p.operator++(); //前置
成员访问运算符
解引用运算符首先检查curr是否在合理范围内,如果是,则返回curr所指对象的一个引用;箭头运算符调用解引用并返回其结果地址:
class StrBlobPtr
{std::string& operator*() const{auto p = check(curr, "dereference past end");return (*p)[curr];}std::string* operator->() const{return & this->operator*();}
}
值得注意的是,这两个运算符都定义成了const成员,这是因为获取一个元素并不会改变该对象的状态。
StrBlob a1 = {"a", "an", "the"};
StrBlobPtr p(a1);
*p = "okay"; //给a1的首元素赋值
cout << p->size() << endl; //打印4,okay的大小
cout << (*p).size() << endl; //等价
和大多数其他运算符一样,我们能令operator*完成任何指定的操作。箭头运算符则不是这样,它只能拥有成员访问这个最基本的含义。形如point->mem的表达式来说,point必须是指向对象的指针或者是一个重载了operator->的类的对象:
(*point).mem; //point为指针
(point.operator->())->mem; //point为重载了operator->的对象
函数调用运算符
如果类重载了函数调用运算符,则我们可以像使用函数一样使用该类的对象。一个类可以定义多个不同版本的调用运算符,相互之间在参数数量或类型上有所区别。
如果定义了函数调用运算符,则该类的对象称作函数对象。
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;
}
函数对象常常作为泛型算法的实参:
for_each(vs.begin(), vs.end(), PrintString(cerr, '\n'));
vs中的对象打印到cerr中,并换行分割。
lambda是函数对象
当我们编写一个lambda后,编译器会将该表达式翻译成一个未命名类的未命名对象。该类中含有一个重载函数调用运算符:
stable_sort(words.begin(), words.end(), [](const string &a, const &b){return a.size() < b.size();});//该lambda等价于该类的一个未命名对象
class ShorterString
{
public://lambda默认值捕获,因此生成的唯一成员函数是const//如果是引用捕获,则不是constbool operator()(const string &a, const &b) const{return a.size() < b.size();}
};//等价lambda表达式写法
stable_sort(words.begin(), words.end(), ShorterString());
- 值捕获:产生的类必须为每个值捕获的变量建立对应的数据成员,同时构建构造函数:
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(), words.end(), SizeComp(sz));
- 引用捕获:不用生成成员变量和构造函数。
标准库定义的函数对象
标准库定义了一组表示算术运算符、关系运算符和逻辑运算符的类,每个类分别定义了一个执行命名操作的调用运算符。
sort(vec.begin(), vec.end(), greater<string>());
可调用对象与function
C++中有几种可调用的对象:函数、函数指针、lambda表示式、bind创建的对象以及重载了函数调用运算符的类。两种不同类型的可调用对象可以同享一种调用形式,例如:int(int, int)
。
可以通过function表示不同类型的可调用对象:
int add(int i, int j) {return i +j;}
auto mod = [](int i ,int j) {return i % j;};
struct divide
{int operater()(int i, int j) {return i / j;}
};map<string, function<int(int, int)>> binops =
{{"+", add},{"-", std::minus<int>()},{"/", divide()},{"*", [](int i, int j){return i * j;}},{"%", mod}
};
值得注意的是,同名可调用对象有重载时,不能存入function类型的对象中。可通过函数指针或者lambda表示式消除二义性。
重载、类型转换与运算符
转换构造函数和类型转换运算符共同定义了类类型转换。
类型转换运算符
类型转换运算符时类的一种特殊成员函数,它负责将一个类类型的值转换成其它类型:operator type() const;
,满足下列条件
- 一个类型转换函数必须是类的成员函数;
- 不能声明返回类型;
- 形参列表必须为空;
- 函数通常是const的。
class SmallInt
{
public:SmallInt(int i = 0) : val(i) {}//虽然没有指定返回类型,但实际会返回type类型operator int() const {return val;}
private:int val;
}
其中构造函数能够将算术类型转化为SmallInt,而类型转换运算符能够将SmallInt类型转化为算术类型:
SmallInt si;
si = 4; //int->SmallInt
si + 3; //SmallInt->int
有时隐式的类型转换看上去会觉得困扰,例如,当istream含有bool的类型转换时,下面的代码可以正常编译:
int i = 42;
cin << i; //cin -> bool -> int
为了防止这样的情况,可以定义显示的类型转换运算符:
class SmallInt
{
public:explicit SmallInt(int i = 0) : val(i) {}explicit operator int() const {return val;}
};SmallInt si = 3;
si +3; //err
static_cast<int>(si) + 3; //ok
值得注意的是,如果表达式被用作条件,则编译器会将显示的类型转换自动应用于它,即显示的类型转换将被隐式的执行:
- if、while、do的条件语句部分;
- for 语句头的条件表达式;
- 逻辑非、或、与的运算对象;
- 条件运算符的条件表达式。
大部分类都定义了向bool转化的显示类型转换运算符,例如IO类型:
while (std::cin >> value)
避免有二义性的类型转换
如果定义了一组类型转换,它们的转换源或者转换目标可以通过其他类型转换联系在一起,则会产生二义性问题:
- 最好只创建一个算术类型的转换
//最好只创建一个算术类型的转换
struct A
{A(int = 0);A(double);operator int() const;operator double() const;
};void f2(long double);
A a;
f2(a); //err,二义性,不知道用哪一个进行类型转换long lg;
A a2(lg); //err,不知道用哪一个构造函数
- 最好不要在两个类之间构建相同的类型转换:
struct B;
struct A
{A() = default;A(const B&);
};struct B
{operator A() const;
};A f(const A&);
B b;
A a = f(b); //err//可以显示指定
A a1 = f(b.operator A());
A a2 = f(A(b));