C語言標準頭的使用

2020-07-16 10:04:28
每個標準庫函數都會被宣告在一個或多個標準頭(standard header)中。這些標準頭也包括了 C 語言標準提供的所有宏和型別的定義。

每個標準頭都包含一組相關的函數宣告、宏和型別定義。例如,數學函數宣告在標頭檔案 math.h 中。標準頭也稱之為標頭檔案(header file),因為每個標準頭內容通常被儲存在一個檔案中。然而,嚴格來說,C 語言標準並沒有強制要求將標準頭組織成檔案。

C 標準定義了以下 29 個標頭檔案(其中有星號標識的是 C11 新增的):

assert.h inttypes.h signal.h stdint.h
threads.h* complex.h iso646.h stdalign.h*
stdio.h time.h ctype.h limits.h
stdarg.h stdlib.h uchar.h* errno.h
locale.h stdatomic.h* stdnoreturn.h* wchar.h
fenv.h math.h stdbool.h string.h
wctype.h float.h setjmp.h  stddef.h
gmath.h      

標頭檔案 complex.h、stdatomic.h 和 threads.h 是可選的。有一些 C11 實現版本可以定義一些標準宏,以指示該版本不包括前述可選的標頭檔案。這裡,如果宏 __STDC_NO_COMPLEX__、宏 __STDC_NO_ATOMICS__,或宏 __STDC_NO_THREADS__ 被定義為值 1,則該實現版本就沒有包括這些宏所對應的可選標頭檔案。

通過 #include 命令,可以把一個標準標頭檔案內容插入到一個原始碼檔案中。其中 #include 命令必須放在所有函數的外面。你可以以任意順序包含多個標準標頭檔案。然而,在針對某個標頭檔案使用 #include 命令之前,程式不可以定義任何與該標頭檔案內識別符號相同的宏名稱。為了確保程式符合該條件,總是在原始碼開始的地方,首先包含所需的標準標頭檔案,然後再包含自己定義的標頭檔案。

執行環境

C 程式只會在兩種執行環境中執行:宿主(hosted)環境或獨立(freestanding)環境。大多數程式在宿主環境中執行:也就是說,在作業系統的控制和支援之下執行。在宿主環境中,可以使用標準庫的全部功能。而且,為宿主環境編譯的程式必須定義 main()函數,它是程式啟動後第一個執行的函數。

為獨立環境設計的程式,執行時不會獲得作業系統的支援。在獨立環境中,由所使用的實現版本自身決定程式啟動後第一個執行的函數是什麼。獨立環境的程式無法使用複數浮點型別,並且只能使用下面的標頭檔案:

float.h stdalign.h stddef.h
iso646.h stdarg.h stdint.h
limits.h stdbool.h stdnoreturn.h

特定的實現版本可能還提供另外的標準庫資源。

函數和宏的呼叫

所有的標準函數都有外部連結。因此,你可以通過在自己的程式中宣告標準庫函數來使用它們,而不需要包含對應的標頭檔案。然而,如果標準函數需要標頭檔案中定義的某個型別,那麼必須包含對應的標頭檔案。

標準庫的函數不一定確保可重入(reentrant)。也就是說,在一個進程中兩次呼叫同一個函數並行執行可能是不安全的行為。之所以制定該規則,其中一個原因是:部分標準函數會使用和修改同一個靜態變數或執行緒變數。

因此,你不能在信號處理進程(signal handling rountine)中呼叫標準庫函數。信號是非同步的,也就是說,程式可能在任何時候收到信號,甚至是正在執行標準庫函數時。當發生這種情況時,如果信號處理器再呼叫的標準函數與正在執行的是同一個,那麼該函數則必須是可重入的。由各個實現版本自行決定哪些函數可重入,或者是否需要對所有標準庫提供可重入版本。

大部分標準庫函數(除了一些顯式指定的函數)都是執行緒安全的(thread-safe),意思是它們可以“同時”被幾個執行緒安全地執行。換句話說,標準函數必須這樣實現:當多個執行緒呼叫它們時,所有它們使用的內部物件都不會造成資料競爭。尤其是它們在不能確保同步性的前提下,不得使用靜態物件。然而,作為程式設計師,需要協調好不同執行緒對函數引數直接或間接參照物件的存取。

在執行操作前,每個流都具有一個相應的鎖,I/O 連結庫中的函數使用該鎖以獲得對這個流的獨占存取許可權。在這種方式下,當幾個執行緒存取同一個給定的流時,標準庫函數防止了資料競爭。

