第十四章

阅读: 评论:0

第十四章

第十四章

  • 重载运算符的参数数量与该运算符作用的运算对象数量一样。一元运算符有一个参数,二元运算符有两个参数
  • 如果一个运算符函数是成员函数,则它第一个(左侧)对象绑定到隐式的this指针上。所以成员运算符函数的参数数量要比运算符运算对象少一个
  • 对运算符函数来说,它要么是类的成员函数,要么至少得有一个类类型的参数
  • 可以像调用普通函数那样直接调用运算符函数
//注意这是 非成员 运算符函数的等价调用
data1 + data2;
operator+(data1, data2);
  • 也能像调用其他成员函数调用成员运算符函数
//注意这是对成员运算符函数的等价调用
data1 += data2;
data1.operator+=(data2);
  • 通常情况下不能重载逗号、取地址、逻辑与和逻辑或运算符
  • 重载运算符的返回类型应该与内置版本的返回类型相同:逻辑运算符和关系运算符返回bool;算术运算符返回一个类类型;赋值运算符和复合赋值运算符应该返回左侧运算对象的引用
  • 定义重载运算符时要决定将其声明为类的成员函数还是普通的非成员函数,下面的准则能帮助我们决定定义为哪种:
    1. 赋值=、下标[]、调用()和成员访问->运算符必须是成员
    2. 改变对象状态的运算符或者与类型密切相关的比如递增、递减和解引用,通常是成员
    3. 具有对称性的运算符可能转换任意一端的运算对象,比如算术、相等性、关系和位运算符,通常是普通的非成员函数
  • 如果想提供含有类对象的混合类型表达式,则运算符必须定义成非成员函数。如果不这么做后果如下:
//如果运算符定义成成员函数,左侧运算对象必须是运算符所属类的一个对象
string s = "world";
string t = s + "!"; //正确
string u = "hi" + s; //这里如果string将+定义成成员函数,将发生错误
//如果string将+定义为成员运算符函数
//则"hi" + s等价为"hi".operator+(s);
//但是"hi"是内置类型const char*,根本没有什么operator+成员函数
//但是string将+定义为普通的非成员函数
//所以这里实际是operator+("hi", s);

输入输出运算符

输出

  • 一般输出运算符<<第一个形参是非常量ostream对象的引用,不是常量是因为要向流写入内容会改变其状态;是引用是因为不能直接复制一个ostream对象。第二个形参一般是一个常量的引用,引用是避免复制形参;常量是因为打印对象不会改变对象的内容
  • 与输出运算符保持一致,operator<<返回它的ostream形参
  • 输出运算符尽量减少格式化操作,比如换行什么的。因为它主要负责打印内容而不是控制格式
  • 输入输出运算符必须是非成员函数,否则它们的左侧运算对象会是类的一个对象。因为经常会输出私有成员,所以IO运算符一般声明为友元
ostream &operator<<(std::ostream &os, const Sales_data &item)
{os << item.isbn() << " " << item.units_sold << " "<< venue << " " << item.avg_price();return os;
}

输入

  • 输入运算符的第一个形参是运算符将要读取的流的引用,第二个形参是将要读入到的(非常量)对象的引用。第二个形参必须是非常量,因为输入运算符本身的目的就是将输入读入到这个对象中
istream &operator>>(std::istream &is, Sales_data &item)
{double price;is >> item.bookNo >> item.units_sold >> price;if (is) //检查输入是否成功venue = price * item.units_sold;elseitem.Sales_data(); //输入失败就赋予默认状态return is;
}
  • 输入运算符必须处理输入可能失败的情况,输出运算符不需要

算术与关系运算符

  • 通常将算术和关系运算符定义为非成员函数来允许对运算符左侧或右侧的对象进行转换
  • 如果一个类定义了算术运算符,则它一般也会定义一个对应的复合赋值运算符。然后用复合赋值运算符来实现算术运算符
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 &&venue == venue;
}
bool operator!=(const Sales_data &lhs, const Sales_data &rhs)
{return !(lhs == rhs);
}
  • 如果定义了==,则这个类也应该定义!=

关系运算符

  • 关系运算符要遵循两点:
    1. 定义顺序关系,令其与关联容器中对关键字的要求一致
    2. 如果类也含有==运算符,则关系运算符要与==保持一致
  • Sales_data类中的compareIsbn通过比较Isbn判断两个对象,也满足条件1,似乎可以作为<。但假设有这种情况:两个对象isbn相同,但是其他成员不同。对<来说,因为isbn相同所以这两个对象谁都不小于谁,道理上讲应该是相等的,但实际并不相等。这就是因为没有满足条件2。
  • 如果还是上面的函数,改成先比较一个成员a,相等再比较下一个成员b。这样可行吗?但又是按不同的需求可能会想先比较b,再比较a。所以这样没有意义。所以对Sales_data来说并不需要一个<运算符

