1. 程式人生 > >C 語言的變數作用域及標頭檔案

C 語言的變數作用域及標頭檔案

下面再介紹另一種分類形式:它分為程式碼塊作用域和檔案作用域。程式碼塊作用域和檔案作用域也有另一種分類方法,區域性作用域和全域性作用域。

程式碼塊作用域:程式碼塊是指一對花括號之間的程式碼,函式的形參雖然是在花括號前定義但也屬於程式碼作用域。在C99中把程式碼塊的概念擴大到包括由for迴圈、while迴圈、do while迴圈、if語句所控制的程式碼。在程式碼塊作用域中,從該變數被定義到程式碼塊末尾該變數都可見。 檔案作用域:一個在所有函式之外定義的變數具有檔案作用域。具有檔案作用域的變數從它的定義處到包含該定義的檔案結尾都是可見的。

2.連結

一個C語言變數具有下列連結之一:外部連結(external linkage),內部連結(internal linkage)或空連結(no linkage)。

空連結:具有程式碼塊作用域或者函式原型作用域的變數就具有空連結,這意味著他們是由其定義所在的程式碼塊或函式原型所私有。 內部連結:具有檔案作用域的變數可能有內部或外部連結,一個具有檔案作用域的變數前使用了static識別符號標識時,即為具有內部連結的變數。一個具有內部連結的變數可以在一個檔案的任何地方使用。 外部連結:一個具有檔案作用域的變數預設是具有外部連結的。但當起前面用static標識後即轉變為內部連結。一個具有外部連結的連結的變數可以在一個多檔案程式的任何地方使用。

例:

static int a;(在所有函式外定義)內部連結變數 int b; (在所有函式外定義) 外部連結變數 main() {

int b;//空連結,僅為main函式私有。

..

}

3.儲存時期

一個C語言變數有以下兩種儲存時期之一:(未包括動態記憶體分配malloc和free等) 靜態儲存時期(static storage duration)和自動儲存時期(automatic storage duration)和動態儲存時期。 靜態儲存時期:如果一個變數具有靜態儲存時期,他在程式執行期間將一直存在。具有檔案作用域的變數具有靜態儲存時期。這裡注意一點:對於具有檔案作用域的變數,關鍵詞static表明連結型別,而不是儲存時期。一個使用了static聲明瞭的檔案作用域的變數具有內部連結,而所有的檔案作用域變數,無論他具有內部連結,是具有外部連結,都具有靜態儲存時期。

自動儲存時期:具有程式碼塊作用域的變數一般情況下具有自動儲存時期。在程式進入定義這些變數的程式碼塊時,將為這些變數分配記憶體,當退出這個程式碼塊時,分配的記憶體將被釋放。

舉例如下:

//example_1.c

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647 #include <stdio.h>  #include <stdlib.h>  voidadd(intnum);//檔案作用域,外部連結,  voidchang_sum();//檔案作用域,外部連結  intsum=1;//檔案作用域 外部連結,靜態儲存時期  intmain(intargc,char*argv[]){intnum=5;//函式作用域,空連結  add(num);printf("main num=%d/n",num);/*輸出5*///內層定義覆蓋原則,當內層程式碼塊定義了一個與外層程式碼塊相同時名字的變數時,  //執行到內層程式碼塊時,使用內層定義的變數,離開內層程式碼塊時,外層變數恢復  //此時sum為for中定義的sum,而不是全域性的sum  for(intsum=0,num=0;num<5;num++)//程式碼塊作用域,空連結,自動儲存時期  {sum+=num;printf("====================/n");printf("for num=%d/n",num);//輸出0-5  printf("for sum=%d/n",sum);//輸出0-5的疊加和  }printf("====================/n");{inti;//程式碼作用域。僅在該大括號內可見。空連結,自動儲存時期  for(i=0;i<10;i++);printf("i=%d/n",i);}//      printf("i=%d/n",i);//編譯通不過  printf("main sum=%d/n",sum);//輸出0。  printf("main num=%d/n",num);// 輸出5  chang_sum();printf("file sum=%d/n",sum);//輸出1。全域性的sum。內層定義覆蓋原則。  system("PAUSE");return0;}voidadd(intnum)//程式碼作用域  {num++;printf("add num= %d/n",num);/*輸出6*/}voidchang_sum(){sum++;printf("chang_sum = %d/n",sum);/*輸出1*/}

