# 練習9:數組和字符串
> 原文:[Exercise 9: Arrays And Strings](http://c.learncodethehardway.org/book/ex9.html)
> 譯者:[飛龍](https://github.com/wizardforcel)
上一個練習中,我們學習了如何創建基本的數組,以及數組怎么樣映射為字符串。這個練習中我們會更加全面地展示數組和字符串的相似之處,并且深入更多內存布局的知識。
這個練習向你展示了C只是簡單地將字符串儲存為字符數組,并且在結尾加上`'\0'`(空字符)。你可能在上個練習中得到了暗示,因為我們手動這樣做了。下面我會通過將它與數字數組比較,用另一種方法更清楚地實現它。
```c
#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;
}
```
在這段代碼中,我們創建了一些數組,并對數組元素賦值。在`numbers`中我們設置了一些數字,然而在`names`中我們實際上手動構造了一個字符串。
## 你會看到什么
當你運行這段代碼的時候,你應該首先看到所打印的數組的內容初始化為0值,之后打印初始化后的內容:
```sh
$ 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
$
```
你會注意到這個程序中有一些很有趣的事情:
+ 我并沒有提供全部的4個參數來初始化它。這是C的一個簡寫,如果你只提供了一個元素,剩下的都會為0.
+ `numbers`的每個元素被打印時,它們都輸出0。
+ `names`的每個元素被打印時,只有第一個元素`'a'`顯示了,因為`'a'`是特殊字符不會顯示。
+ 然后我們首次打印`names`,打印出了`"a"`,因為它在初始化表達式中的`'a'`字符之后都用`'\0'`填充,是以`'\0'`結尾的正確的字符串。
+ 我們接著通過手動為每個元素賦值的辦法建立數組并且再次把它打印出來。看看他們發生了什么改變。現在`numbers`已經設置好了,看看`names`字符串是如何正確打印出我的名字的。
+ 創建一個字符串也有兩種語法:第六行的`char name[4] = {'a'}`,或者第44行的`char *another = "name"`。前者不怎么常用,你應該將后者用于字符串字面值。
注意我使用了相同的語法和代碼風格來和整數數組和字符數組交互,但是`printf`認為`name`是個字符串。再次強調,這是因為對C語言來說,字符數組和字符串沒有什么不同。
最后,當你使用字符串字面值時你應該用`char *another = "Literal"`語法,它會產生相同的東西,但是更加符合語言習慣,也更省事。
## 如何使它崩潰
C中所有bug的大多數來源都是忘了預留出足夠的空間,或者忘了在字符串末尾加上一個`'\0'`。事實上,這些bug是非常普遍并且難以改正的,大部分優秀的C代碼都不會使用C風格字符串。下一個練習中我們會學到如何徹底避免C風格字符串。
使這個程序崩潰的的關鍵就是拿掉字符串結尾的`'\0'`。下面是實現它的一些途徑:
+ 刪掉`name`的初始化表達式。
+ 非故意地設置`name[3] = 'A'`,于是它就沒有終止字符了。
+ 將初始化表達式設置為`'a','a','a','a'}`,于是就有過多的`'a'`字符,沒有辦法給`'\0'`留出位置。
試著想出一些其它的辦法讓它崩潰,并且在`Valgrind`下想通常一樣運行這個程序,你可以看到具體發生了什么,以及錯誤叫什么名字。有時`Valgrind`并不能發現你犯的錯誤,則需要移動聲明這些變量的地方看看是否能找出錯誤。這是C的黑魔法的一部分,有時變量的位置會改變bug。
## 附加題
+ 將一些字符賦給`numbers`的元素,之后用`printf`一次打印一個字符,你會得到什么編譯器警告?
+ 對`names`執行上述的相反操作,把`names`當成`int`數組,并一次打印一個`int`,`Valgrind`會提示什么?
+ 有多少種其它的方式可以用來打印它?
+ 如果一個字符數組占四個字節,一個整數也占4個字節,你可以像整數一樣使用整個`name`嗎?你如何用黑魔法實現它?
+ 拿出一張紙,將每個數組畫成一排方框,之后在紙上畫出代碼中的操作,看看是否正確。
+ 將`name`轉換成`another`的形式,看看代碼是否能正常工作。
- 笨辦法學C 中文版
- 前言
- 導言:C的笛卡爾之夢
- 練習0:準備
- 練習1:啟用編譯器
- 練習2:用Make來代替Python
- 練習3:格式化輸出
- 練習4:Valgrind 介紹
- 練習5:一個C程序的結構
- 練習6:變量類型
- 練習7:更多變量和一些算術
- 練習8:大小和數組
- 練習9:數組和字符串
- 練習10:字符串數組和循環
- 練習11:While循環和布爾表達式
- 練習12:If,Else If,Else
- 練習13:Switch語句
- 練習14:編寫并使用函數
- 練習15:指針,可怕的指針
- 練習16:結構體和指向它們的指針
- 練習17:堆和棧的內存分配
- 練習18:函數指針
- 練習19:一個簡單的對象系統
- 練習20:Zed的強大的調試宏
- 練習21:高級數據類型和控制結構
- 練習22:棧、作用域和全局
- 練習23:認識達夫設備
- 練習24:輸入輸出和文件
- 練習25:變參函數
- 練習26:編寫第一個真正的程序
- 練習27:創造性和防御性編程
- 練習28:Makefile 進階
- 練習29:庫和鏈接
- 練習30:自動化測試
- 練習31:代碼調試
- 練習32:雙向鏈表
- 練習33:鏈表算法
- 練習34:動態數組
- 練習35:排序和搜索
- 練習36:更安全的字符串
- 練習37:哈希表
- 練習38:哈希算法
- 練習39:字符串算法
- 練習40:二叉搜索樹
- 練習41:將 Cachegrind 和 Callgrind 用于性能調優
- 練習42:棧和隊列
- 練習43:一個簡單的統計引擎
- 練習44:環形緩沖區
- 練習45:一個簡單的TCP/IP客戶端
- 練習46:三叉搜索樹
- 練習47:一個快速的URL路由
- 后記:“解構 K&R C” 已死