言指針與數(shù)組關(guān)系深度解析:從底層機(jī)制到實(shí)戰(zhàn)驗(yàn)證)
我相信每個(gè)C語(yǔ)言學(xué)習(xí)者在學(xué)到指針這一章時(shí)都會(huì)有那么一刻覺(jué)得自己懂了然后又會(huì)在做題或調(diào)試時(shí)突然發(fā)現(xiàn)怎么又錯(cuò)了。而在所有指針相關(guān)的概念里最容易讓人懷疑人生的就是指針與數(shù)組的關(guān)系這個(gè)問(wèn)題。數(shù)組名到底是不是指針為什么a[i]可以寫(xiě)成*(ai)為什么數(shù)組傳到函數(shù)里就變了這些看起來(lái)零散的問(wèn)題實(shí)際上都在追問(wèn)同一個(gè)核心指針和數(shù)組在內(nèi)存和編譯器眼中到底是怎么互相理解與轉(zhuǎn)換的。這篇內(nèi)容就是圍繞這個(gè)核心展開(kāi)從底層機(jī)制到實(shí)操驗(yàn)證把兩者的關(guān)系掰開(kāi)揉碎講清楚適合正在學(xué)C語(yǔ)言、或者學(xué)了但概念一直有些模糊的朋友。第28章這個(gè)位置其實(shí)很關(guān)鍵大部分教材會(huì)把指針?lè)旁谧詈髩狠S而講透指針與數(shù)組這一層等于把C語(yǔ)言最硬的一塊骨頭啃下來(lái)。很多人在此之前寫(xiě)的代碼只是能用但看完這一章再回頭寫(xiě)你會(huì)更清楚每一行代碼背后發(fā)生了什么。這篇文章我會(huì)從數(shù)組名的本質(zhì)講起再到下標(biāo)運(yùn)算的底層機(jī)制、指針?biāo)阈g(shù)的步長(zhǎng)問(wèn)題、常見(jiàn)易混淆概念比如指針數(shù)組和數(shù)組指針最后用調(diào)試器實(shí)測(cè)一遍把所有結(jié)論落到真實(shí)的地址輸出上而不是停留在教材這么寫(xiě)的層面。1. 數(shù)組名到底是什么從內(nèi)存地址到符號(hào)含義1.1 數(shù)組名的第一身份首元素地址常量很多教材一上來(lái)就說(shuō)數(shù)組名是數(shù)組的首地址這句話(huà)本身沒(méi)毛病但太容易讓人誤解。因?yàn)槭椎刂泛苋菀鬃屓艘詾閿?shù)組名是一個(gè)指針變量實(shí)際上它不是。數(shù)組名更像是一個(gè)沒(méi)有實(shí)體的地址常量它只在編譯階段有意義在運(yùn)行階段并不占一個(gè)單獨(dú)的變量空間。舉個(gè)例子你寫(xiě)int a[5];編譯器會(huì)在棧上或者靜態(tài)區(qū)為你分配5個(gè)int挨著排總共20字節(jié)假設(shè)int是4字節(jié)。然后a這個(gè)名字作為一個(gè)符號(hào)對(duì)應(yīng)的就是這20字節(jié)內(nèi)存塊中第一個(gè)int元素的首字節(jié)地址。注意它不是一個(gè)指針變量所以你不能寫(xiě)a也不能a somePointer;因?yàn)樗鼪](méi)有自己的存儲(chǔ)位置來(lái)裝一個(gè)地址值。很多人在這里會(huì)犯一個(gè)經(jīng)典錯(cuò)誤覺(jué)得a和a[0]完全等價(jià)。其實(shí)在絕大多數(shù)表達(dá)式中它們確實(shí)等價(jià)但有一個(gè)關(guān)鍵場(chǎng)景會(huì)暴露差異就是sizeof。請(qǐng)看這段代碼#include stdio.h int main(void) { int a[5] {1, 2, 3, 4, 5}; printf(sizeof(a) %zu\n, sizeof(a)); printf(sizeof(a[0]) %zu\n, sizeof(a[0])); return 0; }在我的64位Linux環(huán)境上sizeof(a)輸出20而sizeof(a[0])輸出8。為什么因?yàn)閟izeof(a)問(wèn)的是整個(gè)數(shù)組這個(gè)類(lèi)型int[5]的尺寸而sizeof(a[0])問(wèn)的是int*指針變量的尺寸。這就是數(shù)組名不等于首元素地址的最有力證據(jù)它在sizeof運(yùn)算符面前身份是整個(gè)數(shù)組而不是一個(gè)地址。那為什么在別的場(chǎng)景下a又等價(jià)于a[0]這是C語(yǔ)言在值語(yǔ)義下對(duì)數(shù)組名的特殊處理除少數(shù)幾個(gè)語(yǔ)境sizeof、運(yùn)算符、字符串字面量初始化之外數(shù)組名都會(huì)被退化為一個(gè)指向其首元素的指針。這個(gè)退化不是運(yùn)行時(shí)發(fā)生的而是編譯器在翻譯代碼時(shí)直接把a(bǔ)當(dāng)作指針值使用。所以int *p a;是合法的p拿到了首元素的地址但a本身依然是數(shù)組名。1.2 三個(gè)關(guān)鍵場(chǎng)景的區(qū)分sizeof、、數(shù)組聲明數(shù)組名在以下三個(gè)場(chǎng)景里的表現(xiàn)完全不同建議你把它們列為必須背誦級(jí)別的知識(shí)點(diǎn)因?yàn)閹缀跛锌荚囅葳搴蛯?shí)際代碼問(wèn)題都從這里冒出來(lái)。第一個(gè)場(chǎng)景是sizeof(a)上面已經(jīng)驗(yàn)證過(guò)它計(jì)算的是整個(gè)數(shù)組占用的字節(jié)數(shù)。第二個(gè)場(chǎng)景是a注意這個(gè)表達(dá)式的類(lèi)型并不是int**而是int(*)[5]即指向一個(gè)有5個(gè)int元素的數(shù)組的指針。它的值與a以及a[0]在數(shù)值上相同都是數(shù)組起始地址但類(lèi)型不同所以a 1會(huì)跳過(guò)整個(gè)5個(gè)int20字節(jié)而a 1只跳過(guò)一個(gè)int4字節(jié)。第三個(gè)場(chǎng)景是在數(shù)組聲明里比如int b[5] ...;這里的[5]是類(lèi)型系統(tǒng)的一部分跟在表達(dá)式中使用b時(shí)發(fā)生退化的語(yǔ)義完全不是一回事。我遇到很多初學(xué)者在拿到一個(gè)地址時(shí)喜歡用%x打印出來(lái)看然后發(fā)現(xiàn)a和a打印的數(shù)值一模一樣就以為它們是同一個(gè)東西。這是最典型的后續(xù)踩坑源頭數(shù)值一樣步長(zhǎng)不同類(lèi)型不同用法就完全不同。如果后面做二維數(shù)組或多維數(shù)組的遍歷理解a的類(lèi)型差異直接決定了你的代碼對(duì)不對(duì)。2. 下標(biāo)訪問(wèn)的真相編譯器是如何在底層翻譯a[i]的2.1 從a[i]到*(ai)等價(jià)關(guān)系的邊界條件可以說(shuō)理解指針與數(shù)組關(guān)系最核心的一把鑰匙是知道a[i]在編譯器眼里就是*(a i)。下標(biāo)運(yùn)算符[]本質(zhì)上是一個(gè)語(yǔ)法糖它做的事就是把第i個(gè)元素翻譯成從首地址偏移i個(gè)單位后解引用取出的值。但這個(gè)等價(jià)關(guān)系有一個(gè)大前提a必須能退化為指針。如果a是數(shù)組名它在a[i]這個(gè)表達(dá)式里會(huì)先退化成a[0]然后執(zhí)行*(a[0] i)。因?yàn)橹羔標(biāo)阈g(shù)是根據(jù)a[0]的類(lèi)型int*來(lái)定步長(zhǎng)的每一步移動(dòng)sizeof(int)個(gè)字節(jié)所以a[2]就等同于首地址 2 * 4字節(jié)然后從那個(gè)位置取4字節(jié)出來(lái)。有意思的是因?yàn)檫@個(gè)隱含的加法可以交換加法本身可交換所以a[i]和i[a]是等價(jià)的。你沒(méi)看錯(cuò)2[a]這種寫(xiě)法是合法的它能訪問(wèn)a[2]。你在生產(chǎn)代碼里當(dāng)然不該這么寫(xiě)但明白這個(gè)原理能幫你徹底消除對(duì)下標(biāo)運(yùn)算符的神秘感。它就是一個(gè)加法解引用的簡(jiǎn)寫(xiě)僅此而已。這里有個(gè)很容易踩的坑當(dāng)i是負(fù)數(shù)時(shí)會(huì)發(fā)生什么比如p[-1]。這在語(yǔ)法上合法也很容易踩中越界內(nèi)存。如果你用一個(gè)指針指向數(shù)組中間某個(gè)元素然后用負(fù)下標(biāo)訪問(wèn)它前面的元素這在合法范圍內(nèi)是可以的但一旦越界就是未定義行為。所以記住下標(biāo)不是數(shù)組的特權(quán)它是任何指針都可以使用的操作關(guān)鍵是你自己必須保證偏移出來(lái)的地址仍然在你的合法內(nèi)存范圍內(nèi)。2.2 指針?biāo)阈g(shù)的步長(zhǎng)為什么類(lèi)型決定一切前面反復(fù)提到步長(zhǎng)現(xiàn)在把這個(gè)問(wèn)題說(shuō)透。指針加減整數(shù)移動(dòng)的字節(jié)數(shù)由指針?biāo)赶虻念?lèi)型大小決定而不是由數(shù)組元素個(gè)數(shù)決定更不是固定的1字節(jié)。還是拿int a[5]舉例a 1和(char*)a 1是完全不同的前者移動(dòng)4字節(jié)到第二個(gè)int后者移動(dòng)1字節(jié)到首元素內(nèi)部的第二個(gè)字節(jié)。為了加深印象我們打印一下地址#include stdio.h int main(void) { int a[5] {10, 20, 30, 40, 50}; printf(a %p\n, (void*)a); printf(a 1 %p\n, (void*)(a 1)); return 0; }運(yùn)行結(jié)果里兩個(gè)地址差值是4。如果你把a(bǔ)強(qiáng)制轉(zhuǎn)換成char*再1差值就是1。這就是為什么在寫(xiě)內(nèi)存拷貝、序列化或底層協(xié)議處理時(shí)經(jīng)常需要把指針轉(zhuǎn)成unsigned char*或char*來(lái)逐字節(jié)操作因?yàn)檫@種類(lèi)型指針步長(zhǎng)正好是1字節(jié)你可以精確控制每個(gè)字節(jié)的位置。而如果你直接操作int*每一次移動(dòng)就跨越一個(gè)int想做字節(jié)級(jí)偏移必須強(qiáng)制轉(zhuǎn)換后小心計(jì)算。還有一點(diǎn)指針?biāo)阈g(shù)只應(yīng)該用于數(shù)組內(nèi)部元素之間或數(shù)組末尾后一位的合法地址計(jì)算。C標(biāo)準(zhǔn)規(guī)定在數(shù)組元素之外做指針運(yùn)算末元素之后的位置除外是未定義行為即使你沒(méi)有解引用它。實(shí)際工程中尤其是對(duì)緩沖區(qū)邊界判斷不嚴(yán)時(shí)這種未定義行為可能引發(fā)各種詭異的崩潰排查起來(lái)非常耗時(shí)間。3. 指針數(shù)組、數(shù)組指針與二維數(shù)組最容易混淆的三個(gè)概念3.1 聲明解析技巧從變量名開(kāi)始往左右讀指針數(shù)組和數(shù)組指針這兩組詞聽(tīng)起來(lái)幾乎一樣含義卻南轅北轍。我剛學(xué)C的時(shí)候也總需要停下來(lái)翻書(shū)直到我學(xué)會(huì)了一個(gè)特別實(shí)用的技巧從變量名開(kāi)始先往右看再往左看一層一層剝開(kāi)。int *p[5]變量名是p先往右看看到[5]說(shuō)明p先是一個(gè)數(shù)組再往左看看到*說(shuō)明數(shù)組里的元素是int*類(lèi)型。所以p是一個(gè)數(shù)組里面裝了5個(gè)int指針簡(jiǎn)稱(chēng)指針數(shù)組。聲明順序可以理解為p是一個(gè)由指針構(gòu)成的數(shù)組。再看int (*p)[5]變量名是p但先被*和括號(hào)包裹所以p是一個(gè)指針往右看遇到[5]說(shuō)明這個(gè)指針指向的對(duì)象是一個(gè)含5個(gè)int的數(shù)組。所以p是指向數(shù)組的指針簡(jiǎn)稱(chēng)數(shù)組指針。聲明順序可以理解為p是指向某個(gè)數(shù)組的指針。這個(gè)從內(nèi)到外、先右后左的解析法不只是為了應(yīng)付考試。在聲明二維數(shù)組的行指針時(shí)數(shù)組指針就是關(guān)鍵工具。比如void process(int (*matrix)[4], int rows);這個(gè)函數(shù)簽名里matrix就是一個(gè)數(shù)組指針?biāo)赶蚝?個(gè)int的一維數(shù)組也就是每一行的起始位置。3.2 二維數(shù)組的行指針訪問(wèn)步長(zhǎng)如何決定遍歷方式假設(shè)有int arr[3][4];那么arr作為數(shù)組名會(huì)退化為指向第一個(gè)元素的指針。第一個(gè)元素是什么是arr[0]一個(gè)長(zhǎng)度為4的一維數(shù)組類(lèi)型是int[4]。所以arr退化成int(*)[4]類(lèi)型也就是行指針。這和一維數(shù)組是一致的一維數(shù)組的數(shù)組名退化為指向元素類(lèi)型的指針二維數(shù)組的數(shù)組名退化為指向第一維元素即一行的指針。于是arr 1的步長(zhǎng)就是一整行即4個(gè)int共16字節(jié)。你可以用這個(gè)性質(zhì)非常方便地遍歷二維數(shù)組的每一行#include stdio.h int main(void) { int arr[3][4] { {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12} }; for (int i 0; i 3; i) { printf(row %d starts at %p\n, i, (void*)*(arr i)); } return 0; }注意這里寫(xiě)法是*(arr i)因?yàn)閍rr i得到的是第i行的地址類(lèi)型是int(*)[4]對(duì)它解引用才能拿到那一行的首元素地址類(lèi)型int*。如果你直接寫(xiě)arr[i]編譯器也會(huì)做同樣的換算arr[i]等價(jià)于*(arr i)結(jié)果是int*。再進(jìn)一步arr[i][j]等價(jià)于*(*(arr i) j)這個(gè)雙重解引用與二維數(shù)組的下標(biāo)訪問(wèn)一一對(duì)應(yīng)理解了這個(gè)你在看任何指針怎么遍歷二維數(shù)組的代碼時(shí)都不會(huì)發(fā)怵。3.3 字符串字面量與字符指針別讓只讀坑了你字符串在C語(yǔ)言里就是字符數(shù)組的一種特例它也天然涉及指針與數(shù)組的關(guān)系。先看兩種常見(jiàn)寫(xiě)法char str1[] hello; char *str2 hello;str1是一個(gè)字符數(shù)組它有6個(gè)字節(jié)包含末尾的空字符內(nèi)容存在棧上你可以直接修改比如str1[0] H;。str2則是一個(gè)指針?biāo)赶蛞粋€(gè)字符串字面量。相同的是str2的數(shù)值等于這個(gè)字面量在內(nèi)存中的首字符地址。不同的是字符串字面量通常存放在只讀區(qū)你試圖str2[0] H多半會(huì)在運(yùn)行時(shí)報(bào)段錯(cuò)誤或者行為未定義。這個(gè)問(wèn)題在實(shí)際開(kāi)發(fā)中出現(xiàn)頻率太高了幾乎每個(gè)人都被坑過(guò)。所以我的建議是如果你要修改字符串內(nèi)容一定要用字符數(shù)組來(lái)承載而不是字符指針指向字面量。而如果你只是想把一個(gè)字符串地址傳來(lái)傳去、并且不需要改動(dòng)內(nèi)容用const char*更合適既表達(dá)意圖又防止自己或別人在后面誤寫(xiě)。4. 函數(shù)參數(shù)中的數(shù)組退化為什么數(shù)組傳入函數(shù)后尺寸消失了4.1 退化的本質(zhì)形參里的數(shù)組聲明只是指針的樣子函數(shù)參數(shù)里如果寫(xiě)void f(int arr[10])看起來(lái)像是在傳一個(gè)數(shù)組但C語(yǔ)言的規(guī)則是在函數(shù)參數(shù)聲明中任何數(shù)組類(lèi)型都會(huì)退化為相應(yīng)的指針類(lèi)型。也就是說(shuō)void f(int arr[10])、void f(int arr[])和void f(int *arr)三種寫(xiě)法在編譯器看來(lái)完全等價(jià)都被當(dāng)作int *arr處理。這意味著什么意味著你在函數(shù)內(nèi)部寫(xiě)sizeof(arr)得到的永遠(yuǎn)是指針的大小8字節(jié)而不是數(shù)組的大小40字節(jié)。很多人第一次在函數(shù)里想用sizeof(arr)/sizeof(arr[0])計(jì)算元素個(gè)數(shù)結(jié)果發(fā)現(xiàn)除以之后總是2就是因?yàn)檫@個(gè)原因sizeof(arr)是8字節(jié)sizeof(int)是4字節(jié)于是以為數(shù)組長(zhǎng)度是2實(shí)際上這個(gè)計(jì)算完全無(wú)效。那么數(shù)組的長(zhǎng)度信息到底怎么傳進(jìn)函數(shù)答案是必須單獨(dú)用一個(gè)參數(shù)傳或者用一個(gè)結(jié)構(gòu)體把數(shù)組和它的長(zhǎng)度包在一起。什么參數(shù)里長(zhǎng)度信息自動(dòng)跟著走的場(chǎng)景是不存在的C語(yǔ)言就是這樣傳數(shù)組本質(zhì)上是傳首元素的地址元素個(gè)數(shù)信息被丟棄了。這是C的一個(gè)痛點(diǎn)但也是它的靈活之處。你在設(shè)計(jì)接口時(shí)一定勒緊褲腰帶記住任何接收數(shù)組參數(shù)的函數(shù)幾乎都必須帶上長(zhǎng)度參數(shù)否則你根本不知道邊界在哪。4.2 三種常見(jiàn)的傳參模式對(duì)數(shù)組操作的影響在實(shí)際工程里處理數(shù)組傳參有三種常見(jiàn)模式我簡(jiǎn)單總結(jié)一下各自的適用場(chǎng)景。第一種是void f(int *arr, int n)最樸素把數(shù)組退化成指針同時(shí)傳入元素個(gè)數(shù)適合一維數(shù)組的讀寫(xiě)和遍歷。第二種是void f(int (*matrix)[4], int rows)用數(shù)組指針接收一個(gè)每行固定4列的二維數(shù)組適合矩陣運(yùn)算。第三種是void f(int *matrix, int rows, int cols)把二維數(shù)組強(qiáng)制看作連續(xù)內(nèi)存來(lái)線性訪問(wèn)適合在不知道列數(shù)時(shí)動(dòng)態(tài)管理、或者需要把數(shù)據(jù)當(dāng)一維數(shù)組傳的底層接口。這三種方式各有各的代價(jià)第一種最簡(jiǎn)單第二種類(lèi)型最安全第三種最靈活但需要你自己做下標(biāo)換算比如訪問(wèn)matrix[i][j]要寫(xiě)成matrix[i * cols j]。我自己經(jīng)常在項(xiàng)目里用第三種配合動(dòng)態(tài)分配的內(nèi)存塊。但前提是你必須非常清楚數(shù)組在內(nèi)存中是按行連續(xù)存儲(chǔ)的arr[3][4]其實(shí)就是一塊12個(gè)int的連續(xù)空間。對(duì)這塊連續(xù)空間來(lái)說(shuō)二維只是一個(gè)邏輯劃分物理上并沒(méi)有真正的二維內(nèi)存。這也是指針與數(shù)組關(guān)系里最值得玩味的一點(diǎn)。5. 實(shí)操驗(yàn)證用一個(gè)內(nèi)存地址實(shí)驗(yàn)徹底看透二者關(guān)系5.1 設(shè)計(jì)實(shí)驗(yàn)打印所有關(guān)鍵地址與步長(zhǎng)前面講了這么多理論現(xiàn)在我們把結(jié)論用代碼驗(yàn)證一遍。我建議你也不用特別復(fù)雜的工具就用printf打印地址關(guān)鍵是要把幾個(gè)容易混淆的地址數(shù)值和步長(zhǎng)差異同時(shí)打出來(lái)。下面這個(gè)Demo就是我一直喜歡用來(lái)講解的#include stdio.h int main(void) { int a[5] {10, 20, 30, 40, 50}; printf(a %p\n, (void*)a); printf(a %p\n, (void*)a); printf(a[0] %p\n, (void*)a[0]); printf(a 1 %p\n, (void*)(a 1)); printf(a 1 %p\n, (void*)(a 1)); return 0; }在我的機(jī)器上輸出大致是這種形態(tài)地址數(shù)值因系統(tǒng)而異a 0x7ffd12345670 a 0x7ffd12345670 a[0] 0x7ffd12345670 a 1 0x7ffd12345674 a 1 0x7ffd12345684注意看三個(gè)起始值打印出來(lái)都一樣但a 1和a 1的差值截然不同前者只加了4字節(jié)一個(gè)int后者加了16字節(jié)整個(gè)數(shù)組長(zhǎng)度因?yàn)閍的類(lèi)型是int(*)[5]。如果你只看數(shù)值會(huì)覺(jué)得它們完全一樣這正是概念模糊的人最容易踩坑的地方數(shù)值不等于類(lèi)型類(lèi)型決定步長(zhǎng)和使用方式。5.2 再驗(yàn)證一下下標(biāo)運(yùn)算的等價(jià)性把下面這段也跑了#include stdio.h int main(void) { int a[5] {10, 20, 30, 40, 50}; int *p a; printf(a[2] %d\n, a[2]); printf(*(a2) %d\n, *(a 2)); printf(2[a] %d\n, 2[a]); printf(p[2] %d\n, p[2]); return 0; }這幾種寫(xiě)法的輸出全是30。2[a]雖然可讀性極差但它在語(yǔ)法上完全沒(méi)有問(wèn)題因?yàn)樗褪?(2 a)的簡(jiǎn)寫(xiě)。這類(lèi)實(shí)驗(yàn)的價(jià)值在于讓你在寫(xiě)代碼時(shí)不再被寫(xiě)在括號(hào)左邊的必須是數(shù)組名這種潛意識(shí)束縛。事實(shí)上下標(biāo)運(yùn)算只關(guān)心一個(gè)地址值 一個(gè)整數(shù)偏移至于這個(gè)地址值是來(lái)自數(shù)組名還是來(lái)自指針變量根本不重要。我的建議是你在學(xué)習(xí)完理論后一定親手敲一遍這些實(shí)驗(yàn)代碼然后把打印出來(lái)的地址和預(yù)想結(jié)果對(duì)照。如果有一個(gè)輸出與預(yù)期不符就停下來(lái)想清楚原因這比一口氣讀十頁(yè)書(shū)更管用。因?yàn)橹羔樑c數(shù)組的關(guān)系不是一個(gè)需要背下來(lái)的知識(shí)而是一個(gè)需要建立直覺(jué)的機(jī)制動(dòng)手驗(yàn)證是建立直覺(jué)最快的路。6. 常見(jiàn)問(wèn)題與排查技巧實(shí)錄6.1 六個(gè)高頻陷阱速查表我把這些年自己在調(diào)試中頻繁遇到的指針/數(shù)組相關(guān)的問(wèn)題整理成一個(gè)速查表如果你在寫(xiě)代碼時(shí)碰到類(lèi)似癥狀可以先來(lái)這里對(duì)照現(xiàn)象大概率原因解決辦法sizeof(arr)在函數(shù)內(nèi)得到8數(shù)組在參數(shù)中退化為指針額外傳長(zhǎng)度參數(shù)或用結(jié)構(gòu)體封裝a編譯報(bào)錯(cuò)數(shù)組名是常量不是變量另設(shè)一個(gè)指針變量pa; p;p[5]解引用崩潰指針偏移越界或沒(méi)有指向有效內(nèi)存檢查分配大小和邊界條件對(duì)char *s abc執(zhí)行s[0]x崩潰指向了只讀的字符串字面量改用char s[] abc;a 1用來(lái)遍歷a卻找不到元素跳過(guò)的是整個(gè)數(shù)組結(jié)構(gòu)用a 1或*(a 1)來(lái)訪問(wèn)下一個(gè)數(shù)組int *p[5]被當(dāng)成二維數(shù)組指針使用混淆了指針數(shù)組與數(shù)組指針審查聲明使用(*p)[5]形式或typedef輔助這份表格里的每一項(xiàng)我都曾經(jīng)在真實(shí)項(xiàng)目中碰到過(guò)沒(méi)有一個(gè)是紙上談兵。尤其是第一項(xiàng)sizeof在函數(shù)內(nèi)失效初學(xué)階段很容易覺(jué)得是編譯器壞了其實(shí)編譯器沒(méi)壞壞的是我們對(duì)數(shù)組參數(shù)等于指針參數(shù)這條規(guī)則的掌握程度。6.2 調(diào)試時(shí)的三條獨(dú)家經(jīng)驗(yàn)最后分享幾個(gè)調(diào)試經(jīng)驗(yàn)和習(xí)慣這些在學(xué)校寫(xiě)練習(xí)題時(shí)可能感覺(jué)不到但到了真實(shí)項(xiàng)目里會(huì)救命。第一優(yōu)先用printf打印地址而不是只打印值。用%p并把地址當(dāng)作主要觀察對(duì)象你很快就能建立起地址隨步長(zhǎng)變化的直覺(jué)。如果能看到第一個(gè)地址和第二個(gè)地址的差值你就能推斷出數(shù)組類(lèi)型是不是和你認(rèn)為的一致。第二當(dāng)某個(gè)數(shù)組相關(guān)的問(wèn)題找不到原因時(shí)把代碼縮減到最小可復(fù)現(xiàn)規(guī)模哪怕把數(shù)組縮成3個(gè)元素。很多時(shí)候問(wèn)題出在你以為的類(lèi)型和實(shí)際類(lèi)型不一致上縮規(guī)模后打印sizeof和地址立刻就能暴露矛盾。比如你把一個(gè)二維數(shù)組傳進(jìn)函數(shù)函數(shù)內(nèi)用int*訪問(wèn)但實(shí)參其實(shí)是int(*)[N]這種不匹配用縮減法能快速暴露。第三對(duì)任何涉及到指針運(yùn)算的代碼養(yǎng)成用const表達(dá)意圖的習(xí)慣。如果你不打算修改指針指向的對(duì)象就聲明成const int *p如果你不打算讓指針重新指向別處就聲明成int *const p。這不能消除所有錯(cuò)誤但能把一半以上的誤寫(xiě)擋在編譯階段。我自己在帶人學(xué)C語(yǔ)言時(shí)最喜歡說(shuō)的一句話(huà)是指針和數(shù)組不是兩套互不相干的知識(shí)而是一枚硬幣的兩面。數(shù)組名給了指針一個(gè)起點(diǎn)指針給了數(shù)組一種靈活的訪問(wèn)方式。當(dāng)你不再糾結(jié)數(shù)組名是不是指針這種字面問(wèn)題而是愿意去理解數(shù)組名在表達(dá)式中退化為地址、這個(gè)地址的步長(zhǎng)由類(lèi)型決定這一整套邏輯時(shí)你在C語(yǔ)言上就真正往前走了一大步。之后無(wú)論是鏈表、字符串處理、還有各種基于指針的數(shù)據(jù)結(jié)構(gòu)你會(huì)有一種地基穩(wěn)了的感覺(jué)。