热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

【C++primer】第14章重载运算与类型转换(1)

PartIII:ToolsforClassAuthorsChapter14.OverloadedOperationsandConversions当运算符应用于类类型的对象时

Part III: Tools for Class Authors
Chapter 14. Overloaded Operations and Conversions


当运算符应用于类类型的对象时,运算符重载 (operator overloading) 允许我们定义它的含义。



14.1 基本概念

重载的运算符是具有特殊名字的函数:关键字 operator 后面跟着要定义的运算符符号。
重载运算符有返回类型、形参列表和函数体。

重载运算符函数中形参的个数与运算符的运算对象的个数一样。
在二元运算符中,左侧运算对象传递给第一个形参,右侧运算对象传递给第二个。
除了重载的函数调用运算符 operator() 以外,重载的运算符没有默认实参。

如果一个运算符函数是成员函数,第一个(左侧)运算对象绑定到隐式 this 指针。因此,成员运算符函数的(显式)形参的个数比运算符的运算对象的个数要少一个。

运算符函数要么是类的成员,要么至少有一个类类型的形参。

// error: cannot redefine the built-in operator for ints
int operator+(int, int);

此限制意味着,当将运算符符应用于内置类型的运算对象时,我们无法更改其含义。

可以重载大多数,但不是所有,运算符。

可以重载的运算符:

&#43; - * / % ^ & | ~ ! , &#61; < > <&#61; >&#61; &#43;&#43; -- << >>
&#61;&#61; !&#61; && || &#43;&#61; -&#61; /&#61; %&#61; ^&#61; &&#61; |&#61; *&#61; <<&#61; >>&#61;
[] () -> ->* new new[] delete delete[]

不能重载的运算符&#xff1a;

:: .* . ?:

只能重载已有的运算符&#xff0c;不能发明新的运算符符号。

重载运算符的优先级和结合律与其对应的内置运算符相同。

直接调用一个重载运算符函数

通常情况下&#xff0c;通过在正确类型的实参上使用运算符&#xff0c;可以直接“调用”重载运算符函数。然而&#xff0c;也可以像调用普通函数那样直接调用重载运算符函数。

// equivalent calls to a nonmember operator function
data1 &#43; data2;
// normal expression
operator&#43;(data1, data2); // equivalent function call

显式调用成员运算符函数的方式与其他成员函数相同。

data1 &#43;&#61; data2; // expression-based "call"
data1.operator&#43;&#61;(data2); // equivalent call to a member operator function

一些运算符不应该被重载

一些运算符可以保证运算对象的求值顺序。因为使用重载运算符实际上是一个函数调用&#xff0c;所以这些保证不能应用到重载运算符。
特别是&#xff0c;逻辑与逻辑或逗号运算符的运算对象求值顺序的保证不能保留。
而且&#xff0c;&&|| 运算符的重载版本不保留内置运算符的短路求值属性。

因为这些运算符的重载版本不能保留求值顺序和/或短路求值&#xff0c;因此通常不建议重载它们。

不重载逗号的另一个原因&#xff0c;也是不重载取地址运算符的原因&#xff1a;与大多数运算符不同&#xff0c;C&#43;&#43;语言定义了逗号和取地址运算符在应用与类类型时的含义。因为这些运算符有内置含义&#xff0c;它们通常不应该被重载。

使用的定义与内置含义一致

当设计一个类时&#xff0c;应该总是先考虑该类将提供哪些操作。只有在知道需要什么操作之后&#xff0c;才考虑将每个操作定义为普通函数还是重载运算符。如果操作在逻辑上可以映射到运算符上&#xff0c;那么将其定义为重载运算符是不错的选择&#xff1a;

  • 如果类执行IO操作&#xff0c;则将移位运算符定义成与内置类型的IO操作方式一致。
  • 如果类具有测试相等性的操作&#xff0c;定义operator&#61;&#61;。如果类具有operator&#61;&#61;&#xff0c;则通常也应该具有operator!&#61;
  • 如果类具有单一的自然排序操作&#xff0c;定义 operator<。如果类具有 operator<&#xff0c;则它可能应该具有所有关系运算符。
  • 重载运算符的返回类型通常应与该运算符的内置版本的返回类型兼容&#xff1a;逻辑和关系运算符应返回 bool&#xff0c;算术运算符应返回类类型的值&#xff0c;赋值和复合赋值应该返回对左侧操作数的引用。

