初级程序员面试不靠谱指南(六)
五.很強很偉大的函數指針
??? 我想看到這個標題中“函數指針”幾個字之后,估計有一半人會選擇關掉界面,因為我最開始學習C語言的時候這一章我曾無數次跳過,看到書中那些復雜的星號括號直接就崩潰了,加上老師自己本身也講不清楚,所以學習興趣大減。但是到后面,當我意識到函數指針的牛逼和偉大之后,我不禁開始認真的思考并學習了這部分內容,絕對受益匪淺。如果你想了解很多編程的技巧以及C++的面向對象是如何構造出來的,我建議你應該好好學習函數指針,我也會分兩或者三篇來介紹這個知識,特別是在后面,我將會簡單的展示下用c語言如何能做到C++多態等面向對象的特征,這樣當你遇到面試時有人問:"new和malloc有什么區別"的時候,你再也不必百度出答案以后照本宣科了。
??? 函數指針絕對是C/C++語言中比較讓人惡心的東西之一,面對著眼花繚亂的*和(),很多人直接就跪了,面試的時候經常會遇到函數指針和指針函數有啥區別這樣的問題,從這兩個名字和中國人造詞的方法就可以看出一二,函數指針本質是一個指針,指針函數本質是一個函數,雖然這句話沒什么意義,但是作為一個指導思想可以讓你在理解的時候可以把握一個大方向。
1.函數指針的"函數"。函數指針既然叫這個名字,那么就分別從函數和指針兩個方面來介紹一下好了。為了不至于看到這里就有30%的人關掉界面,首先先從簡單的開始,在c語言里面聲明一個函數是很簡答的一件事,你只要遵循[返回類型][函數名稱][參數列表]這三大部分進行聲明,你就可以聲明出一個函數,比如int f()。這個函數返回類型是int ,函數名稱是f,參數列表為空。為了文章能扯的下去,我們先定義一個返回值為int*的函數開始,也就是int *f()。
???? 在這個之前,先扯點看起來稍微遠一點的知識好了。在c/c++中,!運算符是一個單目運算符,就是說其所需的變量為一個,這個運算符的含義是“邏輯非”,也就是true變成false,false變成true。比如:!b,就表示對b這個變量取反,是不是感覺很弱智了?那么好,你需要理解的是在函數調用的"()"也是一個運算符,不僅僅是在四則運算中采用(),用點裝逼的語句就是,這個括號要廣義的理解。也就是當你調用一個函數的時候,你可以理解為這也是在做一種運算,這種運算的調用方法就是中使用()符號。再通俗一點,如果我在某一個函數中使用f()調用一個函數,這樣也就是我采用這樣一個運算符來進行一種計算,這種“計算”是調用函數,雖然()并不是一種單目運算符,但是為了這個問題更加簡單,采用這樣一種形式,目的是想強調()也是一種運算符,盡管長的很不像。
????? 將()看作運算符的一個重要意義就是,運算符至少都是有優先級和結合性的,而更加不利于理解的一個伏筆就是()的優先級除了在[]運算符之下之外,是優先級最高的一個運算符,更加通俗的一個解釋就是,如果有()運算符那么就要先計算這個運算符,這也就是為什么函數指針的聲明形式看起來那么別扭的原因。比如下面這個例子int *f(),將()看作一個“函數調用”運算,那么就是先進行“函數調用”運算,然后在進行“取地址運算”,通俗的說法是,這是一個函數,返回一個int類型的指針。那么如果我采用括號改變這兩個運算符的優先級,變成int (*f)(),那么這個過程就變成了,先進行“取地址運算”,再進行“函數調用”運算,也就是取出這個地址上的變量,再進行函數調用,通俗的說法就是,這是一個地址,地址上靜靜的矗立著一個函數。
????? 上面的是不是有點繞?那讓我們先暫時忘記上面的內容,仔細來看一下int (*f)(),這是不是一個函數?根據函數的定義,這很明顯不是一個函數,因為你不能把函數的名稱設置為(*f),因此,你也不能這樣寫int (*f)(){},因為這不是函數,沒有函數體,你所做的只是聲明一個指針。這里的()符號都是運算符,在這一個式子里面具有不同的意義。你可以這樣來看待這樣一個奇怪的結構,由于括號可以改變運算符的優先級,所以首先這是一個指針,就像(5+3)*7,首先是計算加法一樣。然后將int ()看做一個部分,看做是一個"函數調用"運算,這個指針指向的是這樣的一個函數調用,它具有的特點是返回值是int,無參數列表,就像int *b,這是一個指針,指向int類型的數據。這里請停下來思考1分鐘,然后再試試你能不能分辨出int *b[2]和int (*b)[2]的區別,如果不能,請再思考1分鐘,如此往復知道思考明白為止。
2.函數指針的"指針"。如果能看到這里,你已經戰勝了至少15%的人了。既然函數指針本質是一個指針,那么就從指針的角度再來看看這玩意兒。如何在C語言里面聲明一個指針,我想是任何一個看過超過50頁c語言的人都能回答的問題,比如說int *f。這個概念絕大多數人都能很容易的理解,所以我們將這個概念嫁接到函數指針這個概念上,相對于整型指針,你可以把函數指針理解為指向函數的指針,就像整型指針的用法是int b=0;int *f=&b;一樣,遞推出聲明一個函數指針以后你也可以像這樣做類似的操作。你可以做這樣的嘗試,定義一個函數fpointed,然后類似普通指針的用法那樣*f=fpointed。
????? 你會發現,如果你運氣比較好的話,可以通過編譯,但是我相信絕大數情況下,你會接到報錯的消息,不過至少你領悟到了一個道理,和所有整數,浮點數等等一樣,函數在程序中也是有一個地址的,雖然你不知道這是怎樣一個形式,但是根據指針指向的是一個地址的基本原則,你至少應該記住這一個概念才不至于太驚訝。為什么函數指針不能隨便指向一個函數呢?只是因為"函數"和"整數"這兩個概念是不同的,雖然都帶有"數",但是就像不是所有的鳥都能飛一樣,不是所有的帶有數的東西都是一類。單從外形上判斷,你能說int f1(int a)和int f2()是一種東西嗎?先不管其他的,前面的比后面的長一大截呢,人都需要用不同的形式進行記錄,更不用說編譯器,所以不存在一種“通用”的函數指針能指向所有函數,這就涉及到函數指針和函數之間的關系問題。
????? 觀察一下f1和f2,對于一個函數什么最不重要?應該是名字,f1同樣可以叫f2,你要是喜歡叫他f22222222都可以,決定它不同于其他某類函數的是它的返回值和它所包含的參數列表,就像人一樣,你叫什么名字并不重要,重要的是你給別人表現出來的能力和你自己所本身包含的涵養,這是你區別于別人并且立于世上的基本。這種情況下,回到1里面說過的,對于一個函數指針,你可以分成兩個部分來看待它,將int 和()看做指向的部分,(*f)看做指針的部分,如果你想聲明指向一個“返回值為int并且帶有一個int參數的函數”的指針,應該怎么做。這時候你應該大膽嘗試,(*f)這個不能變,因為這已經是一個指針,只是沒有明確指向什么,那么按照描述,你要寫出指向的部分應該int (int a),根據函數的聲明中形參的部分,你應該可以猜到這個a是可以省去的,將這兩個部分拼起來,就可以得到這樣一個東西int (*f)(int ),這就是指向一個“返回值為int并且帶有一個int參數的函數”的指針,換句話說,你可以用指向f1的地址,也就是f=&f1,到這里,你可以認為自己已經會聲明函數指針了。
????? 好了,和上面一樣,先暫停1分鐘,思考一下如何聲明出指向一個“返回值為int*并且帶有兩個int參數的函數”的指針。
????? 既然聲明好了,那么怎么使用這個東西呢?還是和上面一樣,如果你定義了這樣一個函數:
int fPointed(int x){printf("pointed %d\r\n",x); }????? 如果你想在main中調用該函數,你會使用fPointed(1)之類的語句去調用。那么如果聲明了一個函數指針并指向它,就像下面這樣,
int (*f)(int ); f=&fPointed;????? 怎樣通過這個函數指針去調用這個函數呢?回想一下普通指針是如何使用的,比如int a=0;int *b=&a;如果你想通過b來取到a內存中所保存的數,你會采用*b這樣的方式,同理,你想去的f里面所指向的函數,同理應該使用*f這樣的方式,只是函數指針畢竟指向的是一個函數,你需要給編譯器一個"函數調用"的運算符,并給與正確的"運算變量"(正確的參數類型及個數),所以完整的調用方式應該如下:
(*f)(2);????? 至此,你已經可以使用函數指針代替函數來進行調用活動了,此處,如果你是第一次看到這些東西并完成想明白上面的內容,應充滿了成就感。
3.函數指針第一次應用。函數指針的應用實在是太廣泛了,并且帶來的方便性和巧妙性絕對是可以令人鼓掌的,和上面的方法一樣,誰的第一次都不容易,所以先從簡單的開始,比如,你想做一個可以進行有"加減乘除"四則運算的小程序,這個程序可以根據你輸入的內容來選擇不同的算法,不管你信不信,這是我研究生入學復試的第一題,當時覺得太弱智了,現在想想,就是這種題目你一樣可以讓別人看到你的與眾不同,所以千萬別小看任何一個問題。最一般的程序,也是90%的人寫的程序一定是下面這樣的:
float Plus (float a, float b) { return a+b; } float Minus (float a, float b) { return a-b; } float Multiply(float a, float b) { return a*b; } float Divide (float a, float b) { return a/b; }float Cal(float a, float b, char opCode) {float result;switch(opCode){case '+' : result = Plus (a, b); break;case '-' : result = Minus (a, b); break;case '*' : result = Multiply (a, b); break;case '/' : result = Divide (a, b); break;}return result; }???? 然后在main里面,通過傳入不同的Code來標示自己想進行的運算,比如Cal(1.0,2.0,‘+’);最后會得到3.0。這個思路就不用多介紹了吧?這還是只有四則運算,你只需寫4個case語句就可以了,如果要有40個不同類型的運算怎么辦?這樣進行維護成本太高。而這問題使用函數指針可以很好的去掉switch從而解決這個問題。
??? 首先根據上面的定義,并且觀察這四則運算,發現返回值都是float,參數都是(float,float),所以你需要的是一個指向"返回值為float并且帶有兩個float參數的函數“指針,很容易寫出來是float (*f)(float,float)。
??? 接著,想想看如何替換掉這個switch語句呢?你可以順著這條路思考,如果我能夠直接傳入一個函數,而不用進行判斷再選擇函數這樣就不用switch了。如何將函數"傳入"函數,這里面需要你再一次從腦海中想起函數指針是一個指針的概念,既然是一個指針,那么就可以作為一個形式參數放在一個函數的參數列表里,就像int f(int *b)一樣,同樣,我們可以講函數指針作為一個函數的參數,只不過看起來更加別扭而已,不管怎么樣,我們可以采用下面這樣的一個函數去掉switch語句:
float Cal2(float a, float b, float (*f)(float, float)) {float result = f(a, b); return float; }???? 在調用的時候可以直接Cal2(1.0,2.0,&Plus),傳入相應的函數地址計算結果,這樣就不用維護一個龐大的switch結構,只需要在調用端傳入相應的函數就可以了。當然這也有一個弊端,就是只能傳入返回值為float并且帶有兩個float參數的函數。
4.函數指針應用one more time。這一個應用不僅僅是為了展示函數指針的用處,更是為了展示程序作為一個工具其實和數學是親密的。很多人一看到用程序實現某某算法就頭大,直接放棄的概率絕對大于50%,雖然這個例子很簡單,但是我很想傳達一個思想,就是計算機的本質是運算,運算絕對離不開算法,所以某種角度上說算法是程序的核心之一,也是學寫程序的一個本質目標之一。
????? 我在學高等數學的時候曾想過如何用程序寫積分運算?無奈那時候水平有限,想破頭也想不出來,因為積分運算參與運算的不僅有數,還有函數,那時候傳數容易,怎么傳函數真是蛋疼了。不過現在根據上面的思路,你可以很容易的想出解決辦法,就是傳入一個函數指針,如果你對積分已經忘了,你可以百度一下相關知識,我也只記得一重積分的運算方法了,所以我也只寫了一個計算一重積分的例子。
????? 稍微回顧一下一重積分的運算方法,可以想起來的是,一重積分有一個上限,一個下限,然后有一個積分變量(已經忘了是不是學名叫這個了),其幾何意義就是這個積分變量的曲線,在上限和下限在曲線上的取值向x軸做垂線,然后這兩條垂線,曲線以及x軸圍成的面積就是這個積分的值。由于曲線不能像計算長方形面積那樣長乘以寬,所以要采用特殊的方法,這個特殊并且精彩絕倫的方法就是將這個區域分成很多寬度很小的近似長方形,分別計算這些長方形的面積,然后將他們加起來,這個長方形的寬度接近無限小的時候,這個面積就是積分的值。唉,以上是我的全部記憶,如果有錯誤,請大聲的指出來。這個思路不難,就是取兩個點的函數的值,用一個很小的變量作為寬度,為了更加精確,我采用的計算梯形的面積,然后將這些面積加起來。你可以將寬度取不同的值,你會發現取的越小,最后結果越精確,但也不能太小,畢竟計算機里的數都是有精度的。我相信,如果畫個圖,你就會頓時明白了,這篇太長了,我下篇再稍微仔細介紹一下這段程序的思路好了,先把代碼貼出來:
float Calculus(float (*f)(float),int start,int end) {float range=end-start;float delta=0.01;float loopIndex=0.0;float sum=0.0;while(range-loopIndex>0.000000001){sum+=(f(loopIndex-delta)+f(loopIndex))*delta/2;loopIndex+=delta;}return sum; }float X2(float x) {return pow(x,2); }float X3(float x) {return pow(x,3); }int main(int argc, char *argv[]) {printf("%f\r\n",Calculus(&X3,0,3));//計算x^3在0-3之間的積分值return 0; }???? 下面super_boy的這句話我覺得對理解這個概念會有幫助,所以我附在后面了~
???? “函數在調用的時候,都會去維護一個棧的平衡,也就是在調用之初,進行壓棧,調用結束的時候進行出棧。而由于函數的參數的不同,就導致壓棧和出棧的次數不同。如果在申明的時候不把參數個數,和類型傳給函數指針的話,就沒法保證在運行的時候棧的平衡。程序也就崩潰了。”
??
?????
?
轉載于:https://www.cnblogs.com/ZXYloveFR/p/3146235.html
總結
以上是生活随笔為你收集整理的初级程序员面试不靠谱指南(六)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 长城汽车与乌兹别克斯坦汽车集团签约,计划
- 下一篇: 大模型加速涌向移动端!ControlNe