鍍金池/ 教程/ C/ 練習(xí)9:數(shù)組和字符串
練習(xí)9:數(shù)組和字符串
練習(xí)6:變量類型
練習(xí)3:格式化輸出
練習(xí)4:Valgrind 介紹
練習(xí)28:Makefile 進(jìn)階
練習(xí)14:編寫并使用函數(shù)
練習(xí)21:高級數(shù)據(jù)類型和控制結(jié)構(gòu)
練習(xí)20:Zed的強(qiáng)大的調(diào)試宏
練習(xí)18:函數(shù)指針
練習(xí)0:準(zhǔn)備
練習(xí)15:指針,可怕的指針
練習(xí)27:創(chuàng)造性和防御性編程
練習(xí)22:棧、作用域和全局
練習(xí)10:字符串?dāng)?shù)組和循環(huán)
練習(xí)8:大小和數(shù)組
練習(xí)16:結(jié)構(gòu)體和指向它們的指針
練習(xí)7:更多變量和一些算術(shù)
練習(xí)23:認(rèn)識達(dá)夫設(shè)備
練習(xí)12:If,Else If,Else
練習(xí)2:用Make來代替Python
練習(xí)1:啟用編譯器
練習(xí)11:While循環(huán)和布爾表達(dá)式
練習(xí)5:一個(gè)C程序的結(jié)構(gòu)
練習(xí)24:輸入輸出和文件
練習(xí)25:變參函數(shù)
練習(xí)13:Switch語句
練習(xí)19:一個(gè)簡單的對象系統(tǒng)
練習(xí)26:編寫第一個(gè)真正的程序
導(dǎo)言:C的笛卡爾之夢
練習(xí)17:堆和棧的內(nèi)存分配

練習(xí)9:數(shù)組和字符串

上一個(gè)練習(xí)中,我們學(xué)習(xí)了如何創(chuàng)建基本的數(shù)組,以及數(shù)組怎么樣映射為字符串。這個(gè)練習(xí)中我們會(huì)更加全面地展示數(shù)組和字符串的相似之處,并且深入更多內(nèi)存布局的知識。

這個(gè)練習(xí)向你展示了C只是簡單地將字符串儲存為字符數(shù)組,并且在結(jié)尾加上'\0'(空字符)。你可能在上個(gè)練習(xí)中得到了暗示,因?yàn)槲覀兪謩?dòng)這樣做了。下面我會(huì)通過將它與數(shù)字?jǐn)?shù)組比較,用另一種方法更清楚地實(shí)現(xiàn)它。

#include <stdio.h>

int main(int argc, char *argv[])
{
    int numbers[4] = {0};
    char name[4] = {'a'};

    // first, print them out raw
    printf("numbers: %d %d %d %d\n",
            numbers[0], numbers[1],
            numbers[2], numbers[3]);

    printf("name each: %c %c %c %c\n",
            name[0], name[1],
            name[2], name[3]);

    printf("name: %s\n", name);

    // setup the numbers
    numbers[0] = 1;
    numbers[1] = 2;
    numbers[2] = 3;
    numbers[3] = 4;

    // setup the name
    name[0] = 'Z';
    name[1] = 'e';
    name[2] = 'd';
    name[3] = '\0';

    // then print them out initialized
    printf("numbers: %d %d %d %d\n",
            numbers[0], numbers[1],
            numbers[2], numbers[3]);

    printf("name each: %c %c %c %c\n",
            name[0], name[1],
            name[2], name[3]);

    // print the name like a string
    printf("name: %s\n", name);

    // another way to use name
    char *another = "Zed";

    printf("another: %s\n", another);

    printf("another each: %c %c %c %c\n",
            another[0], another[1],
            another[2], another[3]);

    return 0;
}

在這段代碼中,我們創(chuàng)建了一些數(shù)組,并對數(shù)組元素賦值。在numbers中我們設(shè)置了一些數(shù)字,然而在names中我們實(shí)際上手動(dòng)構(gòu)造了一個(gè)字符串。

你會(huì)看到什么

當(dāng)你運(yùn)行這段代碼的時(shí)候,你應(yīng)該首先看到所打印的數(shù)組的內(nèi)容初始化為0值,之后打印初始化后的內(nèi)容:

