逗比老師帶你搞定C語言指針

    哈嘍!各位同學們大家好哇!逗比老師又回來了!好久都沒有見到大家了真是想死我了!

    最近呢,我有一個親戚,還在讀大學,正在學C語言,然後他在我的博客上看到了我之前寫過的C教程,結果沒有幾篇就戛然而止了,於是就攢了很多問題來問我。這裏給大家抱歉哈,真的不好意思,逗比老師實在是太忙了,顧不上給大家更新詳細的C教程,這個後面慢慢來。不過有些重要的知識點還是可以單獨拉出來給大家重點攻克一下的。

    以前我記得有個人來想讓我給代課,講一些關於軟件開發的知識,我就問他基礎怎麼樣,C語言會嗎?他說,C語言學得挺好的,就除了指針不太會以外。然後我告訴他,那你壓根就不會C,從頭好好學吧!其實真的是這樣,說得誇張一點,C基本上就是玩了個指針,如果你不會指針的話,那不要說你會C!

    好啦,暫時就先扯這麼多,我們來進入實際內容。

1. 指針是什麼?

    教科書上的概念就不說了,這裏想讓大家知道的是,C語言因爲是一個比較底層的語言,所以,它的很多設計都是更接近於機器的思維的,而不是我們人的思維,所以,如果你能把C當中的很多語法現象用機器運行的方式去解釋,而不是用我們人類思考的方式去解釋的話,那就方便得多。

    那麼我們來回答這個問題,指針是什麼?記住!指針就是一個數而已。這就是本質!其實不只是指針,C當中的任意一個數據類型本質上都是數。我們知道計算機當中存儲數據,最本質都是比特位,我們以8個比特位作爲一個單位來分析(字節)。而所謂的數據類型,只不過是爲了照顧程序員,編譯器按照一種特殊的方式來讀取這部分的數據罷了。

    舉一個簡單的例子來說:有這樣一個字節:10010010,你能知道它表示什麼意思嗎?要想知道意思,我們就得知道它的類型,因爲,同樣的是這一個字節,類型不一樣意義不一樣,假如說它表示一個無符號整數,那麼他應當是146,如果它表示一個有符號的整數,那麼他應該是-109,如果它表示字符,那應該是'm',所以,數據的本質就是一個數,而類型就是我們解讀這個數據的方式。

    指針也不例外,他的本質就是一個數,但是這個數表示的含義並不是數值,而是表示一個內存地址。這裏囉嗦幾句,計算機主要存儲數據的地方就在內存中,因此內存也被成爲主存,而我們要向知道這個數據到底在內存的什麼位置,就需要對內存進行編號。我們把每一個字節,也就是8個bit作爲一個單位進行編號,假如說一個4GB的內存,那麼他的地址就應該是從0x00000000到0xFFFFFFFF,換算成十進制也就是從0到4294967295。照理來說,應當只有內存纔有地址,但是在有些架構的設備上,採用了統一編址,也就是將一些其他硬件(例如寄存器)也編上了內存地址,我們常用的x86體系中,也是把顯存和內存一起編址的,不過這些具體的硬件實現不影響我們上層邏輯,我們還是認爲這個地址就是內存地址。

    所以,所謂的指針類型,其實就是存了一個可以表示地址的數,僅此而已。

2.指針類型有多大?

    在講解這個問題之前,需要先了解一個概念,就是CPU的字長,我們常說的32位CPU還是64位CPU,其實指的就是CPU的字長,又或是叫做尋址空間,也就是說,CPU可以通過多少位二進制來表示一個內存地址,然後訪問它,當然了,這裏說的是理論上最大。

    我們假如CPU的尋址空間只有1位,那麼這1位就只能表示0和1這兩個地址,那麼也就是說,1位CPU的尋址空間是2字節。同樣的,如果是2位CPU,那麼可以表示00,01,10,11這4個地址,也就是說,2位CPU的尋址空間是4字節。以此類推,n位CPU的尋址空間應當是(2^n)個字節,那麼我們計算一下32位CPU的尋址空間應當是4294967295,也就是0xFFFFFFFF,也就是4GB,換句話說,32位CPU最大支持4GB的內存,然而這裏面還有一部分要分給顯存等其他硬件,實際可用的內存也就不夠4G了,這也是我們爲什麼一定要升級到64位CPU的原因,因爲在這個年代,4GB的內存顯然已經不夠用了。而如果是64位CPU,計算一下,可以訪問的空間理論上來說最大有16EB,沒見過EB這個單位吧?1EB=1024PB=1024*1024TB,TB總該見過了吧!現在有個10TB的硬盤已經感覺很大的,這傢伙可是10多萬TB,所以足夠發展幾十年了!

    那麼,既然指針是用來表示地址的,那麼它總得有一個能放得下所有地址的長度吧!所以,32位系統下指針是8字節(也就是32位),64位系統下指針是16字節(也就是64位)。什麼?你看到的打印只有5位!還有7位的?不要驚訝,那只不過是你格式符調的讓他省去了前面的0而已,可以試試用sizeof運算符計算一下,就知道我說的對不對了:

printf("%size of pointer is:lu\n", sizeof(char *));

 3.指針的類型

    其實這個標題是有問題的,C語言就只有一種指針類型,所有的指針都是這一種指針類型,所以,也就沒有什麼所謂的指針的類型,但是我看到很多數據和資料,包括很多人都這樣說了,那我也就入鄉隨俗這麼跟着叫吧。所謂的指針的類型,並不是說這個指針本身的類型,而是表示的是這個指針所指的對象的類型,換言之,就是假如有一天,我們想通過這個指針找到實際那個地址中的數據的時候,我們應該把那個數據當做什麼類型來處理。舉例來說:

int a = 146; // 假如a的地址是0x0000FF01
int *p = &a; // p的值就是0x0000FF01

    需要注意的是,這裏p的類型,int *,是我們自己加上去的,也就是說我們告訴編譯器,p存放了一個地址,並且這個地址裏的數需要按照int的方式解析。當然,我們也可以很任性地給p換成別的類型,比如說:

unsigned short a = 36864;
unsigned short *p = &a; // *p是36864
short *p2 = (short *)&a; // *p2是-28672
char *p3 = (char *)&a; // *p3是'\0'
unsigned *p4 = (unsigned *)&a; // *p4不確定

    那麼這裏,我們看到,p,p2,p3,p4的值都是&a,也就是說,這四個指針的值是相等的,都等於a的地址,我們說他們都“指向”a,但是,我們分別對他們進行解指針運算以後,爲什麼得到了不同的結果呢?首先,我們需要了解的是,a這個變量,到底在內存中怎麼存儲的。

    我們看,a是一個unsigned short類型,我們知道短整型佔2個字節,也就是說,存儲36864這個數,應該有2個字節的,這2個字節應該分別有自己的地址。我們假設這兩個地址分別是0xFF00和0xFF01。我們把36864轉換成二進制,應當是1001 0000 0000 0000,也就是0x9000。在x86體系的計算機中,一般採用大段序,也就是高位存在低字節,那麼也就是0xFF00這個地址存放的是0x00,0xFF01這個地址存放的是0x90。那我們給a進行取值運算,到底取的是哪個呢?是這個變量的首地址,也就是0xFF00,所以,&a的值是0xFF00,那麼,p,p2,p3,p4也就都是0xFF00。既然,這個指針變量中僅僅存的是一個首地址,那麼,當我們需要解指針的時候,編譯器怎麼知道,這個地址所表示的內存究竟是單獨一個字節作爲一個數呢?還是連續兩個字節表示一個數呢?還是連續n個字節表示一個數呢?首位究竟是表示數值還是符號呢?這個數究竟是表示整數呢還是浮點數呢?答案是,不知道!因爲我們現在僅僅知道這個0xFF00是某一個數據的首地址,僅此而已,這時候我們是不能夠進行解指針運算的,原因就像剛纔說的,計算機並不知道怎麼處理這個內存中的內容。

    所以,我們需要告訴計算機,以何種方式來讀取這個地址中的內容。這裏p是unsigned short *類型,也就是我們指明,用“無符號短整型”的方式來讀取0xFF00中的內容,那麼,所謂的“無符號短整型”的方式,也就是說,數據保存在連續的2個字節中,且0xFF00表示低位,0xFF01表示高位,並且,最高位表示數據。計算機就會將其整合成+1001000000000000,也就是36864。

    同樣的道理,p2是short *類型,也就是我們指明用“有符號短整型”的方式來讀取,同樣的是連續兩個字節,但是首位表示符號,那麼計算機就將其整合爲-(0010000000000000),由於負數是以補碼形式存在的,因此求其補,也就是-1110000000000000,也就是-28672。再來說說p3,char *類型,表明用字符形式讀取,連續的一個字節,因此,只會讀取0xFF00這一個字節,這個字節的內容是0x00000000,也就是0,但是,這裏是字符,所以應該是0對應的ASCII碼,也就是字符串結尾符'\0'。p4也是同理,但是unsigned類型是讀取連續4個字節,當然了,這裏我們無法預見0xFF02和0xFF03中的內容是什麼,所以你運行出來的結果可能每次都不太一樣,但是,原理都是,把後面兩個當做了次高位和最高位,然後首位當做數值計算出來的結果。

    所以,說了這麼多其實就想讓大家明白一點,指針本身只是一個數,一個可以表示內存地址的數,僅此而已,所謂的“指針類型”,其實指的是,解指針操作時使用的數據解析方式。那麼我們有沒有辦法定義一個赤裸裸的指針?也就是說,我們僅僅說明它是個指針,但並不制定它指向的數據的類型。有的!那就是void *

