C潛規則篇之防止重定義
C程式編譯時常出現類似xxx redefinition錯誤,除了模組間的命名衝突(命名汙染及static),問題多數與標頭檔案管理有關。大型C工程的標頭檔案管理很麻煩:C原始檔往往包含很多標頭檔案,標頭檔案又包含其他標頭檔案,形成複雜的巢狀包含;C沒有嚴格限定原始檔和標頭檔案的功能邊界,二者都可以包含全域性變數和函式等實體定義。這都可能導致型別或實體定義被重複包含和展開,使編譯器丟擲重定義錯誤。
解決重定義問題分三部分,多數人只知其一而不知其二和其三:
其一,用條件編譯(標頭檔案衛士)防止標頭檔案重複包含
假設原始檔test.c中包含a.h和b.h兩個標頭檔案,而a.h
typedef struct
{
……
}TEST
預處理(見C編譯過程)後,test.c裡包含兩個struct TEST定義,編譯器就會報重定義錯誤。一個巧妙辦法是套用下面標頭檔案模板(俗稱標頭檔案衛士):
#ifndef _HDRNAME_H //_HDRNAME_H按標頭檔案的檔名取名,防止同名衝突
#define _HDRNAME_H
…… (content of header file)
#endif
當頭檔案第一次被包含,_HDRNAME_H還未define,#ifndef條件滿足,前處理器進入#ifndef和#endif之間,_HDRNAME_H被正式define,標頭檔案內容也得到處理。當再次被包含,由於_HDRNAME_H已定義,開頭的#ifndef不再滿足,標頭檔案內容被直接忽略。這樣防止因標頭檔案重複包含引起的型別重定義錯誤。這種做法基本算是C的江湖標準了。
其二,在C原始檔裡定義全域性變數與函式,不要在標頭檔案裡定義
#ifndef能防止標頭檔案重複包含導致的編譯階段型別重定義錯誤,卻無法防止標頭檔案中的全域性變數和函式定義導致的連結階段實體重定義
/************main.c************/
#include "test.h"
void main()
{
test1();
test2();
}
/********** test.h**********/
#ifndef _TEST_H_
#define _TEST_H_
char str1[] = "char1";
char str2[] = "char2";
#endif
/*********test1.c***********/
#include "test.h"
extern char str1[];
void test1()
{
printf(str1);
}
/*********test2.c************/
#include "test.h"
extern char str2[];
void test2()
{
printf(str2);
}
上面情形,有些編譯器報warn,有些可能出現str1和str2重定義error,概念不清的人可能會問:test.h已用#ifndef防止重包含,為什麼還有重定義?
這其實是另一個問題,錯誤根源在於test.h裡包含變數/函式等佔用記憶體的實體元素,而不僅僅是define/struct/union等虛型別。雖然用#ifndef防止test.h重複包含,但注意test1.c和test2.c中都包含test.h,前處理器會把test.h分別附到兩個原始檔開頭,相當於在test1.c和test2.c中重複定義了str1,str2兩個全域性變數。編譯完開始link時,linker會發現test1.obj和test2.obj中都有str1,str2兩個符號,於是報錯,這跟C命名衝突是同一情況。
解決辦法是在.c檔案中定義全域性變數,然後建一個包含所有全域性變數extern宣告的標頭檔案,其他所有使用這些變數的.c檔案中都要包含這個標頭檔案。如下:
/*****main.c*****/
#include "test.h"
char str1[] = "char1";
char str2[] = "char2";
void main()
{
test1();
test2();
}
/***** test.h*****/
#ifndef _TEST_H_
#define _TEST_H_
extern char str1[];
extern char str2[];
#endif
/*****test1.c*****/
#include "test.h"
void test1() { printf(str1); }
/*****test2.c*****/
#include "test.h"
void test2() { printf(str2); }
在標頭檔案中定義函式,錯誤現象和原因類似。因此標頭檔案中可以包含型別定義和實體宣告,不應該包含實體定義。另外,有時遺漏typedef也會導致類似重定義問題:
typedef struct{
….
}TEST_S;
如果遺漏struct前的typedef,TEST_S就變成無名結構體變數而不是原來的自定義型別,放在標頭檔案裡也會出錯。
其三,用wrapper合理使用作用域
有時原始檔要同時包含兩個有同名定義的系統或SDK標頭檔案,如同時包含的兩個第三方庫的API裡有同名的自定義型別,也會導致錯誤。因為一般不方便修改第三方SDK標頭檔案,為解決衝突,可考慮對其中一個庫用wrapper方式封裝。也就是程式設計師自己在一個單獨.c檔案中封裝一套全新API,這套API直接呼叫封裝物件lib裡的函式並一一對應。這樣原lib對應的.h只在wrapper.c檔案裡包含,而對外API的新.h檔案中就可以去掉和其他系統相沖突的定義。