以上示例須在在C99標準下編譯。(gcc支援c99的方法,編譯時加入引數 –std=C99)。從上例中可以比較清楚明白程式碼作用域和檔案作用域的概念。另外注意檔案作用域不僅限於變數也包括函式。在檔案作用域中函式也是以其宣告開始到檔案結尾結束。而且當擁有檔案作用域與擁有程式碼作用域變數同名時,不會發生衝突,而是以最小作用域的變數可見。

4.儲存類修飾符(Storage Class Specifier)

有以下幾種關鍵字,可以修飾變數或函式宣告:

static,用它修飾的變數的儲存空間是靜態分配的,用它修飾的檔案作用域的變數或函式具有Internal Linkage(內部連結)。 auto,用它修飾的變數在函式呼叫時自動在棧上分配儲存空間,函式返回時自動釋放,例如上例中main函式裡的num其實就是用auto修飾的,只不過auto可以省略不寫(此處與編譯器有關,參照編譯器不同而有所變動),auto不能修飾檔案作用域的變數。

register,編譯器對於用register修飾的變數會盡可能分配一個專門的暫存器來儲存,但如果實在分配不開暫存器,編譯器就把它當auto變數處理了,register不能修飾檔案作用域的變數。現在一般編譯器的優化都做得很好了,它自己會想辦法有效地利用CPU的暫存器,所以現在register關鍵字也用得比較少了。 extern,上面講過,連結屬性是根據一個識別符號多次宣告時是不是代表同一個變數或函式來分類的,extern關鍵字就用於多次宣告同一個識別符號。

c語言使用作用域,連結和儲存時期來定義了5種儲存類:自動,暫存器,具有程式碼塊的作用域的靜態、具有外部連結的靜態,以及具有內部連結的靜態。

五種儲存類

儲存類 時期 作用域 連結 宣告方式
自動 自動 程式碼塊 程式碼塊內
暫存器 自動 程式碼塊 程式碼塊內,使用register
具有外部連結的靜態 靜態 檔案之間 外部 所有函式之外
具有內部連結的靜態 靜態 檔案之內 內部 所有函式之外使用關鍵字static
空連結的靜態 靜態 程式碼塊 程式碼塊內,使用關鍵字static

二.標頭檔案的處理和書寫

很多人對C語言中的 “檔案包含”都不陌生了,檔案包含處理在程式開發中會給我們的模組化程式設計帶來很大的好處,通過檔案包含的方法把程式中的各個功能模組聯絡起來是模組化程式設計中的一種非常有利的手段。 標頭檔案的功能:

(1)通過標頭檔案來呼叫庫功能。在很多場合,原始碼不便(或不準)向用戶公佈,只要向用戶提供標頭檔案和二進位制的庫即可。使用者只需要按照標頭檔案中的介面宣告來呼叫庫功能,而不必關心介面怎麼實現的。編譯器會從庫中提取相應的程式碼。

(2)標頭檔案能加強型別安全檢查。如果某個介面被實現或被使用時,其方式與標頭檔案中的宣告不一致,編譯器就會指出錯誤,這一簡單的規則能大大減輕程式設計師除錯、改錯的負擔。

檔案包含處理是指在一個原始檔中,通過檔案包含命令將另一個原始檔的內容全部包含在此檔案中。在原始檔編譯時,連同被包含進來的檔案一同編譯,生成目標目標檔案。怎麼寫檔案件? 怎麼包含才能避免重定義? 其實這個只要瞭解了檔案包含的基本處理方法就可以對檔案包含有一個很好的理解與應用了: 檔案包含的處理方法:

