S14操作重载与类型转换

来源:互联网 发布:易语言dnf注入器源码 编辑:程序博客网 时间:2024/06/04 00:51

S14操作重载与类型转换


一、基本概念

1、重载运算符:名字由关键字operator和其后要定义的运算符号共同组成,包含返回类型、参数列表和函数体
(1)参数列表:数量和运算符的运算对象一样多,即一元运算符有一个参数,二元运算符由左侧对象传递第一个参数右侧对象传递第二个参数
(2)运算符函数作为成员函数,则左侧运算对象绑定到隐式的this指针上,使得显式的参数数量比运算符作用对象少一个
(3)运算符函数或是类的成员,或至少含一个类类型的参数,不能重载内置类型的运算

注意:重载的运算符不影响本身的优先级和结合律,且无权发明新的运算符

注意:不能重载的运算符::: .* . ?:

2、直接调用与间接调用

data1 + data2;operator+(data1, data2);data1 += data2;data1.operator+=(data2);

3、重载运算符本质上是函数调用,因此部分运算符重载会有一些变化,不建议重载:例如逻辑与||运算会先对左侧求值,若为真则表达式直接为真,若为假则再求右侧,而重载后的||则总是会对左右侧都求值,另外也一般不重载逗号,和取地址运算符&,重载运算符要求运算对象至少有一个类类型或枚举类型

注意:一般不应该重载逗号,、取地址&、逻辑与&&和逻辑或||运算符

4、使用与内置类型一致的含义
(1)执行I/O操作,移位运算符应与I/O保持一致
(2)检查相等性定义!=则也应该有==
(3)包含比较操作<则也应该有<=/>=/>等,类似的有+也最好有+=/-=
(4)重载运算符的返回类型应与内置版本兼容,例如逻辑运算和关系运算应返回bool
5、选择重载运算符是成员或非成员
(1)一般法则:

  • 赋值=、下标[]、调用()、成员访问->运算符必须是成员
  • 复合赋值一般是成员但并非必须
  • 改变对象状态或是与给定类型密切相关的运算符通常是成员,如递增、递减、解引用
  • .具有对称性的运算符(即左右可以互换)通常是非成员,如算术、相等性、关系、位运算等

(2)对于对称性的运算符

//若是成员string t = s + "1";   //正确,+的左侧是string sstring t = "1" + s;   //错误,+的左侧是const char*没有+方法//若非成员"1"+s等价于s+"1"等价于operator+("1", s),只要两个参数只要有一个是类类型就正确operator+("1","2");   //错误,两个都是const char*

二、输入和输出运算符

1、重载输出运算符<<
通常,输出运算符第一个形参是非常量的ostream对象的引用,第二个形参是常量的对象的引用,同时一般要返回它的ostream形参
(1)输出运算符尽量减少格式化操作
通常输出运算符应该主要负责打印对象的内容而非控制格式,同时不应该打印换行符,控制格式推荐使用printf
(2)输入输出运算符必须是非成员函数,否则运算符左侧对象将是类的一个对象
2、重载输入运算符>>
通常,输入运算符第一个形参是非常量的流对象的引用,第二个形参是将要读入的非常量的对象的引用,同时一般要返回流对象的引用
3、输入时的错误:输入运算符必须处理输入可能失败的情况,输出运算符无需处理,发生读取操作错误时,输入运算符应该负责从错误中恢复,同时标示错误

三、算数和关系运算符

算术和关系运算符一般定义成非成员函数以允许左右侧运算对象互换,并且由于一般不会改变运算对象,因此形参都是常量的引用,返回一个新值的副本
1、相等运算符
2、关系运算符

注意:由于存在类似a==b逻辑上应有a<=b && a>=b的关系,因此在定义相等运算符和关系运算符的时候需要注意这一点,不能导致出现令人难以理解的关系,例如由于==<定义不同,出现了部分特例既a==ba<b

注意:如果存在唯一一种逻辑可靠的<定义,则才考虑定义<,并且当类同时有==时,<==的结果复合逻辑时才能定义<

注意:在实现上推荐实现==和<,所有其他运算调用==或<来实现,简化实现过程

四、赋值运算符

注意:赋值运算符必须定义为成员函数,并返回左侧运算对象的引用,而复合赋值运算符则也推荐如此实现

五、下标运算符

注意:下标运算符必须是成员函数,且通常以访问元素的引用作为返回,并且会定义两个版本:1.返回普通引用,2.类的常量成员并返回常量引用

六、递增和递减运算符

注意:递增和递减运算符应同时定义前置版本和后置版本,并且应该被定义为类的成员

1、定义前置递增/递减运算符
与内置版本一致,前置应返回递增/递减后对象的引用

