14.1基本概念
二元运算符:左侧对象为第一个参数,右侧对象为第二参数
除了重载的函数调用运算符()外,其他重载运算符都不能含有默认实参。
如果一个运算符函数是成员函数,则它的第一个左侧运算符对象隐式绑定到this指针上,因此成员运算符函数的参数数量比运算符的运算对象总数少一个。
重载的运算符函数,或者是类成员,或者至少含有一个类类型参数(不能都是内置类型,因为不能改变内置类型运算符的含义)
//非成员函数运算符的等价调用
data1+data2;
operator+(data1,data2);
//成员函数运算符的等价调用
data1+= data2;
data1.operator+=(data2);
通常情况下,不应该重载逗号,取地址,逻辑与,逻辑或运算符。
赋值和复合赋值运算符:赋值之后,左右两侧数据相等,并且运算符返回它左侧运算对象的一个引用
当把运算符定义成成员函数时,它的左侧运算对象必须是运算符所属类的一个对象,例如:
string s= "World";
string t = s + "!"; //正确 =右侧相当于s.operator+("!") s+const char *
string u = "hi" + s; //错误 =右侧相当于 "hi".operator+(s) const char * +s
// 上为内置类型 错误
如果上述 string 将+定义成了普通的非成员函数,则 operator+("hi",s) 和其他函数调用一样,每个实参都能被转换成形参类型。唯一要求是 至少有一个运算符对象是类类型,并且两个都可以被转换为类类型。
14.2输入和输出运算符
14.2.1重载输出运算符<<
左侧:非常量ostream引用 << 右侧:常量的引用
左侧非常量是因为:向输入流写入内容会改变其状态,引用是因为:无法拷贝一个ostream对象
右侧引用:希望避免复制。
ostream & operator<<(ostream &os,const Sales_data &item) //os为非常量引用
{
os << item.isbn()<< " " << item.units_sold << " "<<item.revenue;
return os;
}
通常,输出运算符应该主要负责打印对象的内容而非控制格式,输出运算符不应该打印换行符。
14.2.2重载输入运算符
//Sales_data的输入运算符
istream &operator >> (istream &is ,Sales_data &item)
{
double prices;
is >> item.bookNo >> item.units_sold >> price;
if(is) //检查输入是否成功
item.revenue = item.unite_sold *price;
else
item =Sales_data(); //输入失败,对象还原为默认状态。这一步很关键
return is;
}
14.3算术和关系运算符
通常我们把算术和关系运算符定义为非成员函数,以允许对左侧或者右侧的运算对象进行转换。一般不需要改变原对象,所以用常量的引用。
Sales_data operator +(const Sales_data &lhs,const Sales_data &rhs)
{
Sales_data sum = lhs;
sum+=rhs;
return sum;
}
如果类同时定义了算术运算符和相关的复合赋值运算符,则通常情况下应该使用复合赋值来实现算术运算符。
14.3.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);
}
如果类定义了 operator==,则该运算符应该能判断一组给定的对象中是否含有重复数据
应该具有传递性
实现一个,把另一个委托给这一个。
14.4赋值运算符
除了拷贝赋值和移动赋值运算符外,标准库vector还定义了第三种赋值运算符, 该运算符接受花括号内的元素列表作为参数。
vector<string> v;
v = {"a","an","the"};
class StrVec{
public:
StrVec &operator = (initializer_list<string>);
//其他略
}
StrVec &operator = (initializer_list<string> il){
auto data = alloc_n_copy(il.begin(),il.end());
free(); //销毁对象中的元素并释放内存空间
elements = data.first; //更新数据成员使其指向新空间
first_free = cap = data.second;
return *this; //返回左侧对象,与拷贝赋值和移动赋值保持一致。
}
复合赋值运算符
Sales_data & operator +=(const Sales_data &rhs){
unit_sold +=rhs.unit_sold;
revenue +=rhs.revenue;
return *this;
}
赋值运算符必须是成员函数,而复合赋值不必须,但最好是。两者都返回左侧运算对象的引用。
14.5下标运算符
下标运算符通常以访问元素的引用为返回值,因为这样做 下标运算符可以出现在赋值运算符的任意一端。而且需要同时定义好 常量版本和非常量版本。
class StrVec{
public:
string & operator[](size_t n){
return elements[n];
}
const string & operator [](size_t n)const
{
return elements[n];
}
private:
string *elements;
}
const StrVec cvec = svec;
if(svec.size() && svec[0].empty()){
svec[0] = "zero"; //正确,下标运算符返回普通引用
cvec[0] = "zip"; //错误,返回常量引用 不允许修改。
}
14.6 递增和递减运算符
//前置
class StrBlobPtr{
public:
StrBlobPtr & operator ++();
StrBlobPtr & operator --();
};
StrBlobPtr & operator ++(){
//递增递减,都要用check函数检验是否有效。
check(curr,"increment past end of StrBlobPtr");
++curr;
return *this;
}
StrBlobPtr & operator --(){
//递增递减,都要用check函数检验是否有效。
--curr;
check(curr,"increment past end of StrBlobPtr");
return *this;
}
//后置
class StrBlobPtr{
public:
StrBlobPtr & operator ++(int);
StrBlobPtr & operator --(int);
};
StrBlobPtr operator ++(int){
// check(); 后置不需要检查
auto temp = *this;
++*this;
return temp;
}
StrBlobPtr operator ++(int){
// check();
auto temp = *this;
--*this;
return temp;
}
为了与内置版本保持一致,后置运算符应该返回对象的原值,返回的形式是一个值而非引用。
14.7 成员访问运算符
class StrBlobPtr{
public:
string & operator *() const
{
auto p = check(curr,"dereference past end");
return (*p)[curr]; //(*p)所指的vector
}
string * operator ->()const
{ //委托给解引用操作符
return & this->operator*();
}
};
14.8 函数调用运算符
如果类定义了调用运算符,则该类的对象被称为函数对象,因为可以调用这种对象,我们说这些对象的“行为像函数一样”。
class PrintString{
public:
PrintString(ostream &os =cout, char c = ' '):o(os),sep(c) {}
//在这里是void
void operator()(const string &s) const { os<<s<<sep;}
private:
ostream &o;
char sep;
}
PrintString printer;
printer(s);
PrintString errors(cerr,'\n');
errors(s);
14.8.1 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 &a ,const string &b)
{return a.size()<b.size();}
}
stable_sort(words.begin(),words.end(),ShorterString());
14.8.2 标准库定义的函数对象
表示运算符的函数对象类常用来替换算法中的默认运算符。
sort(svec.begin().svec.end(),greater<string>());
第三个greater<string>是一个未命名的对象 ,调用greater的函数对象进行比较。
特别注意:标准库规定其函数对象对于指针同样适用,
vector<string *> nameTable;
//错误,nameTable中的指针彼此没有关系,所以<将产生未定义。
sort(nameTable.begin(),nameTable.end(),[](string *a, string *b) {return a<b;})
//正确
sort(nameTable.begin(),nameTable.end(),less<string *>);
14.8.3 可调用对象与function
C++中有几种可调用的对象:函数,函数指针,lambda表达式,bind创建的对象以及重载了函数调用运算符的类。这些可调用的对象也有类型。
然而两种不同类型的可调用对象却可能共享同一种调用形式。调用形式指明了调用的返回类型以及传递给调用的实参类型。一种调用形式对应一个函数类型。
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)
如果希望使用可调用对象构建一个简单的桌面计算器。为了这一目的,需要定义一个函数表,表中存放不同可调用对象的指针。使用map实现。
//构建从运算符到函数指针的映射关系,其中函数接受两个int,返回一个int
map<string,int(*)(int,int)> binops;
binops.insert({"+",add}); //{"+",add}是一个pair。
但我们不能将mod或者divide存入binops
binops.insert({"%",mod}); //错误:mod不是一个函数指针
//mod是个lambda表达式,而每个lambda有它自己的类类型,该类型与存储在binops中的值的类型不匹配
标准库function类型
function<int(int,int)> //function是一个模板,所谓额外的信息是指该func类型能够表示的对象的调用形式
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;
}
f1(4,2); //调用
使用function重新定义map:
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);
重载的函数与function
int add(int i,int j){}
Sales_data add(const Sales_data &,const Sales_data &){}
binops.insert({"+",add}); //错误,会产生二义性。
//解决二义性:通过存储函数指针而非 函数名字
int (*fp)(int,int) = add;
binops.insert({"+",fp});
//或者使用lambda表达式
14.9 重载、类型转换与运算符
14.9.1 类型转换运算符
operator type() const; // 将类类型的值转换为其他类型。
类型转换可以面向任意类型进行定义,只要该类型能作为函数的返回类型,因此,我们不允许转换成数组或者函数类型,但允许转换成指针或者引用类型。
类型转换运算符没有显示地返回类型,也没有形参,而且必须定义成类的成员函数,不应该改变待转换对象内容,所以应该定义成const。
定义含有类型转换符的类
class SmallInt{
public:
SmallInt(int i = 0):val(i) //int转化为类类型
{
if(i<0||i>255)
throw out_of_range("Bad input");
}
//类型转换符
operator int() const {return val;} //SmallInt对象 转换为 int
private:
size_t val;
}
SmallInt si;
si = 4; //首先将4隐式地转换为SmallInt,然后调用SmallInt::operator=
si +3; //首先将si隐式地转换为int,再加3
显式的类型转换运算符
class SmallInt{
public:
//编译器不会自动执行这一类转换
explicit operator int() const{return val;}
private:
size_t val;
};
//此时
SmallInt si =3; //正确,SmallInt的构造函数不是显式的
si+3; //错误,此时需要隐式转换,但类类型转换是显示地
static_cast<int>(si)+3; //正确,显示转换
但如果表达式被用作条件,则编译器会将显式的类型转换隐式的执行。
if,whiledo等条件语句 for,! && || 等
后续内容不理解,用到回来补充