數(shù)組并非指針
C編程新手最常聽(tīng)到的說(shuō)法之一就是“數(shù)組和指針是相同的”碧信。不幸的是耕蝉,這是一種非常危險(xiǎn)的說(shuō)法铐刘,并不完全正確歧杏。
ANSI C標(biāo)準(zhǔn)6.5.4.2節(jié)建議:
注意以下聲明的區(qū)別:
extern int *x;
extern int y[];
第一條語(yǔ)句聲明x是個(gè)int型的指針镰惦;第二條語(yǔ)句聲明y是個(gè)int型數(shù)組,長(zhǎng)度尚未確定(不完整的類(lèi)型)犬绒,其存儲(chǔ)在別處定義旺入。
標(biāo)準(zhǔn)并沒(méi)有做更細(xì)的規(guī)定。許多C語(yǔ)言書(shū)籍對(duì)數(shù)組與指針何時(shí)相同凯力、何時(shí)不同含糊其辭茵瘾,對(duì)于這個(gè)應(yīng)該重點(diǎn)闡述的話題只是一帶而過(guò)。先看一個(gè)例子:
文件1:
int mango[100];
文件2:
extern int *mango;
這里咐鹤,文件1定義了數(shù)組mango拗秘,但文件2聲明它為指針。這有什么錯(cuò)誤嗎祈惶?無(wú)論如何雕旨,“每個(gè)人都知道”在C語(yǔ)言中扮匠,數(shù)組和指針?lè)浅O嗨啤?wèn)題在于“每個(gè)人”這種說(shuō)法是錯(cuò)誤的凡涩!這相當(dāng)于把整數(shù)和浮點(diǎn)數(shù)混為一談:
文件1:
int guava;
文件2:
extern float guava;
上面這個(gè)int和float的例子非常明顯棒搜,類(lèi)型不匹配,沒(méi)人會(huì)指望這樣的代碼能夠運(yùn)行活箕。但是為什么人們會(huì)認(rèn)為指針和數(shù)組始終應(yīng)該是可以互換的呢力麸?
答案是對(duì)數(shù)組的引用總是可以寫(xiě)成對(duì)指針的引用,而且確實(shí)存在一種指針和數(shù)組的定義完全相同的上下文環(huán)境育韩。
不幸的是克蚂,這只是數(shù)組的一種極為普通的用法,并非所有情況下都是如此筋讨。但是陨舱,人們卻自然而然的歸納并假定在所有的情況下數(shù)組和指針都是等同的,包括上面完全錯(cuò)誤的“數(shù)組定義等同于指針的外部聲明”這種情況版仔。
聲明與定義
在搞清這個(gè)問(wèn)題之前游盲,需要在頭腦里重新整理一些基本的C語(yǔ)言術(shù)語(yǔ)。記住蛮粮,C語(yǔ)言中的對(duì)象必須有且只有一個(gè)定義益缎,但它可以有多個(gè)extern聲明。這里所說(shuō)的對(duì)象跟C++中的對(duì)象并無(wú)關(guān)系然想,這里的對(duì)象只是跟鏈接器有關(guān)的東西莺奔,比如函數(shù)和變量。
定義是一種特殊的聲明变泄,它創(chuàng)建了一個(gè)對(duì)象令哟;聲明簡(jiǎn)單的說(shuō)明了在其他地方創(chuàng)建的對(duì)象的名字,它允許你使用這個(gè)名字妨蛹。
定義 | 只能出現(xiàn)在一個(gè)地方 | 確定對(duì)象的類(lèi)型并分配內(nèi)存屏富,用于創(chuàng)建新的對(duì)象。例如:int my_array[100]; |
聲明 | 可以多次出現(xiàn) | 描述對(duì)象的類(lèi)型蛙卤,用于指代其他地方定義的對(duì)象(例如在其他文件里)例:extern int my_array[]; |
區(qū)分定義和聲明:
1.聲明相當(dāng)于普通的聲明:它所說(shuō)明的并非自身狠半,而是描述其他地方的創(chuàng)建的對(duì)象。
2.定義相當(dāng)于特殊的聲明:它為對(duì)象分配內(nèi)存颤难。
extern 對(duì)象聲明告訴編譯器對(duì)象的類(lèi)型和名字神年,對(duì)象的內(nèi)存分配則在別處進(jìn)行。由于并未在聲明中為數(shù)組分配內(nèi)存行嗤,所以并不需要提供關(guān)于數(shù)組長(zhǎng)度的信息已日。對(duì)于多維數(shù)組,需要提供除最左邊一維之外其他維的長(zhǎng)度——這就給編譯器足夠的信息產(chǎn)生相應(yīng)的代碼栅屏。
對(duì)數(shù)組和對(duì)指針的引用的不同之處
首先需要注意的是“地址y”和“地址y的內(nèi)容”之間的區(qū)別飘千。這是一個(gè)相當(dāng)微妙之處堂鲜,因?yàn)樵诖蠖鄶?shù)編程語(yǔ)言中我們用同一個(gè)符號(hào)來(lái)表示這兩樣?xùn)|西,由編譯器根據(jù)上下文環(huán)境判斷它的具體含義占婉。
以一個(gè)簡(jiǎn)單的賦值為例:
x = y;
x | = | y |
---|---|---|
在這個(gè)上下文環(huán)境里泡嘴,符號(hào)x的含義是x所代表的地址甫恩。 | 在這個(gè)上下文里逆济,符號(hào)y的含義是y所代表的地址的內(nèi)容。 | |
這被稱為左值 | 這被稱為右值 | |
左值在編譯時(shí)可知磺箕,左值表示存儲(chǔ)結(jié)果的地方奖慌。 | 右值直到運(yùn)行時(shí)才知。如無(wú)特別說(shuō)明松靡,右值表示“y的內(nèi)容”简僧。 |
c語(yǔ)言引入“可修改的左值”這個(gè)術(shù)語(yǔ)。
它表示左值允許出現(xiàn)在賦值語(yǔ)句的左邊雕欺,這個(gè)奇怪的術(shù)語(yǔ)是為了與數(shù)組名區(qū)分岛马。
數(shù)組名也用于確定對(duì)象在內(nèi)存中的位置,也是左值屠列,但它不能作為賦值的對(duì)象啦逆。因此,數(shù)組名是個(gè)左值但不是可修改的左值笛洛。
標(biāo)準(zhǔn)規(guī)定賦值符必須用可修改的左值作為它左邊一側(cè)的操作數(shù)夏志。
用通俗的話說(shuō),只能給可以修改的東西賦值苛让。
編譯器為每個(gè)變量分配一個(gè)地址(左值)沟蔑,這個(gè)地址在編譯時(shí)可知,而且該變量在運(yùn)行時(shí)一直保存于這個(gè)地址狱杰。存儲(chǔ)于變量中的值(右值)只有在運(yùn)行時(shí)才可知瘦材。如果需要用到變量中存儲(chǔ)的值,編譯器就發(fā)出指令從指定地址讀入變量值并將它存于寄存器中仿畸。
這里的關(guān)鍵之處在于每個(gè)符號(hào)的地址在編譯時(shí)可知宇色。所以,如果編譯器需要一個(gè)地址(可能還需要加上偏移量)來(lái)執(zhí)行某種操作颁湖,它就可以直接進(jìn)行操作宣蠕,并不需要增加指令首先取得具體的地址。相反甥捺,對(duì)于指針抢蚀,必須首先在運(yùn)行時(shí)取得它的當(dāng)前值,然后才能對(duì)它進(jìn)行解除引用操作(作為以后進(jìn)行查找的步驟之一镰禾。)
1.對(duì)數(shù)組進(jìn)行下標(biāo)引用的步驟:
char a[9] = "abcdefgh";
...
c = a[i];
首先皿曲,編譯器符號(hào)表有數(shù)組a的地址唱逢,假設(shè)為9980;
運(yùn)行時(shí)步驟1:取i的值屋休,將它與9980相加(獲得a[i]對(duì)應(yīng)的偏移地址)
運(yùn)行時(shí)步驟2:取地址(9980+i)的內(nèi)容坞古。
這就是為什么extern char a[]與extern char a[100]等價(jià)的原因。這兩個(gè)聲明都提示a是一個(gè)數(shù)組劫樟,也就是一個(gè)內(nèi)存地址痪枫,數(shù)組內(nèi)的字符可以從這個(gè)地址找到。編譯器并不需要知道數(shù)組總共有多長(zhǎng)叠艳,因?yàn)閿?shù)組長(zhǎng)度只用于表示偏離起始地址的最大偏移量奶陈。從數(shù)組提取一個(gè)字符,只要簡(jiǎn)單的用符號(hào)表里a的地址加上下標(biāo)的偏移量附较,所需要的字符就位于這個(gè)地址中吃粒。
如果聲明extern char *p,它將告訴編譯器p是一個(gè)指針拒课,它指向的對(duì)象是一個(gè)字符徐勃。為了取得這個(gè)字符,必須得到地址p的內(nèi)容早像,把內(nèi)容再作為字符的地址僻肖,并從這個(gè)地址中取得這個(gè)字符。指針的訪問(wèn)要靈活的多扎酷,但需要增加一次額外的提取檐涝。
2.對(duì)指針的引用的步驟:
char *p;
...
c = *p;
首先,編譯器有符號(hào)p的地址法挨,假設(shè)為4624谁榜;
運(yùn)行時(shí)步驟1:取地址4624的內(nèi)容,為5081凡纳;
運(yùn)行時(shí)步驟2:取地址5081的內(nèi)容窃植。
3.對(duì)指針進(jìn)行下標(biāo)引用的步驟:
char *p = "abcdefgh";
...
c = p[i];
首先,編譯器符號(hào)表有一個(gè)p荐糜,假設(shè)地址為4624巷怜;
運(yùn)行時(shí)步驟1:取地址4624的內(nèi)容,為5081暴氏;
運(yùn)行時(shí)步驟2:取得i的值延塑,并將它與5081相加;
運(yùn)行時(shí)步驟3:取地址(5081+i)的內(nèi)容答渔。
對(duì)照1关带、3的訪問(wèn)方式:
char a[] = "abcdefgh"; ... a[3];
char *p = "abcdefgh"; ... p[3];
在這兩種情況下,都可以取得字符‘d’沼撕,但兩者的途徑非常不一樣宋雏。
定義為指針芜飘,但以數(shù)組方式引用,編譯器將會(huì):
a.取得符號(hào)表中p的地址磨总,提取存儲(chǔ)于此處的指針嗦明。
b.把下標(biāo)所表示的偏移量與指針的值相加,產(chǎn)生一個(gè)地址蚪燕。
c.訪問(wèn)上面這個(gè)地址娶牌,取得字符。
編譯器已被告知p是一個(gè)指向字符的指針邻薯。p[i]表示”從p所指的地址開(kāi)始裙戏,前進(jìn)i步,每步都是一個(gè)字符(即每個(gè)元素的長(zhǎng)度為一個(gè)字節(jié))≌痃裕“如果是其他類(lèi)型的指針(如int或double)邪乍,其步長(zhǎng)(每步的字節(jié)數(shù))也各不相同。
既然把p聲明成指針雾袱,那么不管p原先定義為指針還是數(shù)組,都會(huì)按照上面所示的三個(gè)步驟進(jìn)行操作,但是只有當(dāng)p原來(lái)定義為指針時(shí)這個(gè)方法才是正確的寿羞。
定義為數(shù)組,但被聲明為指針的問(wèn)題
假設(shè)p原先的定義是char p[10]赂蠢,p在外部文件被聲明為extern char *p绪穆。
當(dāng)用p[i]這種形式提取這個(gè)聲明的指針p的內(nèi)容時(shí),實(shí)際上得到的是一個(gè)字符虱岂。但按照上面的方法玖院,編譯器卻把取到的字符當(dāng)成是一個(gè)地址,把ACSII字符解釋為地址顯然是牛頭不對(duì)馬嘴第岖,它很可能會(huì)污染程序地址空間的內(nèi)容难菌,并出現(xiàn)莫名其妙的錯(cuò)誤。
指針的外部聲明與數(shù)組定義不匹配的問(wèn)題很容易修正蔑滓,只要修改聲明郊酒,使之與定義相匹配即可,如下所示:
文件1:
int mango[100];
文件2:
extern int mango[];
mango數(shù)組的定義分配了100個(gè)int的空間键袱。
而指針定義 int *raisin燎窘;則申請(qǐng)一個(gè)地址容納該指針。
指針的名字是raisin蹄咖,它可以指向任何一個(gè)int變量(或int數(shù)組)褐健。
指針變量raisin本身始終位于同一個(gè)地址,但它的內(nèi)容在任何時(shí)候都可以不同比藻,指向不同地址的int變量铝量。這些不同的int變量可以有不同的值倘屹。
mango數(shù)組的地址并不能改變,在不同的時(shí)候它的內(nèi)容可以不同慢叨,但它總是表示100個(gè)連續(xù)的內(nèi)存空間纽匙。
數(shù)組和指針的其他區(qū)別
比較數(shù)組和指針的另外一個(gè)方法就是比對(duì)兩者的特點(diǎn)。
指針 | 數(shù)組 |
---|---|
保存數(shù)據(jù)地址 | 保存數(shù)據(jù) |
間接訪問(wèn)數(shù)據(jù)拍谐,首先取得指針的內(nèi)容烛缔,把它作為地址,然后從這個(gè)地址提取數(shù)據(jù)轩拨。如果指針有一個(gè)下標(biāo)[i]践瓷,就把指針的內(nèi)容加上i作為地址,從中提取數(shù)據(jù) | 直接訪問(wèn)數(shù)據(jù)亡蓉,a[i]只是簡(jiǎn)單的以a+i為地址取得數(shù)據(jù)晕翠。 |
通常用于動(dòng)態(tài)數(shù)據(jù)結(jié)構(gòu) | 通常用于存儲(chǔ)固定數(shù)目且數(shù)據(jù)類(lèi)型相同的元素 |
相關(guān)的函數(shù)為malloc()、free() | 隱式分配和刪除 |
通常指向匿名數(shù)據(jù) | 自身即為數(shù)據(jù)名 |
數(shù)組和指針都可以在它們的定義中用字符串常量進(jìn)行初始化砍濒。盡管看上去一樣淋肾,底層的機(jī)制卻不相同。
定義指針時(shí)爸邢,編譯器并不為指針?biāo)赶虻膶?duì)象分配空間樊卓,它只分配指針本身的空間,除非在定義時(shí)同時(shí)賦給指針一個(gè)字符串常量進(jìn)行初始化杠河。例如碌尔,下面的定義創(chuàng)建了一個(gè)字符串常量(為其分配了內(nèi)存):
char *p = "breadfruit";
注意只有對(duì)字符串常量才是如此。不能指望為浮點(diǎn)數(shù)之類(lèi)的常量分配空間券敌,如:
float *pip = 3.141唾戚;//錯(cuò)誤!無(wú)法通過(guò)編譯陪白。
在ANSI C中颈走,初始化指針時(shí)所創(chuàng)建的字符串常量被定義為只讀。如果試圖通過(guò)指針修改這個(gè)字符串的值咱士,程序就會(huì)出現(xiàn)未定義的行為立由。在有些編譯器中,字符串常量被存放在只允許讀取的文本段中序厉,以防止它被修改锐膜。
數(shù)組也可以用字符串常量進(jìn)行初始化:
char a[] = "gooseberry";
與指針不同,由字符串常量初始化的數(shù)組是可以修改的弛房。其中的單個(gè)字符在以后可以改變道盏,比如下面的語(yǔ)句:
strncpy(a, "black", 5);
就將數(shù)組的值修改為"blackberry"。