日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 人文社科 > 生活经验 >内容正文

生活经验

C++拾趣——有趣的操作符重载

發(fā)布時(shí)間:2023/11/27 生活经验 20 豆豆
生活随笔 收集整理的這篇文章主要介紹了 C++拾趣——有趣的操作符重载 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

? ? ? ? 操作符重載是C++語言中一個非常有用的特性。它可以讓我們比較優(yōu)雅的簡化代碼,從而更加方便的編寫邏輯。

為什么要使用操作符重載

? ? ? ? 一種常見的用法是重載<<運(yùn)算符,讓標(biāo)準(zhǔn)輸出可以輸出自定義的類型。比如

#include <iostream>class Sample {friend std::ostream& operator<<(std::ostream &out, const Sample& smp);
private:int _m = 0;
};std::ostream& operator << (std::ostream &out, const Sample& smp) {out << smp._m;return out;
}int main() {Sample sample;std::cout << sample << std::endl; // output 0return 0;
}

? ? ? ? Sample是非標(biāo)準(zhǔn)類型,編譯器并不知道怎么使用標(biāo)準(zhǔn)輸出輸出它——是輸出它的內(nèi)存結(jié)構(gòu)還是輸出它的一些變量呢?這個時(shí)候Sample類的作者通過重載<<運(yùn)算符,告知編譯器“我”是想輸出它的某個成員變量。(轉(zhuǎn)載請指明出于breaksoftware的csdn博客)

? ? ? ? 如果我們不進(jìn)行重載,則可能像如下代碼實(shí)現(xiàn)功能

#include <iostream>class Sample {
public:int value() {return _m;}
private:int _m = 0;
};int main() {Sample sample;std::cout << sample.value() << std::endl; // output 0return 0;
}

? ? ? ? 這種寫法,需要對每個需要輸出的成員變量定義一個“訪問器”,“訪問器”的個數(shù)隨著需要輸出的成員變量個數(shù)線性增加。假如“訪問器”只有在標(biāo)準(zhǔn)輸出時(shí)才使用,且不存在需要單獨(dú)輸出某些變量的場景,這種實(shí)現(xiàn)就顯得不那么“智慧”——大量“訪問器”函數(shù)的重用次數(shù)太低了。

? ? ? ? 有人會提出可以定義一個類似print的函數(shù)來實(shí)現(xiàn)

#include <iostream>class Sample {
public:void print() {std::cout << _m;}
private:int _m = 0;
};int main() {Sample sample;sample.print();std::cout << std::endl; // output 0return 0;
}

? ? ? ? 這種寫法亦不那么“智慧”。這給試圖輸出組合信息的使用者帶來麻煩。本來一行可以輸出類的信息和換行符,在上例中就需要寫兩行。這種邏輯的“割裂”是不優(yōu)雅的。

? ? ? ? 可能有人會說:雖然我認(rèn)同操作符重載是優(yōu)雅的,但是這樣的“教學(xué)例子”仍然讓我無法感知到它的重要性。是的,因?yàn)槔犹唵?。以一個工作中的場景為例:

? ? ? ? 工作中經(jīng)常會用到Json或者XML等格式的數(shù)據(jù),一般情況下,我們都需要將這些格式轉(zhuǎn)換成一個對象來訪問。假如我們不太清楚該格式的具體組織形式以及字段名稱或者值類型,難道我們要一個個遍歷整個對象么?這個時(shí)候,以“肉眼”可以看懂的格式輸出該對象就顯得非常必要了??赡苡腥藭fJson和XML的內(nèi)容是可以肉眼識別的。的確,但是如果該數(shù)據(jù)是一種二進(jìn)制的結(jié)構(gòu)呢?

重載操作符需要遵從“隱性共識”

? ? ? ? C++給了程序員很多自由,但是自由永遠(yuǎn)都是相對的。因?yàn)橹剌d操作符是存在一些隱性的共識,這些共識是我們要遵從的,否則將失去操作符重載的意義,甚至?xí)o使用者帶來極大的困擾。

? ? ? ? 隱性的共識包含幾個部分:

  • 符合自然語義。比如我們重載操作符=,隱性的共識是該操作符將會產(chǎn)生賦值行為。而如果我們什么都不去實(shí)現(xiàn),則違反了共識。再比如,我們重載++操作符,其隱性的共識是需要對關(guān)鍵信息進(jìn)行自增。如果我們實(shí)現(xiàn)時(shí),讓關(guān)鍵信息自減了,那也是明顯違反共識的。
  • 操作符存在關(guān)聯(lián)性。關(guān)聯(lián)性又分為:對等性和復(fù)合性。下面我們將針對這兩個特性進(jìn)行討論。

