鍍金池/ 教程/ C/ 練習(xí)22:棧、作用域和全局
練習(xí)9:數(shù)組和字符串
練習(xí)6:變量類型
練習(xí)3:格式化輸出
練習(xí)4:Valgrind 介紹
練習(xí)28:Makefile 進(jìn)階
練習(xí)14:編寫(xiě)并使用函數(shù)
練習(xí)21:高級(jí)數(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)識(shí)達(dá)夫設(shè)備
練習(xí)12:If,Else If,Else
練習(xí)2:用Make來(lái)代替Python
練習(xí)1:?jiǎn)⒂镁幾g器
練習(xí)11:While循環(huán)和布爾表達(dá)式
練習(xí)5:一個(gè)C程序的結(jié)構(gòu)
練習(xí)24:輸入輸出和文件
練習(xí)25:變參函數(shù)
練習(xí)13:Switch語(yǔ)句
練習(xí)19:一個(gè)簡(jiǎn)單的對(duì)象系統(tǒng)
練習(xí)26:編寫(xiě)第一個(gè)真正的程序
導(dǎo)言:C的笛卡爾之夢(mèng)
練習(xí)17:堆和棧的內(nèi)存分配

練習(xí)22:棧、作用域和全局

許多人在開(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è)文件,并演示了它們包含的東西以及其它作用域概念。

ex22.h 和 ex22.c

你的第一步是創(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訪問(wèn)到。

static(文件)

這個(gè)關(guān)鍵詞某種意義上是extern的反義詞,意思是這個(gè)變量只能在當(dāng)前的.c文件中使用,程序的其它部分不可訪問(wèn)。要記住文件級(jí)別的static(比如這里的THE_AGE)和其它位置不同。

static(函數(shù))

如果你使用static在函數(shù)中聲明變量,它和文件中的static定義類似,但是只能夠在該函數(shù)中訪問(wèn)。它是一種創(chuàng)建某個(gè)函數(shù)的持續(xù)狀態(tài)的方法,但事實(shí)上它跟梢用于現(xiàn)代的C語(yǔ)言,因?yàn)樗鼈兒茈y和線程一起使用。

在上面的兩個(gè)文件中,你需要理解如下幾個(gè)變量和函數(shù):

THE_SIZE

這個(gè)你使用extern聲明的變量將會(huì)在ex22_main.c中用到。

get_ageset_age

它們用于操作靜態(tài)變量THE_AGE,并通過(guò)函數(shù)將其暴露給程序的其它部分。你不能夠直接訪問(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)前值。

ex22_main.c

一旦你寫(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è)代碼塊中(比如ifwhile語(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做了相同的事情,但這一次我直接訪問(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è)變量都在什么位置。

你會(huì)看到什么

這次我想讓你手動(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.hlog_info來(lái)讓你獲得每個(gè)變量打印的具體行號(hào),并且在文件中找到它用于跟蹤。

作用域、棧和Bug

如果你正確完成了這個(gè)練習(xí),你會(huì)看到有很多不同方式在C代碼中放置變量。你可以使用extern或者訪問(wèn)類似get_age的函數(shù)來(lái)創(chuàng)建全局。你也可以在任何代碼塊中創(chuàng)建新的變量,它們?cè)谕顺龃a塊之前會(huì)擁有自己的值,并且屏蔽掉外部的變量。你也可以響函數(shù)傳遞一個(gè)值并且修改它,但是調(diào)用者的變量版本不會(huì)發(fā)生改變。

需要理解的最重要的事情是,這些都可以造成bug。C中在你機(jī)器中許多位置放置和訪問(wèn)變量的能力會(huì)讓你對(duì)它們所在的位置感到困擾。如果你不知道它們的位置,你就可能不能適當(dāng)?shù)毓芾硭鼈儭?/p>

下面是一些編程C代碼時(shí)需要遵循的規(guī)則,可以讓你避免與棧相關(guān)的bug:

  • 不要隱藏某個(gè)變量,就像上面scope_demo中對(duì)count所做的一樣。這可能會(huì)產(chǎn)生一些隱蔽的bug,你認(rèn)為你改變了某個(gè)變量但實(shí)際上沒(méi)有。
  • 避免過(guò)多的全局變量,尤其是跨越多個(gè)文件。如果必須的話,要使用讀寫(xiě)器函數(shù),就像get_age。這并不適用于常量,因?yàn)樗鼈兪侵蛔x的。我是說(shuō)對(duì)于THE_SIZE這種變量,如果你希望別人能夠修改它,就應(yīng)該使用讀寫(xiě)器函數(shù)。
  • 在你不清楚的情況下,應(yīng)該把它放在堆上。不要依賴于棧的語(yǔ)義,或者指定區(qū)域,而是要直接使用malloc創(chuàng)建它。
  • 不要使用函數(shù)級(jí)的靜態(tài)變量,就像update_ratio。它們并不有用,而且當(dāng)你想要使你的代碼運(yùn)行在多線程環(huán)境時(shí),會(huì)有很大的隱患。對(duì)于良好的全局變量,它們也非常難于尋找。
  • 避免復(fù)用函數(shù)參數(shù),因?yàn)槟愀悴磺宄H僅想要復(fù)用它還是希望修改它的調(diào)用者版本。

如何使它崩潰

對(duì)于這個(gè)練習(xí),崩潰這個(gè)程序涉及到嘗試訪問(wèn)或修改你不能訪問(wèn)的東西。

  • 試著從ex22_main.c直接訪問(wèn)ex22.c中的你不能訪問(wèn)變量。例如,你能不能獲取update_ratio中的ratio?如果你用一個(gè)指針指向它會(huì)發(fā)生什么?
  • 移除ex22.hextern聲明,來(lái)觀察會(huì)得到什么錯(cuò)誤或警告。
  • 對(duì)不同變量添加static或者const限定符,之后嘗試修改它們。

附加題

  • 研究“值傳遞”和“引用傳遞”的差異,并且為二者編寫(xiě)示例。(譯者注:C中沒(méi)有引用傳遞,你可以搜索“指針傳遞”。)
  • 使用指針來(lái)訪問(wèn)原本不能訪問(wèn)的變量。
  • 使用Valgrind來(lái)觀察錯(cuò)誤的訪問(wèn)是什么樣子。
  • 編寫(xiě)一個(gè)遞歸調(diào)用并導(dǎo)致棧溢出的函數(shù)。如果不知道遞歸函數(shù)是什么的話,試著在scope_demo底部調(diào)用scope_demo本身,會(huì)形成一種循環(huán)。
  • 重新編寫(xiě)Makefile使之能夠構(gòu)建這些文件。