零基礎學C語言——作用域

這是一個C語言系列文章,如果是初學者的話,建議先行閱讀之前的文章。筆者也會按照章節順序發佈。

在數學中,變量x一般都有其值域,也就是x都可以有哪些值,或者說在什麼數值範圍內x是有效的。同理,C語言中,每個變量或者函數都有其作用域,也就是在什麼範圍內這個變量或函數有效(可被編譯器找到)。

對於變量,根據作用域的不同被分為兩大類——局部變量全局變量

局部變量

所謂的局部變量是指在函數體中定義的變量,這些變量的作用域就是函數內部,即函數返回後,這些變量將被銷燬。這類變量我們一般稱作自動變量

自動變量的定義形式我們在 一文中介紹過:

<code>數據類型 變量名;或數據類型 變量名 = 初始值;/<code>

還有一種自動變量——函數參數,函數參數也是僅在函數內有效的。

此外,C語言還提供了一種作用域在函數內,但函數返回後不會銷燬的變量——靜態變量

靜態變量是指在函數體中,用如下形式定義的變量:

<code>static 數據類型 變量名;或static 數據類型 變量名 = 初始值;/<code>

靜態變量與自動變量的區別是:每次函數調用時,函數內的自動變量所佔用的內存都會被重新分配,其值也會按照語句重新賦值。而靜態變量不同,如果使用定義同時初始化形式定義靜態變量,則靜態變量僅會被初始化一次。並且每次函數調用時靜態變量的值都是沿用上一次調用改變後的值,而不會被重置。

舉個例子:

<code>#include <stdio.h>void foo(void){  static int a = 1;  ++a;  printf("%d\\n", a);}int main(void){  foo();  foo();  return 0;}/<stdio.h>/<code>

這個例子的輸出是:

<code>23/<code>

原因是,第一次進入foo時,靜態變量a被初始化為1,然後自加變為2,所以printf打印的結果是2(第一行)。隨後函數返回main,之後再次調用foo函數。這次foo中a不再被重新賦值為1,而是依舊保持上次被修改後的結果,即2。然後再自加變為3,最後打印其值3(第二行)。

<strong>下面看一個初學者常犯的錯誤:

<code>int *return_array(void){  int array[2] = {1, 2};  return array;}int main(void){  int *ret = return_array();  return 0;}/<code>

這是一個典型的錯誤用法。我們說過,函數內的自動變量的作用域僅限於函數內,當函數返回時,自動變量會被銷燬。因此,main中ret指向的數組,其內容將是不可預知的內容,訪問其內容可能會導致程序崩潰。

想要正常返回一個數組,利用靜態變量是一種解決方案。除此之外,還有一種動態分配內存的方案,將在後續內存管理相關的文章中說明。

全局變量

全局變量是指變量定義於任何函數體之外,且作用域是整個程序範圍內的變量。這類變量又分為兩類——普通全局變量靜態全局變量

普通全局變量定義形式如下:

<code>數據類型 變量名;或數據類型 變量名 = 初始值;/<code>

而靜態全局變量的定義形式為:

<code>static 數據類型 變量名;或static 數據類型 變量名 = 初始值;/<code>

與局部變量中自動變量和靜態變量的定義一樣,但是含義完全不同的。

靜態全局變量與普通全局變量的不同在於作用域範圍。普通全局變量是作用於整個程序範圍內的,而靜態全局變量的作用域則是當前的源文件。

舉例:

<code>/*a.c*/int a = 10;static int b = 100;int main(void){  foo();}/<code>
<code>/*b.c*/#include <stdio.h>void foo(void){  printf("a:%d\\n", a);  printf("b:%d\\n", b);//這句是無法通過編譯的}/<stdio.h>/<code>

如果按照上面代碼創建兩個源文件並編譯,是<strong>無法生成可執行程序的,且會報錯。

原因有二:

1.正如我註釋所寫,b是a.c中的靜態全局變量,作用域僅在a.c,因此b.c無法訪問。

2.全局變量a雖然不是靜態全局變量,但在b.c中缺少聲明,因此無法使用。

下面我們重寫b.c,修正這兩個問題:

<code>/* b.c */#include <stdio.h>extern int a;void foo(void){  printf("a:%d\\n", a);}/<stdio.h>/<code>

這裡,去掉了b的打印,同時增加了全局變量a的聲明。

