第18篇 它不是人,它是恐豬
- 「你不是人」
- 「你是豬」
這是兩句罵人的話,哪一句傷人更深?很好辦,代入情景就可以了:假設你有一個女友,有一天你不知為何惹到她了,她伸出蘭花指,指著你額頭罵你:「你真是豬!」……體會一下?感覺還可以嘛。
如果她伸出蘭花指,指著你的額頭罵的是:「你真不是個人!」。這個……這個感覺很傷人呢。
純字面分析的話,「你是豬」,似乎已然隱含了"你不是人"的前提條件;所以按說,"是豬」的指罵應該要比"不是人"的厲害才對;為什麼現實生活中,人們寧願被罵是豬,也不願意被罵不是人呢?
「老師,其實人們最不願意被罵的是,你是雜種!」,丁小明站起來補充,我用凄厲的眼神示意他閉嘴。
今天我們要從C++程序中「類型即行為、類型即封裝」的角度對以上現象做出解釋。
不管是或不是,"豬"、"人"、"雜種"三者都可以作為 生物上 的 "類型" 加以理解——當然,丁小明補充的"雜種",那已經是複合類型了,以後我們也學到的——然後當我們被指定為某一種類型,或者被指定為不屬於某一種類型時,作為被罵的人,內心人分析重點當然放在自己和特定類型的歸屬感(比如:是豬)或剝離感(比如:不是人)的感受上。
被罵 "你真是豬"……我們心裡想的是: "是不是在說我笨?是不是在說我懶?是不是我些微有些胖了? ……"
被罵"你真不是人"……我們心裡想的是:"天啊,我做了為人所不恥的事?",「天啊,我不孝敬父母嗎?」,"天啊,在她的眼裡,我連那位放火燒人的保姆都不如了嗎?","天啊,在她心裡,我到底是禽獸,還是已經禽獸不如了呢?" ……
如果被罵「你是個雜種」……我們什麼都不想,除了想打人。
物種的本質或許由生物學上的基因(DNA?)什麼決定,但這只是先天。後天的行為也能決定類型。所以,放心吧,女友並不會輕易罵我們不是人。 因為一個人需要不斷地做出一些非人的行為,才會被罵作「真不是個人」。
「老師,那被罵真不是東西,該作何理解?"
「東西就是Object,Object可作為人世間我們所能理解的萬物的基礎類型。如果連東西都不是,估計是在罵你已經是個無法為人類所能理解的東西吧。」
哈,形而上的事談起來就是這麼有趣,特別是和談什麼內存、位元組等形而下的事相比。
但正事還是要說的。假設我們定義這樣一個新類型(結構):
//這裡是代碼注釋,是給人看的,對程序實質沒有任何影響//新類型(結構): 「恐豬」struct DinosaurPig { };
史上到底有沒有恐龍和豬的雜交生物呢?這不重要的,重要的是在整個C/C++標準庫中,從來就不存在過「DinosaurPig」這個結構,所以,我們已然在C++的程序世界裡,創造了一個新物種(新類型)。
聽起來像個上帝,但這又有什麼用?「DinosaurPig」空空如也,沒有數據,沒有行為,外界也不認識它。
上帝說,要有數據。那就加一個名字和一個生命值數據:
struct DinosaurPig{ string name; //名字 int power; //生命力};
上帝說,要有自己的行為。我們學過構造和析構,先為恐豬加上:
struct DinosaurPig{ DinosaurPig() { power = 100; //順便讓生命力初始為100,以後會有更「地道」的初始化方法。 } ~DinosaurPig() { cout << "我是恐豬 " << name << ",我要死了!" << endl; if (power > 0) { cout << "可是我明明還有 " << power << " 點生命力啊!" << endl; } } string name; //名字 int power; //生命力};
測試一下:
#include <iostream>#include <string>using namespace std;struct DinosaurPig{ ……結構內容略,見上……};int main(){ { DinosaurPig pig1; pig1.name = "達達"; } return 0;}
運行,結果如下:
能不能在構造時,就直接指定名字呢?可以,構造函數可以帶入參:
…… DinosaurPig(string aname) { power = 100; //順便讓生命力初始為100,以後會有更「地道」的初始化方法。 name = aname; //以後會有更地道的初始化方法 }……
構造新對象時,方法變成:
……int main(){ { DinosaurPig pig1("達達"); } return 0;}……
光有一頭一尾的生死,沒有中間的日常生活行為,那算什麼生物。身為程序界的上帝,我們不能如此不負責。既然是恐龍,就應該會走路,走路就會消耗生命力。既然是豬,那就應該會吃,吃的時候會增加生命力。現在我們需要為DinosaurPig這個結構類型增加兩個行為,一個叫「Walk」,一個叫「Eat」,要不要考慮及物不及物?不管了,沒叫「Zhou」和"Chi" 是我們的底線。
行為其實也是函數,和構造與析構函數類似,只不過它們可以有明確的名字,並且也需要明確指定函數的返回值類型。我們覺得吃和走其實不需要什麼返回什麼數據。C++使用「void」表示:「沒什麼類型",先就用它。
struct DinosaurPig{ DinosaurPig(string aname) { power = 100; //順便讓生命力初始為100,以後會有更「地道」的初始化方法。 name = aname; //以後會有更地道的初始化方法 } ~DinosaurPig() { cout << "我是恐豬 " << name << ",我要死了!" << endl; if (power > 0) { cout << "可是我明明還有 " << power << " 點生命力啊!" << endl; } } void Walk(int step) { power = power - step; //以後會有更地道的寫法 } void Eat(int count) { power = power + count; //同上,不太地道 } int power; string name;};
「power = power - step」這一行,可能會讓浸淫多年數學研究的你我產生一種直覺反應:哦,power 要等於 (power - step),那step是不是應該固定為0?
不是這樣理解!在C++中,表示相等 的符號是 兩個等號連寫: "==", 單獨的 "=" 表示賦值,即:將右邊計算後的值賦給左邊。就像你有一個銀行帳號,設餘額為 B,則在你取走500元後,新的餘額將是原餘額減去500的意思 : B(新餘額,時間點靠後) = B(舊餘額,時間點靠前)。
基於以上理解,Walk(step),就是讓這頭恐豬走step步,相應減去step點生命力;而Eat正好相反。
現在我們創造的新物種「恐豬」,倒也有模有樣了。以下是一個測試:
int main(){ { DinosaurPig pig1("達達"); pig1.Walk(5); pig1.Eat(50); } return 0;}
還可以為DinosaupPig增加一些行為,比如唱歌,跳舞,求偶什麼的……但現在的重點是第三件事情,上帝說:世間的恐豬們啊,你們不能光這樣吃吃走走,生生死死地自得其樂,你們還要融入這個社會。
這個社會裡,我們認識的第一個人物,通常就是 cout 兄。得先讓 cout 認識 DinosaurPig。現在它們還不認識的,不信請試試:
cout << pig1 ; //編譯錯誤,前者不認識後者。
cout 冷冷地看著 pig1 ,心想:「你丫的什麼物種啊,爺不認識你。」
真是讓人心寒,但沒有辦法,想混代碼的世界,就得有一定的知名度(或識別度)。而想要讓別人知道你,承認你,最簡單的就是:和!它!發!生!關!系!
丁小明淚眼迷離:「老師,一定要這樣嗎?一定只有這條出路了嗎?這就是潛規則對不對?我就不能堅持貞操,守身如玉嗎?」
什麼嘛!在C++中,最深刻的關係,也是類型和類型之間的關係,而類型和類型之間關係的建立,同樣可以通過函數。因為函數代表行為代表動作嘛,至於動作,為師我有說一定是讓恐豬失去貞操的動作嗎?沒有! 我們既然關注 cout,它負的責是往屏幕上輸出。所以要定製的關係,肯定是「輸出關係」。
既然是類型和類型之間的關係,就得先知道cout是哪門哪派的。 cout 屬於 ostream 的類型。ostream 是C++標準庫中既有的一個類型,意思是:「輸出流/output stream」。
要怎麼在 ostream 類型和 DinosaurPig 類型之間綁定某種關係呢?
修改ostream類定義?顯然不行,人家是標準庫中的類,系出名門,我們不能直接修改它的代碼。那就在 DinosaurPig的類中加個行為嘍? 這樣做倒是可以,但……女權主義者瞪了我們一樣。比如說,小明和小紅結婚,如果表達為「小明娶了小紅」,那就是以小明為主要對象,小紅為次要對象;同樣,說「小紅嫁給小明」也不平等。
客觀、平等地表達A和B存在某種關係,最好是既不定義在A類中,也不定義在B類中,就把這種關係放在外部,比如:小明和小紅建立了婚姻關係。這個關係的證明,其實不在小明也不在小紅,而在民政局。
再看一眼眼下還存在錯誤的那行代碼:
cout << pig1;
"<<",也就是輸出操作, 就是我們要定義的行為。
"<<" 在這個上下文中是一個「輸出操作符」。在C++中操作符本質上也是一個函數。因此,以上那行代碼,可以理解為:
<< (cout, pig1);
當然,事實上由於 ostream 類不太理會那些激進的女權主義者,所以它其實對常見的基礎數據類型(比如int, char , double等),都採用「我要娶XXX」的方法,全部定義在自己的類內部,因此,如果是這樣一行代碼:
cout << 10;
本質上其實是:
cout.<<(10);
解讀為:cout 這個對象調用了它所屬的類型 ostream 的一個名為 "<<" 的方法,方法的入參是10。簡單明了地說,就是: 主語 cout,謂語 "輸出操作" , 賓語 10(一個int)。
愛動手,並且手快的同學,必將大有希望的同學,已經寫了如下測試代碼:
#include <iostream>using namespace std;int main(){ cout.<<(10);}
結果編譯失敗。原因是這樣直接將符號當作函數名使用,容易造成語法解析上二義性,所以C++要求在符號前面,加上關鍵詞:"operator"。這詞的意思就是"操作符",作用當然就是明確指明後面那個符號是一個操作符。
來,試一把:
#include <iostream>using namespace std;int main(){ cout.operator << (10);}
編譯,果然通過了!所以請記住: << 也好, + 也好 - 也好…… 許多符號都可以當作一個函數理解。
ostream 不認識我們寫的 恐豬,更沒有明媚正娶。再考慮到"恐豬"這種類型,聽起來就不像是漂亮的物種,所以ostream肯定是不願意和恐豬產生什麼關係,但沒關係啊!在C++這個世界裡,只你要寫一個昭告天下的函數,聲稱A和B有關係,這個世界就會認可這個關係啊,哈哈哈,我知道很多男同學現在心裡想的是什麼!做夢吧,你們,你們又不是活在程序的世界!
我們要昭告天下的函數,它的名字是 " operator << ",它有兩個入參,第1個是ostream類型的數據引用(因為在實際調用時 cout 寫在前面),第二個是 DinosaurPig 類型。如果你有認真學習之前的課程的話,你應當知道,它的返回值類型,應當是ostream,並且知道應當是引用,事實上就是返回第一個入參,以支持級聯調用(記不得了?查前面的課程……):
ostream& operator << (ostream& os, DinosaurPig const& pig){ os << "我是恐豬" << pig.name << ", 我的生命力是" << pig.power << "。"; return os; //直接返回第一個入參 }
以下是完整測試代碼:
#include <iostream>#include <string>using namespace std;struct DinosaurPig{ DinosaurPig(string aname) { power = 100; //順便讓生命力初始為100,以後會有更「地道」的初始化方法。 name = aname; //以後會有更地道的初始化方法 } ~DinosaurPig() { cout << "我是恐豬 " << name << ",我要死了!" << endl; if (power > 0) { cout << "可是我明明還有 " << power << " 點生命力啊!" << endl; } } void Walk(int step) { power = power - step; //以後會有更地道的寫法 } void Eat(int count) { power = power + count; //同上 } int power; string name;};ostream& operator << (ostream& os, DinosaurPig const& pig){ os << "我是恐豬" << pig.name << ", 我的生命力是" << pig.power << "。"; return os; //直接返回第一個入參}int main(){ { DinosaurPig pig("達達"); pig.Walk(5); pig.Eat(50); cout << pig << endl; //沒錯,cout認識「恐豬」了。 }}
推薦閱讀:
※c語言b++<15是b和15比,還是b+1和15比?
※c++不滿足於小黑框控制台,下一步還應該學什麼呢?
※C/C++ 里指針聲明為什麼通常不寫成 int* ptr 而通常寫成 int *ptr ?
※如何使用C++11實現跨平台的定時器timer?
※一個關於C++模板的問題?