干貨:你知道數(shù)組和指針有什么區(qū)別嗎?
掃描二維碼
隨時(shí)隨地手機(jī)看文章
一、能力錯(cuò)覺
當(dāng)書本(或谷歌)擺在眼前時(shí),大腦會(huì)產(chǎn)生錯(cuò)覺,以為學(xué)習(xí)材料也同樣存入了大腦,閱讀畢竟比回想簡單多了。
以為反復(fù)的閱讀資料就是自己已經(jīng)掌握知識(shí),這就是能力錯(cuò)覺。
解決能力錯(cuò)覺的方法:
-
積極回想——讓大腦提取關(guān)鍵概念,而非通過重復(fù)閱讀被動(dòng)地獲取知識(shí),這樣才能高效地學(xué)習(xí)。
現(xiàn)在網(wǎng)絡(luò)上盛行各種it類的視頻教程,我不否認(rèn)不少視頻教程是高質(zhì)量的,但是所有視頻類資料都有一個(gè)問題:
-
可以讓人不用閱讀書籍,減少思考,只要被動(dòng)地聽老師們講課就能舒舒服服地獲取到知識(shí),這很容易會(huì)讓某些初級(jí)的軟件開發(fā)人員形成能力錯(cuò)覺,仿佛視頻里寫過的代碼,解決過的 bug 都是自己已經(jīng)學(xué)到的知識(shí)似的。
有效的解決辦法是:
-
公開學(xué)習(xí)筆記和練習(xí)代碼。公開學(xué)習(xí)筆記的目的是借助外部壓力,高效回想,進(jìn)而提高自己的學(xué)習(xí)標(biāo)準(zhǔn)。
-
另外,公開寫作則會(huì)給你的寫作增加很多維度的外部壓力,你會(huì)想如何讓別人更好地理解我要表達(dá)的意思;如何傳遞更多價(jià)值,讓別人讀完有所收獲;如何讓更多人看到;如何讓別人讀得下去;如何排版讓大家看得更舒服;
二、數(shù)組和指針有什么區(qū)別?
正文目錄:
1. 用于聲明時(shí)兩者有重大區(qū)別
2. 你真的理解聲明和定義嗎?
3. 數(shù)組和指針的底層是如何訪問數(shù)據(jù)的?
4. 哪些場景可以用指針代替數(shù)組?
5. 為什么C語言要把數(shù)組形參退化為指針?
6. 如何使用指針訪問多維數(shù)組?
7. 相關(guān)面試題
寫作目的:
-
正確看待數(shù)組和指針。
測試環(huán)境:
-
Ubuntu 16.04 -
Gcc 5.4.0
1. 用于聲明時(shí)兩者有重大區(qū)別
1) 誤導(dǎo)新手的說法:
由于數(shù)組和指針的所謂等價(jià)性非常接近,不少程序員有時(shí)忽視了二者之間的其他重要區(qū)別 ,最誤導(dǎo)新手的說法之一就是 “數(shù)組和指針是相同的",這是一種非常危險(xiǎn)的說法。
看下面這個(gè)例子:
extern int *x;
extern int x[];
-
第一條語句聲明 x 是個(gè) int 型的指針;
-
第二條語句聲明 x 是個(gè) int 型數(shù)組,長度尚未確定,即存儲(chǔ)長度在別處定義。
2) 為什么有些人會(huì)誤以為指針和數(shù)組總是可以互換?
最主要原因是:
對(duì)數(shù)組的引用 ( x[i] ) 總是可以寫成對(duì)指針的引用 ( *(x+i) )。
-
即確實(shí)存在一種指針和數(shù)組的定義完全相同的上下文環(huán)境。不幸的是,這只是數(shù)組的一種極為普通的用法,并非所有情況下都是如此。
2. 你真的理解聲明和定義嗎?
想要要真正理解為什么 extern int *x 不等于 extern int x[],我們首先需要搞清楚什么是聲明,什么是定義。
1) 鏈接器的視角:
-
C 語言中的對(duì)象必須有且只有一個(gè)定義,但它可以有多個(gè) extern 聲明。這里所說的對(duì)象跟 C++ 中的對(duì)象并無關(guān)系,這里說的對(duì)象是 從鏈接器的視角來看的,鏈接器將各個(gè)函數(shù)、變量都視為對(duì)象。
2) 定義和聲明的聯(lián)系與區(qū)別:
-
定義是一種特殊的聲明,它創(chuàng)建了一個(gè)對(duì)象;聲明簡單地說明了在其他地方創(chuàng)建的對(duì)象的名字。
-
定義只能出現(xiàn)在一個(gè)地方,它指定了對(duì)象的類型并分配內(nèi)存以創(chuàng)建新的對(duì)象。聲明可以多次出現(xiàn) 以描述對(duì)象的類型,用于指代其他地方定義的對(duì)象,它不為對(duì)象分配內(nèi)存。
-
extern 對(duì)象聲明告訴編譯器對(duì)象的類型和名字,對(duì)象的內(nèi)存分配則在別處進(jìn)行。由于并未在聲明中為數(shù)組分配內(nèi)存,所以并不需要提供關(guān)于數(shù)組長度的信息 (多維數(shù)組例外)。
3) 總結(jié)成一句話:
-
定義 = 聲明 + 分配內(nèi)存 (創(chuàng)建對(duì)象)
4) 回過頭來看這個(gè)例子:
extern int *x;
extern int x[];
前者聲明了一個(gè)指針,后者聲明了一個(gè)數(shù)組,那么它們對(duì)應(yīng)的指針和數(shù)組的定義(最重要的是內(nèi)存分配) 能相等嗎?
3. 數(shù)組和指針的底層是如何訪問數(shù)據(jù)的?
現(xiàn)在我們來看看指針和數(shù)組的定義與使用。
1) "地址 X (Address)" 和 "地址 X 的內(nèi)容(Contents of Address)" 之間的區(qū)別:
對(duì)于"地址 X" 和 "地址 X 的內(nèi)容",在 C 語言中是用同一個(gè)符號(hào)來表示這兩樣?xùn)|西,由編譯器根據(jù)上下文環(huán)境判斷它的具體含義。
2) 看下面這個(gè)例子:
X = Y
-
符號(hào) X 的含義是 X 所代表的地址,它是左值,編譯時(shí)可知;
-
符號(hào) Y 的含義是 Y 所代表的地址上的內(nèi)容,它是右值,運(yùn)行時(shí)才知;
-
左值包括可修改的左值和不可修改的左值,C 語言中,一般的數(shù)據(jù)類型都是都可作為可修改的左值,只有數(shù)組是不可修改的左值;
-
數(shù)組的地址在編譯時(shí)可知,編譯器有了這個(gè)地址 (即數(shù)組首地址),就可以直接進(jìn)行讀寫操作。而指針必須在運(yùn)行時(shí)取得它的當(dāng)前值,然后才能對(duì)它進(jìn)行解除引用操作,才能進(jìn)行讀寫操作。
3) 數(shù)組和指針的訪問方式是不同的:
char a[9] = "abcedefgh";
-
上面這個(gè)例子中,a 是一個(gè)數(shù)組。
-
在編譯器符號(hào)表里有一個(gè)符號(hào) a ,它的地址為9980;
-
數(shù)組內(nèi)的字符都可以從這個(gè)地址 + 偏移量找到,編譯器甚至并不需要知道數(shù)組的總長度;
char c = 'F';
char *p = &c;
-
上面這個(gè)例子中,p 是一個(gè)指針。
-
在編譯器符號(hào)表中有一個(gè)符號(hào) p, 它的地址為 4624;
-
p 指向的對(duì)象是一個(gè)字符。為了取得這個(gè)字符,必須得到地址 p 的內(nèi)容 (5081),把它作為字符的地址并從這個(gè)地址中取得這個(gè)字符 ('F')。
4) 當(dāng)定義為指針 (char *p),并以數(shù)組方式 (p[i]) 引用時(shí)會(huì)發(fā)生什么?
char *p = ”abcdefgh”
printf("%c\n", p[3]);
char *a = ”abcdefgh”
printf("%c\n", a[3]);
-
p[3] 和 a[3] 都能成功訪問到字符 'd';
-
a[i] 表示 "從 a 的地址開始,前進(jìn) i 步,每步都是一個(gè)字符(數(shù)組類型的長度)”;
-
p[i] 表示 "從 p 所指的地址開始,前進(jìn) i 步,每步都是一個(gè)字符(即指針?biāo)割愋偷拈L度)”;
-
所以,當(dāng)你用 extern char *p 來聲明 char p[10]時(shí),編譯器會(huì)把 p[i] 當(dāng)成一個(gè)指針(Address),然后去獲取 *(p[i]) (即 Content of Addrss),這時(shí)最好的結(jié)果是程序立馬崩潰,你能快點(diǎn)發(fā)現(xiàn)問題。最糟糕的情況是,程序崩潰在將來的某個(gè)時(shí)刻,你則 debug 到懷疑人生。
4. 哪些場景可以用指針代替數(shù)組?
數(shù)組和指針容易混淆使用的 2 大類場景:
-
聲明
-
在表達(dá)式中使用;
1) 聲明:
聲明的場景包括 3 種:
-
1> 不可以的場景:定義也是一種聲明,定義數(shù)組時(shí)不能用指針的形式;
-
2> 不可以的場景:extern 數(shù)組時(shí)不能改寫成指針的形式, 例如:
int char[10]; // define
extern char a[]; // ok
extern char *a; // error
-
3> 可以的場景:函數(shù)的形參,用數(shù)組形式還是指針形式,隨你自己的喜好。
2) 在表達(dá)式中使用:
-
在表達(dá)式中,指針形式和數(shù)組形式等效。
3) 幾條重要的規(guī)則:
-
規(guī)則1:"表達(dá)式中的數(shù)組名" 就是指針;
-
規(guī)則2:把數(shù)組下標(biāo)可當(dāng)作指針的偏移量;
-
規(guī)則3: "作為函數(shù)參數(shù)的數(shù)組名" 等同于指針;
5. 為什么 C 語言要把數(shù)組形參退化為指針?
1) 出于效率的考慮:-
在 C 語言中,所有非數(shù)組形式的數(shù)據(jù)實(shí)參均以傳值形式(對(duì)實(shí)參作一份拷貝并傳遞給調(diào)用的函數(shù),函數(shù)不能修改作為實(shí)參的實(shí)際變量的值)。
-
如果要拷貝整個(gè)數(shù)組,無論在時(shí)間上還是在內(nèi)存空間上的開銷都可能是非常大的。
2) 出于簡化編譯器的考慮:
-
在 C 語言中,所有的數(shù)組在作為參數(shù)傳遞時(shí)都轉(zhuǎn)換為指向數(shù)組起始地址的指針,而其他的參數(shù)均采用傳值調(diào)用。
-
允許程序員把形參聲明為數(shù)組 (程序員打算傳遞給函數(shù)的東西) 或者指針 (函數(shù)實(shí)際所接收到的東西)。在函數(shù)內(nèi)部,編譯器始終把它當(dāng)作一個(gè)指向數(shù)組第一個(gè)元素的指針。
3) 看下面這個(gè)例子:
static int array[10], array2[10];
static void func1(int *ptr)
{
ptr[1] = 3;
*ptr = 3;
ptr = array2;
}
static void func2(int array[])
{
array[1] = 3;
*array = 3;
array = array2; // OK, because array is a pointer
printf("*array=%d\n", *array);
}
int main(void)
{
func1(array);
func2(array);
array[1] = 3;
*array = 3;
array = array2; // ERROR
return 0;
}
編譯運(yùn)行:
// main 中調(diào)用 array = array2時(shí):
11: error: assignment to expression with array type
// 去掉 main / array = array2時(shí):
$ ./point_array_arg
*array=0
6. 如何使用指針訪問多維數(shù)組?
1) C 語言的多維數(shù)組:
-
采用最右的下標(biāo)先變化原則,其最大的用途是存儲(chǔ)多個(gè)字符串;
-
單個(gè)元素的存儲(chǔ)和引用實(shí)際上是以線性形式排列在內(nèi)存中;
-
不能把一個(gè)數(shù)組賦值給另一個(gè)數(shù)組,因?yàn)閿?shù)組作為一個(gè)整體不能成為賦值的對(duì)象;
-
可以把數(shù)組名賦值給一個(gè)指針,是因?yàn)?strong>在表達(dá)式中的數(shù)組名被編譯器當(dāng)作一個(gè)指針;
-
指針下標(biāo)引用的規(guī)則告訴我們 pea[i][j] 被編譯器解釋為 ((pea + i) + j);
-
可以通過聲明一個(gè)一維指針數(shù)組 ( (char *)pea[4],下標(biāo)方括號(hào)的優(yōu)先級(jí)比指針的星號(hào)高),其中每個(gè)指針指向一個(gè)字符串,來取得類似二維字符數(shù)組的效果;
7. 相關(guān)面試題
1) 找錯(cuò):計(jì)算字符串長度
下面這段程序是為了把字符串轉(zhuǎn)換為大寫:
#include <stdio.h>
void UpperCase(char str[])
{
int test = sizeof(str);
int test2 = sizeof(str[0]);
for(size_t i=0; i<sizeof(str)/sizeof(str[0]); ++i) {
if('a'<=str[i] && str[i]<='z')
str[i] -= ('a'-'A');
}
}
int main(void)
{
char str[] = "aBcDeefGHijKL";
printf("The length of str is %d\n", sizeof(str)/sizeof(str[0]));
UpperCase(str);
printf("result: %s\n", str);
return 0;
}
運(yùn)行結(jié)果:
$ ./sizeof_array
The length of str is 14
result: ABCDEEFGHijKL
問題出在 UpperCase() 里的 sizeof(str),這里的 str 是一個(gè)指針而不是數(shù)組。
正確的寫法有2種:
-
給UpperCase 傳遞長度參數(shù),這是最穩(wěn)妥的方式。 -
在UpperCase 內(nèi)使用strlen 獲取字符串長度,這種方法僅適用于以 ‘\0’ 結(jié)尾的字符數(shù)組;
相關(guān)參考
-
《你必須知道的 495 個(gè) C語言問題》, 第 6.1-6.23 章節(jié) -
《C 專家編程》, 第 4/9/10 章節(jié) -
《C Primer Plus 6th》, 第 10 章節(jié) -
《C 和 C++ 程序員面試秘籍》, 第 3 章 -
《C 和指針》, NULL
-END-
本文授權(quán)轉(zhuǎn)載自公眾號(hào)“嵌入式Hacker”,作者吳偉東Jack
推薦閱讀
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺(tái)僅提供信息存儲(chǔ)服務(wù)。文章僅代表作者個(gè)人觀點(diǎn),不代表本平臺(tái)立場,如有問題,請(qǐng)聯(lián)系我們,謝謝!






