ZWeily的小品文(五)C++入門教程(4) (轉)
§4 過載和預設引數
“:( ……”我的上突然彈出了這麼一張苦瓜臉,不用看也知道是誰了。
“師傅,過來幫幫忙好嗎?”
“不是說過別叫我師傅啊!叫我weily就可以了。怎麼了?又有什麼問題啊?”
“講不清楚,還是您……老人家……過來看一下吧 :p”young還故意把“老人家”這三個字與前後文分開,改成了黑體、三號字,還改了顏色,並且加粗了。
還好我只是剛拿起杯子,還沒喝水,否則,我的鍵盤和顯示器就要遭殃了。
“生氣了?開個玩笑呀,你還是快點過來吧,weily :)”
“我的氣量可沒那麼小,不過下次你在發這種話之前最好先看看我有沒有在喝水或者吃什麼東西,我怕噎著…… 好了,我馬上過來!”按下enter之後,就端著我的茶杯,走了過去。
“怎麼了啊?又有什麼問題啦?”
“我這邊有一些程式碼不能透過編譯,老是報錯,可我又不知道為什麼錯了。”
“嗯,上次我告訴過你,要學會看報的錯。”
“我看了,它說:‘ambiguous call to overloaded function’,也就是過載函式的有二義性。可是,我沒看出有什麼二義性啊!”young的樣子看上去一臉的無辜和茫然,就像一個被錯怪了的孩子。
“喔,原來是二義性問題啊,這個問題的確有時候比較讓人頭痛的,因為c++有這麼一個哲學信仰:它相信潛在的模稜兩可狀態不是一種錯誤(注1)。先讓我看看你的程式碼。”
“嗯,就是這裡。”young把滑鼠指向出錯的那行,“我就是想不通,這麼一個簡單的函式呼叫怎麼會出錯。”
我看了看螢幕,只是一個函式呼叫,就像這樣:
func( i );
“首先我問你,你這個func函式的原型是什麼樣子的?”
“函式原型?哦,就是函式的宣告是吧?在這裡。”她按了一下pageup鍵,然後將函式的宣告指給我看。
void func( char );
void func( long );
“嗯,這樣啊,那你的i這個變數是什麼型別的?”
“是int的。”
“喔,這樣就沒問題了,我都知道了。”
“嗯?知道什麼啦?快說給我聽啊!”young那雙充滿好奇的眼睛盯著我。
“其實,這個是一個常常被人們所忽視的一個模稜兩可的狀態。你是不是認為,從int轉為char,會丟失精度,而轉為long不會,所以,你就認為這個函式呼叫,應該毫無疑問地呼叫func( long )這個函式?”
“是啊,因為我記得solmyr老大曾經說過,當編譯器不知道轉成那個型別好的時候,它會報二義性錯誤。但是在這裡,int轉為char和int轉為long應該很明顯的啊,轉為char會丟失精度,而轉為long應該更好啊。”
“的確,你說的基本上都是對的,而且過去我也是這麼認為的。”
“過去?你也是?”
“是的。但是你只要仔細看看tc++pl,裡面7.4.3那節就有這樣的一個例子的,幾乎和你的這段程式碼一模一樣,bjarne在tc++pl裡明確地指出,這是一個二義性的函式呼叫。不過,很遺憾的是,他也沒有具體解釋為什麼這個呼叫是二義性的。”
“喔,那這個到底是怎麼回事呢?”
“你聽我慢慢說。首先,在過載解析的時候,編譯器是應該按照c++裡的一個規則進行函式選擇的。一共有五種型別的匹配和轉換。最高階別的,就是精確匹配。”
“嗯,這個我知道,就比如我這裡如果有一個函式是void func( int )的,那這裡的呼叫就不會有二義性,肯定呼叫這個以int作為引數的函式。”
“對。但是他還包括數祖名轉換成相應的指標型別,函式名轉換成相應的函式指標,以及非常量型別轉換為對應的常量型別,這些都是很細微的轉換,轉換後的型別和轉換前的是相當的。比如這樣,”
void f(const char*);
char p[ ] = “non-const”;
f( p );
“這裡就有2個轉換,一個是將p這個char的數祖名轉換為相對應的char*型別的指標,第二個就是從char*轉換為const char*,是非常量轉換為常量,這些都屬於精確匹配。”
“噢,知道了。”
“第二種級別的,就是型別的提升。其實這個就牽涉到你提出的那個問題的。在型別的提升中,沒有從int轉換到long的這種提升的。c++裡的這個設計,是繼承自c的,它就是將運算在運算之前,轉換到相應的‘自然’的大小。”
“‘自然’大小?這是什麼?”
“這個在c裡面是非常常見的,例如在c裡,char都被轉換為int進行處理的。關於char和int的關係,在c裡,兩者是等價的,可以說在c裡沒有char這種型別的值,任何char的值都是int的,例如常量’a’,在c裡面,它的型別是int的,而不是char,所以很多c的程式碼裡都是從char到int的隱式型別轉換。當然,在c++裡,還是對兩者有所區分的,也就是更匹配的型別存在時,就用更匹配的型別。例如,”
void f( char );
void f( int );
f( ‘a’ );
“噢,我知道,這裡就沒有二義性,就會呼叫f( char )這個函式。”young似乎有所領悟了。
“對,但是這樣呢?”
void f( int );
void f( unsigned char );
f( ‘a’ );
“這個……”young又陷入了困惑。
“答案就是呼叫f( int ),這裡沒有二義性。記住一點,就是那些比較小的整數型別,包括char,在可以保證原有型別精度的情況下,轉換為int型別,否則,轉換為unsigned int型別(注2)。這就是我前面說的那種‘自然’的轉換。關於這種隱式型別轉換,你可以去看看tc++pl的c.6,那裡有詳細地說明。”
“噢,我知道了。”
“其實這些問題的起因,就是c++繼承了c裡面的一條規則,就是不區分narrowing conversions和wning conversions,所以也就帶來了是從int轉換為char好,還是int轉換為long好這種模稜兩可的狀態。如果區分了narrowing conversions和widening conversions,那麼就要定義隱式的 widening conversions (例如從 int到long) 和顯式的 narrowing conversions (例如從int到char就需要一個顯式型別轉換操作)。但是這種設計是不能接受的,因為像從int到char的這種隱式型別轉換太普遍了,如果一定要變成顯式型別轉換的話,那麼將大大的降低c++與c的相容性,這將使得c++不容易被人們使用和接受,c++的一個設計原則就是不強迫人們去用一種方法完成所需的事情,它會提供選擇,讓人們可以用自己喜歡的方式,而且c++在最初設計的時候,就要做到原來c能做的事情,那麼原來c的程式碼理所當然的應該能被c++接受。”
“所以c++不區分narrowing conversions和widening conversions,而編譯器對於narrowing conversions給出的是警告,而不是錯誤。是這樣子的吧?”
“是的,好的編譯器會對這種可能丟失精度的轉換給出警告。看來對於這個問題你已經理解了大部分內容了。那麼我們再來看看整數和指標的關係。先問你個問題,就是0是什麼?”
“0?是一個整數吧。”
“基本正確,更精確地說,0是一個整數字面常量(literal integer constant)。對於0這個整數字面常量,或者值為0的常量,都可以被隱式的轉化為任何型別的指標。所以,對於下面這段程式碼,是二義性的函式過載。”
void func( char* );
void func( int* );
func( 0 ); //二義性函式呼叫!
“嗯,原來如此,知道了。”
“那麼關於函式的預設引數你知道多少?”
“預設引數?哦,這個我已經看過了,好像最關鍵的一點就是預設引數只能是形式引數列表中的最後的幾個。”
“不是好象,是肯定!這點一定要記住。其實,預設引數也和過載有一定的關係。你看看下面這段程式碼有什麼問題。”
void func( int i );
void func( int i, char ch = ‘a’);
int i;
func( i );
“喔!模稜兩可,這個函式呼叫是二義性的!”young的樣子大概不亞於當年哥倫布發現了新大陸。
“對!對於多引數的函式過載,編譯器在決定呼叫哪個函式的時候,還是遵循最佳匹配的規則,也就是讓儘量多的引數能夠得到最好的匹配。前面說過的兩條,再加上標準轉換、自定義的轉換和省略號在函式定義中的使用,就構成了五條匹配規則,越前面的級別越高,當然,後面這3條規則中,還牽涉到使用者自定義的類的設計問題,這個以後等你學到類和繼承的時候,我們再說。這些規則,在tc++pl的7.4中有描述,你應該去看一下的。”
“書啊?我在看。其實,今天我遇到的問題,就是我剛看了一點關於函式過載的東西,但還有一些沒搞明白。昨天你不在,我就去問pisces,學函式過載的時候要注意點什麼。結果,她就說,我給你一些程式碼,你去把裡面的錯誤全都找出來,並且搞清楚這些錯誤的原因,那就能學好了。最後,還丟給我一句話,盡力避免模稜兩可的狀態!”
“那麼剛才那段程式碼就是她給你的咯?”
“是啊,而且,我搞不定了,去問過她,她也沒說出個所以然,就是叫我以後寫程式碼的時候別寫出這樣的。”
原來又是pisces搞的鬼,自己搞不定,救扔給新來的young,然後再透過young來讓我幫忙解決這個問題。如果我沒猜錯的話,她現在應該就在附近…… 我一轉頭,果然看到她就在邊上扮鬼臉,而且,看樣子就是有了收穫就想溜走。
“pisces啊,你自己有問題就可以直接來問我或者趣聞solmyr啊,何必欺負人家新來的呢?”
“這個…… 我只是看她現在在研究這方面的東西嘛,而且我看她和你混得挺熟的,所以就……”這傢伙還真好意思說。
“好了,這次不和你計較,下次可不能這樣哦……”我話還沒說完,pisces已經回到自己的位子上了,真是的!
咦?怎麼一會兒young也不見了?我用左手的食指和中指推了推鼻樑上的眼鏡,然後四處張望了一下。結果就看到young手上端著個杯子,向這邊走過來。那個杯子不是我的嗎?怎麼會在她手裡?
“師傅,請喝茶,這可是您老人家最喜歡的紅茶,而且還加了蜂蜜的哦!”
紅茶加蜂蜜?我沒聽錯吧?我把杯子接了過來,果然聞到了熟悉的茶香,杯子拿在手裡,有一種溫暖的感覺。
“你怎麼知道我這個習慣的?”我覺得有點好奇,她才來不久啊,怎麼就知道我喜歡紅茶加蜂蜜啊?像pisces來了這麼久了,上次還不是給我弄了一杯咖啡。
“這個啊?保密!”young得意地笑著。
“嗯,不錯!味道剛好!哎,不對哦!你前面叫我什麼來著?我怎麼又聽到‘師’字,還聽到‘老’字,你……”我才反應過來,又被她耍了……
注:1.這個說法來自tt meyers的《effective c++》中的條款26
2. tc++pl中c.6.1 promotions中有詳細的規則描述
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752019/viewspace-956336/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- C++入門教程C++
- Python開發的入門教程(五)-setPython
- react-router4入門教程React
- C++ 入門防爆零教程(上冊)C++
- C++入門教程(11):呼叫函式C++函式
- C++入門教程(9):while 語句C++While
- 轉:區塊鏈入門教程區塊鏈
- [轉]Systemd 入門教程:命令篇
- C++入門教程(12):定義函式C++函式
- C++入門教程(14):過載函式C++函式
- C++入門教程之二:變數C++變數
- C++入門教程(3):語句和縮排C++
- Java入門教程五(數字和日期處理)Java
- Docker 實戰教程之從入門到提高 (五)Docker
- Webpack4系列教程(一) 基礎入門Web
- 五種常用的網站入侵方法!網路安全入門教程網站
- 埠轉發工具Rinetd詳細入門教程
- 【Python入門教程】五個常見的最佳化SQL的技巧!PythonSQL
- 五線譜入門(五)
- C#快速入門教程(4)——類成員的作用域C#
- C++ 四捨五入與不四捨五入C++
- Linux入門(五)Linux
- C++入門教程(20):變數、不變數和常量C++變數
- Flutter狀態管理:Provider4 入門教程(一)FlutterIDE
- Flutter狀態管理:Provider4 入門教程(二)FlutterIDE
- Flutter狀態管理:Provider4 入門教程(三)FlutterIDE
- Jwt的新手入門教程JWT
- C++入門教程(5):基本資料型別和變數C++資料型別變數
- Android入門教程 | RecyclerView使用入門AndroidView
- 五筆輸入法入門
- 《Flask 入門教程》第 4 章:使用靜態檔案Flask
- C++快速入門+20201011C++
- ReactiveCocoa入門教程——第二部分(轉)React
- AudioKit 入門教程
- awk 入門教程
- Maven入門教程Maven
- Aseprite入門教程
- Electron入門教程
- HBase入門教程