? ? ? ? 自增和自減操作符是對等,它們都會對對象的關(guān)鍵信息作出修改。但是對等性要求我們,如果自增是對關(guān)鍵信息增加1,那么自減就是對該信息減少1。不可以產(chǎn)生一次自增,需要幾次自減才會恢復(fù)到原始值的現(xiàn)象。

? ? ? ? 復(fù)合性是指:+操作和+=操作,*操作和*=操作……這種存在組合關(guān)聯(lián)的操作符。比如我們實(shí)現(xiàn)了+操作符的重載,也就需要實(shí)現(xiàn)+=的重載。因?yàn)槲覀儫o法保證別人不去使用+=去進(jìn)行“加”和“賦值”的操作。對于一對操作符,一般來說,我們讓兩者實(shí)現(xiàn)存在協(xié)同關(guān)系,即+使用+=實(shí)現(xiàn),或者+=使用+和=實(shí)現(xiàn)??磦€例子

class Sample {
public:Sample& operator=(const Sample& smp) {_m = smp._m;return *this;}   Sample operator+(const Sample& smp) {Sample tmp(*this);tmp += smp;return tmp;}   Sample& operator+=(const Sample& smp) {_m += smp._m;return *this;}
private:int _m = 0;
}

? ? ? ? 上例中我們使用+=實(shí)現(xiàn)了+操作。這兒一個有趣的點(diǎn)是第4行,我們直接使用了smp._m——_m可是私有變量啊。其實(shí)不用擔(dān)心,因?yàn)閟mp也是Sample對象,且這個重載是Sample類的成員函數(shù),所以在語法上是合法的。

?自增、自減的前置和后置

? ? ? ? 自增(++)和自減(--)是非常獨(dú)特的單目運(yùn)算符,它可以出現(xiàn)在操作數(shù)的前面或者后面。如果出現(xiàn)在前面,則隱性共識是:自增(減)關(guān)鍵信息,并返回自身;如果出現(xiàn)在后面,則隱性共識是:自增(減)關(guān)鍵信息,返回自增(減)之前的自身。其一般實(shí)現(xiàn)是:構(gòu)造一個和自身相同的臨時(shí)對象,自增(減)關(guān)鍵信息,返回臨時(shí)對象。

? ? ? ? 之前有一種與此相關(guān)的面試題。面試官會:A和B兩者寫法,哪個執(zhí)行更高效?

// A
for (int i = 0; i < 8; i++) {}// B
for (int i = 0; i < 8; ++i) {}

? ? ? ? 這個問題就是考察后置自增(減)會構(gòu)造臨時(shí)對象的知識點(diǎn)。但是就此例子來看,這個問題構(gòu)造的并不好。因?yàn)楝F(xiàn)在的編譯器已經(jīng)比較智能了,它會識別該場景不需要構(gòu)造臨時(shí)變量,于是A編譯出的指令和B編譯出的指令是一致的,執(zhí)行效果也是一樣的。

? ? ? ? 由于自增和自減是對等的,簡單起見,之后的討論我只以自增為例。

? ? ? ? 問題來了:

  • 前置和后置是否需要分開實(shí)現(xiàn)?由于兩者執(zhí)行邏輯不同,我們不可能通過重載一個操作符實(shí)現(xiàn)另外一個功能,所以這個答案是“是”。
  • 是否只需要重載前置或者后置?如果我只重載前置,那么使用者只能在使用前置操作符時(shí)才能產(chǎn)生正確的行為,但是使用者不知道后置是不能使用的。這種不對等的行為也是違反“隱性共識”的。所以這個問題的答案是“否”。
  • 前置和后置是同一個操作符,如何在重載聲明上表現(xiàn)出區(qū)別?這個問題的答案就是C++的一個語法糖,也是本文標(biāo)題中“有趣”的一個點(diǎn)。

? ? ? ? C++使用了一種語法糖來區(qū)分前置和后置——前置重載無參數(shù),后置重載有一個int型參數(shù)。看個例子

class Sample {
public:Sample& operator++() {std::cout << "prefix ++" << std::endl;;++_m;return *this;}   Sample operator++(int n) {std::cout << "postfix ++" << n << std::endl;Sample tmp(*this);++*this;return tmp;}
private:int _m = 0;
}

