笨辦法學(xué)C 練習(xí)15:指針,可怕的指針

練習(xí)15:指針,可怕的指針

原文:Exercise 15: Pointers Dreaded Pointers

譯者:飛龍

指針是C中的一個(gè)著名的謎之特性,我會(huì)試著通過(guò)教授你一些用于處理它們的詞匯,使之去神秘化。指針實(shí)際上并不復(fù)雜,只不過(guò)它們經(jīng)常以一些奇怪的方式被濫用,這樣使它們變得難以使用。如果你避免這些愚蠢的方法來(lái)使用指針,你會(huì)發(fā)現(xiàn)它們難以置信的簡(jiǎn)單。

要想以一種我們可以談?wù)摰姆绞絹?lái)講解指針,我會(huì)編寫(xiě)一個(gè)無(wú)意義的程序,它以三種方式打印了一組人的年齡:

#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;
}

在解釋指針如何工作之前,讓我們逐行分解這個(gè)程序,這樣你可以對(duì)發(fā)生了什么有所了解。當(dāng)你瀏覽這個(gè)詳細(xì)說(shuō)明時(shí),試著自己在紙上回答問(wèn)題,之后看看你猜測(cè)的結(jié)果符合我對(duì)指針的描述。

ex15.c:6-10

創(chuàng)建了兩個(gè)數(shù)組,ages儲(chǔ)存了一些int數(shù)據(jù),names儲(chǔ)存了一個(gè)字符串?dāng)?shù)組。

ex15.c:12-13

為之后的for循環(huán)創(chuàng)建了一些變量。

ex15.c:16-19

你知道這只是遍歷了兩個(gè)數(shù)組,并且打印出每個(gè)人的年齡。它使用了i來(lái)對(duì)數(shù)組索引。

ex15.c:24

創(chuàng)建了一個(gè)指向ages的指針。注意int *創(chuàng)建“指向整數(shù)的指針”的指針類(lèi)型的用法。它很像char *,意義是“指向字符的指針”,而且字符串是字符的數(shù)組。是不是很相似呢?

ex15.c:25

創(chuàng)建了指向names的指針。char *已經(jīng)是“指向char的指針”了,所以它只是個(gè)字符串。你需要兩個(gè)層級(jí),因?yàn)?code>names是二維的,也就是說(shuō)你需要char **作為“指向‘指向字符的指針’的指針”。把它學(xué)會(huì),并且自己解釋它。

ex15.c:28-31

遍歷agesnames,但是使用“指針加偏移i”。*(cur_name+i)name[i]是一樣的,你應(yīng)該把它讀作“‘cur_name指針加i’的值”。

ex15.c:35-39

這里展示了訪問(wèn)數(shù)組元素的語(yǔ)法和指針是相同的。

ex15.c:44-50

另一個(gè)十分愚蠢的循環(huán)和其它兩個(gè)循環(huán)做著相同的事情,但是它用了各種指針?biāo)阈g(shù)運(yùn)算來(lái)代替:

ex15.c:44

通過(guò)將cur_namecur_age置為namesage數(shù)組的起始位置來(lái)初始化for循環(huán)。

ex15.c:45

for循環(huán)的測(cè)試部分比較cur_age指針和ages起始位置的距離,為什么可以這樣寫(xiě)呢?

ex15.c:46

for循環(huán)的增加部分增加了cur_namecur_age的值,這樣它們可以只想namesages的下一個(gè)元素。

ex15.c:48-49

cur_namecur_age的值現(xiàn)在指向了相應(yīng)數(shù)組中的一個(gè)元素,我們我可以通過(guò)*cur_name*cur_age來(lái)打印它們,這里的意思是“cur_namecur_age指向的值”。

這個(gè)看似簡(jiǎn)單的程序卻包含了大量的信息,其目的是在我向你講解之前嘗試讓你自己弄清楚指針。直到你寫(xiě)下你認(rèn)為指針做了什么之前,不要往下閱讀。

你會(huì)看到什么

在你運(yùn)行這個(gè)程序之后,嘗試根據(jù)打印出的每一行追溯到代碼中產(chǎn)生它們的那一行。在必要情況下,修改printf調(diào)用來(lái)確認(rèn)你得到了正確的行號(hà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.
$

解釋指針

當(dāng)你寫(xiě)下一些類(lèi)似ages[i]的東西時(shí),你實(shí)際上在用i中的數(shù)字來(lái)索引ages。如果i的值為0,那么就等同于寫(xiě)下ages[0]。我們把i叫做下標(biāo),因?yàn)樗?code>ages中的一個(gè)位置。它也能稱(chēng)為地址,這是“我想要ages位于地址i處的整數(shù)”中的說(shuō)法。

