文章目錄
- 基本概念
- 直接調用一個重載的運算符函數
- 某些運算符不應該被重載
- 使用與內置類型一致的含義
- 選擇作為成員或者非成員
- 輸入和輸出運算符
- 重載輸出運算符<<
- 輸出運算符盡量減少格式化操作
- 輸入輸出運算符必須是非成員函數
- 重載輸入運算符>>
- 算術和關系運算符
- 相等運算符
- 關系運算符
- 賦值運算符
- 復合賦值運算符
- 下標運算符
- 遞增和遞減運算符
- 區分前置和后置運算符
- 顯式地調用后置運算符
- 成員訪問運算符
- 函數調用運算符
- 含有狀態的函數對象類
- lambda是函數對象
- 表示lambda及相應捕獲行為的類
- 標準庫定義的函數對象
- 在算法中使用標準庫函數對象
- transform函數
- 可調用對象與function
- 不同類型可能具有相同的調用形式
- 重載的函數與function
- 重載、類型轉換與運算符
- 類型轉換運算符
- 定義含有類型轉換運算符的類
- 顯式的類型轉換運算符
- 避免有二義性的類型轉換
基本概念
如果一個運算符函數是成員函數,則它的第一個(左側)運算對象綁定到隱式的this指針上,因此,成員運算符函數的(顯式)參數數量比運算符的運算對象總數少一個。
對于一個運算符函數來說,它或者是類的成員,或者至少含有一個類類型的參數:
錯誤,不能為int重定義內置的運算符
int operator+(int,int);
我們可以重載大多數運算符,但不是全部。我們只能重載已有的運算符,而無權發明新的運算符號。
對于一個重載的運算符來說,其優先級和結合律與對應的內置運算符保持一致。
直接調用一個重載的運算符函數
一個非成員運算符函數的等價調用
data1+data2; //普通的表達式
operator+(data1,data2); // 等價的函數調用
調用成員運算符函數
data1+=data2; //基于“調用”的表達式
data1.operator+=(data2); // 對成員運算符函數的等價調用
某些運算符不應該被重載
通常情況下,不應該重載逗號、取地址、邏輯與、邏輯或等運算符。
使用與內置類型一致的含義
選擇作為成員或者非成員
當我們定義重載的運算符時,必須首先決定是將其聲明為類的成員函數還是聲明為一個普通的非成員函數。在某些時候我們別無選擇,因為有的運算符必須作為成員;另一些情況下,運算符作為普通函數比作為成員更好。
%通常定義為非成員
%=通常定義為類成員,因為它會改變對象的狀態
++通常定義為類成員,因為它會改變對象的狀態
->必須定義為類成員,否則編譯會報錯
<<通常定義為非成員
&&通常定義為非成員
==通常定義為非成員
()必須定義為類成員,否則編譯會報錯
輸入和輸出運算符
IO標準庫分別使用>>和<<執行輸入和輸出操作。對于這兩個運算符來說,IO庫定義了用其讀寫內置類型的版本,而類則需要自定義適合其對象的新版本以支持IO操作。
重載輸出運算符<<
通常情況下,輸出運算符的第一個形參是一個非常量ostream對象的引用。之所以ostream是非常量是因為向流寫入內容會改變其狀態,而該形參是引用是因為我們無法直接復制一個ostream對象。
第二個形參一般來說是一個常量的引用,該常量是我們想要打印的類類型。第二個形參是引用的原因是我們希望避免復制實參,而之所以該形參可以是常量是因為(通常情況下)打印對象不會改變對象的內容。
為了與其他輸出運算符保持一致,operator<<一般要返回它的ostream形參。
示例代碼:
ostream& operator<<(ostream&os,const Sales_data &item){//輸出內容os<<item.isbn()<<" "<<item.units_sold;//返回ostreamreturn os;
}
輸出運算符盡量減少格式化操作
用于內置類型的輸出運算符不太考慮格式化操作,尤其不會打印換行符,用戶希望類的輸出運算符也像如此行事。如果運算符打印了換行符,則用戶就無法在對象的同一行內接著打印一些描述性的文本了。相反,令輸出運算符盡量減少格式化操作可以使用戶有權控制輸出的細節。
輸入輸出運算符必須是非成員函數
與iostream標準庫兼容的輸入輸出運算符必須是普通的非成員函數,而不能是類的成員函數。否則,它們的左側運算對象將是我們的類的一個對象:
Sales_data data;
data<<cout; //如果operator<<是Sales_data的成員
因此,如果我們希望為類自定義IO運算符,則必須將其定義為非成員函數。IO運算符通常需要讀寫類的非公有數據成員,所以IO運算符一般被聲明為友元。
重載輸入運算符>>
通常情況下,輸入運算符的第一個形參是運算符將要讀取的流的引用,第二個形參是將要讀入到的(非常量)對象的引用。該運算符通常會返回某個給定流的引用。 第二個形參之所以必須是個非常量是因為輸入運算符本身的目的就是將數據讀入到這個對象中。
輸入運算符必須處理輸入可能失敗的情況,而輸出運算符不需要。
輸入時的錯誤:
- 當流含有錯誤類型的數據時讀取操作可能失敗。
- 當讀取操作到達文件末尾或者遇到輸入流的其他錯誤時也會失敗。
通常情況下,輸入運算符只設置failbit。除此之外,設置eofbit表示文件耗盡,而設置badbit表示流被破壞。
算術和關系運算符
通常,我們把算術和關系運算符定義成非成員函數以允許對左側或右側的運算對象進行轉換。因為這些運算符一般不需要改變運算對象的狀態,所以形參都是常量的引用。
算術運算符通常會計算它的兩個運算對象并得到一個新值,這個值有別于任意一個運算對象,常常位于一個局部變量之內,操作完成后返回該局部變量的副本作為其結果。如果類定義了算術運算符,則它一般也會定義一個對應的復合賦值運算符。此時最有效的方式是使用復合賦值來定義算術運算符:
Sales_data operator+(const Sales_data &lhs,const Sales_data &rhs){Sales_data sum = lhs;//把lhs的數據成員拷貝給sumsum+=rhs; //使用復合賦值運算符將rhs加到sum中return sum;
}
如果類同時定義了算術運算符和相關的復合賦值運算符,則通常情況下應該使用復合賦值來實現算術運算符。
相等運算符
通常情況下,c++中的類通過定義相等運算符來檢驗兩個對象是否相等。也就是說,它們會比較對象的每一個數據成員,只有當所有對應的成員都相等時才認為兩個對象相等。
關系運算符
定義了相等運算符的類也常常(但不總是)包括關系運算符。特別是,因為關聯容器和一些算法要用到小于運算符,所以定義operator<會比較有用。
通常情況下,關系運算符應該:
- 定義順序關系,令其與關聯容器中對關鍵字的要求一致。
- 如果類同時也含有 == 運算符的話,則定義一種關系令其與 == 保持一致。特別是,如果兩個對象是 != 的,那么一個對象應該 < 另外一個。
如果存在唯一一種邏輯可靠的 < 定義,則應該考慮為這個類定義 < 運算符。如果類同時還包含 == ,則當且僅當 < 的定義和 == 產生的結果一致時才定義 < 運算符。
賦值運算符
我們可以重載賦值運算符,不論形參的類型是什么,賦值運算符都必須定義為成員函數。
示例代碼:
復合賦值運算符
復合賦值運算符不非得是類的成員,不過我們還是傾向于把包括復合賦值在內的所有賦值運算都定義在類的內部。為了與內置類型的復合賦值保持一致,類中的復合賦值運算符也要返回其左側運算對象的引用。
示例代碼:
賦值運算符必須定義成類的成員,復合賦值運算符通常情況下也應該這樣做。這兩類運算符都應該返回左側運算對象的引用。
下標運算符
表示容器的類通常可以通過元素在容器中的位置訪問元素,這些類一般會定義下標運算符operator[ ]。
下標運算符必須是成員函數。
為了與下標的原始定義兼容,下標運算符通常以所訪問元素的引用作為返回值,這樣做的好處是下標可以出現在賦值運算符的任意一端。進一步,我們最好同時定義下標運算符的常量版本和非常量版本,當作用于一個常量對象時,下標運算符返回常量引用以確保我們不會給返回的對象賦值。
示例代碼:
遞增和遞減運算符
定義遞增和遞減運算符的類應該同時定義前置版本和后置版本。這些運算符通常應該被定義成類的成員。
為了與內置版本保持一致,前置運算符應該返回遞增或遞減后對象的引用。
區分前置和后置運算符
前置和后置版本使用的是同一個符號,意味著其重載版本所用的名字將是相同的,并且運算對象的數量和類型也相同。為了解決這個問題,后置版本接受一個額外的(不被使用的)int類型的形參。當我們使用后置運算符時,編譯器為這個形參提供一個值為0的實參。盡管從語法上來說,后置函數可以使用這個額外的形參,但是在實際過程中通常不會這么做。這個形參的唯一作用就是區分前置版本和后置版本的函數,而不是真的要在實現后置版本時參與運算。
為了與內置版本保持一致,后置運算符應該返回對象的原值(遞增或遞減之前的值),返回的形式是一個值而非引用。
后置版本示例代碼:
顯式地調用后置運算符
StrBlobPtr p(a1);
p.operator++(0); //調用后置版本的operator++
p.operator++(); //調用前置版本的operator++
成員訪問運算符
箭頭運算符 -> 必須是類的成員。解引用運算符 * 通常也是類的成員。
重載的箭頭運算符必須返回類的指針或者自定義了箭頭運算符的某個類的對象。
函數調用運算符
如果類重載了函數調用運算符,則我們可以像使用函數一樣使用該類的對象。因為這樣的類同時也能存儲狀態,所以與普通函數相比它們更加靈活。
函數調用運算符必須是成員函數,一個類可以定義多個不同版本的調用運算符,相互之間應該在參數數量或類型上有所區別。
如果類定義了調用運算符,則該類的對象稱作函數對象。因為可以調用這種對象,所以我們說這些對象的“行為像函數一樣”。
class absInt{int operator()(int val)const{return val < 0 ? -val : val;}
}int i=-42;
absInt absObj; //含有函數調用運算符的對象
int ui = absObj(i); //將i傳遞給absObj.operator()
即使absObj只是一個對象而非函數,我們也能“調用”該對象。調用對象實際上是在運行重載的調用運算符。該例中,該運算符接受一個int值并返回其絕對值。
含有狀態的函數對象類
和其他類一樣,函數對象類除了operator()之外也可以包含其他成員。函數對象類通常含有一些數據成員,這些成員被用于定制調用運算符中的操作。
示例代碼:
#ifndef PRINTSTRING_H
#define PRINTSTRING_H#include<iostream>
#include<string>
using namespace std;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;
};
#endif
測試代碼:
void testPrintString() {PrintString p1;p1("hello");PrintString p2(cout, '!');p2("hello");
}
輸出結果:
hello hello!
示例代碼:
//IntCompare類
class IntCompare {
public:IntCompare(int v) :val(v) {}bool operator()(int v) { return val == v; }
private:int val;
};//測試代碼vector<int>vec = { 1,2,3,2,1 };int oldVal = 2;int newVal = 200;IntCompare icmp(oldVal);replace_if(vec.begin(),vec.end(),icmp,newVal);for (auto a:vec) {cout << a << " ";}
輸出結果:
1 200 3 200 1
lambda是函數對象
在lambda表達式產生的類中含有一個重載的函數調用運算符,例如:
[](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());
當stable_sort內部的代碼每次比較兩個string時就會“調用”這一對象,此時該對象將調用運算符的函數體,判斷第一個string的大小小于第二個時返回true。
表示lambda及相應捕獲行為的類
當一個lambda表達式通過引用捕獲變量時,將由程序負責確保lambda執行時引用所引的對象確實存在。因此編譯器可以直接使用該引用而無須在lambda產生的類中將其存儲為數據成員。
如果通過值捕獲的變量被拷貝到lambda中,這種lambda產生的類必須為每個值捕獲的變量建立對應的數據成員,同時創建構造函數,令其使用捕獲的變量的值來初始化數據成員。
示例如下:
[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;//該數據成員對應通過值捕獲的變量
}
這個類含有一個數據成員以及一個用于初始化該成員的構造函數。這個合成的類不含有默認構造函數,因此要想使用這個類必須提供一個實參:
auto wc = find_if(words.begin(),words.end(),SizeComp(sz));
lambda表達式產生的類不含默認構造函數、賦值運算符及默認析構函數;它是否含有默認的拷貝 / 移動構造函數則通常要視捕獲的數據成員類型而定。
標準庫定義的函數對象
標準庫定義了一組表示算術運算符、關系運算符和邏輯運算符的類,每個類分別定義了一個執行命名操作的調用運算符。例如,plus類定義了一個函數調用運算符用于對一對運算對象執行+的操作;modulus類定義了一個調用運算符執行二元的%操作;equal_to類執行==。
示例代碼
plus<int>intAdd;negate<int>intNegate;//negate<int>可對int取反int sum = intAdd(10,20);cout << sum << endl;sum = intNegate(intAdd(10, 20));cout << sum << endl;sum = intAdd(10, intNegate(20));cout << sum << endl;
輸出結果:
30
-30
-10
標準庫函數對象,下表所列的類型定義在functional頭文件中
在算法中使用標準庫函數對象
表示運算符的函數對象常用來替換算法中的默認運算符。例如,默認情況下,排序算法使用operator<將序列按照升序排列,如果要執行降序排列的話,我們可以傳入一個greater類型的對象。該類將產生一個調用運算符并負責執行待排序類型的大于運算。例如,如果svec是一個vector<string>
sort(svec.begin(),svec.end(),greater<string>());
示例代碼:
vector<int>vec{ 1,3,5,7,9,2,4,6,8,10 };//統計大于4的值有多少個int num = count_if(vec.begin(),vec.end(),bind2nd(greater<int>(),4));cout << num << endl;vector<string>sv{"hello","hello","hi","nihao","nihao"};//找到第一個不等于hello的字符串auto its = find_if(sv.begin(),sv.end(), bind2nd(not_equal_to<string>(), "hello"));cout << *its << endl;//將所有的值乘以2transform(vec.begin(), vec.end(), vec.begin(), bind2nd(multiplies<int>(), 2));for (auto a:vec) {cout << a << " ";}cout << endl;
輸出結果:
6
hi
2 6 10 14 18 4 8 12 16 20
transform函數
transform函數的作用是:將某操作應用于指定范圍的每個元素。transform函數有兩個重載版本:
transform(first,last,result,op);
first是容器的首迭代器,last為容器的末迭代器,result為存放結果的容器,op為要進行操作的一元函數對象或sturct、class。
transform(first1,last1,first2,result,binary_op);
first1是第一個容器的首迭代 器,last1為第一個容器的末迭代器,first2為第二個容器的首迭代器,result為存放結果的容器,binary_op為要進行操作的二元函數 對象或sturct、class。
可調用對象與function
c++語言中有幾種可調用的對象:函數、函數指針、lambda表達式、bind創建的對象以及重載了函數調用運算符的類。
和其他對象一樣,可調用的對象也有類型。例如,每個lambda有它自己唯一的(未命名)類類型;函數及函數指針的類型則由其返回值類型和實參類型決定,等等。
不同類型可能具有相同的調用形式
上面這些可調用對象分別對其參數執行了不同的算術運算。盡管它們的類型各不相同,但是共享同一種調用形式:int(int,int)
我們可以定義一個函數表用于存儲指向這些可調用對象的“指針”。當程序需要執行某個特定的操作時,從表中查找該調用的函數。
函數表可以很容易的通過map來實現。我們的map可以定義成如下形式:
//構建從運算符到函數指針的映射關系,其中函數接受兩個int、返回一個int
map<string,int(*)(int,int)>binops;
我們可以按照下面的形式將add的指針添加到binops中:
binops.insert({"+",add});//{"+",add}是一個pair
但是我們不能將mod或者divide存入binops中,因為mod是個lambda表達式,而每個lambda有它自己的類類型,該類型于存儲在binops中的類型不匹配。
binops.insert({"%",mod});//錯誤:mod不是一個函數指針
我們可以使用一個名為function的新的標準庫類型解決上述問題,function定義在functional頭文件中,下表列出了function定義的操作:
function是一個模板,和我們使用過的其他模板一樣,當創建一個具體的function類型時我們必須提供額外的信息。示例如下:
function<int(int,int)>
在這里我們聲明了一個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};//f1(4,2): 6
//f2(4,2): 2
//f3(4,2): 8
使用這個function我們可以重新定義map:
map<string,function<int(int,int)>>binops;
我們能把所有可調用對象,包括函數指針、lambda或者函數對象在內,都添加到這個map中:
map<string,function<int(int,int)>>binops={
{"+",add}, //函數指針
{"-",std::minus<int>()}, //標準庫函數對象
{"/",divide()}, //用戶定義的函數對象
{"*",[](int i,int j){return i*j}}, //未命名的lambda
{"%",mod}, //命名的lambda
}
調用操作:
binops["+"](10,5); //調用add(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;
};//測試代碼:SmallInt s1;s1 = 4;s1.print();cout << s1 + 5 << endl;s1 = 3.5;s1.print();cout << s1 + 5 << endl;
輸出結果:
4
9
3
8
因為類型轉換運算符是隱式執行的,所以無法給這些函數傳遞實參,當然也就不能在類型轉換運算符的定義中使用任何形參。同時,盡管類型轉換函數不負責指定返回類型,但實際上每個類型轉換函數都會返回一個對應類型的值:
顯式的類型轉換運算符
當類型轉換運算符是顯式的時候,我們也能執行類型轉換,不過必須通過顯式的強制類型轉換才可以。
class SmallInt {
public:SmallInt(int i = 0) :val(i) {}explicit operator int()const { return val; }void print() { cout << val << endl; }
private:size_t val;
};//測試代碼SmallInt s1;s1 = 4;s1.print();cout << int(s1) + 5 << endl;s1 = 3.5;s1.print();cout << int(s1) + 5 << endl;
如果表達式被用作條件,則編譯器會將顯式的類型轉換自動應用于它。換句話說,當表達式出現在下列位置時,顯式的類型轉換將被隱式地執行:
向bool的類型轉換通常用在條件部分,因此operator bool一般定義成explicit的。
避免有二義性的類型轉換
如果類中包含一個或多個類型轉換,則必須確保在類類型和目標類型之間只存在唯一一種轉換方式。否則的話,我們編寫的代碼很可能會具有二義性。
在兩種情況下,可能產生多重轉換路徑。
- 兩個類提供相同的類型轉換,例如,當A類定義了一個接受B類對象的轉換構造函數,同時B類定義了一個轉換目標是A類的類型轉換運算符時,我們就說它們提供了相同的類型轉換。
- 類定義了多個轉換規則,而這些轉換涉及的類型本身可以通過其他類型轉換聯系在一起。最典型的例子是算術運算符,對某個給定的類來說,最好只定義最多一個與算術類型有關的轉換規則。
除了顯式地向bool類型的轉換之外,我們應該盡量避免定義類型轉換函數并盡可能地限制那些“顯然正確”的非顯式構造函數。