? ? ? ? 第3行是前置實(shí)現(xiàn),它只是簡單的對成員變量進(jìn)行了自增,然后返回對象本身。第9行是后置實(shí)現(xiàn),它在自增前使用了拷貝構(gòu)造函數(shù)構(gòu)造了一個和當(dāng)前對象保存一樣信息的臨時(shí)對象,然后自增當(dāng)前對象,最后返回了臨時(shí)對象。

? ? ? ? 在進(jìn)行后置操作符調(diào)用時(shí),如果沒有指定參數(shù),系統(tǒng)會默認(rèn)傳入0。所以第9行,n的值默認(rèn)是0。

? ? ? ? 介于這種語法,我們還可以如下調(diào)用前置操作

    sample.operator++();

? ? ? ? 或者這樣調(diào)用后置操作。然傳入的是10,系統(tǒng)也的確把10傳入了重載函數(shù),但是我們不應(yīng)該去使用它。因?yàn)檫@只是C++的一個無可奈何的語法糖。

    sample.operator++(10);

? ? ? ? 再回到之前的面試題,如果面試官詢問++sample和sample++哪個效率高些時(shí),你則可以告知是前置高些,因?yàn)楹笾梅绞绞褂昧丝截悩?gòu)造函數(shù)構(gòu)造了一個臨時(shí)對象。

&&、||的短路求值特性

? ? ? ? 除了自增、自減具有“前置”或者“后置”區(qū)別外,還有一組操作符——&&和||具有特殊的屬性——短路求值。假如我們重載&&或者||操作符,則沒法保證該特性,而它卻是“隱性共識”。

if (ptr && ptr->suc()) {// do somethind
}

? ? ? ? 上例中,我們希望先檢測ptr是否為空,然后再調(diào)用suc方法。因?yàn)槟J(rèn)的&&支持短路求值,所以如果ptr為空,則整個判斷結(jié)果為假,那么suc函數(shù)不會被執(zhí)行。

if (ptr->value() > 10 || ptr->value() < -10) {// do something
}

? ? ? ? ||操作的短路求值是:從左向右,只要遇到一個條件為真的,則整個判斷為真。之后的檢測不用執(zhí)行了。所以如果ptr->value()值是20,那么只會判斷20是否大于10(因?yàn)橐呀?jīng)為真),而不會去判斷是否小于-10。

? ? ? ? 但是重載這兩個操作符就會破壞短路求值特性。比如

#include <iostream>class Sample {friend std::ostream& operator<<(std::ostream &out, const Sample& smp);friend bool operator&&(bool pre, const Sample& smp);friend bool operator||(bool pre, const Sample& smp);
public:Sample& operator++() {std::cout << "prefix ++" << std::endl;;++_m;return *this;}Sample operator++(int n) {std::cout << "postfix ++" << n << std::endl;Sample tmp(*this);++*this;return tmp;}bool operator&&(const Sample& smp) {return _m && smp._m;}bool operator||(const Sample& smp) {return _m || smp._m;}private:int _m = 0;
};std::ostream& operator << (std::ostream &out, const Sample& smp) {out << smp._m;return out;
}bool operator&&(bool pre, const Sample& smp) {return pre && smp._m;
}bool operator||(bool pre, const Sample& smp) {return pre || smp._m;
}int main() {Sample* sample = NULL;std::cout << "sample && (*sample) && (*sample)++ " << (sample && (*sample) && (*sample)++) << std::endl;return 0;
}

? ? ? ? 這個程序的執(zhí)行結(jié)果是

postfix ++0
Segmentation fault

? ? ? ? 最后它崩了。如果按照短路求值特性,由于sample為空,則整個運(yùn)算結(jié)果為假。但是重載&&操作符后,(*sample)++被執(zhí)行,從而將導(dǎo)致違例。

? ? ? ? 再看看||的操作

Sample* sample = new Sample;
std::cout << "sample || (*sample) || (*sample)++ " << (sample || (*sample) || (*sample)++) << std::endl;

? ? ? ? 它的輸出是

postfix ++0
prefix ++
sample || (*sample) || (*sample)++ 1

? ? ? ? 如果按照短路求值,由于sample不為空,則整個運(yùn)算結(jié)果為真。但是重載了||操作符后,短路求值特性丟失,于是要把所有||的操作都執(zhí)行一遍(最后執(zhí)行了自增操作)。

(非)成員函數(shù)和隱式構(gòu)造

? ? ? ? 操作符重載可以定義為外部函數(shù)(因?yàn)榭赡軙L問私有變量,所以常常被聲明為友元),也可以定義為成員函數(shù)。