如果i是個(gè)下標(biāo),那么ages又是什么?對(duì)C來(lái)說(shuō)ages是在計(jì)算機(jī)中那些整數(shù)的起始位置。當(dāng)然它也是個(gè)地址,C編譯器會(huì)把任何你鍵入ages的地方替換為數(shù)組中第一個(gè)整數(shù)的地址。另一個(gè)理解它的辦法就是把ages當(dāng)作“數(shù)組內(nèi)部第一個(gè)整數(shù)的地址”,但是它是整個(gè)計(jì)算機(jī)中的地址,而不是像i一樣的ages中的地址。ages數(shù)組的名字在計(jì)算機(jī)中實(shí)際上是個(gè)地址。

這就產(chǎn)生了一種特定的實(shí)現(xiàn):C把你的計(jì)算機(jī)看成一個(gè)龐大的字節(jié)數(shù)組。顯然這樣不會(huì)有什么用處,于是C就在它的基礎(chǔ)上構(gòu)建出類(lèi)型和大小的概念。你已經(jīng)在前面的練習(xí)中看到了它是如何工作的,但現(xiàn)在你可以開(kāi)始了解C對(duì)你的數(shù)組做了下面一些事情:

  • 在你的計(jì)算機(jī)中開(kāi)辟一塊內(nèi)存。
  • ages這個(gè)名字“指向”它的起始位置。
  • 通過(guò)選取ages作為基址,并且獲取位置為i的元素,來(lái)對(duì)內(nèi)存塊進(jìn)行索引。
  • ages+i處的元素轉(zhuǎn)換成大小正確的有效的int,這樣就返回了你想要的結(jié)果:下標(biāo)i處的int。

如果你可以選取ages作為基址,之后加上比如i的另一個(gè)地址,你是否就能隨時(shí)構(gòu)造出指向這一地址的指針呢?是的,這種東西就叫做指針。這也是cur_agecur_name所做的事情,它們是指向計(jì)算機(jī)中這一位置的變量,agesnames就處于這一位置。之后,示例程序移動(dòng)它們,或者做了一些算數(shù)運(yùn)算,來(lái)從內(nèi)存中獲取值。在其中一個(gè)實(shí)例中,只是簡(jiǎn)單地將cur_age加上i,這樣等同于array[i]。在最后一個(gè)for循環(huán)中,這兩個(gè)指針在沒(méi)有i輔助的情況下自己移動(dòng),被當(dāng)做數(shù)組基址和整數(shù)偏移合并到一起的組合。

指針僅僅是指向計(jì)算機(jī)中的某個(gè)地址,并帶有類(lèi)型限定符,所以你可以通過(guò)它得到正確大小的數(shù)據(jù)。它類(lèi)似于將agesi組合為一個(gè)數(shù)據(jù)類(lèi)型的東西。C了解指針指向什么地方,所指向的數(shù)據(jù)類(lèi)型,這些類(lèi)型的大小,以及如何為你獲取數(shù)據(jù)。你可以像i一樣增加它們,減少它們,對(duì)他們做加減運(yùn)算。然而它們也像是ages,你可以通過(guò)它獲取值,放入新的值,或執(zhí)行全部的數(shù)組操作。

指針的用途就是讓你手動(dòng)對(duì)內(nèi)存塊進(jìn)行索引,一些情況下數(shù)組并不能做到。絕大多數(shù)情況中,你可能打算使用數(shù)組,但是一些處理原始內(nèi)存塊的情況,是指針的用武之地。指針向你提供了原始的、直接的內(nèi)存塊訪問(wèn)途徑,讓你能夠處理它們。

在這一階段需要掌握的最后一件事,就是你可以對(duì)數(shù)組和指針操作混用它們絕大多數(shù)的語(yǔ)法。你可以對(duì)一個(gè)指針使用數(shù)組的語(yǔ)法來(lái)訪問(wèn)指向的東西,也可以對(duì)數(shù)組的名字做指針的算數(shù)運(yùn)算。

實(shí)用的指針用法

你可以用指針做下面四個(gè)最基本的操作:

  • 向OS申請(qǐng)一塊內(nèi)存,并且用指針處理它。這包括字符串,和一些你從來(lái)沒(méi)見(jiàn)過(guò)的東西,比如結(jié)構(gòu)體。
  • 通過(guò)指針向函數(shù)傳遞大塊的內(nèi)存(比如很大的結(jié)構(gòu)體),這樣不必把全部數(shù)據(jù)都傳遞進(jìn)去。
  • 獲取函數(shù)的地址用于動(dòng)態(tài)調(diào)用。
  • 對(duì)一塊內(nèi)存做復(fù)雜的搜索,比如,轉(zhuǎn)換網(wǎng)絡(luò)套接字中的字節(jié),或者解析文件。

對(duì)于你看到的其它所有情況,實(shí)際上應(yīng)當(dāng)使用數(shù)組。在早期,由于編譯器不擅長(zhǎng)優(yōu)化數(shù)組,人們使用指針來(lái)加速它們的程序。然而,現(xiàn)在訪問(wèn)數(shù)組和指針的語(yǔ)法都會(huì)翻譯成相同的機(jī)器碼,并且表現(xiàn)一致。由此,你應(yīng)該每次盡可能使用數(shù)組,并且按需將指針用作提升性能的手段。

指針詞庫(kù)

