重载运算与类型转换
- 当运算符作用于类类型的对象时,可以通过运算符重载重新定义该运算符的含义。
基本概念
输入和输出运算符
-
类需要自定义适合其对象的新版本以支持IO操作。 -
重载输出运算符<<
- 通常情况下,输出运算符的第一形参是ostream的非常量引用,第二个形参一般为一个常量的引用,是我们想要打印的类类型;
- 为了保持一致,operatror<<一般返回它的ostream形参;
- 输出运算符应该主要负责打印对象的内容而非控制格式,输出运算符不应该打印换行符;
- 输入输出运算符必须是非成员函数;
ostream &operator<<(ostream &os, const Sales_data &item)
{
os << item.isbn() << " " << item.units_sold << " "
<< item.revenue << " " << item.avg_price();
return os;
}
-
重载输入运算符>>
- 通常情况下,输入运算符的第一个形参为istream的非常量引用,第二个形参为要读入对象的非常量引用;
- 输入运算符必须处理输入可能失败的情况,而输出运算符则不需要;
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;
}
算术和关系运算符
-
通常把算术运算符和关系运算符定义为非成员函数以允许对左侧或右侧的运算对象进行转换; -
形参都是常量引用; -
通常情况下,类定义了一个算术运算符,也会定义一个相应的复合运算符,然后使用复合运算符来实现对应的算术运算符; Sales_data
operator+(const Sales_data &lhs, const Sales_data &rhs)
{
Sales_data sum = lhs;
sum += rhs;
return sum;
}
-
相等运算符operator==
-
比较对象的每一个数据成员,只有当所有对应的成员都相等时才认为两个对象相等; bool operator==(const A &lhs, const A &rhs)
{
return lhs.unm == rhs.num;
}
bool operator!=(const A &lhs, const A &rhs)
{
return !(lhs==rhs);
}
-
如果类定义了operator==,这个类也应该定义operator!=; -
关系运算符operator</>
- 如果存在唯一一种逻辑可靠的<定义,则应该考虑为这个类定义<运算符。如果类还同时包含!=,则当且仅当<的定义和产生的结果一致时才定义<运算符。
赋值运算符
-
无论形参的类型是什么,赋值运算符都必须定义为成员函数,返回左侧运算符对象的引用; class StrVec{
public:
StrVec &operator=(std::initializer_list<std::string>);
}
StrVec &operator=(std::initializer_list<std::string> 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;
}
下标运算符
-
表示容器的类通常可以通过元素在容器中的位置进行访问,一般会定义下标运算符operator[ ]; -
下标运算符必须定义为成员函数; -
最好同时定义下标运算符的常量版本和非常量版本,当作用于一个常量对象时,下标运算符返回常量引用以确保我们不会为返回的对象赋值; 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];}
private:
std::string *elements;
}
递增和递减运算符
-
递增和递减运算符的类应该同时定义前置和后置两个版本; -
递增和递减运算符应定义为成员函数; -
为了和内置版本保持一致,前置运算符应该返回递增或递减后对象的引用; -
区分前置和后置运算符
- 普通的重载无法区分,前置和后置的区别,规定后置版本接受一个额外的(不被使用)int类型的形参,使用后置运算符时,编译器为这个形参提供一个值为0的实参。
class StrBlobPtr{
public:
StrBlobPtr& operator++();
StrBlobPtr operator++(int);
StrBlobPtr& operator--();
StrBlobPtr operator--(int);
}
StrBlobPtr& StrBlobPtr::operator++()
{
check(curr, "increment past end of StrBlobPtr");
++curr;
return *this;
}
StrBlobPtr StrBlobPtr::operator++(int)
{
StrBlob ret = *this;
++*this;
return ret;
}
StrBlobPtr& StrBlobPtr::operator--()
{
--curr;
check(curr, "decrement past begin of StrBlobPtr");
return *this;
}
StrBlobPtr StrBlobPtr::operator--(int)
{
StrBlob ret = *this;
--*this;
return ret;
}
成员访问运算符
-
箭头运算符必须是类的成员,解引用运算符通常也是类的成员,尽管并非必须如此; -
成员访问运算符一般被定义为const; -
对于形如point->mem的表达式来说,point必须指向类对象的指针或者是一个重载了operator->的类的对象。根据point类型的不同,point->mem 分别等价于: (*point).mem;
point.operator()->mem;
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;
}
}
int i = -42;
absInt absObj;
int ui = absObj(i);
函数调用运算符必须是成员函数; -
一个类可以定义多个不同版本的调用运算符,相互之间应该在参数数量或类型上有所区别; -
lambda是函数对象
-
当我们编写了一个lambda之后,编译器将表达式翻译成一个未命名类的未命名对象,这个未命名的类,就重载了函数调用运算符; for_each(...,...,class(a));
[](const string & a , const string & b){return a.size()<b.size();}
class ShortString{
public:
bool operator()(const string & a , const string & b)
{return a.size()<b.size();}
}
stable_sort(words.begin(),words.end(),ShortString());
-
默认情况下,由lambda产生的类当中的函数调用运算符是一个const成员。 -
标准库定义的函数对象
-
标准库定义了一组表示算术运算符、关系运算符、逻辑运算符的类,每个类分别定义了一个执行命名操作的调用运算符,定义在头文件functional中: [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mzJxwAvb-1653118951997)(img/C++类设计者的工具/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3hpYW93YW5iaWFvMTIz,size_16,color_FFFFFF,t_70-16531165224373.png)] -
表示运算符的函数对象类常用来替换算法中的默认运算符,例如排序算法默认使用operator< 来将序列升序排列,如果要执行降序排列,可以传入一个greater 类型的对象: sort(svec.begin(), svec.end(), greater<string>());
-
标准库规定其函数对象对于指针同样适用: vector<string *> nameTable;
sort(nameTable.begin(), nameTable.end(),
[](string *a, string *b) {return a<b;});
sort(nameTable.begin(), nameTable.end(), less<string *>());
-
可调用对象与function
-
c++中的可调用对象:函数,函数指针,lambda,bind创建的对象,重载了函数运算符的类; -
可调用的对象有类型,例如,每个lambda有唯一的类类型,函数及函数指针的类型则由其返回类型和实参类型决定; -
两个不同类型的可调用对象可能共享同一种调用形式,调用形式指明了调用返回类型以及传递给调用的实参类型,一种调用形式对应一个函数类型,例如: int(int,int)
-
可以定义一个函数表用于存储指向这些可调用对象的“指针”,当程序需要执行某个特定的操作时,从表中查找该调用的函数:
map<string,int(*)(int,int)>binops;
binops.insert({"+",add});
binops.insert({"%",mod});
-
标准库function类型
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VIRu9ucL-1653118951998)(img/C++类设计者的工具/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3hpYW93YW5iaWFvMTIz,size_16,color_FFFFFF,t_70-16531172018357.png)]
-
在这里我们声明了一个function类型,它可以表示接受两个int、返回一个int的可调用对象: function<int(int,int)>f1 = add;
function<int(int,int)>f1 = divide();
function<int(int,int)>f1 = [](int i,int j){return i*j};
-
使用这个function可以重新定义map: map<string,function<int(int,int)>>binops;
map<string,function<int(int,int)>>binops={
{"+",add},
{"-",std::minus<int>()},
{"/",divide()},
{"*",[](int i,int j){return i*j}},
{"%",mod},
}
binops["+"](10,5);
-
重载的函数与function:不能(直接)将重载函数的名字存入function类型的对象中,因为存在二义性。
重载、类型转换与运算符
-
可以定义对于类类型的类型转换,转换构造函数和类型转换运算符共同定义了类类型转换,也被称作用户定义的类型转换。 -
类型转换运算符
-
类型转换运算符是类的一种特殊成员函数,它负责将一个类类型的值转换成其他类型: operator type()const;
- 其中type表示某种类型。类型转换运算符可以面向任意类型(除了void之外)进行定义,只要该类型能作为函数的返回类型。因此不允许转换成数组或者函数类型,但允许转换成指针(包括数组指针及函数指针)或者引用类型。
- 一个类型转换函数必须是类的成员函数;它不能声明返回类型,形参列表也必须为空。类型转换函数通常应该是const。
-
定义含有类型转换运算符的类 class SmallInt {
public:
SmallInt(int i = 0) :val(i) {}
operator int()const { return val; }
void print() { cout << val << endl; }
private:
size_t val;
};
-
尽管类型转换函数不负责指定返回类型,但每个类型转换函数都返回一个对应类型的值: [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KORcFtCv-1653118952001)(img/C++类设计者的工具/20200530140711637.png)] -
显式的类型转换运算符:编译器通常不会将一个显示的类型转换运算符用于隐式类型转换,必须通过显式的强制类型转换才可以: class SmallInt{
public:
explicit operator int() const{
return val;
}
};
SmallInt si = 3;
si + 3;
static_cast<int>(si)+3;
- 如果表达式被用作条件,则编译器会将显式的类型转换自动应用于它;
- 向bool的类型转换通常用在条件部分,因此operator bool一般定义成explicit的。
-
避免有二义性的类型转换
-
如果类中包含一个或多个类型转换,则必须确保在类类型和目标类型之间只存在唯一一种转换方式; -
二义性与转换目标为内置类型的多重类型转换:对某个给定的类来说,最好只定义最多一个与算术类型有关的转换规则; [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XF4PwqF7-1653118952001)(img/C++类设计者的工具/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzExNjkwMA==,size_16,color_FFFFFF,t_70-165311840004311.png)] -
除了显式地向bool类型的转换之外,我们应该尽量避免定义类型转换函数并尽可能地限制那些“显然正确”的非显式构造函数。 -
重载函数与用户定义的类型转换 struct C{
C(int);
};
struct D{
D(int);
};
void manip(const C&);
void manip(const D&);
manip(10);
manip(C(10));
当我们调用重载的函数时,如果两个或多个类型转换都提供了同一种可行的匹配,则这些类型转换一样好,在这个过程中,我们不会考虑任何可能出现的标准类型转换的级别,只有当重载函数能通过同一个类型转换函数得到匹配时,我们才会考虑其中出现的标准类型转换.。 struct E{
E(double);
};
void manip2(const C&);
void manip2(const E&);
manip2(10);
-
函数匹配与重载运算符 class SmallInt{
friend
SmallInt operator+(const SamllInt&,const SmallInt &);
public:
SmallInt(int = 0);
operator int() const {return val;}
private:
std::size_t val;
};
SmallInt s1,s2;
SmallInt s3 = s1 + s2;
int i = s3 + 0;
|