赋值与复合赋值运算符

赋值运算符的行为应类似于合成运算符&#xff1a;赋值后&#xff0c;左侧和右侧运算对象应具有相同的值&#xff0c;并且运算符应返回对其左侧运算对象的引用。

如果一个类具有算术运算符或位运算符&#xff0c;那么通常也应提供相应的复合赋值运算符。&#43;&#61; 运算符应定义为具有与内置运算符相同的行为&#xff1a;先执行 &#43;&#xff0c;后执行 &#61;

选择作为成员或非成员实现

下面的准则有助于决定使运算符作为成员或普通非成员函数&#xff1a;

  • 赋值 &#61;、下标 []、调用 ()、成员访问箭头 -> 运算符必须定义为成员。
  • 复合赋值运算符一般应该是成员。但与赋值不同&#xff0c;它们不必需是成员。
  • 改变对象的状态或与给定类型紧密相关的运算符&#xff0c;比如递增、递减、解引用&#xff0c;通常应该是成员。
  • 具有对称性的运算符&#xff0c;即可以转换任何一侧运算对象的运算符&#xff0c;比如算术、相等、关系、位运算符&#xff0c;通常应定义为普通的非成员函数。

程序员希望能够在混合类型的表达式中使用对称性运算符。例如&#xff0c;可以将一个 int 和一个 double 相加。这种加法是对称的&#xff0c;因为可以将任一类型用作左侧或右侧运算对象。如果想提供涉及类对象的相似混合类型表达式&#xff0c;则必须将运算符定义为非成员函数。

如果将运算符定义为成员函数&#xff0c;则左侧运算对象必须是该运算符所属类的对象。

string s &#61; "world";
string t &#61; s &#43; "!"; // ok: we can add a const char* to a string
string u &#61; "hi" &#43; s; // would be an error if &#43; were a member of string

因为 string 将 &#43; 定义为普通的非成员函数&#xff0c;所以 "hi" &#43; s 等同于 operator&#43;("hi", s)。与任何函数调用一样&#xff0c;任何一个实参都可以转换为形参类型。唯一的要求是至少一个运算对象具有类类型&#xff0c;并且两个运算对象都可以&#xff08;明确地&#xff09;转换为字符串。

建议&#xff1a;仅当运算符对用户来说无二义性时&#xff0c;才使用它。若运算符有多个合理的解释&#xff0c;则该运算符具有二义性。



14.2 输入和输出运算符

重载输出运算符 <<

通常情况下&#xff0c;输出运算符的第一个形参是一个指向非const ostream 对象的引用。

第二个形参一般应该是对将要打印的类类型的 const 引用。

为了与其他输出运算符一致&#xff0c;operator<< 一般返回其 ostream 形参。

Sales_data 输出运算符

ostream &operator<<(ostream &os, const Sales_data &item) {os << item.isbn() << " " << item.units_sold << " " << item.revenue << " " << item.avg_price();return os;
}

通常&#xff0c;输出运算符打印对象的内容时应该尽量减少格式化操作。它们不应该打印换行符。

IO运算符必须是非成员函数

Sales_data data;
data << cout; // if operator<

IO运算符通常需要读写非public 数据成员&#xff0c;所以IO运算符通常应该声明为友元。

重载输入运算符 >>

通常&#xff0c;输入运算符的第一个形参是对将要读取的流的引用&#xff0c;第二个形参是对读入到的非const对象的引用。运算符通常返回对其给定流的引用。

Sales_data 输入运算符

