运算符是具有特殊名字的函数,它们的名字由关键字operator和其后要定义的运算符号共同组成。
如果一个运算符函数是成员函数,则第一个(左侧)运算对象绑定到隐式的this指针上,因此成员运算符函数(显式)的数量少一个。
对一个运算符函数来说,它或者是类的成员或者至少含有一个类类型的参数:int operator+(int,int)就是错误的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K9yMKFk8-1641812567952)(C:\Users\15401\AppData\Roaming\Typora\typora-user-images\image-20210904220717966.png)]
data1+data2和operator+(data1,data2);是等价的
某些运算符不应该被重载
有些运算符例如逻辑或或者逻辑与逗号运算符的运算符求值顺序无法保留下来。逻辑或或者逻辑与的重载版本也不能保留下来短路特性。因此不建议被重载。
如何选择是否作为成员函数
- 赋值=、下标[]、调用()、成员访问箭头必须是成员
- 符合赋值运算符一般来说应该是成员,但并非必须,这一点与赋值运算符略有不同
- 改变对象的运算符或者给定类型密切相关的运算符如递增、递减、解引用通常应该是运算符
- 具有对称性的运算符可能转换为任意一端的运算对象,例如算数,相等性,关系和位运算符,通常应该是普通的非成员函数
输入和输出运算符
如我们所致IO运算符分别使用<<和>>执行输入和输出操作。
重载输出运算符
通常情况下 输出运算符的第一个形参是一个非常量的ostream对象的引用。之所以ostream非常量是因为往里面输入会改变其状态。并且一般来说输出运算符重载返回的仍应该是输出运算符从而可以连续输出。
与iostream标准库兼容的输入输出运算符必须是普通的非成员函数而不能是类的成员函数,否则,它们的左侧运算对象将是我们类的一个对象:
Sales_data data;
data<<cout;
//假设输入输出运算符是某个类的成员,那么它也应该是istreawm或者ostream的成员。然而这两个类属于标准库,并且我们无法给标准库中的类添加任何成员。
重载输入运算符
通常来说,输入运算符的第一个形参是将要读取的流的引用。第二个形参是将要读入到的(非常量的)引用。该运算符会返回某个给定流的引用。
istream & operator>>(istream &is,Sales_dat &item){
double price;
is >>item.bbokNo>>item.units_sold>>price;
if(is)//检查输入是否成功
item.revenue = item.units_sold*price;
else
item = Sales_data();
return is; //返回输入流的引用来支持 cin>>a>>b;
}
除了if语句外,这个定义于之前的read函数完全一样。if语句检查读取操作是否成功,如果发生了IO错误,则运算符将给定的对象重置为空Sales_data,这样可以确保对象处于正确的状态。
输入时的错误
执行输入可能遇到如下列的错误:
- 当流含有错误类型的数据读取可能失败。例如在读完bookNo后,输入运算符假定接下来读入的是两个数字数据,一旦输入的不是数字数据,则读取操作以及后续对流的其他使用都将失败。
- 当读取操作到达文件末尾或者遇到输入流的其他错误时也会失败。
当读取操作发生错误时,输入运算符应该负责从流中恢复。
算术和关系运算符
通常情况下,我们把算术和关系运算符定义为非成员函数从而允许左侧或右侧的运算对象进行转换。因为这些运算对象一般都不改变运算对象的状态所以形参都是常量的引用。
Sales_data operator+(cosnt Sales_data &lhs,const Sales_data_data &rhs)
{
Sales_data sum = lhs;
sum+=rhs;
return sum; //返回对应的副本
}
相等运算符
下面是一些==比较符的设计准则
- 定义了==有利于使用标准库算法以及方便用户操作
- 如果类定义了operator,则该运算符要能判断对象中是否有重复数据
- 通常情况下,相等运算符应该具有传递性,换句话会所ab,ba那么a==c;
- 一般来说定义了==也应该定义!=,两者一般可以把工作委托给另外一个,毕竟是互补的
关系运算符
因为一些关联容器和一些算符要用到<运算符。通常情况下应该
- 定义顺序管理,令其与关联容器中对关键字的要求一致并且。如果类同时包含运算符的话,则定义一种关系运算符令其与保持一致。特别是如果两个对象时!=的那么其中一个应该<另外一个。
赋值运算符
拷贝赋值和移动赋值可以把一个类的对象赋值给另一个对象。此外类还可以定义其他赋值运算符让别的类型作为右侧运算对象。如vector就可以接受花括号赋值:
vector<string> vs; v = {"a","ab","abc"};
同样我们可以把运算符添加到StrVec类中
class StrVec{
public:
StrVec &operator=(std::initializer_list<std::string>);
}
Strvec & Strvec::operator=(initializer_list<string> i1){
auto data = alloc_n_copy(i1.begin(),i1.end());
free(); //赋值运算仍然需要先
elements = data.first;
first_free = cap = data.second;
return *this;
}
复合赋值运算符
复合赋值运算符不非得是类的成员,不过我们还是倾向于把包括符合赋值在内的所有复制运算都定义在类的内部。为了和内置类型保持一致,类中的复合赋值运算符也要返回左侧对象的引用。
Sales_dta & Sales_data::operator+=(cosnt Sales_data &rhs){
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}
下标运算符
下标运算符必须是成员函数。如果一个类定义了下标操作符,那么一般会定义两个版本一个返回普通引用一个返回常量引用。
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对象取下标仍然得到不能修改常量的内容
}
privte:
std::string *elements;
}
递增和递减运算符
迭代器类中通常会实现递增和递减运算符。来让元素前后移动。c++语言不要求递增和递减运算符必须是类的成员,但由于其改变的正好是所操作的对象的状态,因此建议定义为成员函数。
和内置类型一样我们也可以定义前置版本和后置版本。
定义前置递增递减运算符
class StrBlobPtr{
public:
StrBlobPtr & operator++(); //前置++返回的一般是要给引用。
StrBlobPtr & operator--();
};
StrBolobPtr & StrBolPtr::operator++(){
check(curr,"increment past end of StrBoloPtr"); //如果已经指向了容器的尾后位置就无法递增它
++curr; //将curr在当前的状态下向前移动一个元素
return *this;
}
定义后置递增递减运算符 通过一个额外的int参数进行区分(但是一般我们不使用这种)
class StrBlobPtr{
public:
StrBlobPtr operator++(int); //后置运算 ,返回的是一个值。返回值后顺便会把原来的进行递增
StrBlobPtr operator--(int);
}
StrBolbPtr & StrBolbPtr::operator++(int){
StrBolbPtr temp = *this;
++*this; //这里调用前置递增符
reutn temp; //返回一个临时变量
}
显式调用
可以通过是否传入int值来显式的调用前置和后置版本
StrBlobPtr p(a1);
p.operator++(0);
p.operator++();
成员访问运算符
在迭代器和智能指针类中常用到解引用运算符(*)。和箭头运算符(->)。我们以如下形式向StrBlobPtr添加如下两种运算符。
class StrBlobPtr{
public:
std::string &operator*() const{ //const表示在执行中不改变这个类。通过解引用来得到某个字符的引用
auto p = check(curr,"derefreence past end");
return (*p)[curr]; //*p是对象所指的vector
}
std::string *operator->()const{
return & this->operator*(); // 调用了解引用运算符然后对其取地址。注意取引用的地址就是取原变量的地址。
}
}
strBlob a1 = {"hi","bye","now"};
StrBlobPtr p(a1); //p指向a1中的vector
*p = "okay"; //给a1的首元素赋值
cout<<(*p).size()<<endl;//打印4,这是a1首元素的大小
cout<<p->size()<<endl;
箭头运算符必须是类的成员。解引用元素符一般也是类的成员(尽管不是必须的)。
对箭头运算符返回值的限定
和大多数运算符一样可以令operator*返回任何我们想指定的操作。换句话会所,我们可以让operator*返回一个固定值都可以。
箭头运算符不同,它永远不能丢点成员访问这个最基本的含义。当我们重载箭头时,可以改变的是箭头从哪个对象中获取成员,而成员获取这个事实不会改变。
对于形如point->mem的表达式来说,point必须指向类对象的指针或者是一个重载了operator->的类的对象。根据point的类型不同,point->mem分别等价于:
(*point).mem; //point是一个内置的指针类型(即某个对象的地址)
point.operator()->mem; //point是类的一个对象
除此之外代码都将发生错误! point->mem
的执行过程如下:
- 如果point是指针,则我们应用内置的箭头运算符。表达式等价于(*point).mem。首先解引用该指针然后从所得的对象中获取指定的成员。如果point所指的类型没有名为mem的成员,程序将发生错误。
- 如果point是定一个operator->的一个对象则我们使用point.operator->()的结果来获取mem。其中如果该结果是一个指针则执行第一步。如果该结果本身含有重载的operator->,则重复调用当前步骤。最终当这一结果结束时 程序会返回所需的内容或者返回一些表示程序错误的信息。
函数调用运算符
如果类重载了函数调用运算符。我们可以像使用函数一样使用该类的对象因为这样的类同时也能存储状态。所以和普通函数相比更加的灵活。
//一个简单的例子
struct absInt{
int operator()(int val)const{
return val<0?-val:val;
}
};
函数调用运算符必须是成员函数,一个类可以定义多个不同版本的调用运算符,相互1
之间应该在参数和类型上有些区别(像重载函数一样)。
如果一个类定义了调用运算符,则该类对象称为函数对象。因为可以调用这种对象,所以我们说这些对象的"行为像函数一样"。
含有状态的函数对象类
和其他类一样,函数对象除了有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;
};
//对象有了调用运算符后就可以比一般的函数多一些状态等。如这里保存了分隔符和输出流,因此就简化了操作如下:
PrintString printer;
printer(s);
PrintString errors(cerr,'\n');
errors(s);
lambda是函数对象
在lambda表达式变成一个未命名类的未命名对象。在lambda表达式产生的类中含有一个重载的函数调用运算符,例如,对于我们传递给stable_sort作为其最后一个实参的lambda表达式来说:
stable_sort(word.begin(),words.end(),[](const string &a,const string &b){return a.size()>b.size()});
//其行为类似于下面的一个类
class ShorterString{
public:
bool operator()(cosnt string &s1,cosnt string &s2)const{ //这里的const表示函数是静态的,如果将lambda声明为可变的就不是静态的了
return s1.size()>s2.size();
}
}
表示lambda以及捕获行为的类
使用引用捕获变量时,程序保证 执行时引用确实存在因此编译器可以直接使用该引用而无需再lambda产生的类中将其存储为数据成员。
通过值捕获的变量拷贝到lambda中,因此这种lambda产生的类必须为每个值捕获的变量建立对应的数据成员,同时创建构造函数,令其使用捕获的变量的值来初始化数据成员。
lambda表达式产生的类不含默认参数、赋值运算符以及默认析构函数;他是否有默认的拷贝/移动构造函数通常要视捕获的数据成员类型而定。
标准库定义的函数对象
标准库定义成为了一组表示算数运算符、关系运算符和逻辑运算符的类,每个类分别定义了一个执行命名操作的调用运算符。例如plus类定义了一个函数调用运算符用于对一对运算对象执行+的操作;modulus类定义了一个调用运算符执行二元的%操作;equal_to类执行==等。
这些类都被定义成为模板的形式:
pulus<int> intAdd; //可执行int加法的函数对
negate<int> intNegate; //取负
int sum = intAdd(10,20);
sum = intNegate(intAdd(10,20));
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dSpCbLwA-1641812567953)(C:\Users\15401\AppData\Roaming\Typora\typora-user-images\image-20210906100457919.png)]
在算法中使用标准库函数对象
表示运算符的函数类常用来替换算法中的默认运算符。如我们所知,在默认情况下排序算法使用operato<将序列按照升序排列。如果要执行降序排列的话我们可以传入一个greater类型的对象。该类将产生一个调用运算符并负责执行待排序类型的>运算。
例如,svec是一个vector<string>,
sort(svec.begin(),svec.end(),greater<string>()); //其中第三个实参是未命名的一个greater<int>对象
标准库规定其函数对象对指针仍然适用。我们可能希望比较指针的内存地址来排序指针的vector。直接这么做会产生未定义的行为,就可以用标准库函数对象实现目的:
vector<string *> nameTable;
sort(nameTable.begin(),nameTable.end(),[](string *a,string *b){return a<b;}); //直接比较指针会产生未定义的行为
sort(nameTable.begin(),nameTable.end(),less<string *>()); //正确,适用标准库函数less函数对象
可调用对象与function
c++中有几种可调用的对象:函数、函数指针、lambda表达式、bind创建的对象以及重载了函数调用符的对象。
和其他对象一样,可调用的对象也有类型,例如每个lambda对象有它唯一的(未命名的)类类型;函数以及函数指针的类型由参数和返回值决定,等等。
然而两种不同类型的可调用对象却可能共享一种调用形式(call signature)。调用形式指明了调用返回的类型以及传递给调用的实参类型。一种调用形式对应一个函数类型,例如:int(int,int)
是一个函数类型,其接受两个int,返回一个int。
不同类型可能会有相同的调用形式
对于几个可调用对象具有同一种调用形式的情况,我们有时希望把他们看成具有相同的类型。例如考虑下列不同类型的对象:
//普通函数
int add(int i, int i){return i+j;}
//lambda对象类
auto mod [](int i, int j){return i%j;}
//函数对象类
struct divide{
int operator()(int denominator,int divisor) {reuturn denominotor/divisor;}
};
上面的几种虽然类型各不相同,但是共享同一种调用形式 int(int,int)
使用这些可调用对象构建一个简单的计算器,需要定一个函数表用来存储指向这些可调用对象的指针。当程序需要执行某个特定操作时,从表中查找该调用的函数。
这里使用map作为函数表,如果只处理二元int的运算可以做如下处理:
map<string ,int(*)(int,int)> binops;
binops..insert({"+",add}); //{"+",add}是一个pair
但是不能将mod插入,因为mod是一个lambda表达式,有自己的类类型。其与我们定义的binops不匹配。
标准库function类型
funciton类型定义在functional头文件中。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-weI7wPM5-1641812567953)(C:\Users\15401\AppData\Roaming\Typora\typora-user-images\image-20210906102721743.png)]
function是一个模板其中传入的额外信息是调用形式。如function<int (int,int)>
因此我们可以继续上面的计算器
functon<int(int,int)>f1 = add;
functon<int(int,int)>f2 = divide();
functon<int(int,int)>f3 = [](int i ,int i){return i*j;};
//重新定义计算器的map
map<string,function<int(int,int)>> binops = {{"+",add},
{"-",std::minus<int>()},
{"/",divide()},
{"*",[](int i,int j){return i*j;}},
{"%",mod}};
重载函数与function
我们不能直接将重载函数的名字存入function类型的对象中:
int add(int i,int j){reutnr i+j;}
Sales_data add(const Sales_data &,const Sales_data&);
map<string ,funciton<int(int,int)>> binops;
binops.insert({"+",add}); //错误哪一个add,这里比较笨不会自己推断是哪一个。
//解决上面对 二义性的问题可以通过存储指针而非函数的名字
int (*fp)(int,int) = add; //指针可以匹配是第一个add版本
binops.insert({"+",fp}); //正确
//同样可以使用lambda来消除二义性
binops.insert({"+",[](int i,int j){return add(a,b);}});
标准库中的fucniton类和旧版本中的unary_function和binary_function没有关联,后两个已经被更通用的bind函数替代了。
重载,类型转换和运算符
之前我们看到了一个实参调用的非显式构造函数定义了一种隐式的类型转换,这种构造函数能将实参类型的对象转换为类类型。我们同样可以定义类类型的类型转换,通过定义类型转换运算符可以做到这一点。转换构造函数和类型转换运算符共同定义了类类型转换,这样的转换有时也称作用户定义的类型转换。
类型转换运算符
类型转换运算符是类的一种特殊成员函数。负责将一个类转换为其他的类型。其格式如下:
operator type()const;
其中type表示某种类型,类型转换运算符可以面向任何类型(除了void)进行定义,只要该类型能作为函数的返回类型。因此我们不允许转换成数组类型或者函数类型,但允许转换为指针(包括数组指针和函数指针)或者引用类型。
一个类型转换函数必须是类的成员函数;它不能声明返回类型,形参列表页必须 为空。类型转换函数通常应该是const。
定义含有类型转换运算的类
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,然后调用operator=
si +3; //首先将si隐式的转换成int,然后执行整数的加法
尽管编译器一次只能执行一个用户定义的类型转换,但是隐式的用户定义类型转换可以置于一个标准(内置)类型转换之前或之后并与其一起使用。
SmallInt si = 3.14; //调用SmallInt(int)构造函数
si+3.14 //内置类型转换将所得的int继续转换成double 隐式的用户转换可以衔接在内置类型转化之前或者之后
类型转换运算符是隐式执行的因此无法给这些函数传参。尽管类型转换函数不负责指定返回类型,但实际上每个类型转换函数都会返回一个对应类型的值。
class SmallInt;
operator Int(SmallInt&); //错误不是成员函数
class SmallInt{
public:
int operator int() const; //错误 不用指定返回类型
operator int(int =0)const; //错误,参数列表不为空
operator int*()const {return 42;};//错误,返回类型要和opeator后面的匹配。应该返回int*类型。
}
类型转换运算符可能产生意外结果
实践中类型转换运算符很少使用。毕竟是隐式转换。 下面的代码有一些问题:
int i = 42;
cin<<i; //使用了输出运算符,但是由于cin定义了bool运算符,因此cin会被转换为true紧接着内置类型转换为1,然后就是1<<42,这明确就离谱
可以使用显式类型转换运算符,来禁止隐式的转化成这个类型。不过也有一个例外:如果表达式被作用于条件语句,那么即使定义的是显式转换也会自动进行转换。
class SmallInt{
public:
explicit operator int()const {return val;}
};
Small si =3;
si +3; //错误,此处需要隐式的类型转换,但类的运算符是显式的
static_cast<int>(si)+3; //正确,显式的请求类型转换
避免有二义性的类型转换
如果一个类型包含一个或者多个转换,则必须保证类类型和目标类型之间只存在唯一的一种转换方式。否则就会有二义性。