程序員C語言快速上手——進階篇(八)

進階篇

程序結構與作用域

過程式、模塊化的C語言程序是由多個源文件(.c文件)構成的,在每一個源文件中,都形成一個文件作用域。所謂作用域,實際上就是指有效範圍。一旦離開這個源文件的範圍,就相當於離開了該源文件的文件作用域。在源文件中定義函數,那麼在函數之外的地方,就屬於全局作用域,即使是多個源文件,只要在函數之外,那它們就都屬於全局作用域,全局作用域,全局都可訪問。而在函數之內的空間聲明變量,那它屬於局部作用域。

局部變量

局部變量是指在某個函數內部聲明的變量。它有兩個含義

  1. 在某個函數內聲明的局部變量,不能被其他的函數使用,意即只在聲明它的函數內有效。
  2. 每次調用函數時,生成的局部變量的儲存空間可能都是不同的,意即局部變量在函數調用結束後,就會釋放,下次調用函數,生成的局部變量又是一個新的。

還要注意一點,在函數的形式參數中聲明的變量,也都是局部變量。

全局變量

與局部變量相對的概念是全局變量,它聲明在所有的函數體之外。全局變量在文件作用域內可見,即從變量被聲明的下一行,一直到當前文件的末尾,它都可以被直接使用,因此全局變量可以被它之後定義的所有函數訪問。

需要注意一點,編譯器會自動將全局變量進行零值初始化。因此在使用時,只需要聲明即可。如果需要手動指定其值進行初始化,則它只能被常量表達式初始化,使用其他的變量表達式初始化是不合法的。

//全局變量(正確)
int minute = 360 -10;

//錯誤!!! 全局變量必須使用常量表達式初始化
int hour = minute/60;

// 訪問全局變量 minute
int f(int h){
    //h 是局部變量
    return h*minute;
}

int main(){
    // 局部變量
    int day=0;
    return 0;
}

static關鍵字

除了局部變量和全局變量,C語言中還有靜態局部變量和靜態全局變量,聲明時使用static關鍵字修飾即代表靜態的意思。

#include <stdio.h>

// 靜態全局變量
static int s_global;

int get_count(){
    // 靜態局部變量
    static int count;
    count++;
    return count;
}

int main(){
    printf("%d\n",get_count());
    printf("%d\n",get_count());
    printf("%d\n",get_count());
    printf("%d\n",get_count());
 
    return 0;
}

靜態全局變量和普通全局變量的區別不是很大,主要體現在訪問權限的區別上。在C語言中,全局變量是在整個程序的生命期中都有效的,換句話說,也就是一旦聲明瞭一個全局變量,則整個程序中都可以訪問,而靜態全局變量,則只在聲明它的那個源文件中可以訪問。靜態全局變量雖然也是在整個程序的生命期中都有效,但它在其他文件中不可見,無法被訪問。關於這一點的細則,在下面的extern關鍵字的使用中做詳細說明。

靜態局部變量和普通局部變量的區別就比較大了,主要有三個區別

  1. 存儲位置不同。靜態局部變量被編譯器放在全局存儲區,雖是局部變量,但是在程序的整個生命期中都存在。而普通局部變量在函數調用結束後就會被釋放。從這一點上看,靜態局部變量和全局變量被放在了相同的儲存位置。
  2. 靜態局部變量會被編譯器自動初始化爲零值。我們都知道普通局部變量的原則是先初始化後使用,而靜態局部變量則和全局變量一樣,會被自動初始化,使用時只需聲明,無需手動初始化。
  3. 靜態局部變量只能被聲明它的函數訪問。靜態局部變量與普通局部變量的訪問權限相同,都只能被聲明它的函數使用。如上例,靜態局部變量count只能被get_count函數使用,即使count變量在整個程序的生命期中都有效,其他函數也無法使用它。

說完了靜態局部變量後,大家肯定疑惑,既然它只在聲明它的函數中使用,那它還有什麼意義呢?直接使用普通局部變量不就行了,幹嘛要用它?我們知道,普通局部變量在函數每次被調用的時候都會生成一個新的,調用結束後又將它釋放,如果一個函數被頻繁調用,這樣性能豈不是很低?因爲需要不停的生成新的局部變量,然後又釋放掉,然後又生成新的……但是給局部變量加上了static修飾後,函數無論被調用多少次,都不會再生成新的局部變量,始終都是複用的同一個變量,這就大大減少了對內存的操作,提升了性能。