赋值运算符

  • 不论形参类型是什么,赋值运算符必须定义为成员函数
  • 之前讲过拷贝赋值运算符和移动赋值运算符,标准库vector还定义了第三种赋值运算符,该运算符接受花括号内的元素列表作为参数
vector<string> v;
v = {"a", "ab", "abc"};
  • 我们也能定义自己的,用之前的StrVec举例
StrVec &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;
}
//因为initializer_list<string>确保il和this指的不是同一个对象
//所以不用考虑自赋值

复合赋值运算符

  • 复合赋值运算不要求一定是类的成员,不过一般还是定义为成员。和内置类型的一致,复合赋值运算符也要返回其左侧运算对象的引用
Sales_data& Sales_data::operator+=(const Sales_data &rhs)
{units_sold += rhs.units_sold;revenue += venue;return *this;
}

下标运算符

  • 下标运算符必须是成员函数
  • 与下标原始定义相同,下标运算符通常以所访问元素的引用作为返回值。这样下标就可以出现在赋值运算符的任意一端了
  • 最好同时定义下标运算符的常量版本和非常量版本,当作用于一个常量对象时,下标运算符返回常量引用以确保我们不会给返回的对象赋值。
class StrVec
{
public:string& operator[](size_t n) { return elements[n]; }const string& operator[](size_t n) const { return elements[n]; }
//...
private:string *elements;
};
//...
StrVec svec;
const StrVec cvec = svec;
if (svec.size() && svec[0].empty()) //如果svec有元素且第一个元素为空
{svec[0] = "zero"; //正确,下标运算符返回string的引用//相当于svec.operator[](0) = "zero",返回引用的函数得到左值,所以能在运算符左侧cvec[0] = "Zip"; //错误!对cvec取下标返回的是常量引用//相当于cvec.operator[](0) = "Zip",因为返回值是常量引用,所以不能修改对象的值
}

递增递减运算符

  • 不要求递增递减必须是类的成员,但是它们改变的是操作对象的状态,所以最好设定为成员函数
  • 定义递增递减运算符应该同时定义前置版本和后置版本

定义前置运算符

  • 前置运算符应该返回递增或递减后对象的引用,我们必须和内置版本一致
  • 编写动态内存使用示例中StrBlobPtr的递增递减操作
class StrBlobPtr
{
public:StrBlobPtr operator++();StrBlobPtr operator--();
};
//原来的incr函数,现在改写成自增
StrBlobPtr& StrBlobPtr::opeartor++()
{check(curr, "increment past end of StrBlobPtr");++curr;return *this;
}
StrBlobPtr& StrBlobPtr::opeartor--()
{//这里避免出现curr本身是0的情况,如果本身是0则递减会产生一个无效下标--curr;check(curr, "decrement past begin of StrBlobPtr");return *this;
}

区分前置后置

  • 前置后置运算符,运算对象和类型都是一样的。为了解决这个问题,后置版本接受一个额外的(不被使用)int类型形参。当使用后置运算符时编译器会为这个形参提供一个值为0的实参。语法上后置函数可以使用这个形参,但实际过程中并不会。这个形参的作用就是区分前置后置而已
  • 接下来就能定义后置运算符,还是要和内置版本保持一致,后置运算符返回对象的原值(递增递减之前的值),并且返回的是值而非引用
class StrBlobPtr
{
public:StrBlobPtr operator++(int); //后置版本StrBlobPtr operator--(int);
};
//注意现在返回的都不是引用了
//并且都不用检查有效性,只有前置才检查
StrBlobPtr StrBlobPtr::opeartor++(int)
{StrBlobPtr ret = *this; //记录当前值++*this; //向前移动一个元素,然后因为前置++,所以会检查有效性return ret;
}
StrBlobPtr StrBlobPtr::opeartor--(int)
{StrBlobPtr ret = *this;--*this;return ret;
}
//可以看得出,后置版本都是调用前置版本来完成实际工作的

显式调用运算符

StrBlobPtr p(a1);
p.operator++(0); //调用后置版本
p.operator++(); //调用前置版本
//要想调用后置版本,传入的值必不可少

成员访问运算符

class StrBlobPtr
{
public:string& operator*() const{auto p = check(curr, "dereference past end");//检查p是否在正确范围内,是则返回curr所指元素的引用return (*p)[curr];//(*p)是对象所指的vector}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,即okay的长度
cout << (*p).size() << endl;
  • 箭头运算符必须是类的成员,解引用通常也是,但没说必须是
  • 箭头运算符和别的运算符不一样,不管怎么重载,它永远不能丢掉“成员访问这个最基本的含义”。
point->mem;
//如果point是指针,我们会用内置的箭头运算符,表达式会等价于(*point).mem//如果point是定义了operator->的类的对象
//则使用point.operator()的结果获取mem

函数调用运算符

  • 如果类重载了函数调用运算符,那就可以像使用函数一样使用该类对象。这样的类对象同时存储了状态,称该类的对象为函数对象。
struct absInt
{int operator()(int val) const{ return val < 0 ? -val : val; }
};
int i = -42;
absInt absObj;
int ui = absObj(i);
//看起来就像调用函数一样,但要记得absObj是对象
  • 函数调用运算符必须是成员函数。一个类可以定义多个版本的调用运算符,但是相互之间要在参数数量或类型上有区别
  • 函数对象类通常含有一些数据成员,专门用于调用运算符中的操作
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;//os和sep用来协助调用运算符打印string
};

重载、类型转换与运算符

