許多人在開(kāi)始編程時(shí),對(duì)“作用域”這個(gè)概念都不是很清楚。起初它來(lái)源于系統(tǒng)棧的使用方式(在之前提到過(guò)一些),以及它用于臨時(shí)變量?jī)?chǔ)存的方式。這個(gè)練習(xí)中,我們會(huì)通過(guò)學(xué)習(xí)站數(shù)據(jù)結(jié)構(gòu)如何工作來(lái)了解作用域,然后再來(lái)看看現(xiàn)代C語(yǔ)言處理作用域的方式。
這個(gè)練習(xí)的真正目的是了解一些比較麻煩的東西在C中如何存儲(chǔ)。當(dāng)一個(gè)人沒(méi)有掌握作用域的概念時(shí),它幾乎也不能理解變量在哪里被創(chuàng)建,存在以及銷(xiāo)毀。一旦你知道了這些,作用域的概念會(huì)變得易于理解。
這個(gè)練習(xí)需要如下三個(gè)文件:
ex22.h
用于創(chuàng)建一些外部變量和一些函數(shù)的頭文件。
ex22.c
它并不像通常一樣,是包含main
的源文件,而是含有一些ex22.h
中聲明的函數(shù)和變量,并且會(huì)變成ex22.o
。
ex22_main.c
main
函數(shù)實(shí)際所在的文件,它會(huì)包含另外兩個(gè)文件,并演示了它們包含的東西以及其它作用域概念。
你的第一步是創(chuàng)建你自己的ex22.h
頭文件,其中定義了所需的函數(shù)和“導(dǎo)出”變量。
#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
的用法,我將會(huì)在你創(chuàng)建完ex22.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);
}
這兩個(gè)文件引入了一些新的變量?jī)?chǔ)存方式:
extern
這個(gè)關(guān)鍵詞告訴編譯器“這個(gè)變量已存在,但是他在別的‘外部區(qū)域’里”。通常它的意思是一個(gè).c
文件要用到另一個(gè).c
文件中定義的變量。這種情況下,我們可以說(shuō)ex2.c
中的THE_SIZE
變量能變?yōu)?code>ex22_main.c訪(fǎng)問(wèn)到。
static
(文件)
這個(gè)關(guān)鍵詞某種意義上是extern
的反義詞,意思是這個(gè)變量只能在當(dāng)前的.c
文件中使用,程序的其它部分不可訪(fǎng)問(wèn)。要記住文件級(jí)別的static
(比如這里的THE_AGE
)和其它位置不同。
static
(函數(shù))
如果你使用static
在函數(shù)中聲明變量,它和文件中的static
定義類(lèi)似,但是只能夠在該函數(shù)中訪(fǎng)問(wèn)。它是一種創(chuàng)建某個(gè)函數(shù)的持續(xù)狀態(tài)的方法,但事實(shí)上它跟梢用于現(xiàn)代的C語(yǔ)言,因?yàn)樗鼈兒茈y和線(xiàn)程一起使用。
在上面的兩個(gè)文件中,你需要理解如下幾個(gè)變量和函數(shù):
THE_SIZE
這個(gè)你使用extern
聲明的變量將會(huì)在ex22_main.c
中用到。
get_age
和set_age
它們用于操作靜態(tài)變量THE_AGE
,并通過(guò)函數(shù)將其暴露給程序的其它部分。你不能夠直接訪(fǎng)問(wèn)到THE_AGE
,但是這些函數(shù)可以。
update_ratio
它生成新的ratio
值并返回舊的值。它使用了函數(shù)級(jí)的靜態(tài)變量ratio
來(lái)跟蹤ratio
當(dāng)前的值。
print_size
打印出ex22.c
所認(rèn)為的THE_SIZE
的當(dāng)前值。
一旦你寫(xiě)完了上面那些文件,你可以接著編程main
函數(shù),它會(huì)使用所有上面的文件并且演示了一些更多的作用域轉(zhuǎn)換:
#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;
}
我會(huì)把這個(gè)文件逐行拆分,你應(yīng)該能夠找到我提到的每個(gè)變量在哪里定義。
ex22_main.c:4
使用了const
來(lái)創(chuàng)建常量,它可用于替代define
來(lái)創(chuàng)建常量。
ex22_main.c:6
一個(gè)簡(jiǎn)單的函數(shù),演示了函數(shù)中更多的作用域問(wèn)題。
ex22_main.c:8
在函數(shù)頂端打印出count
的值。
ex22_main.c:10
if
語(yǔ)句會(huì)開(kāi)啟一個(gè)新的作用域區(qū)塊,并且在其中創(chuàng)建了另一個(gè)count
變量。這個(gè)版本的count
變量是一個(gè)全新的變量。if
語(yǔ)句就好像開(kāi)啟了一個(gè)新的“迷你函數(shù)”。
ex22_main.c:11
count
對(duì)于當(dāng)前區(qū)塊是局部變量,實(shí)際上不同于函數(shù)參數(shù)列表中的參數(shù)。
ex22_main.c:13
將它打印出來(lái),所以你可以在這里看到100,并不是傳給scope_demo
的參數(shù)。
ex22_main.c:16
這里是最難懂得部分。你在兩部分都有count
變量,一個(gè)數(shù)函數(shù)參數(shù),另一個(gè)是if
語(yǔ)句中。if
語(yǔ)句創(chuàng)建了新的代碼塊,所以11行的count
并不影響同名的參數(shù)。這一行將其打印出來(lái),你會(huì)看到它打印了參數(shù)的值而不是100。
ex22_main.c:18-20
之后我將count
參數(shù)設(shè)為3000并且打印出來(lái),這里演示了你也可以修改函數(shù)參數(shù)的值,但并不會(huì)影響變量的調(diào)用者版本。
確保你瀏覽了整個(gè)函數(shù),但是不要認(rèn)為你已經(jīng)十分了解作用娛樂(lè)。如果你在一個(gè)代碼塊中(比如if
或while
語(yǔ)句)創(chuàng)建了一些變量,這些變量是全新的變量,并且只在這個(gè)代碼塊中存在。這是至關(guān)重要的東西,也是許多bug的來(lái)源。我要強(qiáng)調(diào)你應(yīng)該在這里花一些時(shí)間。
ex22_main.c
的剩余部分通過(guò)操作和打印變量演示了它們的全部。
ex22_main.c:26
打印出MY_NAME
的當(dāng)前值,并且使用get_age
讀寫(xiě)器從ex22.c
獲取THE_AGE
。
ex22_main.c:27-30
使用了ex22.c
中的set_age
來(lái)修改并打印THE_AGE
。
ex22_main.c:33-39
接下來(lái)我對(duì)ex22.c
中的THE_SIZE
做了相同的事情,但這一次我直接訪(fǎng)問(wèn)了它,并且同時(shí)演示了它實(shí)際上在那個(gè)文件中已經(jīng)修改了,還使用print_size
打印了它。
ex22_main.c:42-44
展示了update_ratio
中的ratio
在兩次函數(shù)調(diào)用中如何保持了它的值。
ex22_main.c:46-51
最后運(yùn)行scope_demo
,你可以在實(shí)例中觀察到作用域。要注意到的關(guān)鍵點(diǎn)是,count
局部變量在調(diào)用后保持不變。你將它像一個(gè)變量一樣傳入函數(shù),它一定不會(huì)發(fā)生改變。要想達(dá)到目的你需要我們的老朋友指針。如果你將指向count
的指針傳入函數(shù),那么函數(shù)就會(huì)持有它的地址并且能夠改變它。
上面解釋了這些文件中所發(fā)生的事情,但是你應(yīng)該跟蹤它們,并且確保在你學(xué)習(xí)的過(guò)程中明白了每個(gè)變量都在什么位置。
這次我想讓你手動(dòng)構(gòu)建這兩個(gè)文件,而不是使用你的Makefile
。于是你可以看到它們實(shí)際上如何被編譯器放到一起。這是你應(yīng)該做的事情,并且你應(yīng)該看到如下輸出:
$ 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
確保你跟蹤了每個(gè)變量是如何改變的,并且將其匹配到所輸出的那一行。我使用了dbg.h
的log_info
來(lái)讓你獲得每個(gè)變量打印的具體行號(hào),并且在文件中找到它用于跟蹤。
如果你正確完成了這個(gè)練習(xí),你會(huì)看到有很多不同方式在C代碼中放置變量。你可以使用extern
或者訪(fǎng)問(wèn)類(lèi)似get_age
的函數(shù)來(lái)創(chuàng)建全局。你也可以在任何代碼塊中創(chuàng)建新的變量,它們?cè)谕顺龃a塊之前會(huì)擁有自己的值,并且屏蔽掉外部的變量。你也可以響函數(shù)傳遞一個(gè)值并且修改它,但是調(diào)用者的變量版本不會(huì)發(fā)生改變。
需要理解的最重要的事情是,這些都可以造成bug。C中在你機(jī)器中許多位置放置和訪(fǎng)問(wèn)變量的能力會(huì)讓你對(duì)它們所在的位置感到困擾。如果你不知道它們的位置,你就可能不能適當(dāng)?shù)毓芾硭鼈儭?/p>
下面是一些編程C代碼時(shí)需要遵循的規(guī)則,可以讓你避免與棧相關(guān)的bug:
scope_demo
中對(duì)count
所做的一樣。這可能會(huì)產(chǎn)生一些隱蔽的bug,你認(rèn)為你改變了某個(gè)變量但實(shí)際上沒(méi)有。get_age
。這并不適用于常量,因?yàn)樗鼈兪侵蛔x的。我是說(shuō)對(duì)于THE_SIZE
這種變量,如果你希望別人能夠修改它,就應(yīng)該使用讀寫(xiě)器函數(shù)。malloc
創(chuàng)建它。update_ratio
。它們并不有用,而且當(dāng)你想要使你的代碼運(yùn)行在多線(xiàn)程環(huán)境時(shí),會(huì)有很大的隱患。對(duì)于良好的全局變量,它們也非常難于尋找。對(duì)于這個(gè)練習(xí),崩潰這個(gè)程序涉及到嘗試訪(fǎng)問(wèn)或修改你不能訪(fǎng)問(wèn)的東西。
ex22_main.c
直接訪(fǎng)問(wèn)ex22.c
中的你不能訪(fǎng)問(wèn)變量。例如,你能不能獲取update_ratio
中的ratio
?如果你用一個(gè)指針指向它會(huì)發(fā)生什么?ex22.h
的extern
聲明,來(lái)觀察會(huì)得到什么錯(cuò)誤或警告。static
或者const
限定符,之后嘗試修改它們。Valgrind
來(lái)觀察錯(cuò)誤的訪(fǎng)問(wèn)是什么樣子。scope_demo
底部調(diào)用scope_demo
本身,會(huì)形成一種循環(huán)。Makefile
使之能夠構(gòu)建這些文件。