(1) 處理時間:檔案包含也是以”#”開頭來寫的(#include ), 那麼它就是寫給前處理器來看了, 也就是說檔案包含是會在編譯預處理階段進行處理的。

(2) 處理方法:在預處理階段,系統自動對#include命令進行處理,具體做法是:降包含檔案的內容複製到包含語句(#include )處,得到新的檔案,然後再對這個新的檔案進行編譯。

抓住這兩點,那麼這個就沒有什麼難的了。。。

首先,先對#include指令的作用和實際驗證一下。 #include指令是預處理指令,它僅僅是將#incluce “A.h”中的A.h的內容替換它自己所在位置,和C語言中的巨集的使用類似。而且A.h並不是說必須是.h的檔案,只要是文字檔案都可以的。下面我給出兩個例子證明一下。

例1:有以下兩個檔案,main.c和main.n

12345678910111213141516 //file1 main.c  #include<stdio.h>  #include<stdlib.h>  #include "main.n"//包含了main.n的文字檔案。  intmain(){n=2;printf("n=%d/n",n);return1;}//file2 main.n  intn;

這時我們對main.c進行編譯 gcc main.c -o main.exe(我在windows系統下),你會發現能編譯通過並打印出n的值。如果你使用預編譯引數-E,會在預編譯後的檔案中發現其原因所在。使用 gcc -E main.c -o main.cpp。開啟main.cpp後在檔案最後會有如下內容。

123456789101112 # 3 "main.c" 2  # 1 "main.n" 1  intn;# 5 "main.c" 2  intmain(){printf("n=%d/n",n);system("pause");return1;}

以上的示例應該能比較明顯解釋#include的作用,和使用方法了。但是在實際開發中,這種使用方式是嚴重的不規範行為,強烈建議不要使用。同樣下邊的例子也是一樣的建議。

例2:

(1)包含.c檔案:

123456789101112131415161718192021222324252627282930313233343536 //file1:  main.c  #include <stdio.h>  #include <stdlib.h>  #include "test.c"  intmain(intargc,char*argv[]){m=5;for(inti=0;i<5;i++){add();m++;test();}system("PAUSE");return0;}12://end of file1  //file2:test.c  staticintn;intm;intadd();voidtest(){intt_sum;printf("m = %d/n",m);printf("n = %d/n",n++);t_sum=add();printf("add = %d/n",t_sum);}intadd(){staticintsum;sum++;returnsum;}//end of file2

這個例子是採用 包含.c檔案 的方法實現的。 在編譯時,直接去編譯main.c檔案,前處理器會先把test.c檔案中的內容複製到main.c中來,然後再對新的main.c進行編譯。

編譯命令:

  • gcc main.c -o main

可以看到,這裡並沒有對test.c進行編譯,但還是生成了最終的main可執行程式。

也可以通過命令來觀察一下預處理的結果, 編譯命令:

  • gcc -E main.c -o main.cpp(僅預處理)

在main.cpp檔案末尾可以看來下面一段程式碼:

12345678910111213141516171819202122232425262728293031 # 3 "main.c" 2  # 1 "test.c" 1  staticintn;//此處是test.c的內容  intm;intadd();voidtest(){intt_sum;printf("m = %d/n",m);printf("n = %d/n",n++);t_sum=add();printf("add = %d/n",t_sum);}intadd(){staticintsum;sum++;returnsum;}# 4 "main.c" 2//此處是main.c的內容  intmain(intargc,char*argv[]){m=5;for(inti=0;i<5;i++){add();m++;test();}

可見,其實就是將test.c檔案中的內容新增到了main函式之前,然後對新的檔案進行編譯,生成最終的可執行程式。
這次如果還是按照上面的方法只編譯main.c的話就會出錯,因為變數m和函式add並沒有在main.c中定義,所以編譯時需要將test.c一起編譯,

編譯命令:

  • gcc -c main.c -o main.o #編譯main.c
  • gcc -c fun.c -o fun.o #編譯fun.c
  • gcc main.o fun.o -o main #用main.o fun.o生成main

到這裡大家應該已經理解包含#include檔案和多檔案程式的本質區別了。

