練習(xí)15:指針敷矫,可怕的指針
譯者:飛龍
指針是C中的一個著名的謎之特性,我會試著通過教授你一些用于處理它們的詞匯汉额,使之去神秘化曹仗。指針實際上并不復(fù)雜,只不過它們經(jīng)常以一些奇怪的方式被濫用蠕搜,這樣使它們變得難以使用怎茫。如果你避免這些愚蠢的方法來使用指針,你會發(fā)現(xiàn)它們難以置信的簡單妓灌。
要想以一種我們可以談?wù)摰姆绞絹碇v解指針轨蛤,我會編寫一個無意義的程序蜜宪,它以三種方式打印了一組人的年齡:
#include <stdio.h>
int main(int argc, char *argv[])
{
// create two arrays we care about
int ages[] = {23, 43, 12, 89, 2};
char *names[] = {
"Alan", "Frank",
"Mary", "John", "Lisa"
};
// safely get the size of ages
int count = sizeof(ages) / sizeof(int);
int i = 0;
// first way using indexing
for(i = 0; i < count; i++) {
printf("%s has %d years alive.\n",
names[i], ages[i]);
}
printf("---\n");
// setup the pointers to the start of the arrays
int *cur_age = ages;
char **cur_name = names;
// second way using pointers
for(i = 0; i < count; i++) {
printf("%s is %d years old.\n",
*(cur_name+i), *(cur_age+i));
}
printf("---\n");
// third way, pointers are just arrays
for(i = 0; i < count; i++) {
printf("%s is %d years old again.\n",
cur_name[i], cur_age[i]);
}
printf("---\n");
// fourth way with pointers in a stupid complex way
for(cur_name = names, cur_age = ages;
(cur_age - ages) < count;
cur_name++, cur_age++)
{
printf("%s lived %d years so far.\n",
*cur_name, *cur_age);
}
return 0;
}
在解釋指針如何工作之前,讓我們逐行分解這個程序祥山,這樣你可以對發(fā)生了什么有所了解圃验。當你瀏覽這個詳細說明時,試著自己在紙上回答問題枪蘑,之后看看你猜測的結(jié)果符合我對指針的描述损谦。
ex15.c:6-10
創(chuàng)建了兩個數(shù)組,ages
儲存了一些int
數(shù)據(jù)岳颇,names
儲存了一個字符串數(shù)組照捡。
ex15.c:12-13
為之后的for
循環(huán)創(chuàng)建了一些變量。
ex15.c:16-19
你知道這只是遍歷了兩個數(shù)組话侧,并且打印出每個人的年齡栗精。它使用了i
來對數(shù)組索引。
ex15.c:24
創(chuàng)建了一個指向ages
的指針瞻鹏。注意int *
創(chuàng)建“指向整數(shù)的指針”的指針類型的用法悲立。它很像char *
,意義是“指向字符的指針”新博,而且字符串是字符的數(shù)組薪夕。是不是很相似呢?
ex15.c:25
創(chuàng)建了指向names
的指針赫悄。char *
已經(jīng)是“指向char
的指針”了规哲,所以它只是個字符串败去。你需要兩個層級,因為names
是二維的,也就是說你需要char **
作為“指向‘指向字符的指針’的指針”秃症。把它學(xué)會陨献,并且自己解釋它寸士。
ex15.c:28-31
遍歷ages
和names
谜诫,但是使用“指針加偏移i
”。*(cur_name+i)
和name[i]
是一樣的痪蝇,你應(yīng)該把它讀作“‘cur_name
指針加i
’的值”鄙陡。
ex15.c:35-39
這里展示了訪問數(shù)組元素的語法和指針是相同的。
ex15.c:44-50
另一個十分愚蠢的循環(huán)和其它兩個循環(huán)做著相同的事情躏啰,但是它用了各種指針算術(shù)運算來代替:
ex15.c:44
通過將cur_name
和cur_age
置為names
和age
數(shù)組的起始位置來初始化for
循環(huán)柔吼。
ex15.c:45
for
循環(huán)的測試部分比較cur_age
指針和ages
起始位置的距離,為什么可以這樣寫呢丙唧?
ex15.c:46
for
循環(huán)的增加部分增加了cur_name
和cur_age
的值愈魏,這樣它們可以只想names
和ages
的下一個元素。
ex15.c:48-49
cur_name
和cur_age
的值現(xiàn)在指向了相應(yīng)數(shù)組中的一個元素,我們我可以通過*cur_name
和*cur_age
來打印它們培漏,這里的意思是“cur_name
和cur_age
指向的值”溪厘。
這個看似簡單的程序卻包含了大量的信息,其目的是在我向你講解之前嘗試讓你自己弄清楚指針牌柄。直到你寫下你認為指針做了什么之前畸悬,不要往下閱讀。
你會看到什么
在你運行這個程序之后珊佣,嘗試根據(jù)打印出的每一行追溯到代碼中產(chǎn)生它們的那一行蹋宦。在必要情況下,修改printf
調(diào)用來確認你得到了正確的行號:
$ make ex15
cc -Wall -g ex15.c -o ex15
$ ./ex15
Alan has 23 years alive.
Frank has 43 years alive.
Mary has 12 years alive.
John has 89 years alive.
Lisa has 2 years alive.
---
Alan is 23 years old.
Frank is 43 years old.
Mary is 12 years old.
John is 89 years old.
Lisa is 2 years old.
---
Alan is 23 years old again.
Frank is 43 years old again.
Mary is 12 years old again.
John is 89 years old again.
Lisa is 2 years old again.
---
Alan lived 23 years so far.
Frank lived 43 years so far.
Mary lived 12 years so far.
John lived 89 years so far.
Lisa lived 2 years so far.
$
解釋指針
當你寫下一些類似ages[i]
的東西時咒锻,你實際上在用i
中的數(shù)字來索引ages
冷冗。如果i
的值為0,那么就等同于寫下ages[0]
惑艇。我們把i
叫做下標蒿辙,因為它是ages
中的一個位置。它也能稱為地址滨巴,這是“我想要ages
位于地址i
處的整數(shù)”中的說法思灌。
如果i
是個下標,那么ages
又是什么恭取?對C來說ages
是在計算機中那些整數(shù)的起始位置泰偿。當然它也是個地址,C編譯器會把任何你鍵入ages
的地方替換為數(shù)組中第一個整數(shù)的地址蜈垮。另一個理解它的辦法就是把ages
當作“數(shù)組內(nèi)部第一個整數(shù)的地址”甜奄,但是它是整個計算機中的地址,而不是像i
一樣的ages
中的地址窃款。ages
數(shù)組的名字在計算機中實際上是個地址。
這就產(chǎn)生了一種特定的實現(xiàn):C把你的計算機看成一個龐大的字節(jié)數(shù)組牍氛。顯然這樣不會有什么用處晨继,于是C就在它的基礎(chǔ)上構(gòu)建出類型和大小的概念。你已經(jīng)在前面的練習(xí)中看到了它是如何工作的搬俊,但現(xiàn)在你可以開始了解C對你的數(shù)組做了下面一些事情:
- 在你的計算機中開辟一塊內(nèi)存紊扬。
- 將
ages
這個名字“指向”它的起始位置。 - 通過選取
ages
作為基址唉擂,并且獲取位置為i
的元素餐屎,來對內(nèi)存塊進行索引。 - 將
ages+i
處的元素轉(zhuǎn)換成大小正確的有效的int
玩祟,這樣就返回了你想要的結(jié)果:下標i
處的int
腹缩。
如果你可以選取ages
作為基址,之后加上比如i
的另一個地址,你是否就能隨時構(gòu)造出指向這一地址的指針呢藏鹊?是的润讥,這種東西就叫做指針。這也是cur_age
和cur_name
所做的事情盘寡,它們是指向計算機中這一位置的變量楚殿,ages
和names
就處于這一位置。之后竿痰,示例程序移動它們脆粥,或者做了一些算數(shù)運算,來從內(nèi)存中獲取值影涉。在其中一個實例中变隔,只是簡單地將cur_age
加上i
,這樣等同于array[i]
常潮。在最后一個for
循環(huán)中弟胀,這兩個指針在沒有i
輔助的情況下自己移動,被當做數(shù)組基址和整數(shù)偏移合并到一起的組合喊式。
指針僅僅是指向計算機中的某個地址孵户,并帶有類型限定符,所以你可以通過它得到正確大小的數(shù)據(jù)岔留。它類似于將ages
和i
組合為一個數(shù)據(jù)類型的東西夏哭。C了解指針指向什么地方,所指向的數(shù)據(jù)類型献联,這些類型的大小竖配,以及如何為你獲取數(shù)據(jù)。你可以像i
一樣增加它們里逆,減少它們进胯,對他們做加減運算。然而它們也像是ages
原押,你可以通過它獲取值胁镐,放入新的值,或執(zhí)行全部的數(shù)組操作诸衔。
指針的用途就是讓你手動對內(nèi)存塊進行索引盯漂,一些情況下數(shù)組并不能做到。絕大多數(shù)情況中笨农,你可能打算使用數(shù)組就缆,但是一些處理原始內(nèi)存塊的情況,是指針的用武之地谒亦。指針向你提供了原始的竭宰、直接的內(nèi)存塊訪問途徑空郊,讓你能夠處理它們。
在這一階段需要掌握的最后一件事羞延,就是你可以對數(shù)組和指針操作混用它們絕大多數(shù)的語法渣淳。你可以對一個指針使用數(shù)組的語法來訪問指向的東西,也可以對數(shù)組的名字做指針的算數(shù)運算伴箩。
實用的指針用法
你可以用指針做下面四個最基本的操作:
- 向OS申請一塊內(nèi)存入愧,并且用指針處理它。這包括字符串嗤谚,和一些你從來沒見過的東西棺蛛,比如結(jié)構(gòu)體。
- 通過指針向函數(shù)傳遞大塊的內(nèi)存(比如很大的結(jié)構(gòu)體)巩步,這樣不必把全部數(shù)據(jù)都傳遞進去旁赊。
- 獲取函數(shù)的地址用于動態(tài)調(diào)用。
- 對一塊內(nèi)存做復(fù)雜的搜索椅野,比如终畅,轉(zhuǎn)換網(wǎng)絡(luò)套接字中的字節(jié),或者解析文件竟闪。
對于你看到的其它所有情況离福,實際上應(yīng)當使用數(shù)組。在早期炼蛤,由于編譯器不擅長優(yōu)化數(shù)組妖爷,人們使用指針來加速它們的程序。然而理朋,現(xiàn)在訪問數(shù)組和指針的語法都會翻譯成相同的機器碼絮识,并且表現(xiàn)一致。由此嗽上,你應(yīng)該每次盡可能使用數(shù)組次舌,并且按需將指針用作提升性能的手段。
指針詞庫
現(xiàn)在我打算向你提供一個詞庫兽愤,用于讀寫指針彼念。當你遇到復(fù)雜的指針語句時,試著參考它并且逐字拆分語句(或者不要使用這個語句烹看,因為有可能并不好):
type *ptr
type
類型的指針,名為ptr
洛史。
*ptr
ptr
所指向位置的值惯殊。
*(ptr + i)
(ptr
所指向位置加上i
)的值。
譯者注:以字節(jié)為單位的話也殖,應(yīng)該是
ptr
所指向的位置再加上sizeof(type) * i
土思。
&thing
thing
的地址务热。
type *ptr = &thing
名為ptr
,type
類型的指針己儒,值設(shè)置為thing
的地址崎岂。
ptr++
自增ptr
指向的位置。
我們將會使用這份簡單的詞庫來拆解這本書中所有的指針用例闪湾。
指針并不是數(shù)組
無論怎么樣冲甘,你都不應(yīng)該把指針和數(shù)組混為一談。它們并不是相同的東西途样,即使C讓你以一些相同的方法來使用它們江醇。例如,如果你訪問上面代碼中的sizeof(cur_age)
何暇,你會得到指針的大小陶夜,而不是它指向數(shù)組的大小。如果你想得到整個數(shù)組的大小裆站,你應(yīng)該使用數(shù)組的名稱age
条辟,就行第12行那樣。
譯者注宏胯,除了
sizeof
羽嫡、&
操作和聲明之外,數(shù)組名稱都會被編譯器推導(dǎo)為指向其首個元素的指針胳嘲。對于這些情況厂僧,不要用“是”這個詞,而是要用“推導(dǎo)”了牛。
如何使它崩潰
你可以通過將指針指向錯誤的位置來使程序崩潰:
- 試著將
cur_age
指向names
颜屠。可以需要C風(fēng)格轉(zhuǎn)換來強制執(zhí)行鹰祸,試著查閱相關(guān)資料把它弄明白甫窟。 - 在最后的
for
循環(huán)中,用一些古怪的方式使計算發(fā)生錯誤蛙婴。 - 試著重寫循環(huán)粗井,讓它們從數(shù)組的最后一個元素開始遍歷到首個元素。這比看上去要困難街图。
附加題
- 使用訪問指針的方式重寫所有使用數(shù)組的地方浇衬。
- 使用訪問數(shù)組的方式重寫所有使用指針的地方。
- 在其它程序中使用指針來代替數(shù)組訪問餐济。
- 使用指針來處理命令行參數(shù)耘擂,就像處理
names
那樣。 - 將獲取值和獲取地址組合到一起絮姆。
- 在程序末尾添加一個
for
循環(huán)醉冤,打印出這些指針所指向的地址秩霍。你需要在printf
中使用%p
。 - 對于每一種打印數(shù)組的方法蚁阳,使用函數(shù)來重寫程序铃绒。試著向函數(shù)傳遞指針來處理數(shù)據(jù)。記住你可以聲明接受指針的函數(shù)螺捐,但是可以像數(shù)組那樣用它颠悬。
- 將
for
循環(huán)改為while
循環(huán),并且觀察對于每種指針用法哪種循環(huán)更方便归粉。