istream &operator>>(istream &is, Sales_data &item) {double price; // no need to initialize; we&#39;ll read into price before we use itis >> item.bookNo >> item.units_sold >> price;if (is) // check that the inputs succeededitem.revenue &#61; item.units_sold * price;elseitem &#61; Sales_data(); // input failed: give the object the default statereturn is;
}

注意&#xff1a;输入运算符必须处理输入失败的可能性&#xff1b;输出运算符一般不需要。

输入时的错误

输入运算符可能发生的错误&#xff1a;

  • 当流中包含错误的数据类型时&#xff0c;读取操作失败。
  • 读取操作失败也可能是&#xff0c;遇到文件结尾或输入流上的其他错误。

实践&#xff1a;如果有错误&#xff0c;输入运算符应该决定如何进行错误恢复。

标示错误

一些输入运算符需要做额外的数据验证。输入运算符可能需要设置流的条件状态来标明错误&#xff0c;即使从技术来将实际的IO是正确的。通常输入运算符只设置 failbit。而 eofbit、badbit 等错误最好留给IO库自己标示。



14.3 算术和关系运算符

通常情况下&#xff0c;为了允许对左侧或右侧运算对象进行转换&#xff0c;将算术与关系运算符定义为非成员函数。
这些运算符应该不需要改变运算对象的状态&#xff0c;所以形参通常是指向const的引用。

算术运算符通常生成一个新值&#xff0c;它是两个运算对象计算的结果。
定义算术运算符的类一般也会定义对应的复合赋值运算符。

实践&#xff1a;同时定义了算术运算符和相关复合赋值的类&#xff0c;通常应使用复合赋值来实现算术运算符。

// assumes that both objects refer to the same book
Sales_data operator&#43;(const Sales_data &lhs, const Sales_data &rhs) {Sales_data sum &#61; lhs; // copy data members from lhs into sumsum &#43;&#61; rhs; // add rhs into sumreturn sum;
}

相等运算符

bool operator&#61;&#61;(const Sales_data &lhs, const Sales_data &rhs) {return lhs.isbn() &#61;&#61; rhs.isbn() && lhs.units_sold &#61;&#61; rhs.units_sold && lhs.revenue &#61;&#61; rhs.revenue;
}
bool operator!&#61;(const Sales_data &lhs, const Sales_data &rhs) {return !(lhs &#61;&#61; rhs);
}

设计准则&#xff1a;

  • 如果类有一个操作来确定两个对象是否相等&#xff0c;它应该将这个函数定义成 operator&#61;&#61; 而不是命名函数&#xff1a;用户无需学习记忆一个新操作名称&#xff1b;使用库容器和算法更方便。
  • 如果类定义 operator&#61;&#61;&#xff0c;此运算符一般应该确定给定的对象是否包含相等的数据。
  • 如果类定义 operator&#61;&#61;&#xff0c;它应该也定义 operator!&#61;
  • 相等或不相等运算符中的一个应该将工作委托给另一个。即&#xff0c;其中一个应该负责比较对象的实际工作&#xff0c;另一个应该调用实际工作的运算符。

关系运算符

定义了相等运算符的类通常&#xff08;但不总是&#xff09;定义关系运算符。特别地&#xff0c;因为关联容器和一些算法使用小于运算符&#xff0c;定义 operator< 会有用。

一般情况下关系运算符应该&#xff1a;

  1. 定义顺序关系&#xff0c;要与用作关联容器的关键字的要求一致&#xff1b;
  2. 如果类具有 &#61;&#61;&#xff0c;定义关系要与其一致。特别是&#xff0c;如果两个对象 !&#61;&#xff0c;那么一个对象应该 < 另一个。

如果 < 存在唯一的逻辑定义&#xff0c;则类通常应定义 < 运算符。但是&#xff0c;如果类也具有 &#61;&#61;&#xff0c;则仅当 <&#61;&#61; 的定义产生一致的结果时才定义 <



14.4 赋值运算符

除了复制赋值和移动赋值运算符&#xff08;将类类型的一个对象赋值给相同类型的另一个对象&#xff09;之外&#xff0c;类还可以定义允许其他类型作为右侧运算对象的其他赋值运算符。

例如&#xff0c;除了复制赋值和移动赋值运算符之外&#xff0c;库 vector 类还定义了第三个赋值运算符&#xff0c;该运算符接受一个使用花括号括起来的元素列表。可以如下使用该运算符&#xff1a;

vector<string> v;
v &#61; {"a", "an", "the"};

可以把这个运算符加到 StrVec 类中&#xff1a;

class StrVec {
public:StrVec &operator&#61;(std::initializer_list<std::string>);// other members as in § 13.5
};StrVec &StrVec::operator&#61;(initializer_list<string> il) {// alloc_n_copy allocates space and copies elements from the given rangeauto data &#61; alloc_n_copy(il.begin(), il.end());free(); // destroy the elements in this object and free the spaceelements &#61; data.first; // update data members to point to the new spacefirst_free &#61; cap &#61; data.second;return *this;
}

复合赋值运算符

复合赋值运算符不需要是成员。但是&#xff0c;我们倾向于在类内定义所有的赋值&#xff0c;包括复合赋值。
复合赋值运算符应该返回指向其左侧运算对象的引用。

// member binary operator: left-hand operand is bound to the implicit this pointer
// assumes that both objects refer to the same book
Sales_data& Sales_data::operator&#43;&#61;(const Sales_data &rhs) {units_sold &#43;&#61; rhs.units_sold;revenue &#43;&#61; rhs.revenue;return *this;
}



14.5 下标运算符

可以通过位置提取元素的表示容器的类&#xff0c;通常定义下标运算符 operator[]

下标运算符必须是成员函数。

为了与下标的普通含义兼容&#xff0c;下标运算符通常返回对所获取元素的引用。通过返回引用&#xff0c;下标可以在赋值的任何一侧使用。

如果类有下标运算符&#xff0c;通常应该定义两种版本&#xff1a;一种返回普通引用&#xff0c;另一种是 const 成员&#xff0c;返回 const 引用。当下标应用于 const 对象时&#xff0c;下标应返回对 const 的引用&#xff0c;这样就不能将赋值给返回的对象。

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]; }// other members as in § 13.5
private:std::string *elements; // pointer to the first element in the array
};// assume svec is a StrVec
const StrVec cvec &#61; svec; // copy elements from svec into cvec
// if svec has any elements, run the string empty function on the first one
if (svec.size() && svec[0].empty()) {svec[0] &#61; "zero"; // ok: subscript returns a reference to a stringcvec[0] &#61; "Zip"; // error: subscripting cvec returns a reference to const
}



14.6 递增和递减运算符

定义了递增或递减运算符的类应该同时定义前置和后置版本。这些运算符通常应该定义为成员。

定义前置递增/递减运算符

class StrBlobPtr {
public:// increment and decrementStrBlobPtr& operator&#43;&#43;(); // prefix operatorsStrBlobPtr& operator--();// other members as before
};

为了与内置运算符一致&#xff0c;前置运算符应该返回指向递增或递减后对象的引用。

// prefix: return a reference to the incremented/decremented object
StrBlobPtr& StrBlobPtr::operator&#43;&#43;() {// if curr already points past the end of the container, can&#39;t increment itcheck(curr, "increment past end of StrBlobPtr");&#43;&#43;curr; // advance the current statereturn *this;
}StrBlobPtr& StrBlobPtr::operator--() {// if curr is zero, decrementing it will yield an invalid subscript--curr; // move the current state back one elementcheck(-1, "decrement past begin of StrBlobPtr");return *this;
}

区分前置与后置运算符

为了与前置版本区分&#xff0c;后置版本接受一个额外的 int 类型的形参。当使用后置运算符时&#xff0c;编译器为这个形参提供 0 作为实参。正常情况下&#xff0c;后置运算符执行的工作不需要该参数。其唯一目的是将后置函数与前置版本区分开。

class StrBlobPtr {
public:// increment and decrementStrBlobPtr operator&#43;&#43;(int); // postfix operatorsStrBlobPtr operator--(int);// other members as before
};

为了与内置运算符一致&#xff0c;后置运算符应该返回原值&#xff08;递增或递减之前的值&#xff09;&#xff0c;返回形式是值&#xff0c;而不是引用。

// postfix: increment/decrement the object but return the unchanged value
StrBlobPtr StrBlobPtr::operator&#43;&#43;(int) {// no check needed here; the call to prefix increment will do the checkStrBlobPtr ret &#61; *this; // save the current value&#43;&#43;*this; // advance one element; prefix &#43;&#43; checks the incrementreturn ret; // return the saved state
}StrBlobPtr StrBlobPtr::operator--(int) {// no check needed here; the call to prefix decrement will do the checkStrBlobPtr ret &#61; *this; // save the current value--*this; // move backward one element; prefix -- checks the decrementreturn ret; // return the saved state
}

因为 int 形参不会被使用&#xff0c;所以不需要为它命名。

显式调用后置运算符

StrBlobPtr p(a1); // p points to the vector inside a1
p.operator&#43;&#43;(0); // call postfix operator&#43;&#43;
p.operator&#43;&#43;(); // call prefix operator&#43;&#43;



14.7 成员访问运算符

解引用 * 和箭头 -> 运算符通常用于表示迭代器的类和智能指针类中。

箭头运算符必须是成员。解引用运算符不需要是成员&#xff0c;但通常应该也是成员。