包含檔案僅僅是在c預編譯時進行再次整合,最終的還是合併成一個檔案編譯,生成執行檔案。

而多檔案的編譯,是多個檔案分別編譯,(也可能是在編譯時新增必須的標識),然後通過連結器將各個檔案連結後加載形成可執行檔案。

這種方式會使得我們的定義和宣告分開,不容易產生重定義。而且也利於模組化,僅通過標頭檔案來給出介面,而隱藏具體的實現。

預處理時會把標頭檔案中的內容複製到包含它的檔案中去,而複製的這些內容只是聲名,不是定義,所以它被複制再多份也不會出現”重定義”的錯誤。。。

前面說了標頭檔案的方法也是模組化程式設計中的一種非常有利的手段。把同一類功能寫到一個.c檔案中,這樣可以把他們劃為一個模組,另外再對應的寫上一

個.h檔案做它的宣告。這樣以後再使用這個模組時只需要把這兩個檔案新增進工程,同時在要使用模組內函式或變數的檔案中包含.h檔案就可以了。

舉個很實際的例子,在微控制器、ARM或其他嵌入式開發中,每一個平臺可能本身都有多種不同的硬體模組,使用時需要去寫相應的驅動程式,

這樣就可以把各個硬 件模組的驅動程式作為一個模組(比如lcd驅動對對應lcd.c和lcd.h,IIC驅動對應I2C.c和I2C.h等),當具體使用到某個模組時,

只需 要在將對應的.c和.h檔案新增進工程,並在檔案中包含對就的.h檔案即可。

根據以上的原理理解和實際中使用的一些問題及模組化的原則,對標頭檔案寫法給出以下幾點個人建議作為基礎:

(1) 按相同功能或相關性組織.c和.h檔案,同一檔案內的聚合度要高,不同檔案中的耦合度要低。介面通過.h檔案給出。

(2) 對應的.c檔案中寫變數、函式的定義,並指定連結範圍。對於變數和函式的定義時,僅本檔案使用的變數和函式,要用static限定為內部連結防止外部呼叫。

(3) 對應的.h檔案中寫變數、函式的宣告。僅宣告外部需要的函式,和必須給出變數。有時可以通過使用設定和修改變數函式宣告,來減少變數外部宣告。

(4) 如果有資料型別的宣告 和 巨集定義 ,請寫的標頭檔案(.h)中,這時也要注意模組化問題,如果資料型別僅本檔案使用則不必在寫標頭檔案中,而寫在原始檔(.c)中,會提高聚合度。減少不必要的格式外漏。

(5) 標頭檔案中一定加上#ifndef…#define….#endif之類的防止重包含的語句

(6) 標頭檔案中不要包含其他的標頭檔案,標頭檔案的互相包含使的程式組織結構和檔案組織變得混亂,同時給會造成潛在的錯誤,同時給錯誤查詢造成麻煩。如果出現,標頭檔案中型別定義需要其他標頭檔案時,將其提出來,單獨形成全域性的一個原始檔和標頭檔案。

(7)模組的.c檔案中別忘包含自己的.h檔案

以上幾點僅是個人觀點,供大家討論,如果有意見或是認為不合理或是有更合理的方式請討論指出。

補充1:

按照c語言的規則,變數和函式必須是先宣告再使用。可以多次宣告,但不可以多次定義。

補充2:變數的定義和宣告。

“宣告”僅僅是告訴編譯器某個識別符號是:變數(什麼型別)還是函式(引數和返回值是什麼)。要是在後面的程式碼中出現該識別符號,編譯器就知道如何處理。記住最重要的一點:宣告變數不會導致編譯器為這個變數分配儲存空間。 C語言專門有一個關鍵字(keyword)用於宣告變數或函式:extern。帶有extern的語句出現時,編譯器將只是認為你要告訴它某個識別符號是什麼,除此之外什麼也不會做(直接變數初始化除外)。
編譯器在什麼情況下將語句認為是定義,什麼情況下認為是宣告。這裡給出若干原則:

#1 帶有初始化的語句是定義

例如:

int a = 1; //定義

#2 帶有extern的語句是宣告(除非對變數進行初始化)

例如:

extern int a; //宣告

extern int b = 2; //定義

#3既沒有初始化又沒有extern的語句是“暫時定義”(tentative definition) C語言中,外部變數只能被(正式)定義一次:

int a = 0; int a = 0; //錯誤!重複定義

又或者:

int a = 0; double a = 0.1; //錯誤!識別符號a已經被使用

暫時定義有點特殊,因為它是暫時的,我們不妨這樣看: 暫時定義可以出現無數次,如果在連結時系統全域性空間沒有相同名字的變數定義,則暫時定義“自動升級”為(正式的)定義,這時系統會為暫時定義的變數分配儲存空間,此後,這些相同的暫時定義(加起來)仍然只算作是一個(正式)定義。

例如:

相關推薦

C 語言變數作用檔案

下面再介紹另一種分類形式:它分為程式碼塊作用域和檔案作用域。程式碼塊作用域和檔案作用域也有另一種分類方法,區域性作用域和全域性作用域。 程式碼塊作用域:程式碼塊是指一對花括號之間的程式碼,函式的形參雖然是在花括號前定義但也屬於程式碼作用域。在C99中把程式碼塊的概念擴大到包括由for迴圈、while迴圈、d

C語言如何定義,新增檔案

C語言標頭檔案的常用格式如下: #ifndef LABLE #define LABLE //程式碼部分 #endif 其中,LABLE為一個唯一的標號,命名規則跟變數的命名規則一樣。 常根據它

C語言中自帶的檔案(.h)所包含的函式

由於之前沒有好好學習過C語言,所以對其自帶標頭檔案所包含的內容總是不清楚,每次寫程式碼都是盲目的#include很多.h,現在重新整理一下,發現了不少很好的函式,以方便複習查閱。 不完全統計,C語言標

C++】C語言標準庫以及標準檔案

靜態連結庫(Static Link Library)——   Linux 下的 .a 和 Windows 下的 .lib。 ANSI C 標準共定義了 15 個頭檔案,稱為“C標準庫”,所有的編譯器都必須支援,如何正確並熟練的使用這些標準庫,可以反映出一個程式

C++】變數定義在.h檔案導致 multiple definition of 的解決方法和根本原因

說明:出現這個錯誤,請你先檢查重複定義的變數是否是定義在了.h標頭檔案中,如果是,請您耐心的看完這篇文章,他會告訴你錯誤的根本原因。 如果你很著急,不想弄清楚原因,請直接按下面的方法更改: 假設重複定

golang基礎學習---Go 語言變數作用

Go 語言變數作用域 作用域為已宣告識別符號所表示的常量、型別、變數、函式或包在原始碼中的作用範圍。 Go 語言中變數可以在三個地方宣告: 函式內定義的變數稱為區域性變數 函式外定義的變數稱為全域性變數 函式定義中的變數稱為形式引數 接下來讓我們具體瞭解區域性變數、全域性變

Go語言基礎(八)—— Go語言變數作用、Go語言陣列、Go語言指標

Go語言變數作用域 作用域為已宣告識別符號所表示的常量、型別、變數、函式或包在原始碼中的作用範圍。  Go 語言中變數可以在三個地方宣告: • 函式內定義的變數稱為區域性變數  • 函式外定義的變數稱為全域性變數  • 函式定義中的變數稱為形式引數&nb

c++ stl棧容器stack的pop(),push()等用法介紹檔案

c++ stl棧stack介紹 C++ Stack(堆疊) 是一個容器類的改編,為程式設計師提供了堆疊的全部功能,——也就是說實現了一個先進後出(FILO)的資料結構。 c++ stl棧stack的標頭檔案為:  #include <stack>  c++ st

c++ stl容器vector刪除(erase),遍歷等基本用法介紹檔案