  • 第七章讲过如果构造函数只接受一个实参,则相当于定义了那个实参类型转换为此类类型的隐式转换机制。我们能自己定义类型转换运算符来实现类类型向其他类型的转换。这两个操作共同定义了类类型转换
  • 类能转换的类型必须是能作为函数返回类型的类型,所以不能转换成数组或函数类型,但可以转换成指针(包括数组指针和函数指针)
  • 类型转换函数必须是类的成员函数,不能声明返回类型,形参列表必须为空,且通常是const的
class SmallInt
{
public:SmallInt(int i = 0) : val(i){if (i < 0 || i > 255)throw out_of_range("Bad Smallint value");}operator int() const { return val; }
private:size_t val;
};
//这里int能转换成SmallInt,也能从SmallInt转换成int
SmallInt si;
si = 4; //4隐式转换成SmallInt,然后使用拷贝赋值运算符
si + 3; //si隐式转换成int,然后相加
si = 3.14; //内置先将double转换成int,然后同上
si + 3.14; //同上
  • 有时候隐式类型转换会发生意想不到的错误,为了避免一些错误,我们使用显式的类型转换运算符
class SmallInt
{
public://加上了explicexplicit operator int() const { return val; }
};
SmallInt si = 3; //正确,构造函数不是显式的
si + 3; //现在就错误了,因为explic不允许隐式转换
static_cast<int>(si) + 3; //正确,显式转换
  • 该规定存在一个例外,如果表达式被用作条件(if、while、do等等),显式的类型转换还是会被隐式执行

避免类型转换二义性

  • 下面就发生了典型的二义性错误
struct B;
struct A
{A() = default;A(const B&);
};
struct B
{operator A() const;
};
A f(const A&);
B b;
A a = f(b); //发生二义性错误!
//这里究竟是f(B::operator A())
//还是f(A::A(const B&))?
//只能显式调用来执行正确的操作
A a1 = f(b.operator A());
A a2 = f(A(b));
  • 还有一些不常见的二义性问题
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())//还是f(A::operator double())呢long lg;
A a2(lg); //使用的是A::A(int)还是A::A(double)呢
//这里会产生二义性是因为标准类型转换级别一致
//long向double转还是向int都差不多
//下面就不会发生二义性问题
short s = 42;
A a3(s);
//因为short提升为int优于转换为double
  • 尽量遵循两个规则:
    1. 不要令两个类执行相同的类型转换。比如Foo类有一个接受Bar类对象的构造函数,则不要在Bar类中再定义转换目标是Foo类的类型转换运算符
    2. 当已经定义了一个转换算术类型的类型转换时,不要再定义接受算术类型的重载运算符
  • 重载函数和转换构造函数的二义性
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&); //还是二义性错误
//调用重载函数时,如果多个用户定义的类型转换都提供了可行匹配
//则我们认为这些类型一样好。并且不会考虑任何可能出现的标准类型转换的级别

函数匹配和重载运算符

class SmallInt
{friend SmallInt operator+(const SmallInt&, const Small&);
public:SmallInt(int = 0);operator int() const { return val; }
private:size_t val;
};
SmallInt s1, s2;
SmallInt s3 = s1 + s2; //使用重载的+
int i = s3 + 0; //二义性错误
//为什么会发生二义性错误呢?
//因为这句既可以理解成把s3转换成int,然后使用内置加法
//也可以理解成将0转换成SmallInt,使用重载的加法
  • 所以我们对同一个类既提供了转换目标是算术类型的类型转换,有提供了重载的运算符,那将会遇到重载运算符和内置运算符的二义性问题

本文发布于:2024-02-01 22:27:09,感谢您对本站的认可!

本文链接:https://www.4u4v.net/it/170679762639837.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:
留言与评论(共有 0 条评论)
   
验证码:

Copyright ©2019-2022 Comsenz Inc.Powered by ©

网站地图1 网站地图2 网站地图3 网站地图4 网站地图5 网站地图6 网站地图7 网站地图8 网站地图9 网站地图10 网站地图11 网站地图12 网站地图13 网站地图14 网站地图15 网站地图16 网站地图17 网站地图18 网站地图19 网站地图20 网站地图21 网站地图22/a> 网站地图23