StrBlobPtr &StrBlobPtr::operator--(){    --curr;    check(curr, "decrement past begin of StrBlobPtr"); //检查是否下标越界    return *this;}

2、定义后置递增/递减运算符
与内置版本一致,后置应返回对象递增/递减前的原值且非引用

注意:后置版本通过接受一个额外不被使用的int类型的形参来与前置版本区别,编译器为之提供值为0的实参,该int参数随不会被使用,但是若要显式调用后置运算符时需要传入一个0,同样用于区分

StrBlobPtr StrBlobPtr::operator--(int){    StrBlobPtr ret = *this;   //不需要检查    --*this;                  //调用前置版本来完成递增/递减任务,并带有检查    return ret;               //返回原值的拷贝而非引用}p.operator++(0);              //调用后置版本p.operator--();               //调用前置版本

七、成员访问运算符

注意:箭头运算符永远不能丢掉访问成员这个基本含义,当重载箭头运算符时,可以改变的是箭头从哪个对象当中获取成员,而”获取成员”本身不能改变

注意:->的重载函数必须返回类的指针,或是定义了->的类的某个类对象

1、对于形如point->mem的表达式来说,point必须是指向类的指针或者是一个重载了->的对象

(1)point是指针,则point->mem就是(*point).mem,若point指向的类没有mem成员,则报错
(2)point是重载了->的类,则point->mem就是point.operator->()->mem,即通过重载->,使得在表达point->转换为调用point的重载函数point.operator->(),结合->返回类型,则point.operator->()的结果要么是一个指针,此时回到(1)中的步骤,要么是一个有->的对象,继续重载执行(2)中的步骤,即成员访问运算·->·的重载本质上是改变了箭头获取成员的源和路径,最终完成的操作一定是获取成员,类似进行了“迭代”的操作

2、实例

struct A { int foo, bar; };struct B{    A a;    A *operator->() { return &a; }};struct C{    B b;    B operator->() { return b; }};struct D{    C c;    C operator->() { return c; }};D d; d->foo;  //等价于d.operator->().operator->().operator->()->foo//此时d是类而不是指针,d->调用重载->变成了d.operator->(),发现返回的是C类型的c而不是指针//则进一步调用C类型中的重载->变成了d.operator->().operator->(),发现返回的是B类型的b而不是指针,//则进一步调用B类型中的重载->变成d.operator->().operator->().operator->(),发现返回的是指向A类型的指针,//则->等价于(*),即获取A中的成员foo

八、函数调用运算符

如果类重载了函数调用运算符(),则我们可以像使用函数一样使用该类的对象,并称该类的对象为函数对象

1、lambda是函数对象
lambda表达式产生的类不含默认构造函数、赋值运算符及默认析构函数,是否含有默认的拷贝/移动构造函数取决于捕获的数据成员类型

auto wc = find_if(words.begin(), words.end(), [sz](const string &a){ return a.size() >= sz; });//该lambda表达式效果类似如下class SizeComp{public:    SizeComp(size_t n) : sz(n) { }    //返回类型、参数、函数体都与lambda一致    bool operator()(const string &s) const { return s.size() >= sz; }private:    size_t sz;     //sz对应捕获的变量};auto wc = find_if(words.begin(), words.end(), SizeComp(sz));  //SizeComp(sz)通过sz生成了匿名的函数对象

2、标准库函数定义的函数对象,定义在functional头文件中

算术           关系               逻辑plut<T>       equal_to<T>       logical_and<T>minus<T>      not_equal_to<T>   logical_or<T>multiplies<T> greater<T>        logical_not<T>divides<T>    greater_equal<T>  modulus<T>    less<T>   negate<T>     less_equal<T> 

在算法中使用标准库函数对象

sort(svec.begin(), svec.end(), greater<string>());   //使得比较基于greater函数对象,起到了>的效果//直接比较两个不相关指针的大小是未定义行为,然而可以使用一个标准库函数对象来实现这种比较vector<string *> nameTables;sort(.., .., [](string *a, string *b){ return a < b; });  //错误,a/b两个指针无法直接比较大小sort(.., .., less<string *>());                           //正确,标准库函数对象可以比较指针大小

3、可调用对象与function
(1)C++中可调用对象有:函数、函数指针、lambda表达式、bind创建的对象、重载了函数调用运算符()的类

  • 函数类型:可调用的对象也有类型
  • 调用形式:一种调用形式对应一个函数类型,如int(int, int)是一种接受两个int返回一个int的函数类型

(2)标准库function类型定义在functional头文件中

function<T> f;          //f是用来储存可调用对象的空function,这些可调用对象的调用形式与函数类型T相同function<T> f(nullptr); //显式构造一个空functionfunction<T> f(obj);     //在f中存储可调用对象obj的副本f                       //将f作为条件,当f含有一个可调用对象时为真,否则为假f(args)                 //调用f中的对象,参数是argsresult_type             //成员,该function类型的可调用对象返回的类型map<string, function<int(int, int)>> binops = {    //使用function才能将各种可调用对象都加入map    {"+", add},                                    //函数指针    {"-", std::minus<int>()},                      //标准库函数对象    {"/", divide()},                               //用户定义的函数对象    {"*", [](int i, int j) { return i * j; }},     //未命名的lambda    {"%", mod}                                     //命名了的lambda}                                                  //可调用对象类型不同但都能放入function<int(int,int)>binops["+"](10, 5);                                //调用add(10, 5),以此类推其他调用

(3)重载函数与function
不能直接将重载函数的名字存入function类型的对象中,可以使用存储函数指针或是使用lambda来消除二义性

class A{};int add(int a, int b);A add(A a, A b);int main(){    map<string, function<int(int, int)>> f;    int(*a)(int, int) = add;    f.insert({ "+", add });                                //错误,add无法确定与哪个重载函数匹配    f.insert({ "+", a });                                  //正确,利用指针消除二义性    f.insert({ "+",[](int a, int b) {return a + b; } });   //正确,直接添加未命名的lambda函数对象    return 0;}

九、重载、类型转换与运算符

1、类型转换运算符
类型转换运算符是类的一种特殊成员函数,负责将一个类类型的值转换成其他类型,不允许转换成数组或者函数类型但允许转换成指针(包括数组指针和函数指针)或者引用类型,并且类型转换运算符没有显示的返回类型也没有形参,必须是成员函数,一般不应该改变待转换的对象因此是const成员
(1)定义含有类型转换运算符的类

class SmallInt;operator int(SmallInt&);                   //错误,非成员函数class SmallInt{public:    int operator int() const;              //错误,指定了返回类型    operator int(int = 0) const;           //错误,指定了参数    operator int*() const { return 42; }   //错误,42与int*不匹配    operator int() const { return val; }   //正确private:    size_t val;}

注意:慎用类型转换运算符,当想要转换的类型之间不存在逻辑明确的一对一映射关系时不定义类型转换运算符

(2)类型转换运算符可能产生意外的结果
一般情况下,实践中类很少提供类型转换运算符,但是定义向bool的类型转换是比较普遍的,然而定义隐式的类型转换可能产生一些意外的结果
(3)显式的类型转换运算符
为了避免出现意外情况,在类型转换运算符前加上explicit来强制显式调用(参考显式构造函数),此时只能显式调用类型转换才能执行类型转换

...explicit operator int() const { return val; }...SmallInt si = 3;            //正确,构造函数不是显式的,此处会隐式将3转换成SmallIntsi + 3;                     //错误,此处需要隐式转换,然而转换运算符explicit是显式的static_cast<int>(si) + 3;   //正确,显式调用类型转换

对于显式的转换符在以下情况会被隐式调用:

  • if/while/do语句的条件部分
  • for语句头的条件表达式
  • 逻辑运算符!/||/&&的运算对象
  • 条件运算符?:的条件表达式

(4)转换为bool
bool的类型转换通常用在条件部分,因此operator bool一般定义为explicit

2、避免有二义性的类型转换
多重转换路径:

  • A定义了接受B的转换构造函数,B定义了目标是A的类型转换运算符,两者提供相同的类型转换
  • 定义了多个转换规则

注意:通常情况下不要为类定义相同的类型转换,也不要定义两个及以上的转换源/转换目标是算术类型的转换

(1)实参匹配和相同的类型转换

struct B;struct A{    A(const B&);}struct B{    operator A() const;}A f(const A&);B b;A a = f(b);                 //二义性错误,f(B::operator A()) or f(A::A(const B&)) ?A a1 = f(b.operator A());   //显式调用,正确A a2 = f(A(b));             //显式调用,正确

注意:在这里强制类型转换也无法解决二义性,只能显示调用

(2)二义性与转换目标为内置类型的多重类型转换

struct A{    A(int = 0);               //不要定义多个源都是算术类型的类型转换    A(double);    operator int() const;     //不要定义多个目标都是算数类型的类型转换    operator double() const;}void f2(long double);A a;f2(a);      //二义性错误,f(A::operator int()) or f(A::operator double())?long lg;A a2(lg);   //二义性错误,A::A(int) or A::A(double) ?short s = 42;A a3(s);      //正确,short提升至int优于short提升至double,使用A::A(int)

注意:当使用用户定义的类型转换中包括标准类型转换时,则标准类型转换的级别决定最佳匹配过程

注意:除了显式向bool类型转换外,尽可能避免定义类型转换函数,并尽可能限制非显式构造函数

3、函数匹配与重载运算符
当调用一个命名函数时,同名的成员函数和非成员函数不会彼此重载;当通过类类型的对象/指针/引用调用函数时,只考虑成员函数;而在表达式中使用重载的运算符时,成员函数与非成员函数都会在考虑范围内

注意:如果一个类同时提供了转换目标是算数类型的类型转换和重载的运算符,则会遇到二义性问题

原创粉丝点击