一封來自惡魔的挑戰邀請函,那些你見過或者沒見過的C語言指標都在這裡了
前言
相信大多數的同學都是第一門能接觸到語言是C/C++,其中的指標也是比較讓人頭疼的部分了,因為光是指標都能專門出一本叫《C和指標》的書籍,足見指標的強大。但如果不慎誤用指標,這些指標很大可能就會像惡魔一樣把你的程式給直接搞崩潰。
3個月前,我編寫了一份這些指標都是惡魔嗎?.c
的檔案,裡面從大多數常用的指標型別,一路延伸到純粹只是在窺探編譯器所能產生的恐怖造物,為了增加趣味性,我還把這些指標都劃分了段位,只有辨識出該段位絕大多數的指標才能升段。目前看到的同學基本上都分佈在青銅到黃金的水平。現在我要將這些惡魔般的指標公諸於世,歡迎大家前來接受挑戰自虐。
前置宣告:
- 題目會包括陣列、指標、函式,以及它們的各種謎之複合體;
- 本文後面提及的一些指標不考慮什麼實用性,就當做是玩個遊戲,但適當情況下會對這些指標做必要講解;
- 如果你對指標開始產生不適、恐懼感,建議你提前離開,以免傷到你對C語言的熱情;
- 你想從這些指標裡面挑一道作為自己的題目?隨你喜歡。
這些指標都是惡魔嗎?
青銅(答對所有題升至該段位,正確率100%)
請用文字描述下列指標、陣列的具體型別:
int * p0; int arr0[10]; int ** p1; int arr1[10][10]; int *** p2; int arr2[10][10][10];
下面適當留白以供思考,想好後就可以往下翻看答案。
青銅題解
對於初學C指標的同學基本上應該都能答出來:
int * p0; // p0是 int指標 int arr0[10]; // arr0是 int陣列(10元素) int ** p1; // p1是 int二級指標 int arr1[10][10]; // arr1是 int二維陣列(10*10元素) int *** p2; // p2是 int三級指標 int arr2[10][10][10]; // arr2是 int三維陣列(10*10*10元素)
白銀(答對4題升至該段位,正確率80%)
請用文字描述下列指標、陣列、函式的具體型別:
int (*p3)[10];
int *p4[10];
int *func0(int);
int func1(int * p);
int func2(int arr[]);
這些指標還是比較常見、實用的,想好後就可以往下翻看答案。
白銀題解
int (*p3)[10];
中的p3
與*
先結合,說明p3
是一個指標,然後把(*p3)
拿開,剩下的就是p3
這個指標所指之物(即int[10]
)。答案:p3
是一個指向[int陣列(10元素)]的指標
,符號化描述即p3
是int(*)[10]
型別。
int *p4[10];
中的p4
考慮到優先順序,會先與[]
先結合,而不是*
,說明p4
是一個含10元素的陣列,然後把p4[10]
拿開,則元素型別為int*
。答案:p4
是一個int指標的陣列(10元素)
,符號化描述即p4
是int* [10]
型別。
int *func0(int);
中的func0
先與括號結合,並且括號內僅是形參型別,說明func0
是一個函式,返回值型別為int*
。答案:f0是函式(形參為int, 返回值為int指標)
int func1(int * p);
答案:func1是 函式(形參為int指標, 返回值為int)
int func2(int arr[]);
中,留意int arr[]
的寫法,僅在函式中才可以這樣寫,是因為編譯器將arr
判定為指標型別,即和int * arr
的寫法是等價的。 答案:func2是 函式(形參為int指標, 返回值為int)
黃金(答對7題升至該段位,正確率70%)
請用文字描述下列函式的具體型別。而對於指標,請描述其可讀寫的情況(可以程式碼描述):
int func3(int arr[10]);
int func4(int *arr[10]);
int func5(int(*arr)[10]);
int func6(int arr[10][10]);
int func7(int arr[][10]);
int func8(int **arr);
const int * p5;
int const * p6;
int * const p7;
const int * const p8;
警告: 到這一步如果你對這些指標已經有所不適的話,建議提前離開,以免你產生了放棄C/C++語言的想法。如果你硬要堅持的話。。。想好後就可以往下翻看答案。
黃金題解
int func3(int arr[10]);
你以為這裡int arr[10]
就覺得這個函式的形參是一個int[10]
那麼簡單麼?那就錯了。事實上這裡的arr
仍然是int *
型別!你要想,如果將一個數組按值傳遞的話就以為著需要拷貝一份陣列給該函式用,10個就算了,那int arr[1000000000]
呢,一次copy就可以享受爆棧的快樂了。因此這裡編譯器會將其視作int *
型別,並無視掉後面的10,實際上就是將指標按值傳遞,這樣做可以節省大量記憶體,但多了一層間接性與越界風險(收益遠大於風險)。這裡的10實際上也僅僅是要提醒作為開發者的你,傳入的陣列(or指標)必須保證其地址後面sizeof(int) * 10
位元組都要能夠訪問。你可以傳入元素個數大於等於10的陣列,至於小於10的話...後果自負。答案:func3是 函式(形參為int指標, 返回值為int)
int func4(int *arr[10]);
這道題也好說了,即arr
實際上是int **
型別,而作為開發者的你,需要保證傳入一個元素個數大於等於10的int指標陣列。答案:func4是 函式(形參為int二級指標, 返回值為int)
準則1:函式形參中所謂的陣列實際上都是指標型別
int func5(int(*arr)[10]);
注意arr
本身又不是一個數組,而是指標!一個指向陣列的指標! 答案:func5是 函式(形參為指向[int陣列(10元素)]的指標, 返回值為int)
int func6(int arr[10][10]);
你以為arr
是int**
嗎?那就又錯了。如果退化成int**
型別的話,那麼對於傳入的指標做類似arr[3][5]
的操作是十分危險的。通常int**
用於指向兩個維度都是動態分配的二維陣列(一個動態的指標陣列,每個指標是一個動態陣列),即把第一行的元素都當做int*
而不是int
來看待。把一個二維陣列強制變成變成int**
,再解除一次引用就會引起野指標的危險操作。因此實際上編譯器只會對第一維度的[10]
當做*來處理,即等價於int func6(int (*arr)[10]);
。 答案:func6是 函式(形參為指向[int陣列(10元素)]的指標, 返回值為int)
準則2:對於函式形參中的多維陣列,只會將第一維度作為指標處理
int func7(int arr[][10]);
和上一題等價。答案:func7是 函式(形參為指向[int陣列(10元素)]的指標, 返回值為int)
int func8(int **);
這裡只接受兩個維度都是動態分配的二維陣列(即int指標陣列)。 答案:func8是 函式(形參為int二級指標, 返回值為int)
const int * p5;
《C++ Primer》稱其為頂層const,即指向常量的指標,其所指資料不可修改,但指標本身可以替換,例:
p5 = NULL; // 正確!
*p5 = 5; // 錯誤!
而像const int num = 5
這種也是頂層const。
int const * p6;
和p5
等價。
int * const p7;
《C++ Primer》稱其為底層const,即指標本身為常量,其所指資料可以修改,但指標本身不可以替換,例:
p5 = NULL; // 錯誤!
*p5 = 5; // 正確!
const int * const p8;
包含了頂層與底層const,這樣所指和資料與指標本身都不可以修改。
鑽石(答對6題升至該段位,正確率75%)
請用文字描述下列指標、函式、函式指標的具體型別:
int (*pfunc1)(int);
int (*pfunc2[10])(int);
int (*(*pfunc3)[10])(int);
int func9(int (*pf)(int, int), int);
const int ** p9;
int * const * p10;
int ** const p11;
int * const * const p12;
實用性正在逐步降低中...
鑽石題解
int (*pfunc1)(int);
答案:pfunc1是 函式(形參為int, 返回值為int)的指標
,符號化描述即int(*)(int)
int (*pfunc2[10])(int);
f2先與[10]結合,說明f2是一個數組,把f2[10]
拿開,則元素型別為int(*)(int)
。答案:pfunc2是 函式(形參為int, 返回值為int)的指標陣列(10元素)
int (*(*pfunc3)[10])(int);
函式沒法作為陣列的元素,但函式指標可以。經過前面的磨難,應該可以看出來這是一個指向陣列的指標,陣列的元素是函式指標。 答案:pfunc3是 指向[函式(形參為int, 返回值為int)的指標陣列(10元素)]的指標
int func9(int (*pf)(int, int), int);
一個函式裡面需要接受一個函式指標作為形參,通常將以這種方式傳遞的函式叫做回撥函式。 答案:func9是 函式(形參為{函式(形參為{int, int}, 返回值為int)的指標, int}, 返回值為int)
const int ** p9;
具體可以參考下面的示範:
p9 = NULL; // 正確!
*p9 = NULL; // 正確!
**p9 = 5; // 錯誤!
int * const * p10;
具體可以參考下面的示範:
p10 = NULL; // 正確!
*p10 = NULL; // 錯誤!
**p10 = 5; // 正確!
int ** const p11;
具體可以參考下面的示範:
p11 = NULL; // 錯誤!
*p11 = NULL; // 正確!
**p11 = 5; // 正確!
int * const * const p12;
具體可以參考下面的示範:
p12 = NULL; // 錯誤!
*p12 = NULL; // 錯誤!
**p12 = 5; // 正確!
大師(答對5題升至該段位,正確率62.5%)
如果你有幸能夠堅持到這一步,或者已經放棄治療想看看後續內容,那麼接下來你將要面對的可能是各種匪夷所思的、惡魔般指標,這些奇奇怪怪的寫法甚至能夠通過編譯,簡直就是惡魔。
現在允許你使用一種偽lambda的描述方式,來對函式或函式指標進行拆解。示例如下:
int (*pfunc1)(int); // (*pfunc1)(int)->int
int f1(int); // f1(int)->int
箭頭所指的為返回值型別。
那麼。。。祝你好運,請用偽lambda描述方式拆解下面函式和函式指標:
int (*pfunc4)(int*());
int (*func10(int[]))[10];
int (*func11(int[], int))(int, int);
int (*(*pfunc5)(int))(int[10], int);
int (*(*pfunc6)(int[10]))[10];
int (*(*pfunc7[10])(int[10]))[10];
int (*pfunc8(int(*(int(*())))));
int (*(*(*pfunc9)[10])(int[], int))(int, int);
大師題解
int (*pfunc4)(int*());
基本上都倒在了形參的int*()
這種什麼鬼寫法是吧,不然這怎麼能叫惡魔指標呢,哈哈哈... 反正在這篇文章裡,就讓可讀性就統統見鬼去吧!如果你有Visual Studio的話,把這份宣告貼上到VS,然後游標放在上面,你會發現實際上形參的int*()
會被解讀為int*(*)()
。 答案:(*pfunc4)((*pf)()->int*)->int
int (*func10(int[]))[10];
這個在《C++ Primer》上似曾相識,如果你之前在裡面做過類似的題目話,就會知道這個函式,返回的是一個指向陣列的指標。你可以將該函式類似於函式呼叫的部分func10(int*)
拿掉,剩下的就是返回值型別int(*)[10]
了。 答案:func10(int*)->int(*)[10]
int (*func11(int[], int))(int, int);
函式返回了一個函式指標。 答案:func11(int*, int)->int(*)(int, int)
int (*(*pfunc5)(int))(int[10], int);
函式指標,所指函式返回了一個函式指標。 答案:(*pfunc5)(int)->((*)(int*, int)->int)
int (*(*pfunc6)(int[10]))[10];
答案:(*pfunc6)(int*)->int(*)[10]
int (*(*pfunc7[10])(int[10]))[10];
答案:(*pfunc7[10])(int*)->int(*)[10]
int (*pfunc8(int(*(int(*())))));
這又是什麼鬼玩意???我們先根據現有的經驗來進行層層解耦。首先像這種int(*())
的外層括號是可以去掉的,只是一個誤導,然後就變成了int*()
的鬼形式,然後編譯器會認為它是int*(*)()
。那答案也就呼之欲出了。 答案:(*pfunc8)((*pf1)((*pf2)()->int*)->int*)->int*
int (*(*(*pfunc9)[10])(int[], int))(int, int);
答案:((*pfunc9)[10])(int*, int)->((*pf)(int, int)->int)
結語
如果你能完成上面的所有題目,那麼你將獲得隱藏稱號:人形編譯器。
這裡的指標幾乎就是你這輩子能見到的所有指標了。至於其餘變種指標,基本上都圍繞這上面提到的方法構成。畢竟我們還沒加上C++的引用呢...
歡迎在評論裡面留下自己的段位證明(請誠實對待)。坑挖的太大也難免會有一些錯漏,歡迎指正。
現在,我們都是惡魔了