對於 C/C++ 函數指針的困惑?
我測試發現上圖中四種調用方式都沒有問題,我就比較困惑了,這裡面到底是怎麼工作的
因為函數類型可以隱式地轉換成函數指針類型(這種隱式轉換叫退化,類似的行為還有數組類型退化成指針類型),並且函數調用操作可以同時應用在函數和函數指針類型上,就這麼簡單【n4618
4.3 (1) An lvalue of function type T can be converted to a prvalue of type 「pointer to T」. The result is a pointer to the function.
5.2.2 (1) A function call is a postfix expression followed by parentheses containing a possibly empty, comma-separated list of initializer-clauses which constitute the arguments to the function. The postfix expression shall have function type or function pointer type.
非常看不慣「非常看不慣那種直接照搬教科書和標準文檔的回答,完全沒有一點自己的思考,把程序語言學習搞得和文科背書一樣。我們又不是不識字,這種白紙黑字的規定誰看不懂。」這個說法
如陳碩所說,這就是規定如此,記住即可和其他語言不同,由於本身的特殊性和歷史包袱,C和C++不是規則性很強的語言,不要以為可以隨便舉一反三地「獨立思考」,多背書,切忌自作聰明=========================
補充:並非不讓大家獨立思考,而是任何學問都有要學要背的,也有需要在此之上發揮主觀獨立思考的,只是不同領域或不同學問,對兩者要求的側重點不同,C和C++這方面,發揮思考去寫代碼,去充分使用其各種特性來實現更好的效果,當然是值得肯定的,但是前提是能正確理解並使用,而要正確理解恰恰需要比別的語言更多的「死記硬背」,同樣是C和C++程序員,只會基礎和固定模式的「書獃子」往往比只學了皮毛然後靠自己「發揮」的人更靠譜,因為就算前者生產力低,至少是正數,後者的生產力不少時候是負數
關於C++的不規則性有很多例子,比如:
short、int、long、long long是有符號的,在其前面加unsigned會變成對應的無符號的,unsigned char也是無符號的,但是,char不一定是有符號的printf和scanf的格式,int是%d,long int是%ld,long long int是%lld,float是%f,double是%lf,於是看到有人對long double用%llf,其實,應該是%Lf,另外,printf中%f和%lf都可以配float和double中任意一個,原因從promotion規則上面找評論里有朋友說不能靠彙編來學C,的確是這樣的,實際上有時候你不知道標準條款,靠彙編也不清楚發生了什麼事情:
現代C/C++編譯器有多智能?能做出什麼厲害的優化? - 冒泡的回答 - 知乎
其他一些可能掉坑的地方:
RapidJSON的IsLosslessDouble函數中的兩個volatile是做什麼用的呢? - 冒泡的回答 - 知乎C和C++中有哪些容易被坑的undefined behaviour?補充+1:對於上面引用的下劃線中的「我們又不是不識字,這種白紙黑字的規定誰看不懂。」,說真的,如果你能把C++白紙黑字的標準(不多,千把頁)完全看懂並掌握,那你已經是C++大神了,不要擔心變成只是看懂書但是實踐能力差的書獃子,實際情況是相反的:如果沒有大量深入的實踐,以及編譯原理、os和計算機硬體基礎知識打底,還真的是很難啃懂
======================
看了評論,又仔細review了下我的回答,貌似我只說背書重要,而沒說實踐不重要吧,如果哪個大佬因為認為實踐不重要,「只有」背書重要的,麻煩取消贊並和 羅夏 討論一下;如果有人指出我哪句話有很強的「實踐不重要」的誤導性,麻煩也指出來,至少我寫答案的初衷不在於此
正如羅夏所說,我這個答案就是告訴上面提到的匿名答主「不要亂髮揮」,至於他為啥理解為我是在裝逼,貌似我也沒說把書看完「就可以」的話,恰恰相反,我說了如果你真看完掌握,那已經是大神了,對不起,我不是大神,我也沒看完,我的學習途徑也是實踐+查資料,但我依然認為在C++上背書重要,而且自己也在不斷背內容這個答案並不是回答題主的,因為題主這個問題其他答案有解釋清楚的,我所反對的是上面引用的這段話,是一個匿名答主的,說舉一反三是指通過一個彙編實現就去揣摩含義並將其作為「真理」的行為,注意,我並沒有反對通過彙編去學習C++,我反對的是將其作為「真相」甚至以此來輕視書本的態度,就像這個匿名答主,覺得自己通過彙編好像看到了真相,不但反書本,還直接攻擊其他引用標準來回答這個問題的答主們,這已經不僅僅是回答內容是否對錯的問題了,如果只是他回答有偏差,我去評論區指一下即可,何必弄一個答案來強調呢簡單說,我對C++學習的看法是:背書和實踐結合,但是相對其他語言來說,前者的佔比要更大,而且可能是很大,更忌諱由後者代替前者的行為;遇到相關問題的時候,第一先反應到這可能是跟標準有關,第二自己再實踐,直接搬出實踐來證明你以為的真理,依我看這才是裝逼,而且還裝錯了至於有人說,標準那麼多,全部弄完不現實,我同意,我也說了我並沒有完全把這書背完,實際要背完對於一般人也不現實,據說全世界C++最好的幾個人加起來,也沒覆蓋全書(因為書是團隊弄的吧),並不一定需要全部掌握再寫代碼吧,我也沒說過這話
但是,你能不能把書弄完,有沒有這個時間,這個方式是不是性價比很高,是不是可行,這都是現實問題,現實是一回事,方向和態度是另一回事,實踐總結的東西當然重要,但是第一要謹慎使用,因為可能你只是試出來的,沒有理論依據的話,不一定就永遠對的,第二,對於具體問題如果發現自己通過實踐的總結可能是錯的,即便只是一個理論上的錯誤,也接納改進我並不是不會犯這種錯誤,前不久還被RednaxelaFX糾正了一個C語言函數老式聲明方式的認知問題很顯然,你需要這張圖,看C++作者怎麼說
忘了改答案了_(:з)∠)_ 腦殘了
[劃掉]因為,實際上,C/C++語法上函數是沒辦法直接調用用的,能直接調用的是函數指針。
你平時調用函數會觸發一個隱藏的函數-&>函數指針的轉換。你對函數指針解引用後調用,結果還會重新獲取函數指針再調用。
函數給函數指針賦值也一樣,會自動轉換成函數指針。也就是加不加其實都一樣,一個隱式一個顯式。[/劃掉]
規定如此。
函數指針本來要解指針的,但是C++為了讓函數指針可以像函數對象一樣工作,所以強行規定不解指針也可以調用……
我感覺是因為這裡沒有其他的語義了吧,你直接調用這個指針,總不可能意思是call到棧和靜態變數裡面去(這樣現在來說會觸發cpu的禁止執行機制吧)
一個函數就是一個標號,就等於它的首地址,而調用這個首地址就是在調用函數
感覺和數組比較類似呢
哦也不對,和數組還是有區別的,直接寫函數指針確實是有那麼一個雖然不科學但是確定的語義的,倒是數組名倒是在編譯器看來和指針完全沒有區別╮(╯_╰)╭非常看不慣那種直接照搬教科書和標準文檔的回答,完全沒有一點自己的思考,把程序語言學習搞得和文科背書一樣。我們又不是不識字,這種白紙黑字的規定誰看不懂。
從 C 語言層面,函數指針之所以和其他指針表現不同,根本的原因在於前者是指向的是函數而後者指向的是數據。在 C 語言中,函數和數據是完全不同的物種(不然 C 語言就是 FP 了),即他們有完全不同的類型描述,他們在編譯後分布在完全不同的內存段,他們在執行時也由完全不同的寄存器來控制。
具體來說,如果我們有一個數據指針,對其解引:
int i = *j;
在彙編層面的表示是這樣的:
mov ecx, [eax] ; ecx: i,eax: j, [eax]: *j
很容易理解,不想贅述。
但如果我們有一個函數指針,對其解引:
typedef int (*myfunction)(int a, int b);
// good
myfunction f1 = func;
// bad, 無法編譯
myfunction * f2 = func;
// good
myfunction f3 = func;
在彙編層面的表示是這樣的:
mov ecx, [eip-0x5d] ; [eip-0x5d]: func, ecx: f1/f3
情況就會稍複雜了,這裡主要有三個問題:
- 為什麼 func 本身已經是一個地址了?
- 為什麼既然 func 本身就是地址了,我們對 func 進行二次顯式取址無法得到一個指針的指針?
- 為什麼既然 func 本身就是地址了,我們對 func 進行二次顯式取址仍然得到的是一個指針?
這三個問題只要不是讀死書,稍微自己動一下腦筋,都不難想到答案:
- 因為函數在編譯後都會存放在代碼段(code segment)里,所以除了定址訪問我們別無選擇;
- 因為 C 語言在編譯期需要確定所有的函數調用,像 eip-0x5d 的偏移 0x5d 是在編譯期就確定的恆值(代碼段通常不可改)。但如果我們能對 func 二層取址,然後再使用二層解引去調用,函數調用編譯期可知的前提就破壞了,因為 eip-0x5d 本身在編譯期是不可知的。設想我們如果可以:
myfunction * f2 = func;
然後使用:
(**f2)(1, 2);
去調用,那麼試問編譯器如何確定 **f2 呢,即在不知道 f2 的情況下對其解引呢?
- 關於這個問題,我的想法是這樣的:因為在 C 語言最初的設計中,函數不是一等公民,函數只有簽名而沒有類型(這也是 C 語言需要頭文件的原因)。所以,在設計函數作為函數參數而需要類型時遇到了問題。
C 語言本質上是高級彙編,而函數在彙編里本質上就是代碼段(code segment)的一個起址,所以很自然就想出了函數指針這種類型。這是一種純粹拍腦袋的、想當然的糟糕設計,因為函數指針這種類型根本沒法解引(因為沒有函數類型沒法接,所以函數指針乾脆被設計成解引後還是原函數指針),所以在 C 語言中才會出現像下面這樣啼笑皆非的函數指針的使用:
myfunction f3 = func;
f3(1, 2);
(**f3)(1, 2);
(**********f3)(1, 2);
C++ 當初一股腦繼承了 C 的整套方案,自然也免不了被 C 的遺坑所荼。但這幾年 C++ 也在積極地使用現代化的程序語言理論解決這樣的歷史問題。在 C++11 以後,要描述一個函數,不是萬不得已,盡量不要用函數指針,而使用 std::function,即不要使用:
typedef int (*myfunction)(int a, int b);
myfunction f = func;
而改寫成:
typedef std::function&
myfunction f(func);
你需要這本書:《C和指針 Pointers On C》 Kenneth A.Reek, 徐波這本書是C語言進階教程,包含C語言所有內容,只是書的結構不適合入門,C語言的問題不管深淺基本都能在這裡面找到解答,案頭書必備。
也不用當心這本書是否過時,因為過時的部分這裡面都沒有提到,絲毫沒有違反C99的地方,作者真是天才!
因為不會引起歧義。就像你說「肉夾饃」或者「饃夾肉」一樣,無論怎麼說別人都不會理解錯,所以兩種說法都支持了。無論是(*func)()還是func()你都是想調用函數,而不可能有第二種想法。這個和傳遞數組類似,無論你是否取數組地址,都只允許你傳遞數組地址,而不可能是別的。
因為函數不等同於變數啊。函數本來就是只能通過其首地址進行操作,沒有別的用法。那就乾脆把所有錯誤用法默認都用正確實現進行實現好了。
感覺都沒說到點子上呀,《征服C指針》中有特別說明:
為了照顧在表達式中的函數名可以解讀成「指向函數的指針」這一規則,ANSI C有如下規定: * 表達式中的函數自動轉換成「指向函數的指針」。取址運算符和sizeof運算符時例外。(函數的sizeof一般無法計算) * 函數調用運算符()的操作數不是「函數」,而是「函數指針」。因此,(****demofunc)(1, 2)也能順利運行。每次解引用都會變函數,但是因為在表達式中就瞬間變指針。
PS:不知書中內容過時了沒。
語法糖而已。當初設計的時候讓你少打幾次鍵盤。
同意 @冒泡的說法。
非常看不慣「非常看不慣那種直接照搬教科書和標準文檔的回答,完全沒有一點自己的思考,把程序語言學習搞得和文科背書一樣。我們又不是不識字,這種白紙黑字的規定誰看不懂。」這個說法
如陳碩所說,這就是規定如此,記住即可和其他語言不同,由於本身的特殊性和歷史包袱,C和C++不是規則性很強的語言,不要以為可以隨便舉一反三地「獨立思考」,多背書,切忌自作聰明作者:冒泡鏈接:對於 C/C++ 函數指針的困惑?來源:知乎著作權歸作者所有,轉載請聯繫作者獲得授權。
這東西都不會的人,和他講彙編,是在逗笑嗎?
函數類型會隱式轉換為函數指針,與數組會隱式轉換為指針是相似的,具體請看C++標準
摘抄一個在Google工作的朋友寫的博客:c++指針難以理解?那你聽說過智能指針嗎? 代碼例子粘貼不過來,自己去原鏈接看可以。
對於初級計算機編程語言學習者來說,c與c++當中的指針是一個不折不扣的門檻。變數前面加了」」與」*「理解起來就複雜了百倍。那麼有多少人知道unique_ptr是什麼意思?今天我就來梳理一下c指針的一些有趣知識點。
基本知識點
首先,先來重溫一下指針的基本知識。為了保證文章的一致性和易讀性,我們先定義一下一個類:
那麼定義一個指針的方法如下:
可以讀作」生成了一個laitn指針指向一個Laitn類的對象」。訪問指針對象使用-&>符號:
指針也可以賦值:
指針本身也可以作為參數傳入函數:
智能指針
自從c++11標準之後,加入了智能指針(smart pointer)的概念。該概念的引入很大程度上減少錯誤使用內存而導致的內存泄露。智能指針實現中兩個關鍵詞是std::unique_ptr與std::shared_ptr。
std::unique_ptr顧名思義就是「指向某對象的唯一指針」。unique_ptr的生成方式如下:
以上的智能指針的代碼利用了c++ template,類似於Java當中的generics。可以理解為unique_ptr與make_unique的通過template實現為一個模板,可以用來生成任意類的unique_ptr。
與普通指針相比,unique_ptr不允許賦值操作,也不允許通過new來直接初始化。以下的操作是通不過編譯的:
通過這些操作,嚴格保證了只有一份Laitn對象的拷貝,並且只有unique_laitn指向它。如果確實需要賦值以及作為參數傳遞該怎麼做?可以通過std::move(),所以以下操作是允許的:
而std::shared_ptr的使用則自由了許多,與基本指針的使用基本類似,賦值傳參數等也被允許。
感覺c++這部分就是比較亂的。
以下答案憑印象。如果你給函數指針賦值的話,是像圖中那樣的。如果你把函數作為模板函數的參數,來推導模板的話,又分情況。加"",會推導成函數指針,調用加"*"。
不加"",會推導成類似引用的東西,可以像引用一樣調用!雖然我們知道沒有函數引用這種東西!
記得用這個類型創建變數會退化成指針。這兩種類型其實是不一樣的,如果你用模板特化能看出來。
如果是類的非靜態成員變數做參數,必須加"",會推導成指針。
說點題外話:編程的老是覺得自己的工作含金量高,所以會點東西之後就有鶴立雞群之感,就像暴發戶炫耀自己有錢,體制內炫耀自己地位一樣。哪怕你看他其實沒什麼成就,水平也不過如此,但是傲慢卻先漲起來了,真讓他去做計算機外的行業,估計同樣不會有什麼出息。(*p)()是正常的p()是簡寫。
這幾個調用方法是從C繼承下來的,記得初學C時從老外的哪本書里有看到過對這個問題有描述(好像是《C語言參考手冊》5th中吧),是說這兩種寫法以前有過爭執。一派認為畢竟是指針,所以得解引用。但是另一派人則說可它是函數,不用解引用。後來乾脆就都保留了。另外提到《C語言參考手冊》順便說下,記得當時看這書時發現了不少詭異的寫法,當時有些在我看來甚至是「上古時代」的寫法。挖墳還得看老外~~~
從人的角度出發,畢竟計算機科學是主觀科學。假如我們讓函數和指向函數的指針存在使用上的區別,那麼區別又能是什麼呢,如果想不出來區別,那麼他們就是一樣的,僅此而已。數據和指向數據的指針存在使用上的區別是因為指針僅僅代表一個位元組所在的位置,而數據可以佔據很多位元組,因此也就有了指針類型,這樣拿數據的時候就方便了,不用手工指定位元組數了。
推薦閱讀:
※有沒有使用「==」判斷浮點數相等與否出現錯誤的例子?
※怎樣開發一款有限元軟體,從哪些方面學習?
※C++ #include " " 與 <>有什麼區別?
※C++ 函數返回局部變數的std::move()問題?
※在開發大C++工程的時候如何判斷和避免循環include?