Vectors 包含著一系列連續儲存的元素,其行為和陣列類似。訪問Vector中的任意元素或從末尾新增元素都可以在常量級時間複雜度內完成,而查詢特定值的元素所處的位置或是在Vector中插入元素則是線性時間複雜度。 &...    Vectors 包

深入c語言_作用

我們知道,C語言中變數的作用域有4種,我們不去討論函式作用域,因為涉及到goto語句。剩下的三種作用域,程式碼塊作用域,檔案作用域,原型作用域。 程式碼塊作用域:位於一堆花括號之間的所有語句是程式碼塊,在程式碼塊的開始位置宣告的識別符號的作用域就是程式碼塊作用域。從宣告開

關於字元驅動中的變數,巨集,檔案等的簡介 module_init module_exit

  ssize_t second_drv_read(struct file *file, char __user *buf, size_t size, loff_t *ppos) { } 這是一個檔案讀函式  ssize_t是signed size_t,size_t是標準C庫中定義的

C/C++筆試必須熟悉掌握的檔案系列(二)——math.h/cmath

1. 說明   “math.h”是C語言中數學函式庫,包含我們常用的一些數學計算上會使用到的函式。C++中有對應相同作用的標頭檔案“cmath”,當然C++中兩個標頭檔案都可以使用,C++向C相容。

例項理解c++中向前宣告與引用檔案的區別

使用C++程式設計,編譯時經常出現這種錯誤"error: invalid use of incomplete type ‘class xxx’",或“error: forward declaration of ‘class xxx’”. 解決這種錯誤就是用理解c++中向前宣告與引用標頭檔案的

c++:一個程式多個源/檔案

1、一個程式,一個原始檔的做法#include<iostream> #include<cstring> using namespace std; class Student { private: char Name[20

防止變數重複定義、檔案重複包含、巢狀包含

【轉自】 http://hi.baidu.com/zengzhaonong/blog/item/8a8871062d481f7f03088106.html #include檔案的一個不利之處在於一個頭檔案可能會被多次包含,為了說明這種錯誤,考慮下面的程式碼: #include "x.h"#include "

c++中兩個類的檔案互相包含編譯出錯的解決辦法

首先我們需要問一個問題是:為什麼兩個類不能互相包含標頭檔案? 所謂互相包含標頭檔案,我舉一個例子:我實現了兩個類:圖層類CLayer和符號類CSymbol,它們的大致關係是圖層裡包含有符號,符號裡定義一個相關圖層指標,具體請參考如下程式碼(注:以下程式碼僅供說明問題,不作為

C語言檔案作用寫法

標頭檔案幾個好處: 1,標頭檔案可以定義所用的函式列表,方便查閱你可以呼叫的函式; 2,標頭檔案可以定義很多巨集定義,就是一些全域性靜態變數的定義,在這樣的情況下,只要修改標頭檔案的內容,程式 就可以做相應的修改,不用親自跑到繁瑣的程式碼內去搜索。 3,標頭檔案只是宣告,不佔記憶體空間,要知道

關於C語言變數作用的個人心得

    這是本人的第一篇部落格,內容簡單總結淺陋。但這會是我寫部落格的開始,好啦!廢話不多說。。。。     學過C語言的同學可能都知道,在C中變數都具有作用域的說法。以下是標準的解釋和案例:    &

C++區域性變數、全域性變數作用範圍&動態記憶體管理

本文主要介紹C++中的區域性變數、全域性變數、以及動態記憶體管理的變數。本文不涉及靜態變數static,所以描述的觀點不包括static變數。 區域性變數和全域性變數 區域性變數一般指函式內部的區域性變數,這部分的變數儲存在棧裡面,當函式呼叫結束,這些區域性變數就會

(轉載)C語言中常用的幾個檔案庫函式 (stdio.h ,string.h ,math.h ,stdlib.h)

不完全統計,C語言標準庫中的標頭檔案有15個之多,所以我主要介紹常用的這四個標頭檔案stdio.h ,string.h ,math.h ,stdlib.h ,以後用到其他的再做補充。下面上乾貨: 1.<stdio.h>:定義了輸入輸出函式、型別以及巨集,函式