4. 泛型指針

int a = 8;
void *p = &a; // p仍然是指針,值仍然是a的地址
// *p = 0; // error

    所以這個的void *和其他的寫法一樣,還是表示,p是一個指針類型,裏面存的數是表示一個內存地址的。只不過區別在於,使用void *定義的指針不能夠進行解指針運算,道理很簡單,因爲我們沒有告訴計算機到底用哪種方式來解析這個指針所指向的數據。將這樣的指針,教科書上普遍稱爲“泛型指針”,之所以這樣命名,我想可能是因爲他覺得這樣的指針什麼類型都能指所以叫“泛型”。但是,根據我之前的描述,相信大家也都能看出來,其實根本就不存在泛型不泛型這一說,&a就是一個簡簡單單的數而已,你存到什麼類型的指針下都是允許的,甚至,我們還可以存到一個整型變量裏,像這樣:

int a = 0;
unsigned long long p = (unsigned long long)&a;

    這樣寫一點問題都沒有,這樣我們定義的p,同樣是a的地址,但是我們之後可以把它當做一個普通的整數來對待。當然了,反過來一樣可以,比如這樣:

unsigned long long a = 0xFF00;
int *p = (int *)a;
*p = 0xAE08;

    如果你很勤快的話,讀到這裏你一定會在心裏偷偷罵逗比,說,我試過了!這樣明明不行,你看,我的編譯器都給我報錯了!唉,逗比心裏苦啊。你先別急,你真的都比逗比了。要想解釋爲什麼,首先我們需要讀懂這3行代碼。第一行,定義了一個無符號64位整型變量,這沒什麼說的。第二行,把a的值賦值給了p。那麼p的值就是0xFF00,但是因爲p是指針類型,所以,表示的含義就是0xFF00這個內存地址了。第三行,解指針p,也就是給0xFF00這個內存地址(由於是int,也包括後面3字節)的位置按照大段序存放0xAE08,並且首位表示符號。也就是說,給0xFF00存0x08,0xFF01存0xAE,0xFF02存0x00,0xFF03存0x00。這個絕對可行,但是爲什麼你在IDE上這一行會報錯呢?那是因爲,我們在IDE上編譯出的程序默認是應用程序,應用程序是受控於操作系統的,也就是說,操作系統負責這個進程的內存管理,你只能用操作系統分配給你這個進程的內存,而不能使用其他的內存。因此,我們指定他往0xFF00這個地方寫數據,顯然就越權了,所以,這個操作被禁止了。如果我們寫的程序是運行在16位實模式下的,那麼這樣是完全OK的,程序運行到此句的時候,就會給0xFF00-0xFF03這4個字節中寫數據。

    說來說去,還是再強調一下指針的本質,就是一個數,就這麼簡單。