注意,這個全局變量的聲明使用了extern關鍵字。extern關鍵字用於告知編譯器,用其聲明的變量或者函數是全局作用域的,需要從可執行程序涉及到的全部源文件中尋找。

同名覆蓋

不知是否有讀者想過,如果全局變量和局部變量同名,那麼函數內的變量的值會是什麼呢?

看一個例子:

<code>#include <stdio.h>int a = 1;int main(void){  int a = 2;  printf("%d\\n", a);  return 0;}/<stdio.h>/<code>

這段代碼的執行結果是:2。

這裡存在同名覆蓋原則:同名的局部變量會覆蓋同名的全局變量

函數作用域

函數的作用域與全局變量的作用域相同,畢竟在C語言中函數內部無法再定義函數。

提供給外部其他源文件使用的函數的聲明形式如下:

<code>extern 返回值類型 函數名(參數列表...);/<code>

給本文件內使用的函數的聲明形式如下:

<code>static 返回值類型 函數名(參數列表...);/<code>

並且,函數對編譯器的可見性也取決於函數聲明的位置,例如:

<code>int main(void){  foo();  return 0;}static void foo(void);void foo(void){}/<code>

如此聲明foo函數,編譯器<strong>依舊會報錯,因為foo函數的定義對main不可見。如果將foo函數的static聲明提前到main函數前(即本例中放在第一行),則可正常編譯。

塊作用域

前面關於 的文章中並未提及一種特殊的語句——塊語句

這種語句是以大括號({})擴起的,其大括號內部可以是單條語句,也可以是多條語句。

<code>{  ...//一條或多條語句}/<code>

這並非是說C語言中看到大括號就是塊語句。函數的大括號並不屬於塊語句,其餘則皆為塊語句,包括if-else、for、while等結構中涉及大括號的部分。

我們先來看一個例子:

<code>#include <stdio.h>int main(void){  int a = 1;  {    int a = 2;    printf("In block a:%d\\n", a);  }  printf("Out of block a:%d\\n", a);  return 0;}/<stdio.h>/<code>

運行結果為:

<code>In block a:2Out of block a:1/<code>

這個例子告訴我們兩個事實:

  1. 塊內同名變量將覆蓋外層同名變量
  2. 塊內定義的變量在塊外無法訪問,即塊結構內的自動變量會隨塊結構完結而銷燬。

頭文件與源文件

之前的文章中,所涉及到的例子都是放在.c文件中的。然而C語言中並不只有.c文件。

在C語言中有兩種文件——頭文件源文件

源文件就是我們所說的文件名後綴以.c結尾的文件,其中的代碼一般都是各類函數的定義。

頭文件是文件名以.h結尾的文件。這類文件中一般記錄一些結構定義、函數聲明、變量聲明、類型定義等。關於結構體和類型定義我們後續文章會有專門說明。

什麼情況下需要頭文件呢?我們來看個例子:

<code>
<code>/*b.c*/extern void foo(void);void bar(void){  foo();}/<code>
<code>/*c.c*/void foo(void){}/<code>

可以看到,a.c和b.c都用到了c.c中的foo函數,因此它們都需要聲明foo函數。如果這時我對foo函數的返回類型做了修改,那麼我需要到聲明foo的其他源文件中修改其聲明。如果我有20個源文件中都用到了foo呢?那麼此時的修改會不會引起混亂呢?因此,頭文件就派上了用場。

我們看下修改後的代碼:

<code>/*a.c*/#include "c.h"extern void bar(void);int main(void){  foo();  bar();  return 0;}/<code>
<code>/*b.c*/#include "c.h"void bar(void){  foo();}/<code>
<code>/*c.c*/void foo(void){}/<code>
<code>/*c.h*/extern void foo(void);/<code>

如此,我們將foo的extern聲明僅寫一份放在c.h頭文件中。

然後利用預編譯的include指令,將c.h的內容引入到需要使用foo函數的a.c和b.c文件中。關於include的更詳細介紹,將在預編譯宏文章中給出。目前只需要知道,在編譯時,include會將其後緊跟的文件名所指定的文件中的內容原封不動展開(可看作複製)進使用該include指令的源文件中,且展開點就是include指令所在位置。


喜歡的小夥伴可以關注碼哥,也可以給碼哥留言評論,如有建議或者意見也歡迎私信碼哥,我會第一時間回覆。


分享到:


相關文章: