C++ Primer 之 讀書筆記 第十四章
Overloaded Operations and Conversions
(重載操作和轉換)
14.1 定義重載操作符(Defining an Overload Operators)
重載操作符必須至少有一個操作數的類型是類。
重載操作符必須至少有一個操作數是類類型和枚舉類型。(An overloaded operator must have at least one operand of class or enumeration type.)
重載操作符不會改變原來的優先級和關聯關系。(Precedence and Associativity Are Fixed)
短路徑求值不再保留(Short-Ciruit Evaluation Is Not Preserved)
這是和內置數據類型不同的。if expression1 && expression2, 對于內置數據類型如果expression1為false,那么expression2就不再判斷。但是這個短路徑求值方法在操作符重載時不再保留,就是說無論expression1是否為FALSE,都要判斷expression1。
類成員對非類成員(Class Member versus Nonmember)
這個命題來自于操作符重載即可以是類成員也可以是非類成員
// member binary operator: left-hand operand bound to implicit this pointer Sales_item& Sales_item::operator+=(const Sales_item&); // nonmember binary operator: must declare a parameter for each operand Sales_item operator+(const Sales_item&, const Sales_item&); |
它們的區別在于:
類成員返回的是引用,而非類成員返回的是對象。
重載操作符的設計原則
對于類類型對象來說大多數的操作都是沒有實際意義的,除非這個類提供了重載操作符。
但是在我們設計類的過程中,不能為了操作符重載而操作符重載。應該先去定義類的接口,然后根據接口提供的操作,把對應的操作轉換為操作符重載。(The best way to design operators for a class is first to design the class' public interface. Once the interface is defined, it is possible to think about which operations should be defined as overloaded operators. Those operations with a logical mapping to an operator are good candidates. For example,)
復合賦值操作符(Compound Assignment Operators)
首先說一下子的是啥是符合賦值操作符,Google說,這些都是哈:
+= -= *= /= %= &= ^= |= <<= >>= |
復合賦值操作符(加法) 復合賦值操作符(減法) 復合賦值操作符(乘法) 復合賦值操作符(除法) 復合賦值操作符(取余) 復合賦值操作符(按位與) 復合賦值操作符(按位異或) 復合賦值操作符(按位或) 復合賦值操作符(按位左移) 復合賦值操作符(按位右移) |
大師給出的原則是:既然你重載了+,那么+=你也要負責哦。(If a class has an arithmetic operator, then it is usually a good idea to provide the corresponding compound-assignment operator as well.)
相等和邏輯關系操作符
- 用作關聯容器的鍵(key)的類應該定義’<’操作符(less than operator)。(Classes that will be used as the key type of an associative container should define the < operator.)
- 如果類的對象要保存在序列容器里,那么一定要為這個類定義等于和小于操作符(’==’, ‘<’)。(Even if the type will be stored only in a sequential container, the class ordinarily should define the equality (==) and less-than (<) operators.)
- 如果類定義了等于操作符,那么還應該定義不等于操作符。(If the class defines the equality operator, it should also define !=.)
- 同樣,如果類定義了小于操作符(<),那么它應該定義所有的四種關系操作符(>, >=, <, and <=)。( If the class defines <, then it probably should define all four relational operators (>, >=, <, and <=).)
14.2 輸入輸出操作符(Input and Output Operators )
重載輸出操作符<<
基本的框架結構應該是:
// general skeleton of the overloaded output operator ostream& operator <<(ostream& os, const ClassType &object) { // any special logic to prepare object // actual output of members os << // ... // return ostream object return os; } |
另外有幾點注意事項:
1. IO操作符重載必須是非成員函數。
2. 輸出重載應該以最小的方式打印對象的內容。
3. 第一個參數ostream引用,是第二個參數必須是const引用,返回值是ostream引用。
For example:
ostream& operator<<(ostream& out, const Sales_item& s) { out << s.isbn << ""t" << s.units_sold << ""t" << s.revenue << ""t" << s.avg_price(); return out; } |
其實,我覺得這很像Java中重載tostring()函數。
重載輸入操作符>>
For example:
istream& operator>>(istream& in, Sales_item& s) { double price; in >> s.isbn >> s.units_sold >> price; // check that the inputs succeeded if (in) s.revenue = s.units_sold * price; else s = Sales_item(); // input failed: reset object to default state return in; } |
有幾點注意事項:
- 返回值是istream的引用。
- 第二個參數不能是const,因為讀入的數據就是寫入到這個對象里的。
- 對輸入有效性的判斷是在使用讀入的數據之前進行的:if (in)。
進一步說,在讀入數據時,我們還有可能需要進行數據有效性的判斷。那么How to do?簡單地說,就是要在重載的輸入操作符中增加有效性的判斷,如果輸入的數據不滿足有效性,那么就對istream的failbit置位。同樣的道理,也可以處理badbit,eofbit。(Usually an input operator needs to set only the failbit. Setting eofbit would imply that the file was exhausted, and setting badbit would indicate that the stream was corrupted. These errors are best left to the IO library itself to indicate.)
14.3 Arithmetic and Relational Operators
算術操作符
- 為了和內置操作保持一致,重載的加法操作返回的右值對象。(Note that to be consistent with the built-in operator, addition returns an rvalue, not a reference.)
- 定義算術運算符和相對應的復合操作符的類應該通過使用復合操作符來實現算數操作符。(Classes that define both an arithmetic operator and the related compound assignment ordinarily ought to implement the arithmetic operator by using the compound assignment.)注意這個順序是不能反的,也就是不能用算術運算符去實現復合操作符。很容易理解,因為算術操作符返回的是對象,而復合操作符返回的*this。
相等操作符
- 定義等于操作符(==)就要同時定義不等于操作符(!=)。二者之間一個完成實際的工作,另一個僅僅是調用前者。
- 如果類定義了等于操作符(==),那么就更加容易利用標準庫的算法了。
關系操作符
關系操作符是指:<, >。
要注意如果定義關系操作符,要保證它們和等于操作符不沖突。如果沖突,那就要有取舍,大師給的Sales_item的例子,就無需定義關系操作符,為了避免和等于操作符沖突。P520的說明仔細琢磨后,有點意思。
14.4 Assignment Operators
l 賦值操作符必須是成員函數。(every assignment operator, regardless of parameter type, must be defined as a member function.)
l 賦值操作符可以重載
class string { public: string& operator=(const string &); // s1 = s2; string& operator=(const char *); // s1 = "str"; string& operator=(char); // s1 = 'c'; // .... }; |
正是因為string對賦值操作符進行了重載,下面的賦值才都是有效的:
string car ("Volks"); car = "Studebaker"; // string = const char* string model; model = 'T'; // string = char |
l 賦值應該返回*this的引用。
14.5 下標操作符(Subscript Operator )
(string和vector是最好的例子。)
l 下標操作符必須是類成員函數。
l 需要定義下標操作符的類要定義兩個版本的下標操作,一個是非const成員,返回值是引用;另一個是const成員,返回值是const引用。(a class that defines subscript needs to define two versions: one that is a nonconst member and returns a reference and one that is a const member and returns a const reference.)
14.6 MemberAccess Operators
是指:解引用(*)和箭頭( ->)操作符。它們主要是用在實現智能指針(smart pointer)。
先說說這兩個抓狂的操作符
我是這么掰持的:
箭頭實際上是*.操作。什么意思?(*ptr).fun()對應的就是ptr->fun(),由于’.’操作符的優先級高于解引用*,所以->的優先級也應高于解引用。所以*ptr->sp實際上就是*(ptr->sp)。
再看看大師給的這個經典的smart pointer的例子
糾結。
class ScrPtr { friend class ScreenPtr; Screen *sp; size_t use; ScrPtr(Screen *p): sp(p), use(1) { } ~ScrPtr() { delete sp; } }; |
/* * smart pointer: Users pass to a pointer to a dynamically allocated Screen, which * is automatically destroyed when the last ScreenPtr goes away */ class ScreenPtr { public: // no default constructor: ScreenPtrs must be bound to an object ScreenPtr(Screen *p): ptr(new ScrPtr(p)) { } // copy members and increment the use count ScreenPtr(const ScreenPtr &orig): ptr(orig.ptr) { ++ptr->use; } ScreenPtr& operator=(const ScreenPtr&); // if use count goes to zero, delete the ScrPtr object ~ScreenPtr() { if (--ptr->use == 0) delete ptr; } // 以下是定義的解引用和箭頭操作 Screen &operator*() { return *ptr->sp; } //返回的是Screen對象的引用 Screen *operator->() { return ptr->sp; } //返回的是指向Screen對象的指針 const Screen &operator*() const { return *ptr->sp; } const Screen *operator->() const { return ptr->sp; } private: ScrPtr *ptr; // points to use-counted ScrPtr class }; |
對于解引用操作,那是好理解滴。
糾結在箭頭操作上了L
箭頭可以看做是二元操作符。它的兩個操作數是對象以及函數名稱。為了獲得成員接引用對象。即使這樣,箭頭操作符不需要顯式的形參。(Operator arrow is unusual. It may appear to be a binary operator that takes an object and a member name, dereferencing the object in order to fetch the member. Despite appearances, the arrow operator takes no explicit parameter.)
我們可以把右邊的操作數看做是是標識符,它對于類的一個成員。(the right-hand operand is an identifier that corresponds to a member of a class.)
KAO,那怎么理解
point->action(); |
- 首先根據優先級,要把它看做是:(point->action) ();
- point是對象,可以看做是point.operator->()->action()。(這個問題讓我想的很糾結,人笨起來,擋都擋不住。以至于中午在SOGO都還在想這個問題,然后忽的一下醒悟,幸福。)operator->()可以簡單的看做是對象成員,返回值是指針,然后執行->action()部分,這里的->則是地地道道的箭頭操作符了。
對箭頭重載返回值的約束條件
返回值要么是類類型的指針,或者是類類型的對象,不過這個類要定義了它自己的箭頭操作符。(The overloaded arrow operator must return either a pointer to a class type or an object of a class type that defines its own operator arrow.)
14.7 自增和自減操作符Increment and Decrement Operators
- 首先這兩個操作符要是類的成員。
- 其次為了和內置的操作保持一致。前綴操作符應該返回的是引用,這個引用是對應到增加或減少后的對象。
- 為了區分前綴和后綴操作,后綴操作符重載包含一個額外的int類型的形參。
定義后自增/自減操作符
要區分前自增/自減和后自增/自減。區分的方法是后自增/自減要包含有一個int類型的形參。但是這個int類型的形參并不會被使用。僅僅是為了和前自增/自減加以區分。
// postfix: increment/decrement object but return unchanged value CheckedPtr CheckedPtr::operator++(int) { // no check needed here, the call to prefix increment will do the check CheckedPtr ret(*this); // save current value ++*this; // advance one element, checking the increment return ret; // return saved state } |
返回值
對比前自增的返回值是自增后的對象的引用,后自增返回值是沒有自增的對象,而不是引用。
顯式調用自增
CheckedPtr parr(ia, ia + size); // iapoints to an array of ints parr.operator++(0); // call postfix operator++ parr.operator++(); // call prefix operator++ |
14.8 Call Operator and Function Objects
調用操作符是指:operator()
基本定義方法:
struct absInt { int operator() (int val) { return val < 0 ? -val : val; } }; |
調用:
int i = -42; absInt absObj; // object that defines function call operator unsigned int ui = absObj(i); // calls absInt::operator(int) |
函數調用操作符必須定義為成員函數。一個類可以定義多個版本的調用操作符,每個調用操作符定義的形參的類型和數量不同。
如果看上面的這個用法的例子,實在看不出調用操作符有啥優點,但是如果結合類庫算法來使用,就能夠看出調用操作符的靈活性。
定義:
// determine whether a length of a given word is longer than a stored bound class GT_cls { public: GT_cls(size_t val = 0): bound(val) { } bool operator()(const string &s) { return s.size() >= bound; } private: std::string::size_type bound; }; |
調用:
for (size_t i = 0; i != 11; ++i) cout << count_if(words.begin(), words.end(),GT_cls (i)) << " words " << i << " characters or longer" << endl; |
這,要是用函數定義來寫,就不得不寫11個不同的函數,而這些函數僅僅是要判斷的長度不同。冗余!如果這樣寫,簡直就是紡織女工寫的程序。
標準庫定義的函數對象
每個標準庫函數對象實際上都代表了一種操作,并且它們還是模板:o 模板類型代表了操作數的類型。(The Template Type Represents the Operand(s) Type)
plus<Type> minus<Type> multiplies<Type> divides<Type> modulus<Type> negate<Type> |
applies + applies - applies * applies / applies % applies - |
equal_to<Type> not_equal_to<Type> greater<Type> greater_equal<Type> less<Type> less_equal<Type> |
applies == applies != applies > applies >= applies < applies <= |
logical_and<Type> logical_or<Type> logical_not<Type> |
applies && applies | applies ! |
一般的運算
plus<int> intAdd; // function object that can add two int values negate<int> intNegate; // function object that can negate an int value // uses intAdd::operator(int, int) to add 10 and 20 int sum = intAdd(10, 20); // sum = 30 // uses intNegate::operator(int) to generate -10 as second parameter // to intAdd::operator(int, int) sum = intAdd(10, intNegate(10)); // sum = 0 |
除了一般的運算,這些標準庫定義的庫函數還可以用在算法調用中使用。
// passes temporary function object that applies > operator to two strings sort(svec.begin(), svec.end(), greater<string>()); |
函數對象的函數適配器
為啥要用適配器?
適配器有兩類:
1) Binders:綁定器。把二元函數對象轉換為一元的函數對象
又分成:bind1st, bind2nd.
2) Negators:取反器。函數對象的真值取反
又分成:not1, not2.
14.9 Conversions and Class Types
為什么轉換有用?
知了J
問題的提出是這樣的,如果我們需要定義一個SmartInt類,這個類的值被限制在0-255之間,也就是無符號字符(unsigned char),同時這個類還要支持所有的算術運算。如果沒有類型轉換,意味著我們要定義48個操作符。L
但是很幸運,C++提供了類型轉換機制,類定義應用在自己對象上的轉換規則。
SmallInt si(3); si + 3.14159; // 先把si轉換成int,再由int轉換成double,前者是類類型轉換,后者是標準類型轉換 |
轉換操作符
定義
轉換操作符必須是成員函數。它定義的是從一種類類型值轉換成其它類型的值。(A conversion operator is a special kind of class member function. It defines a conversion that converts a value of a class type to a value of some other type.)
class SmallInt { public: SmallInt(int i = 0): val(i) { if (i < 0 || i > 255) throw std::out_of_range("Bad SmallInt initializer"); } operator int() const { return val; } private: std::size_t val; }; |
一般形式:
operator type(); |
可以看出轉換操作符是沒有形參的,另外也不定義返回值的類型。(The function may not specify a return type, and the parameter list must be empty.)
這里的type即可是內置類型,也可以是類類型,甚至還可以是由typedef定義的名稱。(type represents the name of a built-in type, a class type, or a name defined by a typedef.)
另外轉換操作符強調的是轉換,它并不改變對象的值,因此轉換操作符一般都定義成const成員。例如上面的定義:operator int() const { return val; }
轉換類型
但是轉換類型也是有限制的,
首先void是不行的,數組(array)或者函數類型(function type)也是不可以的。
其次指針是可以的,無論是指向數據的還是指向函數的,都是可以的。
第三引用類型也是可以的。
類類型轉換只能應用一次(Class-Type Conversions and Standard Conversions)
類類型轉換后面只能跟標準類型轉換,而不能再跟類類型轉換了。(A class-type conversion may not be followed by another class-type conversion.)
糾結了L :
大師給了這個定義,注意si這個對象的創建過程是先把intVal通過類類型轉換為SmallInt,再調用拷貝構造函數而生成的。
但是如果SmallInt里定義了形參是Integral的構造函數,那SmallInt si(intVal);調用的是哪個?L
int calc(int); Integral intVal; SmallInt si(intVal); // ok: convert intVal to SmallInt and copy to si |
標準類型轉換優先于類類型轉換
因此下面的定義是有效的:
void calc(SmallInt); short sobj; // sobj 先由short 轉換為 int,這是標準類型轉換 // int通過 SmallInt(int) 構造函數轉換為 SmallInt ,這是使用構造函數來執行隱式轉換 calc(sobj); |
實參匹配和轉換
(這部分其實在第一次閱讀的時候可以跳過。因為都屬于advance類型的主題,比較糾結。今天北京又是一個高溫無雨,不過濕度有點大了,可怕的桑拿天L讀這部分。)
類類型轉換在帶來便利的同時也是編譯錯誤的源泉。用起來要格外的小心。之所以會是這樣,是由于有多種方式來實現一個類型到另一個類型的轉換。(Problems arise when there are multiple ways to convert from one type to another.)
實參匹配和多種轉換操作符
一般情況下,類提供到兩種內置類型的轉換,或者轉換到兩種內置類型的做法都是錯誤的。(Ordinarily it is a bad idea to give a class conversions to or from two built-in types.)例如Smallint類提供了int,還提供了double,那么下面的代碼就會讓編譯器很糾結:
void extended_compute(long double); SmallInt si; extended_compute(si); // error: ambiguous |
si可以轉換成int和double,然后呢,都可以通過標準類型轉換轉換為long double,完了,編譯器不知道該用那個了。
結論就是:
如果在調用時可以用到兩個轉換操作符,那么如果存在標準轉換等級時,那么跟隨轉換操作符的標準轉換等級就是用來確定最佳匹配的依據。(If two conversion operators could be used in a call, then the rank of the standard conversion, if any, following the conversion function is used to select the best match.
)
實參匹配和構造函數轉換
例如Smallint類提供了int,還提供了double類型的構造函數,那么下面的代碼就會產生二義性:
void manip(const SmallInt &); double d; int i; long l; manip(l); // error: ambiguous |
l有兩種方式來處理:
- 先標準轉換(long->double),再調用構造函數SmallInt(double)。
- 先標準轉換(long->int)),再調用構造函數SmallInt(int)。
因此編譯器就會報錯了。
結論:
如果類定義有兩個構造函數的類型轉換,標準轉換的級別(如果存在)用來判斷最佳匹配。(When two constructor-defined conversions could be used, the rank of the standard conversion, if any, required on the constructor argument is used to select the best match.)
當兩個類都定義轉換引起二義性
一個類定義的是構造函數的類型轉換,另一個類是類型轉換操作(conversion operation):
class Integral; class SmallInt { public: SmallInt(Integral); // convert from Integral to SmallInt // ... }; class Integral { public: operator SmallInt() const; // convert from SmallInt to Integral // ... }; void compute(SmallInt); Integral int_val; compute(int_val); // error: ambiguous |
int_val這個Integral類型如何轉換為SmallInt呢?因為有兩種個方法,編譯器是不會選擇的。
解決辦法就是顯式調用:
compute(int_val.operator SmallInt()); // ok: use conversion operator compute(SmallInt(int_val)); // ok: use SmallInt constructor |
結論:
最好的方法避免二義性是避免一對類都提供彼此間的隱式轉換。(The best way to avoid ambiguities or surprises is to avoid writing pairs of classes where each offers an implicit conversion to the other.)
重載確定類實參(Overload Resolution and Class Arguments)
標準轉換跟隨轉換操作符
哪個函數最匹配取決于在匹配的不同函數里是否包含一個或多個類類型轉換。(Which function is the best match can depend on whether one or more class-type conversions are involved in matching different functions.)
如果兩個轉換序列使用同樣的轉換操作,那么跟著類類型轉換的標準類型轉換作為選擇標準(The standard conversion sequence following a class-type conversion is used as a selection criterion only if the two conversion sequences use the same conversion operation.)
很難理解的概念,但是一看例子就了然了J
void compute(int); void compute(double); void compute(long double); SmallInt si; compute(si); |
調用的是void compute(int);因為是精確匹配。
多個轉換和重載確定
如果類類型里定義了兩個內置類型的轉換,例如SmallInt定義有轉換操作int()和double(),那么還是上面的例子compute(si);編譯器就會報錯,因為存在兩種調用方式:
void compute(int);
void compute(double);
編譯器傻了。
解決辦法:為了避免二義性而顯式強制轉換(Explicit Constructor Call to Disambiguate)
SmallInt si; compute(static_cast<int>(si)); // ok: convert and call compute(int) |
標準轉換和構造函數
兩個類都定義了相同類型的構造函數:
class SmallInt { public: SmallInt(int = 0); }; class Integral { public: Integral(int = 0); }; void manip(const Integral&); void manip(const SmallInt&); manip(10); // error: ambiguous |
解決方法就是顯式調用構造函數:
manip(SmallInt(10)); // ok: call manip(SmallInt) manip(Integral(10)); // ok: call manip(Integral) |
另外,即使SmallInt中定義的類型轉換是short,也是沒意義的,編譯器依然會報錯。事實上,當選擇重載版本的調用時,一個調用標準類型轉換,另一個不需要,這無關緊要。( The fact that one call requires a standard conversion and the other does not is immaterial when selecting among overloaded versions of a call.)編譯器必不是更喜歡直接的構造函數。
重載,轉換和操作符(Overloading, Conversions, and Operators)
這個問題的提出是這樣滴:
ClassX sc; int iobj = sc + 3; |
這樣一個語句,你說這是操作符重載,或者是類類型轉換(構造函數,轉換操作符)?
重載確定和操作符
成員函數和非成員函數都是有可能的這一事實改變選擇候選函數的方式。(The fact that member and nonmember functions are possible changes how the set of candidate functions is selected.)
操作符的候選函數
根據表達式中用到的操作符,候選函數包括內置的操作符以及所有一般的非成員函數版本的操作符。除此之外,如果左邊的操作數是類類型,那么候選函數還應包括類定義的重載操作符。(In the case of an operator used in an expression, the candidate functions include the built-in versions of the operator along with all the ordinary nonmember versions of that operator. In addition, if the left-hand operand has class type, then the candidate set will contain the overloaded versions of the operator, if any, defined by that class.)
調用自己決定要考慮的名字的范圍(the call itself determines the scope of names that will be considered.)如果調用是通過類對象來完成的,那么只會考慮類的成員函數。
轉換能夠導致內置操作符的二義性(Conversions Can Cause Ambiguity with Built-In Operators)
同一個類即提供到算術類型的轉換函數又重載操作符,就會導致重載操作符和內置操作符的二義性。
這樣的例子就是SmallInt即定義到int的轉換函數,又重載操作符’+’,那么int i = s3 + 0;就會產生二義性的錯誤,是把s3轉換成int再去計算,還是把0通過構造函數轉換成SmallInt再去計算呢?
class SmallInt { public: SmallInt(int = 0); // convert from int to SmallInt // conversion to int from SmallInt operator int() const { return val; } // arithmetic operators friend SmallInt operator+(const SmallInt&, const SmallInt&); private: std::size_t val; }; SmallInt s3 = s1 + s2; // ok: uses overloaded operator+ int i = s3 + 0; // error: ambiguous |
規則(rules of thumb)
針對類進行重載操作符(overloaded operators)、構造函數轉換(conversion constructors)以及轉換函數(conversion functions)設計必須小心。構造函數轉換(conversion constructors)以及轉換函數(conversion functions)又可以統稱為轉換操作(conversion operators)。如果類既定義了轉換操作又重載操作符,那么很容易產生二義性。(ambiguities are easy to generate if a class defines both conversion operators and overloaded operators.)這里有兩條規則:
1. 不要定義相互轉換類。(Never define mutually converting classe)
2. 避免定義轉換到內置算術類型的轉換函數。(Avoid conversions to the built-in arithmetic types.)特別的,如果定義了這類函數,那么
l 不要重載以算術類型為形參的操作符。(Do not define overloaded versions of the operators that take arithmetic types.)
l 不要定義超過一種的轉換到算術類型的轉換函數。(Do not define a conversion to more than one arithmetic type.)如果定義了到int的轉換函數就不要定義轉換到double的轉換函數。
posted on 2009-07-01 08:33 amenglai 閱讀(855) 評論(0) 編輯 收藏 所屬分類: C++ Primer 之 讀書筆記