5.多級指針

    我相信,教科書上,甚至很多資深老專家在講解C指針的時候,都一定會把多級指針列成一個專門的一節,然後去給你非常耐心地講解什麼是單級指針,什麼是多級指針,它們之間有什麼不同,使用時應該分別注意什麼問題。所以我在這裏也使用這樣一個標題。但是,說真理不怕得罪人。其實根本就不存在什麼單級多級指針,就像我之前說的,指針其實就一種類型,就是指針類型。單級指針也好,多級指針也好,都是指針,所以,逃不出這個本質,他存的就是個可以表示內存地址的數,僅此而已。

    那麼,我們常說的這個多級指針是什麼呢?請看下面的例子:

int *a = 0;
int *p = &a;
int **q = &p;

    我想這應該是很多人見過的講解多級指針很經典的例子。p是單級指針,q是多級指針。爲什麼呢?因爲p前面是1顆星,q前面是2顆星。emmm....這種解釋,乍聽起來確實有道理,但其實這和幾顆星星沒什麼關係。我們來解讀一下這3行代碼。前兩行不用解釋了,直接看最後一行。q是個(暫時我們就先按習慣叫二級)指針吧,它的值是p的地址。而p,我們不管他是不是指針,也不管他多少級,總之不可否認的是,p在這裏是一個變量,就和a一樣,都是一個普通的變量,裏面存着某一個值。那麼,既然p是個變量,那麼這個變量也肯定是要在內存中存儲的吧。既然要在內存中存儲,那這片內存空間也肯定是有自己的地址的吧。舉個例子來說,假如a是在0xFF02這個位置,p在0xAF00這個位置,那麼,a的值是5,所以0xFF02就是0x05,0xFF03-0xFF05都是0x00,然後,p的值是a的地址,也就是0xFF02,所以,0xAF00的值是0x02,0xAF01的值是0xFF,0xAF02和0xAF03都是0x00,用個表格來表示如下(爲了方便,以下指針用32位系統中的。如果是64位的類似,只不過要佔8字節):

地址 備註
0xAF00 0x02 p的最低字節
0xAF01 0xFF p的次低字節
0xAF02 0x00 p的次高字節
0xAF03 0x00 p的最高字節
……    
0xFF02 0x05 a的最低字節
0xFF03 0x00 a的次低字節
0xFF04 0x00 a的次高字節
0xFF05 0x00 a的最高字節

    看了這張內存分佈表,我想大家應該更明白了,這個指針p不是何方妖孽,就是個普普通通的變量而已,和a一樣的。所以,我們給p進行取址運算,就會得到p的首地址,也就是0xAF00,然後,我們再把它存在q當中。假如q的地址是0x8400,那麼補充上表:

地址 備註
0x8400 0x00 q的最低字節
0x8401 0xAF q的次低字節
0x8402 0x00 q的次高字節
0x8403 0x00 q的最高字節

    所以我們看到了,q也是一個普普通通的變量而已,和p和a都一樣。那麼這個類型定義前面讓人費解的兩顆星到底是什麼呢?是這樣的,C語言中,如果一個類型是Type,那麼如果我們定義一個指針,並且指定用Type這種方式來解指針的話,我們就將這種指針類型定義爲Type *,所以,a是int型,p是a的指針,因此p的類型就是int *; 而q是p的指針,p的類型是int *,所以q的類型就是int **。如果還是不太清晰的話,不妨我們介紹一個技巧,typedef重命名變量類型,假如我們將“指向int型變量的指針”這種類型定義爲Type1,那麼就有:

typedef int *Type1; // Type1就表示int *
int a = 0;
Type1 p = &a;
Type1 *q = &p;

    我們把int *類型表示爲Type1,那麼自然,p就是Type1類型的,這個Type1就相當於一個新的類型,可以和int, double, char這些同方法使用。那麼既然q是指向p的,也就是說,當我們對q解指針的時候,要按照Type1這種方式解,那麼自然,q的類型就是Type1。現在我們再來說,q是一級指針還是二級指針?顯然沒有意義了。

    指針問題把握住兩點,第一,指針就是個數,再普通不過的數;第二,指針類型其實都是同一種類型,所謂的類型只是指定解指針時使用的數據讀取方式。這就OK了。

    至於明明都是一樣的地址,爲什麼要進行強轉操作,這是因爲控制類型安全,這是編譯器控制的,它怕你寫錯,畢竟,我們從int *轉化成char *這種操作還是不常見的,所以,顯式寫出來防止出錯。默認情況下,Type類型取址得到Type *類型,Type *類型解指針得到Type類型,所謂的多級指針,把星放在類型裏,整體替換那個Type就行了。如果顯式強轉的話,那就隨意了,根本沒有類型和所謂級數的限制,我們把char ***轉成int *,甚至轉換成int都沒有任何問題。