class StrBlobPtr {
public:std::string& operator*() const{auto p &#61; check(curr, "dereference past end");return (*p)[curr]; // (*p) is the vector to which this object points}std::string* operator->() const{// delegate the real work to the dereference operatorreturn & this->operator*(); // 返回解引用运算符返回的元素的地址}// other members as before
};

注意&#xff1a;这两个运算符被定义成 const 成员。获取一个元素不需要改变 StrBlobPtr 的状态。

StrBlob a1 &#61; {"hi", "bye", "now"};
StrBlobPtr p(a1); // p points to the vector inside a1
*p &#61; "okay"; // assigns to the first element in a1
cout << p->size() << endl; // prints 4, the size of the first element in a1
cout << (*p).size() << endl; // equivalent to p->size()

对箭头运算符返回值的限定

与大多数其他运算符一样&#xff0c;可以定义 operator* 来执行我们指定的任何操作&#xff0c;尽管这样做不太好。比如&#xff0c;可以定义 operator* 来返回固定值&#xff0c;或打印对象的内容&#xff0c;或者其他。
对于重载箭头&#xff0c;情况并非如此。箭头运算符永远不会失去其成员访问的基本含义。当重载箭头时&#xff0c;可以改变的是箭头从那个对象中获取指定成员。而箭头获取成员的事实不能改变。

当编写 point->mem 时&#xff0c;point 必须是指向类对象的指针&#xff0c;或者是带有重载的 operator-> 的类的对象。根据 point 的类型&#xff0c;point->mem 等效于&#xff1a;

(*point).mem; // point is a built-in pointer type
point.operator()->mem; // point is an object of class type

否则代码是错误的。

注意&#xff1a;重载的箭头运算符必须返回一个指向类类型的指针&#xff0c;或者一个定义了自己的箭头运算符的类类型的对象。


【C&#43;&#43; primer】目录


推荐阅读
  • 本文介绍了使用kotlin实现动画效果的方法,包括上下移动、放大缩小、旋转等功能。通过代码示例演示了如何使用ObjectAnimator和AnimatorSet来实现动画效果,并提供了实现抖动效果的代码。同时还介绍了如何使用translationY和translationX来实现上下和左右移动的效果。最后还提供了一个anim_small.xml文件的代码示例,可以用来实现放大缩小的效果。 ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • Java序列化对象传给PHP的方法及原理解析
    本文介绍了Java序列化对象传给PHP的方法及原理,包括Java对象传递的方式、序列化的方式、PHP中的序列化用法介绍、Java是否能反序列化PHP的数据、Java序列化的原理以及解决Java序列化中的问题。同时还解释了序列化的概念和作用,以及代码执行序列化所需要的权限。最后指出,序列化会将对象实例的所有字段都进行序列化,使得数据能够被表示为实例的序列化数据,但只有能够解释该格式的代码才能够确定数据的内容。 ... [详细]
  • 本文介绍了OC学习笔记中的@property和@synthesize,包括属性的定义和合成的使用方法。通过示例代码详细讲解了@property和@synthesize的作用和用法。 ... [详细]
  • XML介绍与使用的概述及标签规则
    本文介绍了XML的基本概念和用途,包括XML的可扩展性和标签的自定义特性。同时还详细解释了XML标签的规则,包括标签的尖括号和合法标识符的组成,标签必须成对出现的原则以及特殊标签的使用方法。通过本文的阅读,读者可以对XML的基本知识有一个全面的了解。 ... [详细]
  • 不同优化算法的比较分析及实验验证
    本文介绍了神经网络优化中常用的优化方法,包括学习率调整和梯度估计修正,并通过实验验证了不同优化算法的效果。实验结果表明,Adam算法在综合考虑学习率调整和梯度估计修正方面表现较好。该研究对于优化神经网络的训练过程具有指导意义。 ... [详细]
  • Imtryingtofigureoutawaytogeneratetorrentfilesfromabucket,usingtheAWSSDKforGo.我正 ... [详细]
  • 海马s5近光灯能否直接更换为H7?
    本文主要介绍了海马s5车型的近光灯是否可以直接更换为H7灯泡,并提供了完整的教程下载地址。此外,还详细讲解了DSP功能函数中的数据拷贝、数据填充和浮点数转换为定点数的相关内容。 ... [详细]
  • 本文介绍了Swing组件的用法,重点讲解了图标接口的定义和创建方法。图标接口用来将图标与各种组件相关联,可以是简单的绘画或使用磁盘上的GIF格式图像。文章详细介绍了图标接口的属性和绘制方法,并给出了一个菱形图标的实现示例。该示例可以配置图标的尺寸、颜色和填充状态。 ... [详细]
  • 个人学习使用:谨慎参考1Client类importcom.thoughtworks.gauge.Step;importcom.thoughtworks.gauge.T ... [详细]
  • 开发笔记:实验7的文件读写操作
    本文介绍了使用C++的ofstream和ifstream类进行文件读写操作的方法,包括创建文件、写入文件和读取文件的过程。同时还介绍了如何判断文件是否成功打开和关闭文件的方法。通过本文的学习,读者可以了解如何在C++中进行文件读写操作。 ... [详细]
  • 怎么在PHP项目中实现一个HTTP断点续传功能发布时间:2021-01-1916:26:06来源:亿速云阅读:96作者:Le ... [详细]
  • 纠正网上的错误:自定义一个类叫java.lang.System/String的方法
    本文纠正了网上关于自定义一个类叫java.lang.System/String的错误答案,并详细解释了为什么这种方法是错误的。作者指出,虽然双亲委托机制确实可以阻止自定义的System类被加载,但通过自定义一个特殊的类加载器,可以绕过双亲委托机制,达到自定义System类的目的。作者呼吁读者对网上的内容持怀疑态度,并带着问题来阅读文章。 ... [详细]
  • 基于Socket的多个客户端之间的聊天功能实现方法
    本文介绍了基于Socket的多个客户端之间实现聊天功能的方法,包括服务器端的实现和客户端的实现。服务器端通过每个用户的输出流向特定用户发送消息,而客户端通过输入流接收消息。同时,还介绍了相关的实体类和Socket的基本概念。 ... [详细]
  • 本文介绍了Android中的assets目录和raw目录的共同点和区别,包括获取资源的方法、目录结构的限制以及列出资源的能力。同时,还解释了raw目录中资源文件生成的ID,并说明了这些目录的使用方法。 ... [详细]
author-avatar
WO
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有