? ? ? ? 以二目操作符為例。如果操作符重載被定義為成員函數(shù),則重載函數(shù)的參數(shù)(如果有的話)是操作符右側(cè)值。因?yàn)槌蓡T函數(shù)隱藏了this指針,所以操作符左側(cè)值就是this指針指向的對象。

? ? ? ? 如果定義為外部函數(shù),則函數(shù)的兩個參數(shù)分別為操作符的左、右值。

#include <iostream>class Sample {friend std::ostream& operator<<(std::ostream &out, const Sample& smp);friend Sample operator+(const Sample& smpL, const Sample& smpR);
public:Sample() {}Sample(int n) : _m(n) {}Sample operator+(const Sample& smpR) {Sample tmp(*this);tmp += smpR;return tmp;}Sample& operator+=(const Sample& smp) {_m += smp._m;return *this;}
private:int _m = 0;
};std::ostream& operator << (std::ostream &out, const Sample& smp) {out << smp._m;return out;
}Sample operator+(const Sample& smpL, const Sample& smpR) {return Sample(smpL._m + smpR._m);
}

? ? ? ? 上面例子第14行是加法的成員函數(shù)式的重載,第33行是友元式的重載。

? ? ? ? 這兩種實(shí)現(xiàn)是有區(qū)別的,區(qū)別就是對隱式構(gòu)造函數(shù)的處理。

? ? ? ? 如果只有成員函數(shù)式重載,則下面的調(diào)用方式可以工作。因?yàn)椴僮鞣髠?cè)值是Sample對象。

Sample sample;
sample = sample + 2;

? ? ? ? 但是下面的代碼不能編譯通過,因?yàn)樽髠?cè)值是個整型。

sample = 2 + sample;

? ? ? ? 如果想解決這個問題,就可以將加法重載設(shè)置為外部形式。這樣編譯器會將2隱式構(gòu)造成一個Sample臨時(shí)對象(調(diào)用Sample(int n)構(gòu)造函數(shù))。

? ? ? ? 但是如果隱式構(gòu)造成本比較大,比較建議的方案是明確化,比如

Sample operator+(int n, const Sample& smpR) {return Sample(n + smpR._m);
}

? ? ? ? 但是不是所有重載都可以設(shè)置為成員函數(shù)形式,比如上面例子中頻繁出現(xiàn)的<<重載。因?yàn)樗糜谥С謽?biāo)準(zhǔn)輸出,于是操作符左側(cè)值是std::ostream對象,這樣它就不能聲明為成員函數(shù)了。

? ? ? ? 也不是所有重載都可以設(shè)置為外部函數(shù)形式,比如賦值(=)、下標(biāo)([])、調(diào)用(())等。

函數(shù)對象

? ? ? ? 函數(shù)很容易理解,但是函數(shù)對象是什么?

? ? ? ? 下面是一般函數(shù)調(diào)用,函數(shù)名是some_method,它有兩個參數(shù),返回了一個type類型數(shù)據(jù)。

type a = some_method(arg1, arg2);

? ? ? ? 我們將注意力移到括號(())上,它是一個操作符。因?yàn)镃++提供了“操作符重載”這樣的武器,我們是不是可以將some_method想象成某個類?一種方式是

class Method {                                                                                                      
public:int operator ()(int n, int m) const {return n * m;}
};int main() {Method m;std::cout << m(3, 4) << std::endl;std::cout << Method()(4, 5) << std::endl;return 0;
}

? ? ? ? 相較于第10行和第11行,第10行的調(diào)用方式更像普通的函數(shù)調(diào)用,但是它有一個缺點(diǎn):需要顯式的申明一個函數(shù)對象。第11行構(gòu)造了一個臨時(shí)對象——它沒有名字,但是連續(xù)兩個()讓人感覺還是很“異類”。

? ? ? ? 一種比較優(yōu)雅的方式是:

class Method {
public:Method(int n, int m) : _n(n), _m(m) {}operator int() const {return _n * _m;}private:Method() {}private:int _n = 0;int _m = 0;
};int main() {std::cout << Method(2, 3) << std::endl;return 0;
}

? ? ? ? 這兒用到了轉(zhuǎn)換操作符的概念。我們使用“operator 類型()”的形式定義一個轉(zhuǎn)換操作,這樣該類對象可以直接轉(zhuǎn)換成type類型。

? ? ? ? “操作符重載”給我們提供了強(qiáng)大的工具,使我們可以編寫出便于使用的類。但是它也藏著各種語法糖,通過本文,希望朋友們可以了解到它一些好玩的“糖”。

總結(jié)

以上是生活随笔為你收集整理的C++拾趣——有趣的操作符重载的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。