6.數組指針

      如果你前面的都聽懂了,或者是,熟悉我的套路的話,應該就能猜到我下面想說什麼了。沒錯,數組指針也是個指針,只不過解指針的時候用“數組”這種方式來解。把握住這個原則就錯不了,相信逗比!

    不過數組指針的語法可能稍微複雜一些。在詳細介紹之前,先給大家灌輸一個印象,就是說,C語言中的類型表示(我這裏單純指的是形式上)有兩種,一種叫簡單類型,一種的複雜類型。(不要嫌我囉嗦,再強調一遍,這是形式!形式!不是實際的簡單複雜,只是形式上。其實他們可以轉化的。)使用簡單類型定義變量(或者是別名)的時候,類型在左,變量名(或者別名)在右。複雜類型時,類型在兩邊,變量名(或者別名)在中間。什麼意思呢?我們看下面這個例子:

int a; // 簡單類型
int b[3]; // 複雜類型

    int就是一個簡單類型,我們定義變量的時候,int在左,a在右。簡單類型的指針類型同樣是個簡單類型,比如說int *, int **等。而b是一個複雜類型,b的類型其實是int [3]類型,也就是說左邊的int和右面的[3]共同表示b這個變量的類型。複雜類型的指針一般也是個複雜類型,比如說現在要將的數組指針。

int b[3];
int (*p)[3] = &b;

    b的類型是int [3],解釋爲,一個擁有3個整型元素的數組類型。管他是什麼什麼數組還是什麼,歸根結底就是個類型唄,那b也就是個變量唄,是變量就要存在內存裏唄,存在內存裏又得有首地址唄,那&b不就表示這個b的首地址了嘛。我們自然可以有一個指針來存這個地址,那要是想解指針之後得到一個int [3]類型,那麼這個指針的類型就是int (*)[3]。大家注意這種寫法,C語言中遇到複雜類型時有一個解讀原則,就是先找小括號,如果有小括號,那麼,以小括號爲起點,如果沒有小括號則找中心,以中心爲起點,從裏向外讀。比如說這裏int (*)[3]類型,先找到小括號,小括號裏括着一顆星,那麼就表示,這種類型應該是{外面的類型}的指針。那麼,外面的類型是什麼?是int [3]。所以,這種類型就是一個int [3]類型的指針,解釋爲,一個指向{擁有3個整型元素的數組}的指針類型。所以簡稱爲數組指針,那麼,它是指針,自然就滿足我之前說的所有的這一切的特性,不再贅述。我們再來看下面這個例子:

int *c[3];

    請問c是什麼類型?也就是說這個int *[3]類型怎麼解釋呢?按照剛纔的原則,沒有括號所以找中心。中心怎麼找呢?如果規範來寫,變量名本身中心,如果不規範寫或者省略了變量名的話,這時候大家也不要怕,中心的左邊一定是一個單詞(就是找英文字母啦)或是*,中心的右邊一定是括號。所以,單詞或*和括號的分界處就是中心。所以這裏的中心就是*和[的中間,往外一層發現左邊是*,右邊是[],到底跟哪個呢?這裏另一個原則是,星號*的意義跟着左邊走。雖然我們在書寫的時候,規範要求是把*貼右來寫,但是,在解釋類型的時候,*的意義要跟左邊走。所以,這裏的*應該和左邊是一個整體,那麼自然就應該先讀[3]了,也就是說,這是一個有3個元素的數組。然後再往外,右邊空了,左邊還有一個int *,於是,這種類型被解釋爲:一個擁有3個{指向整型的指針}類型的元素的數組。簡稱爲指針數組,所以,它是數組,並不知指針,但它的元素是指針。也就是說,b是數組,但是b[0]就是指針。

    我剛纔提到說,規範的寫法是*要貼右,但是解釋的時候要貼左,這是爲什麼呢?因爲C語言允許這樣的語法:

int a, *b, **c; // a是int,b是int *,c是int **
int *d, e, f; // d是int *,e和f是int

    怎麼說呢,這樣用起來有時候還挺方便的,但是,可能C語言的設計者也沒想這麼多,可能是一種設計缺陷吧,所以,如果你*貼左的話,下面這種寫法可能有容易讓人誤會:

int* a, b, c; // a是int *,b和c都是int

    上面這行代碼可能就會讓人誤以爲a,b,c都是int *,而其實只有a是int *。當然,這還沒完,還有更容易被誤解的寫法:

int (*p)[3], a, *b;

    有木有暈掉?哈哈,這一行代碼,p是一個數組指針,a是int,b是int *。要不要來點更刺激的?

int* p, (*q)[3], *r[3];

    你能順利解析出p,q和r的類型嗎?這裏p是一個int *類型,q是一個數組指針,r是一個指針數組。所以,爲了避免這種反人類的寫法誤導他人,我們還是別這麼搞了!小心過幾天你自己都看不懂……

    不過爲了把問題徹底搞懂,我們再來看另外一種方式的反人類的類型,請看下面的代碼,解釋q是什麼類型:

int (*(*p[3]))[4];

    按照我剛纔介紹的方法,看看你能不能正確解析出p的類型,答案是:p是一個有3個{指向{指向{有4個整型元素的數組}類型的指針}類型的指針}類型元素的數組。如果你這都能答對,那麼恭喜你,你昇華了!如果沒有,沒關係,殺手鐗還沒用呢。遇到這種比較複雜的類型,建議大家不要直接這麼去寫,因爲真的太反人類了,還是用typedef吧,按照剛纔說的原則,新類型名和變量名放的位置是一樣的,比如說上面那種反人類的類型,就可以用下面的方法來定義:

// int (*(*p[3]))[4];
typedef int Type1[4];
// Type1 *(*p[3]);
typedef Type1 *Type2;
// Type2 *p[3];
typedef Type2 *Type3;
// Type3 p[3];

    是不是看起來舒服多了?OK,關於數組這裏還有一點要強調的就是,當一個數組類型被寫在函數參數中,這個數組類型會自動轉化爲指針類型,也就是一個Type [n]類型的數組,無論n是幾,寫在參數中都會變成Type *。值得注意的是,僅僅是參數本身是數組類型纔會轉化爲指針,和數組元素的類型沒有關係,下面舉個例子:

void test(
    int a[5],
    int b[],
    int *c[3],
    int (*d)[3],
    int **e,
    int **f[3],
    int *(*g)[4],
    int h[2][3],
    int i[4][7][9]
) {
    // a -> int *
    // b -> int *
    // c -> int **
    // d -> int (*)[3]
    // e -> int **
    // f -> int ***
    // g -> int *(*)[4]
    // h -> int (*)[3]
    // I -> int (*)[4][7]
}

    總之一句話,只有數組纔會變成相對應的指針,如果它本身不是數組,那麼該是什麼還是什麼(當然也包括指針)。正是因爲C語言的這種設計,才造成了我們如果給一個函數傳數組的話,就不得不再專門傳一個參數來表示元素個數以防止緩衝區溢出。因爲雖然形式上是數組,但其實傳遞的是數組首元素的指針,所以,並不能從這個首元素的指針這個參數上得知數組的元素個數以及數組上介。

    還有一點值得注意的是,數組類型轉化爲其對應的指針類型(也就是數組首元素的指針類型),換句話說就是Type [n]類型轉化爲Type *類型不需要強轉,可以隱式轉換,比如下面的例子:

int arr[3];
int *p = arr; // 不用寫成int *p = (int *)arr;

    覺得自己把上面的都掌握了,很高興是嗎?那我告你,高興太早啦!我還沒講完呢!

7. 函數指針

    我們知道函數是用來存儲代碼塊的,不過熟悉馮·諾依曼體系結構的同學應該都清楚,在我們當前這種計算機體系下,數據和指令擁有二進制等價性,也就是說,數據和指令本身沒有本質區別,只不過我們把它認爲是數據時就可以進行數據的操作,認爲是指令時就可以執行(當然就是機器碼層面的執行了)。所以,函數這種東西,它用來存儲代碼塊,也就是用來存儲指令唄。那既然指令和數據沒啥區別了,那它應該和其他的變量(也就是數據)一樣嘛,也是存在內存裏的嘛,也有首地址嘛,所以也就有對應的指針類型嘛。所以,函數指針存儲的就是這段函數的首地址,自然,我們可以通過一個地址找到一個函數,然後按照函數的執行方式執行它。

int sum(int a, int b) {
    return a + b;
}

    假如我們有這樣一個像上面這樣的函數,要想獲取地址怎麼辦呢?這裏需要注意的是,對於變量,變量名本身代表的是變量的引用(也就是代表實體),而要表示地址,需要進行取址運算,也就是&符號。但是函數不一樣,對於函數,函數名本身就代表的是函數的地址,而函數名後面加小括號則表示執行函數。所以,我們可以用這樣的方式得到sum函數的地址:

void *p = sum; // 直接寫函數名就表示函數首地址了,但是注意不能寫sum(),或者sum(1, 2)這樣

    不過這樣寫有點逃避主題了,雖然說指針都是一種類型,這樣用泛型指針來存儲沒有問題,但是畢竟,我們存指針的目的,還是需要在適當的時候來通過指針找到實體的(也就是解指針),所以,還是應該掌握實際的函數指針類型的書寫方式:

int (*p)(int , int) = sum;

    這裏的寫法和數組指針類似,函數類型也可以看做一個複雜類型,只不過這裏不是數組的中括號了,而是參數列表的小括號。對於數組來說,左邊的部分表示元素類型,右邊表示數組上界的元素個數。而對於函數來說,左邊是返回值,右邊的參數列表。記住這樣的原則就沒問題了。當賦值過以後,我們可以通過給p加參數來調用sum函數的方法。比如說p(1, 2),其實就等於sum(1, 2)。

    但是,並不能把p和sum當做相同的東西,因爲p的本質還是指針,我們也可以給p進行取址操作,還可以給p賦值,比如說

int (**q)(int, int) = &p; // 取址
p = NULL; // 賦值

    p也滿足所有變量的作用域和生命週期。但函數一定是全局的(特指C函數哈,C++可不一定),作用域也僅僅和static關鍵字有關,也不能取址和賦值,所以它和指針是不一樣的語法體系,要區分開。

    好啦,準備好迎接挑戰了嗎?試試下面幾個變量類型的解釋吧:

int (*(*p)[3])(int (*)[5]);
int (*q(int *))(int (*)());
int r(void (*)(int (*)()));

    emmm...如果實在看不出來的話,就別爲難自己了吧,實際應用的時候記住typedef!這裏公佈一下答案吧:

    p是一個指向{擁有3個{指向{返回值爲int,參數爲{一個參數,第一個參數是{一個指向{存儲了5個整型元素的數組}類型的指針}}的函數}類型的指針}類型元素的數組}類型的指針。

    q是一個指向{返回值爲指向{返回值爲整型,參數爲{一個參數,第一個參數是{返回值爲整型,參數爲空的函數}}的函數}的指針,參數爲{一個參數,第一個參數是一個指向整型的指針}的函數}類型的指針。

    r是一個返回值爲整型,參數爲{一個參數,第一個參數是{一個指向{返回值爲空,參數是{一個參數,第一個參數是{返回值爲整型,參數爲空的函數}}的函數}類型的指針}}的函數。(這是一個函數聲明)。

8.指針綜合應用示例

    呼~~~~好啦,終於是把指針講完了,最後,我們來實戰一下吧。看問題:

    請實現一個函數,返回兩個參數中較大的一個,需要支持任意類型。(默認大端序存儲)

    這個問題很簡單,但是難點就在,需要支持任意類型。如果確定了類型,那這個代碼自然好寫,但是不確定類型,我們就需要通過一些參數來確定這個類型的性質。C語言沒有類似於C++的模板這樣的語法,不支持泛型編程,因此,我們只能從單個字節入手。由於題目制定了大端序,我們就不用判斷字節序了。首先變量的長度是需要知道的,其次,需要比較的兩個變量的首地址是需要知道的。除此之外還有一個非常重要的因素就是,我們得知道,這種類型下,到底什麼纔算大,什麼纔算小。這是什麼意思呢?如果我們處理的類型是數值,那麼,自然對於大小比較有着數學上的定義,但是,萬一不是數值呢?萬一這個類型是個結構體呢?比如說它表示學生信息,兩個學生信息到底怎麼算大怎麼算小?這些也是我們需要知道的。

    所以,總結下來,我們的函數應當接受4個參數,分別是:

1. 需要比較的第一個變量的首地址

2. 需要比較的第二個變量的首地址

3. 要比較的類型的長度

4. 要比較的類型的大小判斷方法

    返回值是較大的變量的地址。

    前兩個參數,由於我們不知道變量類型,所以,只能使用泛型指針void *,當然,返回值也應該是void *,第三個參數好說,整型就好(當然也可以傳無符號整型,這個的選擇依賴看你想支持的數據的最大長度,二來時看你想不想用類似於-1這樣的負數來表示異常值,這裏就不詳細說明了)。第四個參數,由於是一種判斷方法,那麼,調用我們這個程序的人,應該是用函數實現的,通過傳兩個參數,來判斷第一個參數是否大於第二個參數,我們需要在函數裏調用這個比較函數得到結果,因此,這裏應當傳入函數指針。

    說到這個函數指針,自然,我們還需要明確這個函數的參數和返回值了。我們要求調用我們接口的人,應當實現一個函數,來判斷這種類型下的數據,第一個數據是否大於第二個數據,參數應當是:

1. 需要比較的第一個變量的首地址

2. 需要比較的第二個變量的首地址

    返回值是0或1,表示不大於或大於。

    好了,設計完畢我們就可以開始編碼了,我編的代碼是這樣的:

void *getBiggerOne(void *n1, void *n2, int type_size, int (*isBigger)(void *, void *)) {
    if (isBigger(n1, n2)) { // 如果n1 > n2
        return n1;
    }
    return n2;
}

    那麼,我們可以寫一個測試用例,比如說我用一個結構體類型來測試,這個結構體表示學生的信息,而我定義,學校較大的就是學生年齡較大的,可以這樣來測試:

typedef struct {
    int id;
    int age;
    char name[20];
} Student;

int isBigger_Student(void *n1, void *n2) { // 用來判斷n1是否大於n2
    Student *s1 = (Student *)n1;
    Student *s2 = (Student *)n2;
    if (s1->age > s2->age) { // 比較兩個Student的方式是比較其age
        return 1;
    }
    return 0;
}

int main(int argc, const char * argv[]) {
    Student s1 = {
        id: 1,
        age: 15,
        name: "Jack"
    };
    Student s2 = {
        id: 2,
        age: 13,
        name: "Tom"
    };
    
    Student *bigger = getBiggerOne(&s1, &s2, sizeof(Student), isBigger_Student);
    printf("The information of bigger Student:\nid:%d\nage:%d\nname:%s\n", bigger->id, bigger->age, bigger->name);
    
    return 0;
}

    這是輸出結果:

The information of bigger Student:
id:1
age:15
name:Jack

    我們成功地將年齡較大的學生打印了出來。

9.總結

    好啦!總算是找到一個機會把指針給大家講完了,真的是累死逗比了!到現在,不知道你還認同不認同我一開始說的那句話,如果你不會指針,那就別說你會C!真的,C語言有一多半都是在玩指針,只有你指針玩好了,才能說你完全掌握了C語言。

    最後,再次總結要點:指針本身就是一個數,只不過可以表示地址。指針只有一種類型,而所謂的類型只不過是用於解指針時指定數據的解析方法。複雜類型是名稱在中間,類型在兩邊,解析時要從裏往外。數組類型在傳入函數參數時會轉化爲數組首元素的指針。

    最後的最後,如果大家還有什麼問題歡迎在下方留言,我們來共同探討。

    【本文爲逗比老師全權擁有,允許轉載,但是務必在開頭標註轉載源鏈接和作者信息,不得惡意拷貝和更改。】

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章