# 練習15:指針,可怕的指針
> 原文:[Exercise 15: Pointers Dreaded Pointers](http://c.learncodethehardway.org/book/ex15.html)
> 譯者:[飛龍](https://github.com/wizardforcel)
指針是C中的一個著名的謎之特性,我會試著通過教授你一些用于處理它們的詞匯,使之去神秘化。指針實際上并不復雜,只不過它們經常以一些奇怪的方式被濫用,這樣使它們變得難以使用。如果你避免這些愚蠢的方法來使用指針,你會發現它們難以置信的簡單。
要想以一種我們可以談論的方式來講解指針,我會編寫一個無意義的程序,它以三種方式打印了一組人的年齡:
```c
#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;
}
```
在解釋指針如何工作之前,讓我們逐行分解這個程序,這樣你可以對發生了什么有所了解。當你瀏覽這個詳細說明時,試著自己在紙上回答問題,之后看看你猜測的結果符合我對指針的描述。
ex15.c:6-10
創建了兩個數組,`ages`儲存了一些`int`數據,`names`儲存了一個字符串數組。
ex15.c:12-13
為之后的`for`循環創建了一些變量。
ex15.c:16-19
你知道這只是遍歷了兩個數組,并且打印出每個人的年齡。它使用了`i`來對數組索引。
ex15.c:24
創建了一個指向`ages`的指針。注意`int *`創建“指向整數的指針”的指針類型的用法。它很像`char *`,意義是“指向字符的指針”,而且字符串是字符的數組。是不是很相似呢?
ex15.c:25
創建了指向`names`的指針。`char *`已經是“指向`char`的指針”了,所以它只是個字符串。你需要兩個層級,因為`names`是二維的,也就是說你需要`char **`作為“指向‘指向字符的指針’的指針”。把它學會,并且自己解釋它。
ex15.c:28-31
遍歷`ages`和`names`,但是使用“指針加偏移`i`”。`*(cur_name+i)`和`name[i]`是一樣的,你應該把它讀作“‘`cur_name`指針加`i`’的值”。
ex15.c:35-39
這里展示了訪問數組元素的語法和指針是相同的。
ex15.c:44-50
另一個十分愚蠢的循環和其它兩個循環做著相同的事情,但是它用了各種指針算術運算來代替:
ex15.c:44
通過將`cur_name`和`cur_age`置為`names`和`age`數組的起始位置來初始化`for`循環。
ex15.c:45
`for`循環的測試部分比較`cur_age`指針和`ages`起始位置的距離,為什么可以這樣寫呢?
ex15.c:46
`for`循環的增加部分增加了`cur_name`和`cur_age`的值,這樣它們可以只想`names`和`ages`的下一個元素。
ex15.c:48-49
`cur_name`和`cur_age`的值現在指向了相應數組中的一個元素,我們我可以通過`*cur_name`和`*cur_age`來打印它們,這里的意思是“`cur_name`和`cur_age`指向的值”。
這個看似簡單的程序卻包含了大量的信息,其目的是在我向你講解之前嘗試讓你自己弄清楚指針。直到你寫下你認為指針做了什么之前,不要往下閱讀。
## 你會看到什么
在你運行這個程序之后,嘗試根據打印出的每一行追溯到代碼中產生它們的那一行。在必要情況下,修改`printf`調用來確認你得到了正確的行號:
```shell
$ 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`中的數字來索引`ages`。如果`i`的值為0,那么就等同于寫下`ages[0]`。我們把`i`叫做下標,因為它是`ages`中的一個位置。它也能稱為地址,這是“我想要`ages`位于地址`i`處的整數”中的說法。
如果`i`是個下標,那么`ages`又是什么?對C來說`ages`是在計算機中那些整數的起始位置。當然它也是個地址,C編譯器會把任何你鍵入`ages`的地方替換為數組中第一個整數的地址。另一個理解它的辦法就是把`ages`當作“數組內部第一個整數的地址”,但是它是整個計算機中的地址,而不是像`i`一樣的`ages`中的地址。`ages`數組的名字在計算機中實際上是個地址。
這就產生了一種特定的實現:C把你的計算機看成一個龐大的字節數組。顯然這樣不會有什么用處,于是C就在它的基礎上構建出類型和大小的概念。你已經在前面的練習中看到了它是如何工作的,但現在你可以開始了解C對你的數組做了下面一些事情:
+ 在你的計算機中開辟一塊內存。
+ 將`ages`這個名字“指向”它的起始位置。
+ 通過選取`ages`作為基址,并且獲取位置為`i`的元素,來對內存塊進行索引。
+ 將`ages+i`處的元素轉換成大小正確的有效的`int`,這樣就返回了你想要的結果:下標`i`處的`int`。
如果你可以選取`ages`作為基址,之后加上比如`i`的另一個地址,你是否就能隨時構造出指向這一地址的指針呢?是的,這種東西就叫做指針。這也是`cur_age`和`cur_name`所做的事情,它們是指向計算機中這一位置的變量,`ages`和`names`就處于這一位置。之后,示例程序移動它們,或者做了一些算數運算,來從內存中獲取值。在其中一個實例中,只是簡單地將`cur_age`加上`i`,這樣等同于`array[i]`。在最后一個`for`循環中,這兩個指針在沒有`i`輔助的情況下自己移動,被當做數組基址和整數偏移合并到一起的組合。
指針僅僅是指向計算機中的某個地址,并帶有類型限定符,所以你可以通過它得到正確大小的數據。它類似于將`ages`和`i`組合為一個數據類型的東西。C了解指針指向什么地方,所指向的數據類型,這些類型的大小,以及如何為你獲取數據。你可以像`i`一樣增加它們,減少它們,對他們做加減運算。然而它們也像是`ages`,你可以通過它獲取值,放入新的值,或執行全部的數組操作。
指針的用途就是讓你手動對內存塊進行索引,一些情況下數組并不能做到。絕大多數情況中,你可能打算使用數組,但是一些處理原始內存塊的情況,是指針的用武之地。指針向你提供了原始的、直接的內存塊訪問途徑,讓你能夠處理它們。
在這一階段需要掌握的最后一件事,就是你可以對數組和指針操作混用它們絕大多數的語法。你可以對一個指針使用數組的語法來訪問指向的東西,也可以對數組的名字做指針的算數運算。
## 實用的指針用法
你可以用指針做下面四個最基本的操作:
+ 向OS申請一塊內存,并且用指針處理它。這包括字符串,和一些你從來沒見過的東西,比如結構體。
+ 通過指針向函數傳遞大塊的內存(比如很大的結構體),這樣不必把全部數據都傳遞進去。
+ 獲取函數的地址用于動態調用。
+ 對一塊內存做復雜的搜索,比如,轉換網絡套接字中的字節,或者解析文件。
對于你看到的其它所有情況,實際上應當使用數組。在早期,由于編譯器不擅長優化數組,人們使用指針來加速它們的程序。然而,現在訪問數組和指針的語法都會翻譯成相同的機器碼,并且表現一致。由此,你應該每次盡可能使用數組,并且按需將指針用作提升性能的手段。
## 指針詞庫
現在我打算向你提供一個詞庫,用于讀寫指針。當你遇到復雜的指針語句時,試著參考它并且逐字拆分語句(或者不要使用這個語句,因為有可能并不好):
`type *ptr`
`type`類型的指針,名為`ptr`。
`*ptr`
`ptr`所指向位置的值。
`*(ptr + i)`
(`ptr`所指向位置加上`i`)的值。
> 譯者注:以字節為單位的話,應該是`ptr`所指向的位置再加上`sizeof(type) * i`。
`&thing`
`thing`的地址。
`type *ptr = &thing`
名為`ptr`,`type`類型的指針,值設置為`thing`的地址。
`ptr++`
自增`ptr`指向的位置。
我們將會使用這份簡單的詞庫來拆解這本書中所有的指針用例。
## 指針并不是數組
無論怎么樣,你都不應該把指針和數組混為一談。它們并不是相同的東西,即使C讓你以一些相同的方法來使用它們。例如,如果你訪問上面代碼中的`sizeof(cur_age)`,你會得到指針的大小,而不是它指向數組的大小。如果你想得到整個數組的大小,你應該使用數組的名稱`age`,就行第12行那樣。
> 譯者注,除了`sizeof`、`&`操作和聲明之外,數組名稱都會被編譯器推導為指向其首個元素的指針。對于這些情況,不要用“是”這個詞,而是要用“推導”。
## 如何使它崩潰
你可以通過將指針指向錯誤的位置來使程序崩潰:
+ 試著將`cur_age`指向`names`。可以需要C風格轉換來強制執行,試著查閱相關資料把它弄明白。
+ 在最后的`for`循環中,用一些古怪的方式使計算發生錯誤。
+ 試著重寫循環,讓它們從數組的最后一個元素開始遍歷到首個元素。這比看上去要困難。
## 附加題
+ 使用訪問指針的方式重寫所有使用數組的地方。
+ 使用訪問數組的方式重寫所有使用指針的地方。
+ 在其它程序中使用指針來代替數組訪問。
+ 使用指針來處理命令行參數,就像處理`names`那樣。
+ 將獲取值和獲取地址組合到一起。
+ 在程序末尾添加一個`for`循環,打印出這些指針所指向的地址。你需要在`printf`中使用`%p`。
+ 對于每一種打印數組的方法,使用函數來重寫程序。試著向函數傳遞指針來處理數據。記住你可以聲明接受指針的函數,但是可以像數組那樣用它。
+ 將`for`循環改為`while`循環,并且觀察對于每種指針用法哪種循環更方便。
- 笨辦法學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” 已死