前言
相信大多數的同學都是第一門能接觸到語言是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 (*func12(int(*(int(*())))));
int (*(*(*pfunc8)[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 (*func12(int(*(int(*())))));
這又是什麼鬼玩意???我們先根據現有的經驗來進行層層解耦。首先像這種int(*())
的外層括號是可以去掉的,只是一個誤導,然後就變成了int*()
的鬼形式,然後編譯器會認爲它是int*(*)()
。那答案也就呼之欲出了。 答案:(*func12)((*pf1)((*pf2)()->int*)->int*)->int*
int (*(*(*pfunc8)[10])(int[], int))(int, int);
答案:((*pfunc8)[10])(int*, int)->((*pf)(int, int)->int)
結語
如果你能完成上面的所有題目,那麼你將獲得隱藏稱號:人形編譯器。
這裏的指針幾乎就是你這輩子能見到的所有指針了。至於其餘變種指針,基本上都圍繞這上面提到的方法構成。畢竟我們還沒加上C++的引用呢...
坑挖的太大也難免會有一些錯漏,歡迎指正。
現在,我們都是惡魔了