# 練習22:棧、作用域和全局
> 原文:[Exercise 22: The Stack, Scope, And Globals](http://c.learncodethehardway.org/book/ex22.html)
> 譯者:[飛龍](https://github.com/wizardforcel)
許多人在開始編程時,對“作用域”這個概念都不是很清楚。起初它來源于系統棧的使用方式(在之前提到過一些),以及它用于臨時變量儲存的方式。這個練習中,我們會通過學習站數據結構如何工作來了解作用域,然后再來看看現代C語言處理作用域的方式。
這個練習的真正目的是了解一些比較麻煩的東西在C中如何存儲。當一個人沒有掌握作用域的概念時,它幾乎也不能理解變量在哪里被創建,存在以及銷毀。一旦你知道了這些,作用域的概念會變得易于理解。
這個練習需要如下三個文件:
`ex22.h`
用于創建一些外部變量和一些函數的頭文件。
`ex22.c`
它并不像通常一樣,是包含`main`的源文件,而是含有一些`ex22.h`中聲明的函數和變量,并且會變成`ex22.o`。
`ex22_main.c`
`main`函數實際所在的文件,它會包含另外兩個文件,并演示了它們包含的東西以及其它作用域概念。
## ex22.h 和 ex22.c
你的第一步是創建你自己的`ex22.h`頭文件,其中定義了所需的函數和“導出”變量。
```c
#ifndef _ex22_h
#define _ex22_h
// makes THE_SIZE in ex22.c available to other .c files
extern int THE_SIZE;
// gets and sets an internal static variable in ex22.c
int get_age();
void set_age(int age);
// updates a static variable that's inside update_ratio
double update_ratio(double ratio);
void print_size();
#endif
```
最重要的事情是`extern int THE_SIZE`的用法,我將會在你創建完`ex22.c`之后解釋它:
```c
#include <stdio.h>
#include "ex22.h"
#include "dbg.h"
int THE_SIZE = 1000;
static int THE_AGE = 37;
int get_age()
{
return THE_AGE;
}
void set_age(int age)
{
THE_AGE = age;
}
double update_ratio(double new_ratio)
{
static double ratio = 1.0;
double old_ratio = ratio;
ratio = new_ratio;
return old_ratio;
}
void print_size()
{
log_info("I think size is: %d", THE_SIZE);
}
```
這兩個文件引入了一些新的變量儲存方式:
`extern`
這個關鍵詞告訴編譯器“這個變量已存在,但是他在別的‘外部區域’里”。通常它的意思是一個`.c`文件要用到另一個`.c`文件中定義的變量。這種情況下,我們可以說`ex2.c`中的`THE_SIZE`變量能變為`ex22_main.c`訪問到。
`static`(文件)
這個關鍵詞某種意義上是`extern`的反義詞,意思是這個變量只能在當前的`.c`文件中使用,程序的其它部分不可訪問。要記住文件級別的`static`(比如這里的`THE_AGE`)和其它位置不同。
`static`(函數)
如果你使用`static`在函數中聲明變量,它和文件中的`static`定義類似,但是只能夠在該函數中訪問。它是一種創建某個函數的持續狀態的方法,但事實上它跟梢用于現代的C語言,因為它們很難和線程一起使用。
在上面的兩個文件中,你需要理解如下幾個變量和函數:
`THE_SIZE`
這個你使用`extern`聲明的變量將會在`ex22_main.c`中用到。
`get_age`和`set_age`
它們用于操作靜態變量`THE_AGE`,并通過函數將其暴露給程序的其它部分。你不能夠直接訪問到`THE_AGE`,但是這些函數可以。
`update_ratio`
它生成新的`ratio`值并返回舊的值。它使用了函數級的靜態變量`ratio`來跟蹤`ratio`當前的值。
`print_size`
打印出`ex22.c`所認為的`THE_SIZE`的當前值。
## ex22_main.c
一旦你寫完了上面那些文件,你可以接著編程`main`函數,它會使用所有上面的文件并且演示了一些更多的作用域轉換:
```c
#include "ex22.h"
#include "dbg.h"
const char *MY_NAME = "Zed A. Shaw";
void scope_demo(int count)
{
log_info("count is: %d", count);
if(count > 10) {
int count = 100; // BAD! BUGS!
log_info("count in this scope is %d", count);
}
log_info("count is at exit: %d", count);
count = 3000;
log_info("count after assign: %d", count);
}
int main(int argc, char *argv[])
{
// test out THE_AGE accessors
log_info("My name: %s, age: %d", MY_NAME, get_age());
set_age(100);
log_info("My age is now: %d", get_age());
// test out THE_SIZE extern
log_info("THE_SIZE is: %d", THE_SIZE);
print_size();
THE_SIZE = 9;
log_info("THE SIZE is now: %d", THE_SIZE);
print_size();
// test the ratio function static
log_info("Ratio at first: %f", update_ratio(2.0));
log_info("Ratio again: %f", update_ratio(10.0));
log_info("Ratio once more: %f", update_ratio(300.0));
// test the scope demo
int count = 4;
scope_demo(count);
scope_demo(count * 20);
log_info("count after calling scope_demo: %d", count);
return 0;
}
```
我會把這個文件逐行拆分,你應該能夠找到我提到的每個變量在哪里定義。
ex22_main.c:4
使用了`const`來創建常量,它可用于替代`define`來創建常量。
ex22_main.c:6
一個簡單的函數,演示了函數中更多的作用域問題。
ex22_main.c:8
在函數頂端打印出`count`的值。
ex22_main.c:10
`if`語句會開啟一個新的作用域區塊,并且在其中創建了另一個`count`變量。這個版本的`count`變量是一個全新的變量。`if`語句就好像開啟了一個新的“迷你函數”。
ex22_main.c:11
`count`對于當前區塊是局部變量,實際上不同于函數參數列表中的參數。
ex22_main.c:13
將它打印出來,所以你可以在這里看到100,并不是傳給`scope_demo`的參數。
ex22_main.c:16
這里是最難懂得部分。你在兩部分都有`count`變量,一個數函數參數,另一個是`if`語句中。`if`語句創建了新的代碼塊,所以11行的`count`并不影響同名的參數。這一行將其打印出來,你會看到它打印了參數的值而不是100。
ex22_main.c:18-20
之后我將`count`參數設為3000并且打印出來,這里演示了你也可以修改函數參數的值,但并不會影響變量的調用者版本。
確保你瀏覽了整個函數,但是不要認為你已經十分了解作用娛樂。如果你在一個代碼塊中(比如`if`或`while`語句)創建了一些變量,這些變量是全新的變量,并且只在這個代碼塊中存在。這是至關重要的東西,也是許多bug的來源。我要強調你應該在這里花一些時間。
`ex22_main.c`的剩余部分通過操作和打印變量演示了它們的全部。
ex22_main.c:26
打印出`MY_NAME`的當前值,并且使用`get_age`讀寫器從`ex22.c`獲取`THE_AGE`。
ex22_main.c:27-30
使用了`ex22.c`中的`set_age`來修改并打印`THE_AGE`。
ex22_main.c:33-39
接下來我對`ex22.c`中的`THE_SIZE`做了相同的事情,但這一次我直接訪問了它,并且同時演示了它實際上在那個文件中已經修改了,還使用`print_size`打印了它。
ex22_main.c:42-44
展示了`update_ratio`中的`ratio`在兩次函數調用中如何保持了它的值。
ex22_main.c:46-51
最后運行`scope_demo`,你可以在實例中觀察到作用域。要注意到的關鍵點是,`count`局部變量在調用后保持不變。你將它像一個變量一樣傳入函數,它一定不會發生改變。要想達到目的你需要我們的老朋友指針。如果你將指向`count`的指針傳入函數,那么函數就會持有它的地址并且能夠改變它。
上面解釋了這些文件中所發生的事情,但是你應該跟蹤它們,并且確保在你學習的過程中明白了每個變量都在什么位置。
## 你會看到什么
這次我想讓你手動構建這兩個文件,而不是使用你的`Makefile`。于是你可以看到它們實際上如何被編譯器放到一起。這是你應該做的事情,并且你應該看到如下輸出:
```sh
$ cc -Wall -g -DNDEBUG -c -o ex22.o ex22.c
$ cc -Wall -g -DNDEBUG ex22_main.c ex22.o -o ex22_main
$ ./ex22_main
[INFO] (ex22_main.c:26) My name: Zed A. Shaw, age: 37
[INFO] (ex22_main.c:30) My age is now: 100
[INFO] (ex22_main.c:33) THE_SIZE is: 1000
[INFO] (ex22.c:32) I think size is: 1000
[INFO] (ex22_main.c:38) THE SIZE is now: 9
[INFO] (ex22.c:32) I think size is: 9
[INFO] (ex22_main.c:42) Ratio at first: 1.000000
[INFO] (ex22_main.c:43) Ratio again: 2.000000
[INFO] (ex22_main.c:44) Ratio once more: 10.000000
[INFO] (ex22_main.c:8) count is: 4
[INFO] (ex22_main.c:16) count is at exit: 4
[INFO] (ex22_main.c:20) count after assign: 3000
[INFO] (ex22_main.c:8) count is: 80
[INFO] (ex22_main.c:13) count in this scope is 100
[INFO] (ex22_main.c:16) count is at exit: 80
[INFO] (ex22_main.c:20) count after assign: 3000
[INFO] (ex22_main.c:51) count after calling scope_demo: 4
```
確保你跟蹤了每個變量是如何改變的,并且將其匹配到所輸出的那一行。我使用了`dbg.h`的`log_info`來讓你獲得每個變量打印的具體行號,并且在文件中找到它用于跟蹤。
## 作用域、棧和Bug
如果你正確完成了這個練習,你會看到有很多不同方式在C代碼中放置變量。你可以使用`extern`或者訪問類似`get_age`的函數來創建全局。你也可以在任何代碼塊中創建新的變量,它們在退出代碼塊之前會擁有自己的值,并且屏蔽掉外部的變量。你也可以響函數傳遞一個值并且修改它,但是調用者的變量版本不會發生改變。
需要理解的最重要的事情是,這些都可以造成bug。C中在你機器中許多位置放置和訪問變量的能力會讓你對它們所在的位置感到困擾。如果你不知道它們的位置,你就可能不能適當地管理它們。
下面是一些編程C代碼時需要遵循的規則,可以讓你避免與棧相關的bug:
+ 不要隱藏某個變量,就像上面`scope_demo`中對`count`所做的一樣。這可能會產生一些隱蔽的bug,你認為你改變了某個變量但實際上沒有。
+ 避免過多的全局變量,尤其是跨越多個文件。如果必須的話,要使用讀寫器函數,就像`get_age`。這并不適用于常量,因為它們是只讀的。我是說對于`THE_SIZE`這種變量,如果你希望別人能夠修改它,就應該使用讀寫器函數。
+ 在你不清楚的情況下,應該把它放在堆上。不要依賴于棧的語義,或者指定區域,而是要直接使用`malloc`創建它。
+ 不要使用函數級的靜態變量,就像`update_ratio`。它們并不有用,而且當你想要使你的代碼運行在多線程環境時,會有很大的隱患。對于良好的全局變量,它們也非常難于尋找。
+ 避免復用函數參數,因為你搞不清楚僅僅想要復用它還是希望修改它的調用者版本。
## 如何使它崩潰
對于這個練習,崩潰這個程序涉及到嘗試訪問或修改你不能訪問的東西。
+ 試著從`ex22_main.c`直接訪問`ex22.c`中的你不能訪問變量。例如,你能不能獲取`update_ratio`中的`ratio`?如果你用一個指針指向它會發生什么?
+ 移除`ex22.h`的`extern`聲明,來觀察會得到什么錯誤或警告。
+ 對不同變量添加`static`或者`const`限定符,之后嘗試修改它們。
## 附加題
+ 研究“值傳遞”和“引用傳遞”的差異,并且為二者編寫示例。(譯者注:C中沒有引用傳遞,你可以搜索“指針傳遞”。)
+ 使用指針來訪問原本不能訪問的變量。
+ 使用`Valgrind`來觀察錯誤的訪問是什么樣子。
+ 編寫一個遞歸調用并導致棧溢出的函數。如果不知道遞歸函數是什么的話,試著在`scope_demo`底部調用`scope_demo`本身,會形成一種循環。
+ 重新編寫`Makefile`使之能夠構建這些文件。
- 笨辦法學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” 已死