進階篇
程序結構與作用域
過程式、模塊化的C語言程序是由多個源文件(.c文件)構成的,在每一個源文件中,都形成一個文件作用域。所謂作用域,實際上就是指有效範圍。一旦離開這個源文件的範圍,就相當於離開了該源文件的文件作用域。在源文件中定義函數,那麼在函數之外的地方,就屬於全局作用域,即使是多個源文件,只要在函數之外,那它們就都屬於全局作用域,全局作用域,全局都可訪問。而在函數之內的空間聲明變量,那它屬於局部作用域。
局部變量
局部變量是指在某個函數內部聲明的變量。它有兩個含義
- 在某個函數內聲明的局部變量,不能被其他的函數使用,意即只在聲明它的函數內有效。
- 每次調用函數時,生成的局部變量的儲存空間可能都是不同的,意即局部變量在函數調用結束後,就會釋放,下次調用函數,生成的局部變量又是一個新的。
還要注意一點,在函數的形式參數中聲明的變量,也都是局部變量。
全局變量
與局部變量相對的概念是全局變量,它聲明在所有的函數體之外。全局變量在文件作用域內可見,即從變量被聲明的下一行,一直到當前文件的末尾,它都可以被直接使用,因此全局變量可以被它之後定義的所有函數訪問。
需要注意一點,編譯器會自動將全局變量進行零值初始化。因此在使用時,只需要聲明即可。如果需要手動指定其值進行初始化,則它只能被常量表達式初始化,使用其他的變量表達式初始化是不合法的。
//全局變量(正確)
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
關鍵字的使用中做詳細說明。
靜態局部變量和普通局部變量的區別就比較大了,主要有三個區別
- 存儲位置不同。靜態局部變量被編譯器放在全局存儲區,雖是局部變量,但是在程序的整個生命期中都存在。而普通局部變量在函數調用結束後就會被釋放。從這一點上看,靜態局部變量和全局變量被放在了相同的儲存位置。
- 靜態局部變量會被編譯器自動初始化爲零值。我們都知道普通局部變量的原則是先初始化後使用,而靜態局部變量則和全局變量一樣,會被自動初始化,使用時只需聲明,無需手動初始化。
- 靜態局部變量只能被聲明它的函數訪問。靜態局部變量與普通局部變量的訪問權限相同,都只能被聲明它的函數使用。如上例,靜態局部變量
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.h
、h2.h
、h3.h
三個頭文件
h1.h
內容
#include "h3.h"
……
h2.h
內容
#include "h3.h"
……
可以看到,h1.h
、h2.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.h
、h2.h
、h3.h
三個頭文件都應當使用頭文件保護機制。