舉個生活中的例子,如果你在公司樓下有一個固定的私人停車位,那麼你每天上班只需要把車停在固定的地方就好,如果你沒有私人停車位,那你每天到公司樓下,都需要四處去找一個空位子停車,豈不是很麻煩,效率又低,弄不好因爲找停車位導致打卡遲到。

既然靜態局部變量這麼好,那是不是可以濫用呢?還是回到上面的例子,如果你是公司特聘人員,一個月只需要上兩天班,那麼你有必要在公司樓下買一個固定的私人停車位嗎?顯然是沒有必要的,因此當函數不會被頻繁調用時,不應當考慮使用靜態局部變量。

最後需要特別注意,靜態局部變量會一直保存上次的值,因爲它一直都存在。基於這個特性,我們通常可以使用靜態局部變量做計數器,如上例,每次調用get_count函數時,對靜態局部變量count自增1,打印結果如下:

1
2
3
4

靜態函數
static關鍵字除了可以修飾變量,還可以用來修飾函數。在C++、Java等面向對象的編程語言中,都存在類似於private的權限訪問控制,而C語言中的static關鍵字,就類似這種private,被它修飾的函數只能在當前源文件中使用,在其他源文件中無法被訪問。通常來說,C語言編寫的大型的模塊化工程中,不需要共享的函數都應該使用static關鍵字來修飾。

需要特別注意,由於C語言沒有命名空間的概念,它只有一個全局作用域,當你的C程序十分龐大時,存在幾百上千個函數時,很難保證函數不會同名。當然,通過嚴格的代碼規範,命名規範,可以人爲的保證函數不會同名,但我們可以保證自己寫的函數不會同名,卻無法保證引入的外部庫的函數不會和我們的函數同名。一旦函數同名了,就會形成命名衝突,這就是爲什麼我們看一些C語言編寫的開源庫時,變量名、函數命名非常的複雜,名字很長,多個單詞大寫或以下劃線分隔,這樣怪異的命名很大程度上就是爲了避免命名衝突。基於此,我們編寫非公開、非共享的函數時,都應當使用static修飾,以此來避免一部分命名衝突問題。static修飾的函數,只在當前源文件中可見,在另一個源文件中聲明一個同名的函數,就不會產生命名衝突。

示例
編寫f1.c源文件

int get_count(){
    static int count;
    count++;
    return count;
}

編寫main.c源文件

#include <stdio.h>

int get_count();

int main(){
    printf("%d\n",get_count());
    printf("%d\n",get_count());
    return 0;
}

編譯:gcc f1.c main.c -o main
編譯、運行正常

修改f1.c,添加static修飾

static int get_count(){
    static int count;
    count++;
    return count;
}

編譯報錯,在main.c源文件中無法使用靜態函數get_count

extern關鍵字

在說明extern關鍵字前,先來看一個示例
編寫t1.c

// 全局變量
int s_global=12;

編寫main.c

#include <stdio.h>

int main(){
    printf("s_global=%d\n",s_global);
    return 0;
}

編譯:gcc t1.c main.c -o main
這樣會直接報錯: error: 's_global' undeclared (first use in this function)

這好像和我們前面說的有些不符,全局變量是在整個程序的生命期都有效的,在全局可訪問的,但是現在卻報錯了。大家要注意前面的措辭,全局變量在文件作用域內可見,即從變量被聲明的下一行,一直到當前文件的末尾,它都可以被直接使用。這裏的關鍵就是直接使用,在t1.c源文件中是可以直接使用的,但是main.c中就無法直接使用了。

當全局變量離開了它的文件作用域後,無法直接使用,這時候我們需要另一個關鍵字extern來幫助我們使用它。

修改main.c

#include <stdio.h>
// 寫在函數外部,表示在當前文件中的任意地方都可以使用s_global
// extern int s_global;

int main(){
	// 寫在函數內部,僅在函數中使用
	extern int s_global;
    printf("s_global=%d\n",s_global);
    return 0;
}

再次編譯成功,運行結果

s_global=12

在這裏,extern int s_global;並不是重新聲明變量的意思,它表示的是引用全局變量s_global,一定要注意,如果不存在一個全局變量s_global,是無法編譯的,也就是說,使用extern來引用全局變量時,全局變量一定要存在。