$ make ex9
cc -Wall -g    ex9.c   -o ex9
$ ./ex9
numbers: 0 0 0 0
name each: a   
name: a
numbers: 1 2 3 4
name each: Z e d 
name: Zed
another: Zed
another each: Z e d
$

你會(huì)注意到這個(gè)程序中有一些很有趣的事情:

  • 我并沒有提供全部的4個(gè)參數(shù)來初始化它。這是C的一個(gè)簡寫,如果你只提供了一個(gè)元素,剩下的都會(huì)為0.
  • numbers的每個(gè)元素被打印時(shí),它們都輸出0。
  • names的每個(gè)元素被打印時(shí),只有第一個(gè)元素'a'顯示了,因?yàn)?code>'a'是特殊字符不會(huì)顯示。
  • 然后我們首次打印names,打印出了"a",因?yàn)樗诔跏蓟磉_(dá)式中的'a'字符之后都用'\0'填充,是以'\0'結(jié)尾的正確的字符串。
  • 我們接著通過手動(dòng)為每個(gè)元素賦值的辦法建立數(shù)組并且再次把它打印出來。看看他們發(fā)生了什么改變?,F(xiàn)在numbers已經(jīng)設(shè)置好了,看看names字符串是如何正確打印出我的名字的。
  • 創(chuàng)建一個(gè)字符串也有兩種語法:第六行的char name[4] = {'a'},或者第44行的char *another = "name"。前者不怎么常用,你應(yīng)該將后者用于字符串字面值。

注意我使用了相同的語法和代碼風(fēng)格來和整數(shù)數(shù)組和字符數(shù)組交互,但是printf認(rèn)為name是個(gè)字符串。再次強(qiáng)調(diào),這是因?yàn)閷語言來說,字符數(shù)組和字符串沒有什么不同。

最后,當(dāng)你使用字符串字面值時(shí)你應(yīng)該用char *another = "Literal"語法,它會(huì)產(chǎn)生相同的東西,但是更加符合語言習(xí)慣,也更省事。

如何使它崩潰

C中所有bug的大多數(shù)來源都是忘了預(yù)留出足夠的空間,或者忘了在字符串末尾加上一個(gè)'\0'。事實(shí)上,這些bug是非常普遍并且難以改正的,大部分優(yōu)秀的C代碼都不會(huì)使用C風(fēng)格字符串。下一個(gè)練習(xí)中我們會(huì)學(xué)到如何徹底避免C風(fēng)格字符串。

使這個(gè)程序崩潰的的關(guān)鍵就是拿掉字符串結(jié)尾的'\0'。下面是實(shí)現(xiàn)它的一些途徑:

  • 刪掉name的初始化表達(dá)式。
  • 非故意地設(shè)置name[3] = 'A',于是它就沒有終止字符了。
  • 將初始化表達(dá)式設(shè)置為'a','a','a','a'},于是就有過多的'a'字符,沒有辦法給'\0'留出位置。

試著想出一些其它的辦法讓它崩潰,并且在Valgrind下想通常一樣運(yùn)行這個(gè)程序,你可以看到具體發(fā)生了什么,以及錯(cuò)誤叫什么名字。有時(shí)Valgrind并不能發(fā)現(xiàn)你犯的錯(cuò)誤,則需要移動(dòng)聲明這些變量的地方看看是否能找出錯(cuò)誤。這是C的黑魔法的一部分,有時(shí)變量的位置會(huì)改變bug。

附加題

  • 將一些字符賦給numbers的元素,之后用printf一次打印一個(gè)字符,你會(huì)得到什么編譯器警告?
  • names執(zhí)行上述的相反操作,把names當(dāng)成int數(shù)組,并一次打印一個(gè)intValgrind會(huì)提示什么?
  • 有多少種其它的方式可以用來打印它?
  • 如果一個(gè)字符數(shù)組占四個(gè)字節(jié),一個(gè)整數(shù)也占4個(gè)字節(jié),你可以像整數(shù)一樣使用整個(gè)name嗎?你如何用黑魔法實(shí)現(xiàn)它?
  • 拿出一張紙,將每個(gè)數(shù)組畫成一排方框,之后在紙上畫出代碼中的操作,看看是否正確。
  • name轉(zhuǎn)換成another的形式,看看代碼是否能正常工作。