現(xiàn)在我打算向你提供一個(gè)詞庫(kù),用于讀寫(xiě)指針。當(dāng)你遇到復(fù)雜的指針語(yǔ)句時(shí),試著參考它并且逐字拆分語(yǔ)句(或者不要使用這個(gè)語(yǔ)句,因?yàn)橛锌赡懿⒉缓茫?/p>

type *ptr

type類(lèi)型的指針,名為ptr。

*ptr

ptr所指向位置的值。

*(ptr + i)

ptr所指向位置加上i)的值。

譯者注:以字節(jié)為單位的話,應(yīng)該是ptr所指向的位置再加上sizeof(type) * i。

&thing

thing的地址。

type *ptr = &thing

名為ptr,type類(lèi)型的指針,值設(shè)置為thing的地址。

ptr++

自增ptr指向的位置。

我們將會(huì)使用這份簡(jiǎn)單的詞庫(kù)來(lái)拆解這本書(shū)中所有的指針用例。

指針并不是數(shù)組

無(wú)論怎么樣,你都不應(yīng)該把指針和數(shù)組混為一談。它們并不是相同的東西,即使C讓你以一些相同的方法來(lái)使用它們。例如,如果你訪問(wèn)上面代碼中的sizeof(cur_age),你會(huì)得到指針的大小,而不是它指向數(shù)組的大小。如果你想得到整個(gè)數(shù)組的大小,你應(yīng)該使用數(shù)組的名稱(chēng)age,就行第12行那樣。

譯者注,除了sizeof&操作和聲明之外,數(shù)組名稱(chēng)都會(huì)被編譯器推導(dǎo)為指向其首個(gè)元素的指針。對(duì)于這些情況,不要用“是”這個(gè)詞,而是要用“推導(dǎo)”。

如何使它崩潰

你可以通過(guò)將指針指向錯(cuò)誤的位置來(lái)使程序崩潰:

  • 試著將cur_age指向names??梢孕枰狢風(fēng)格轉(zhuǎn)換來(lái)強(qiáng)制執(zhí)行,試著查閱相關(guān)資料把它弄明白。
  • 在最后的for循環(huán)中,用一些古怪的方式使計(jì)算發(fā)生錯(cuò)誤。
  • 試著重寫(xiě)循環(huán),讓它們從數(shù)組的最后一個(gè)元素開(kāi)始遍歷到首個(gè)元素。這比看上去要困難。

附加題

  • 使用訪問(wèn)指針的方式重寫(xiě)所有使用數(shù)組的地方。
  • 使用訪問(wèn)數(shù)組的方式重寫(xiě)所有使用指針的地方。
  • 在其它程序中使用指針來(lái)代替數(shù)組訪問(wèn)。
  • 使用指針來(lái)處理命令行參數(shù),就像處理names那樣。
  • 將獲取值和獲取地址組合到一起。
  • 在程序末尾添加一個(gè)for循環(huán),打印出這些指針?biāo)赶虻牡刂?。你需要?code>printf中使用%p。
  • 對(duì)于每一種打印數(shù)組的方法,使用函數(shù)來(lái)重寫(xiě)程序。試著向函數(shù)傳遞指針來(lái)處理數(shù)據(jù)。記住你可以聲明接受指針的函數(shù),但是可以像數(shù)組那樣用它。
  • for循環(huán)改為while循環(huán),并且觀察對(duì)于每種指針用法哪種循環(huán)更方便。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 指針是C語(yǔ)言中廣泛使用的一種數(shù)據(jù)類(lèi)型。 運(yùn)用指針編程是C語(yǔ)言最主要的風(fēng)格之一。利用指針變量可以表示各種數(shù)據(jù)結(jié)構(gòu); ...
    朱森閱讀 3,615評(píng)論 3 44
  • 重新系統(tǒng)學(xué)習(xí)下C++;但是還是少了好多知識(shí)點(diǎn);socket;unix;stl;boost等; C++ 教程 | 菜...
    kakukeme閱讀 20,456評(píng)論 0 50
  • C語(yǔ)言指針的總結(jié) 1. 變量 不同類(lèi)型的變量在內(nèi)存中占據(jù)不同的字節(jié)空間。 內(nèi)存中存儲(chǔ)數(shù)據(jù)的最小基本單位是字節(jié),每一...
    xx_cc閱讀 4,122評(píng)論 11 39
  • 《岡仁波齊》是由張楊執(zhí)導(dǎo)的電影,由尼瑪扎堆、楊培、斯朗卓嘎等主演。 該片主要講述了尼瑪扎堆等十一個(gè)藏...
    時(shí)空說(shuō)閱讀 263評(píng)論 0 0
  • 當(dāng)?shù)谝豢|陽(yáng)光劃過(guò)樹(shù)梢,鳥(niǎo)兒在枝頭鳴叫,我便肩負(fù)著喜悅的心情,將要去邂逅一片未知的天地,一個(gè)斑斕的巴蜀寶地。 收拾好...
    挪威的森林126閱讀 333評(píng)論 0 0

友情鏈接更多精彩內(nèi)容