extern主要是用來修飾變量的,當然也可以用來修飾函數,通常C語言中的函數都使用頭文件包含的方式引入聲明,但我們也可以使用extern修飾。實際上C語言中的函數聲明默認都是包含extern的,無需手動指定。

//以下兩種是等價的,無需手動指定extern關鍵字
int get_count();
extern int get_count();

小拓展
有時候我們可能會看到extern “C”這樣的聲明,請注意,這不是C語言的語法,也不屬於C語言。有些C++程序員,經常把C語言和C++語言搞混,實際上這是兩種不同的語言,C++也並不是很多人說的那樣,完全是C語言的超集,更準確的說法應該是,C++是一種獨立的語言,它兼容C語言的絕大多數語法,但並不是百分百完全兼容。C++除了兼容的C語言的語法,另一部分就是它獨立的內容。如果不能完全清楚這兩種語言的邊界,就會發生語法弄混的情況。

在C++中,當需要調用純C語言編寫的函數時,通常會使用extern “C”聲明,表明這是純C語言的內容。

模塊化開發的補充

頭文件的嵌套包含

所謂嵌套包含,就是指在一個頭文件中,還可以使用#include預編譯指令,包含其他的頭文件。
例如,我們編寫一個頭文件bool.h

#define Bool int
#define False 0
#define True 1

在以上頭文件中,我們使用宏定義了新類型Bool,接着編寫func.h頭文件

#include "bool.h"

// 聲明一個函數,返回值爲Bool類型,值可以是False 或者True 
Bool check();

頭文件的保護

如果一個源文件將同一個頭文件包含兩次,那麼就可能會產生編譯錯誤。因此,在C語言的模塊化開發中,一定要避免將同一個頭文件包含兩次。但是,有時候這種包含不是明顯的,而是一種隱式的包含,不易察覺,不知不覺就犯下了錯誤。

如下例,分別創建h1.hh2.hh3.h三個頭文件
h1.h內容

#include "h3.h"
……

h2.h內容

#include "h3.h"
……

可以看到,h1.hh2.h兩個頭文件分別都包含了一個相同的h3.h頭文件,那麼如果在main.c中分別包含這兩個頭文件

// main.c
#include "h1.h"
#include "h2.h"
……

這樣一來,實際上就等同於在main.c中將h3.h頭文件include了兩次,顯然違背了我們上面說的,不能在一個源文件中將同一個頭文件包含兩次的原則。因爲所謂頭文件包含,實際上就是將頭文件中的聲明覆制到當前源文件中,那麼上例中h3.h一定會被複制兩次。

問題出來了,該如何解決呢?在複雜的大型工程中,頭文件被重複包含的問題一定是避免不了的,這個時候就需要我們上一章講的條件編譯知識出來救場了。

修改h3.h文件

內容如下

// 如果沒有定義過_H_H3_ 宏,則定義一個_H_H3_ 宏
#ifndef _H_H3_
#define _H_H3_

// 聲明的內容 ……

#endif

改造頭文件之後,再去源文件使用,就不會存在重複包含的問題了。

注意,這裏使用#ifndef#endif將整個頭文件中的全部內容包裹起來,然後在#ifndef之後通過#define定義一個宏,這樣一來,#ifndef#endif之間的內容就只會被預編譯一次,而不會重複包含。這種機制,被戲稱爲頭文件衛士,或者稱爲頭文件保護。如果對於這種寫法不太理解,可以使用上一章介紹的gcc -E命令,生成預編譯代碼查看,即可明瞭。

最後,需特別注意的地方是宏的名字,這裏是_H_H3_,使用頭文件包含這種機制時,宏定義的名字一定要獨特,避免重複,以免導致各種不可預知的問題。通常宏的名字要全部大寫,並用下劃線來分隔單詞或縮寫,在這個宏的名稱中,最好包含當前頭文件的文件名,例如H3

在C語言中,我們以後自己編寫頭文件,建議在所有編寫的頭文件中都使用這種頭文件保護機制,因爲你不知道什麼時候,你的這個頭文件可能就會被重複包含,如上例,h1.hh2.hh3.h三個頭文件都應當使用頭文件保護機制。

歡迎關注我的公衆號:編程之路從0到1

編程之路從0到1

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章