一惕虑、背景
先來個問題,下面的一段代碼編譯時會存在問題嗎睛挚?
#include <stdio.h>
#include <string.h>
int main(void)
{
unsigned char a[2];
bzero(a, sizeof(a));
if (a) {
printf("a is valid!\n");
}
return 0;
}
很明顯邪蛔,這個程序是沒有問題的,至少語法上沒有問題扎狱。但是使用
gcc -Wall
選項編譯時侧到,會出現(xiàn)如下的編譯警告:
warning: the address of ‘a(chǎn)’ will always evaluate as ‘true’ [-Waddress]
自然,判斷一個數(shù)組是否為真是多余的淤击,因為它確定無疑是真的匠抗!另一方面,把判斷條件換為
if (a != NULL)
時污抬,是不會產(chǎn)生編譯警告的汞贸。就是這樣一個在語法上沒有問題的小程序,引起了我的興趣印机。我想就此一探指針與數(shù)組之間的微妙關系著蛙。
二、數(shù)組不是指針
1. 先上定義
數(shù)組耳贬,用來存儲一個固定大小的相同類型元素的順序集合。在c語言中猎唁,數(shù)組屬于構(gòu)造數(shù)據(jù)類型咒劲。
有限個類型相同的變量的集合命名為數(shù)組名。組成數(shù)組的各個變量稱為數(shù)組的分量诫隅,也稱為數(shù)組的元素或下標變量腐魂。用于區(qū)分數(shù)組的各個元素的數(shù)字編號稱為下標。
數(shù)組是用于儲存多個相同類型數(shù)據(jù)的集合逐纬。
指針是一種保存變量地址的變量蛔屹。
指針(Pointer)的值直接指向(points to)存在存儲器中另一個地方的值。由于通過地址能找到所需的變量單元豁生,可以說兔毒,地址指向該變量單元漫贞。
2. 定義與聲明的區(qū)分
經(jīng)常會遇到下面一段程序代碼的情況:
/* 文件 1 */
int a[10];
/* 文件 2 */
extern int *a;
/* 下面是開始使用a[i]的代碼 */
很多人認為這是沒有問題的,畢竟育叁,在c語言中數(shù)組與指針非常相似迅脐,甚至可以互換。那么豪嗽,把定義為數(shù)組的變量聲明為指針使用有什么問題呢谴蔑?
對于下面的這段代碼,沒有人懷疑它是錯誤的:
/* 文件 1 */
int i;
/* 文件 2 */
extern float i;
/* 下面是開始使用i的代碼 */
對于定義與聲明類型不匹配的情況龟梦,沒人指望它能正常運行隐锭。但是數(shù)組與指針的類型也不相同啊,為什么還要指望它能正常運行呢计贰?
c語言中的變量必須有且只有一個定義钦睡,但它可以有多個extern
聲明,定義創(chuàng)建了一個對象蹦玫,并為其分配了內(nèi)存空間赎婚。而聲明只是簡單地說明了在其他地方創(chuàng)建的對象的名字,它允許你使用這個名字而已樱溉。
編譯器為每個變量分配一個地址挣输,地址編譯時可知,并且該變量在運行時一直保存在這個地址福贞。相反撩嚼,存儲在變量中的值只有在運行時才可知。當需要從變量中存儲的值時挖帘,編譯器從指定地址讀出變量的值在于寄存器完丽。
對于數(shù)組,編譯器可以直接對其操作拇舀,不需要增加指令首先取得具體的地址逻族。對于指針,必須首先在運行時取得它的當前值骄崩,然后才能對它進行解除引用操作聘鳞。
所以,extern char a[]
和 extern char a[10]
等價要拂。編譯器并不需要知道數(shù)組具體長度抠璃,它只產(chǎn)生偏離起始地址的偏移地址。從數(shù)組中取一個字符脱惰,只要簡單地從符號表顯示的a的地址加上下標搏嗡,需要的字符就在這個地址中。
而聲明為 extern char *p
,編譯器認為p是一個指針采盒,它指向的對象是一個字符旧乞,為了取到這個字符,要先取得地址p的內(nèi)容纽甘,然后把它作為字符的地址良蛮,從而取出字符。
可見悍赢,指針的訪問比數(shù)組增加一次額外的提取决瞳。
同樣的道理,下面的使用會導致什么后果呢左权?
/* 文件 1 */
int *a;
/* 文件 2 */
extern int a[];
/* 下面是開始使用a的代碼 */
外部數(shù)組實際定義為指針皮胡,但作為數(shù)組使用,需要對內(nèi)存進行直接的引用赏迟,但這里編譯器所執(zhí)行的卻是對內(nèi)存進行間接引用屡贺。因為編譯器認為它是一個指針,而非數(shù)組锌杀。
再回過頭來看一下這個問題甩栈,p聲明為 extern char *p
,而它原本的定義是'char p[10]'時糕再,當用p[i]使用p時量没,實際上得到的是一個字符,但編譯器會把它當成一個指針突想。把ASCII碼解釋成地址顯然很荒謬殴蹄,這時程序崩潰你應該感到很高興,不然猾担,這個bug可能會破壞程序地址空間的內(nèi)容袭灯,出現(xiàn)莫名其妙的錯誤。
3. 數(shù)組和指針的特性對比表
數(shù)組 | 指針 |
---|---|
保存數(shù)據(jù) | 保存數(shù)據(jù)的地址 |
直接訪問數(shù)據(jù) | 間接訪問數(shù)據(jù)绑嘹,先取出指針的內(nèi)容稽荧,把它作為地址,然后從該地址取出數(shù)據(jù) |
通常用于存儲固定數(shù)目且類型相同的元素 | 通常用于動態(tài)數(shù)據(jù)結(jié)構(gòu) |
隱式分配和刪除 | 使用malloc()和free() |
自身為數(shù)據(jù)名 | 通常指向匿名數(shù)據(jù) |
4. 字符串常量
指針和數(shù)組的另一個區(qū)別是定義字符串常量時工腋。使用指針定義一個字符串常量姨丈,如:
char *p = "abcdefg";
此時,編譯器會為字符串常量分配內(nèi)存夷蚊。注意,只有字符串常量才是如此髓介,不會為浮點數(shù)之類的常量分配空間惕鼓。在ANSI C中,初始化指針所創(chuàng)建的字符串常量被定義為只讀唐础,如果試圖通過指針修改這個字符串常量的值箱歧,就會出現(xiàn)未定義的錯誤矾飞。
使用數(shù)組定義字符串常量時,如:
char a[] = "abcdefg";
字符串的值是可以修改的呀邢。
三洒沦、數(shù)組是指針
1. 數(shù)組是指針的情況
其實,在實際的應用中价淌,數(shù)組和指針可以等同的情況要比它們不能互換的場景多得多申眼。例如,牢記以下準則:
所有作為函數(shù)參數(shù)的數(shù)組名總是可以通過編譯器轉(zhuǎn)換為指針蝉衣。
而在任何其他情況下括尸,數(shù)組的聲明就是數(shù)組,指針的聲明就是指針病毡,兩者不可互換濒翻。但在使用數(shù)組時,數(shù)組總是可以寫成指針的形式啦膜,兩者可以互換有送。
數(shù)組與指針相同的情況總結(jié)如下:
- 表達式中的數(shù)組名被編譯器當作一個指向該數(shù)組第一個元素的指針
- 下標總是與指針的偏移量相同
- 在函數(shù)參數(shù)的聲明中,數(shù)組名被編譯器當作指向該數(shù)組第一個元素的指針
2. 為什么把數(shù)組當成指針
基于以下原因僧家,c語言把數(shù)組形參當作指針使用:
- 把傳遞給函數(shù)的數(shù)組參數(shù)轉(zhuǎn)換為指針是由于效率的考慮
- 把作為形參的數(shù)組和指針等同起來也是出于效率的原因雀摘。c語言中,所有非數(shù)組形式的實參均傳值啸臀,但如果要拷貝整個數(shù)組届宠,開銷太大,而且絕大多數(shù)情況下乘粒,也不需要豌注。
3. 數(shù)組可以當作指針的情況總結(jié)
- 用
a[i]
這樣的形式對數(shù)組訪問總是被編譯器解釋成按*(a+i)
的指針訪問 - 指針始終都是指針,它不會改寫成數(shù)組灯萍。但可以用下標形式訪問指針轧铁。
- 在作為函數(shù)的參數(shù)時,一個數(shù)組的聲明可以看作是指針旦棉。作為函數(shù)參數(shù)的數(shù)組始終會被編譯器修改成為指向數(shù)組第一個元素的指針齿风。
- 當把一個數(shù)組定義為函數(shù)參數(shù)時,可以把它定義為數(shù)組绑洛,也可以定義為指針救斑。不管選擇哪種方式,在函數(shù)內(nèi)部事實上獲得的都是一個指針真屯。
- 在其他所有情況下脸候,定義和聲明必須一致。
四、總結(jié)一下
理解了數(shù)組和指針何時可以互換何時必須各自使用之后运沦,對于下面語句的含義應該很清楚了:
/* 數(shù)組名和指針都可以作為函數(shù)的參數(shù)使用 */
char a[10];
char *p;
i = strlen(a);
j = strlen(p);
/* 這個語句清晰地展示了數(shù)組和指針的可互換性 */
printf("%s %s", a, p);
/* 下面這行語句要仔細理解其原理哦 */
printf("array at location 0x%x holds string %s", a, a);
/* 下面兩行可互換的程序也超常見 */
int main(int argv, char *argv[]);
int main(int argv, char **argv);
今后泵额,程序出現(xiàn)錯誤時認真分析,避免因誤用導致的程序錯誤携添,遇到指針和數(shù)組混合使用的情況時多多總結(jié)嫁盲,相信一定會有更多收獲和進步!