程式設計師要保障呼叫函數和類函數宏時,傳入有效的引數。錯誤的引數會造成嚴重的執行錯誤。需要避免的典型錯誤包括:

(1) 引數值超出函數的值域,如下例所示:
double x = -1.0, y = sqrt(x)

(2) 指標引數沒有指向一個物件或函數,相當於使用一個未經初始化的指標引數進行函數呼叫,如下例所示:
char *msg;  strcpy( msg, "eror" );

(3) 引數型別不符合可選引數函數的型別要求。在下面的範例中,轉換修飾符%f呼叫時需要一個浮點型指標引數,但 &x 是一個 double 指標:
double x;   canf( "%f", &x );

(4) 陣列地址引數所指向的陣列不夠大,不足以容納該函數所要寫入的資料。如下例所示:
char name[] = "Hi "; strcat( name, "Alice" );

標準庫中的宏充分利用了括號,所以可以像使用一般識別符號一樣在表示式中使用這些標準宏。而且,標準庫中每個類函數宏都只會使用其引數一次。這意味著,可以像呼叫普通函數一樣呼叫這些宏,即便把具有副作用的表示式作為這些類函數宏的引數也可以。如下例所示:
int c = 'A';
while ( c <= 'Z' ) putchar( c++ );           // 輸出:'ABC… XYZ'

標準庫函數可能同時以宏和函數方式實現。如果這樣的話,對於一個給定的函數名,同一個標頭檔案中會包含一個函數原型和一個宏定義。因此,在包含該標頭檔案之後,每次使用該函數名都會呼叫宏。下面的例子呼叫宏或函數 toupper()把小寫字母轉換為大寫字母:
#include <ctype.h>
/* ... */
  c = toupper(c);                       // 呼叫宏toupper(),如果存在的話

然而,如果指定需要呼叫一個函數,而不是同名的宏,可以利用 #undef 命令取消宏定義:
#include <ctype.h>
#undef toupper                  // 移除任何同名的宏定義
/* ... */
  c = toupper(c)                        // 呼叫函數toupper()

把名稱放在括號內,也可以呼叫函數而非宏:
#include <ctype.h>
/* ... */
  c = (toupper)(c)                      // 呼叫函數toupper()

最後的一個做法,你可以忽略包含宏定義的標頭檔案,直接在原始碼檔案中明確宣告該函數:
extern int toupper(int);
/* ... */
  c = toupper(c)                        // 呼叫函數toupper()

保留的識別符號

在程式中選擇識別符號使用時應特別注意,必須知道哪些識別符號被保留給標準庫使用。保留的識別符號包括:

(1) 所有以下劃線開始後面接著第二個下劃線或者大寫字母的識別符號,均被保留。因此不能使用諸如 _x 或 _Max 形式的識別符號,甚至不能作為區域性變數或標籤。

(2) 以下劃線開始,但不符合上一點的所有其他識別符號,都被保留為檔案。因此,不能使用諸如 _a_ 形式識別符號作為函數名或全域性變數名,但是可以作為引數、區域性變數和標籤名稱。結構成員和聯合成員也可以使用下劃線開頭的名稱作為識別符號,但第二個字元不可以是下劃線或大寫字母。

(3) 在標準標頭檔案中被宣告為外部連結的識別符號,被保留為外部連結識別符號。這類識別符號包括函數名以及全域性變數名,例如 errno。雖然無法把這些外部連結識別符號宣告為自己的函數或物件名稱,但是可以用於其他目的。例如,在一個沒有包含 string.h 的原始檔內,可以定義一個名為 strcpy()的靜態函數。

(4) 在所包含的標頭檔案中定義的所有宏識別符號,都是被保留的。

(5) 在標準標頭檔案中被宣告為檔案的識別符號,在它們自身名稱空間範圍內,是被保留的。一旦在原始檔中包含一個標頭檔案,在同一個名稱空間中,不能將在該標頭檔案中宣告為檔案的識別符號用作其他目的,或作為宏名稱。

雖然這裡所列出的一些條件有“漏洞”,允許在某些名稱空間或配合靜態連結重複使用某些識別符號,但是識別符號過多重用很容易造成混淆,通常最安全的方式是完全規避標準標頭檔案中的識別符號。

在下面的各節中,為了未來 C 標準的擴充,我們也會列出一些被保留的識別符號。前面列表中的最後三點也適用於這些保留的識別符號。