標籤:
聲明是告訴編譯器一些資訊,以協助編譯器進行文法分析,避免編譯器報錯。而定義是告訴編譯器產生一些代碼,並且這些代碼將由連接器使用。即:聲明是給編譯器用的,定義是給連接器用的。這個說明顯得很模糊,為什麼非要弄個聲明和定義在這攪和?那都是因為C++同意將程式拆成幾段分別書寫在不同檔案中以及上面提到的編譯器只從上朝下編譯且對每個檔案僅編譯一次。
編譯器編譯器時,只會一個一個源檔案編譯,並分別產生相應的中間檔案(對VC就是.obj檔案),然後再由連接器統一將所有的中間檔案串連形成一個可執行檔。問題就是編譯器在編譯a.cpp檔案時,發現定義語句而定義了變數a和b,但在編譯b.cpp時,發現使用a和b的代碼,如a++;,則編譯器將報錯。為什嗎?如果不報錯,說因為a.cpp中已經定義了,那麼先編譯b.cpp再編譯a.cpp將如何?如果源檔案的編譯順序是特定的,將大大降低編譯的靈活性,因此C++也就規定:編譯a.cpp時定義的所有東西(變數、函數等)在編譯b.cpp時將全部不算數,就和沒編譯過a.cpp一樣。那麼b.cpp要使用a.cpp中定義的變數怎麼辦?為此,C++提出了聲明這個概念。
因此變數聲明long a;就是告訴編譯器已經有這麼個變數,其名字為a,其類型為long,其對應的地址不知道,但可以先作個記號,即在後續代碼中所有用到這個變數的地方做上記號,以告知連接器在串連時,先在所有的中間檔案裡尋找是否有個叫a的變數,其地址是多少,然後再修改所有作了記號的地方,將a對應的地址放進去。這樣就實現了這個檔案使用另一個檔案中定義的變數。
所以聲明long a;就是要告訴編譯器已經有這麼個變數a,因此後續代碼中用到a時,不要報錯說a未定義。函數也是如此,但是有個問題就是函式宣告和函數定義很容易區別,因為函數定義後一定接一複合陳述式,但是變數定義和變數聲明就一模一樣,那麼編譯器將如何識別變數定義和變數聲明?編譯器遇到long a;時,統一將其認為是變數定義,為了能標識變數聲明,可藉助C++提出的修飾符extern。
修飾符就是聲明或定義語句中使用的用以修飾此聲明或定義來向編譯器提供一定的資訊,其總是接在聲明或定義語句的前面或後面,如:
extern long a, *pA, &ra;
上面就聲明(不是定義)了三個變數a、pA和ra。因為extern表示外部的意思,因此上面就被認為是告訴編譯器有三個外部的變數,為a、pA和ra,故被認為是聲明語句,所以上面將不分配任何記憶體。同樣,對於函數,它也是一樣的:
extern void ABC( long ); 或 extern long AB( short b );
上面的extern等同於不寫,因為編譯器根據最後的“;”就可以判斷出來上面是函式宣告,而且提供的“外部”這個資訊對於函數來說沒有意義,編譯器將不予理會。extern實際還指定其後修飾的標識符的修飾方式,實際應為extern"C"或extern"C++",分別表示按照C語言風格和C++語言風格來解析聲明的標識符。
C++是強型別語言,即其要求很嚴格的類型匹配原則,進而才能實現前面說的函數重載功能。即之所以能幾個同名函數實現重載,是因為它們實際並不同名,而由各自的參數類型及個數進行了修飾而變得不同。如void ABC(), *ABC( long ), ABC( long, short );,在VC中,其各自名字將分別被變成“[email protected]@YAXXZ”、“[email protected]@[email protected]”、“[email protected]@[email protected]”。而extern long a, *pA, &ra;聲明的三個變數的名字也發生相應的變化,分別為“[email protected]@3JA”、“[email protected]@3PAJA”、“[email protected]@3AAJA”。上面稱作C++語言風格的標識符修飾(不同的編譯器修飾格式可能不同),而C語言風格的標識符修飾就只是簡單的在標識符前加上“_”即可(不同的編譯器的C風格修飾一定相同)。如:extern"C" long a, *pA, &ra;就變成_a、_pA、_ra。而上面的extern"C" void ABC(), *ABC( long ), ABC( long, short );將報錯,因為使用C風格,都只是在函數名前加一底線,則將產生3個相同的符號(Symbol),錯誤。
為什麼不能有相同的符號?為什麼要改變標識符?不僅因為前面的函數重載。符號和標識符不同,符號可以由任一字元組成,它是編譯器和連接器之間溝通的手段,而標識符只是在C++語言級上提供的一種標識手段。而之所以要改變一下標識符而不直接將標識符作為符號使用是因為編譯器自己內部和連接器之間還有一些資訊需要傳遞,這些資訊就需要符號來標識,由於可能使用者寫的標識符正好和編譯器內部自己用的符號相同而產生衝突,所以都要在程式員定義的標識符上面修改後再用作符號。既然符號是什麼字元都可以,那為什麼編譯器不讓自己內部定的符號使用標識符不能使用的字元,如前面VC使用的“?”,那不就行了?因為有些C/C++編譯器及連接器溝通用的符號並不是什麼字元都可以,也必須是一個標識符,所以前面的C語言風格才統一加上“_”的首碼以區分程式員定義的符號和編譯器內部的符號。即上面能使用“?”來作為符號是VC才這樣,也許其它的編譯器並不支援,但其它的編譯器一定支援加了“_”首碼的標識符。這樣可以聯合使用多方代碼,以在更大範圍上實現代碼重用,在《C++從零開始(十八)》中將對此詳細說明。
當書寫extern void ABC( long );時,是extern"C"還是extern"C++"?在VC中,如果上句代碼所在源檔案的副檔名為.cpp以表示是C++原始碼,則將解釋成後者。如果是.c,則將解釋成前者。不過在VC中還可以通過修改項目選項來改變上面的預設設定。而extern long a;也和上面是同樣的。
因此如下:
extern"C++" void ABC(), *ABC( long ), ABC( long, short );
int main(){ ABC(); }
上面第一句就告訴編譯器後續代碼可能要用到這個三個函數,叫編譯器不要報錯。假設上面程式放在一個VC項目下的a.cpp中,編譯a.cpp將不會出現任何錯誤。但當串連時,編譯器就會說符號“[email protected]@YAXXZ”沒找到,因為這個項目只包含了一個檔案,串連也就只串連相應的a.obj以及其他的一些必要庫檔案(後續文章將會說明)。連接器在它所能串連的所有對象檔案(a.obj)以及庫檔案中尋找符號“[email protected]@YAXXZ”對應的地址是什麼,不過都沒找到,故報錯。換句話說就是main函數使用了在a.cpp以外定義的函數void ABC();,但沒找到這個函數的定義。應注意,如果寫成int main() { void ( *pA ) = ABC; }依舊會報錯,因為ABC就相當於一個地址,這裡又要求計算此地址的值(即使並不使用pA),故同樣報錯。
為了消除上面的錯誤,就應該定義函數void ABC();,既可以在a.cpp中,如main函數的後面,也可以重建一個.cpp檔案,加入到項目中,在那個.cpp檔案中定義函數ABC。因此如下即可:
extern"C++" void ABC(), *ABC( long ), ABC( long, short );
int main(){ ABC(); } void ABC(){}
如果你認為自己已經瞭解了聲明和定義的區別,並且清楚了聲明的意思,那我打賭有50%的可能性你並沒有真正理解聲明的含義,這裡出於篇幅限制,將在《C++從零開始(十)》中說明聲明的真正含義,如果你是有些C/C++編程經驗的人,到時給出的範例應該有50%的可能性會令你大吃一驚。
聲明的含義
前面已經解釋過聲明是什麼意思,在此由於成員函數的定義規則這種新的定義文法,必須重新考慮聲明的意思。注意一點,前面將一個函數的定義放到main函數定義的前面就可以不用再聲明那個函數了;同樣如果定義了某個變數,就不用再聲明那個變數了。這也就是說定義語句具有聲明的功能,但上面成員函數的定義語句卻不具有聲明的功能,下面來瞭解聲明的真正意思。
聲明是要求編譯器產生映射元素的語句。所謂的映射元素,就是前面介紹過的變數及函數,都只有3欄(或3個欄位):類型欄、名字欄和地址欄(成員變數類型的這一欄就放位移值)。即編譯器每當看到聲明語句,就產生一個映射元素,並且將對應的地址欄空著,然後留下一些資訊以告訴連接器——此.obj檔案(編譯器編譯源檔案後產生的檔案,對於VC是.obj檔案)需要一些符號,將這些符號找到後再修改並完善此.obj檔案,最後串連。
回想之前說過的符號的意思,它就是一字串,用於編譯器和連接器之間的通訊。注意符號沒有類型,因為連接器只是負責尋找符號並完善(因為有些映射元素的地址欄還是空的)中間檔案(對於VC就是.obj檔案),不進行文法分析,也就沒有什麼類型。
定義是要求編譯器填充前面聲明沒有書寫的地址欄。也就是說某變數對應的地址,只有在其定義時才知道。因此實際的在棧上分配記憶體等工作都是由變數的定義完成的,所以才有聲明的變數並不分配記憶體。但應注意一個重點,定義是產生映射元素需要的地址,因此定義也就說明了它產生的是哪個映射元素的地址,而如果此時編譯器的映射表(即之前說的編譯器內部用於記錄映射元素的變數表、函數表等)中沒有那個映射元素,即還沒有相應元素的聲明出現過,那麼編譯器將報錯。
但前面唯寫一個變數或函數定義語句,它照樣正常並沒有報錯啊?實際很簡單,只需要將聲明和定義看成是一種語句,只不過是向編譯器提供的資訊不同罷了。如:void ABC( float );和void ABC( float ){},編譯器對它們相同看待。前者給出了函數的類型及類型名,因此編譯器就只填寫映射元素中的名字和類型兩欄。由於其後只接了個“;”,沒有給出此函數映射的代碼,因此編譯器無法填寫地址欄。而後者,給出了函數名、所屬類型以及映射的代碼(空的複合陳述式),因此編譯器得到了所有要填寫的資訊進而將三欄的資訊都填上了,結果就表現出定義陳述式完成了聲明的功能。
對於變數,如long a;。同上,這裡給出了類型和名字,因此編譯器填寫了類型和名字兩欄。但變數對應的是棧上的某塊記憶體的首地址,這個首地址無法從代碼上表現出來(前面函數就通過在函式宣告的後面寫複合陳述式來表現相應函數對應的代碼所在的地址),而必須由編譯器內部通過計算獲得,因此才硬性規定上面那樣的書寫算作變數的定義,而要變數的聲明就需要在前面加extern。即上面那樣將導致編譯器進行內部計算進而得出相應的地址而填寫了映射元素的所有資訊。
上面難免顯得故弄玄虛,那都是因為自訂類型的出現。考慮成員變數的定義,如:
struct ABC { long a, b; double c; };
上面給出了類型——long ABC::、long ABC::和double ABC::;給出了名字——ABC::a、ABC::b和ABC::c;給出了地址(即位移)——0、4和8,因為是結構型自訂類型,故由此語句就可以得出各成員變數的位移。上面得出三個資訊,即可以填寫映射元素的所有資訊,所以上面可以算作定義語句。對於成員函數,如下:
struct ABC { void AB( float ); };
上面給出了類型——void ( ABC:: )( float );給出了名字——ABC::AB。不過由於沒有給出地址,因此無法填寫映射元素的所有資訊,故上面是成員函數ABC::AB的聲明。按照前面說法,只要給出地址就可以了,而無需去管它是定義還是聲明,因此也就可以這樣:
struct ABC { void AB( float ){} };
上面給出類型和名字的同時,給出了地址,因此將可以完全填寫映射元素的所有資訊,是定義。上面的用法有其特殊性,後面說明。注意,如果這時再在後面寫ABC::AB的定義語句,即如下,將錯誤:
struct ABC { void AB( float ){} };
void ABC::AB( float ) {}
上面將報錯,原因很簡單,因為後者只是定義,它只提供了ABC::AB對應的地址這一個資訊,但映射元素中的地址欄已經填寫了,故編譯器將說重複定義。再單獨看成員函數的定義,它給出了類型void ( ABC:: )( float ),給出了名字ABC::AB,也給出了地址,但為什麼說它只給出了地址這一資訊?首先,名字ABC::AB是不符合標識符規則的,而類型修飾符ABC::必須通過類型定義符“{}”才能夠加上去,這在前面已多次說明。因此上面給出的資訊是:給出了一個地址,這個地址是類型為void ( ABC:: )( float ),名字為ABC::AB的映射元素的地址。結果編譯器就尋找這樣的映射元素,如果有,則填寫相應的地址欄,否則報錯,即唯寫一個void ABC::AB( float ){}是錯誤的,在其前面必須先通過類型定義符“{}”聲明相應的映射元素。這也就是前面說的定義僅僅填充地址欄,並不產生映射元素。
聲明的作用
定義的作用很明顯了,有意義的映射(名字對地址)就是它來做,但聲明有什麼用?它只是組建類型對名字,為什麼非得要類型對名字?它只是告訴編譯器不要發出錯誤說變數或函數未定義?任何東西都有其存在的意義,先看下面這段代碼。
extern"C" long ABC( long a, long b );
void main(){ long c = ABC( 10, 20 ); }
假設上面代碼在a.cpp中書寫,編譯組建檔案a.obj,沒有問題。但按照之前的說明,串連時將錯誤,因為找不到符號_ABC。因為名字_ABC對應的地址欄還空著。接著在VC中為a.cpp所在工程添加一個新的源檔案b.cpp,如下書寫代碼。
extern"C" float ABC( float a ){ return a; }
編譯並串連,現在沒任何問題了,但相信你已經看出問題了——函數ABC的聲明和定義的類型不符,卻串連成功了?
注意上面關於串連的說明,串連時沒有類型,只管符號。上面用extern"C"使得a.obj要求_ABC的符號,而b.cpp提供_ABC的符號,剩餘的就只是連接器將b.obj中_ABC對應的地址放到a.obj以完善a.obj,最後串連a.obj和b.obj。
那麼上面什麼結果,由於需要考慮函數的實現細節,這在《C++從零開始(十五)》中再說明,而這裡只要注意到一件事:編譯器即使沒有地址也依舊可以產生代碼以實現函數操作符的功能——函數調用。之所以能這樣就是因為聲明時一定必須同時給出類型和名字,因為類型告訴編譯器,當某個操作符涉及到某個映射元素時,如何產生代碼來實現這個操作符的功能。也就是說,兩個char類型的數字乘法和兩個long類型的數字乘法編譯產生的程式碼不同;對long ABC( long );的函數調用代碼和void ABC( float )的不同。即,操作符作用的數字類型的不同將導致編譯器產生的程式碼不同。
那麼上面為什麼要將ABC的定義放到b.cpp中?因為各源檔案之間的編譯是獨立的,如果放在a.cpp,編譯器就會發現已經有這麼個映射元素,但類型卻不匹配,將報錯。而放到b.cpp中,使得由連接器來完善a.obj,到時將沒有類型的存在,只管符號。下面繼續。
struct ABC { long a, b; void AB( long tem1, long tem2 ); void ABCD(); };
void main(){ ABC a; a.AB( 10, 20 ); }
由上面的說法,這裡雖然沒有給出ABC::AB的定義,但仍能編譯成功,沒有任何問題。仍假設上面代碼在a.cpp中,然後添加b.cpp,在其中書寫下面的代碼。
struct ABC { float b, a; void AB( long tem1, long tem2 ); long ABCD( float ); };
void ABC::AB( long tem1, long tem2 ){ a = tem1; b = tem2; }
這裡定義了函數ABC::AB,注意如之前所說,由於這裡的函數定義僅僅只是定義,所以必須在其前面書寫類型定義符“{}”以讓編譯器產生映射元素。但更應該注意這裡將成員變數的位置換了,這樣b就映射的是0而a映射的是4了,並且還將a、b的類型換成了float,更和a.cpp中的定義大相徑庭。但沒有任何問題,編譯串連成功,a.AB( 10,20 );執行後a.a為0X41A00000,a.b為0X41200000,而*( float* )&a.a為20,*( flaot* )&a.b為10。
為什嗎?因為編譯器只在當前編譯的那個源檔案中遵循類型匹配,而編譯另一個源檔案時,編譯其他源檔案所產生的映射元素全部無效。因此聲明將類型和名字綁定起來,而名字就代表了其所關聯的類型的地址類型的數字,而後繼代碼中所有操作這個數位操作符的編譯產生都將受這個數位類型的影響。即聲明是告訴編譯器如何產生代碼的,其不僅僅只是個文法上說明變數或函數的語句,它是不可或缺的。
還應注意上面兩個檔案中的ABC::ABCD成員函數的聲明不同,而且整個工程中(即a.cpp和b.cpp中)都沒有ABC::ABCD的定義,卻仍能編譯串連成功,因為聲明並不是告訴編譯器已經有什麼東西了,而是如何產生代碼。
標頭檔
上面已經說明,如果有個自訂類型ABC,在a.cpp、b.cpp和c.cpp中都要使用它,則必須在a.cpp、b.cpp和c.cpp中,各自使用ABC之前用類型定義符“{}”重新定義一遍這個自訂類型。如果不小心如上面那樣在a.cpp和b.cpp中寫的定義不一樣,則將產生很難尋找的錯誤。為此,C++提供了一個先行編譯指令來幫忙。
先行編譯指令就是在編譯之前執行的指令,它由先行編譯器來解釋執行。先行編譯器是另一個程式,一般情況,編譯器廠商都將其合并進了C++編譯器而只提供一個程式。在此說明先行編譯指令中的包含指令——#include,其格式為#include <檔案名稱>。應注意先行編譯指令都必須單獨佔一行,而<檔案名稱>就是一個用雙引號或角括弧括起來的檔案名稱,如:#include "abc.c"、#include "C:\abc.dsw"或#include 。它的作用很簡單,就是將引號或角括弧中書寫的檔案名稱對應的檔案以ANSI格式或MBCS格式(關於這兩個格式可參考《C++從零開始(五)》)解釋,並將內容原封不動地替換到#include所在的位置,比如下面是檔案abc的內容。
struct ABC { long a, b; void AB( long tem1, long tem2 ); };
則前面的a.cpp可改為:
#include "abc"
void main() { ABC a; a.AB( 10, 20 ); }
而b.cpp可改為:
#include "abc"
void ABC::AB( long tem1, long tem2 ){ a = tem1; b = tem2; }
這時,就不會出現類似上面那樣在b.cpp中將自訂類型ABC的定義寫錯了而導致錯誤的結果(a.a為0X41A00000,a.b為0X41200000),進而a.AB( 10, 20 );執行後,a.a為10,a.b為20。
注意這裡使用的是雙引號來括住檔案名稱的,它表示當括住的只是一個檔案名稱或相對路徑而沒有給出全路徑時,如上面的abc,則先搜尋此時被編譯的源檔案所在的目錄,然後搜尋編譯器自定的包含目錄(如:C:\Program Files\Microsoft Visual Studio .NET 2003\Vc7\include等),裡面一般都放著編譯器內建的SDK的標頭檔(關於SDK,將在《C++從零開始(十八)》中說明),如果仍沒有找到,則報錯(注意,一般編譯器都提供了一些選項以使得除了上述的目錄外,還可以再搜尋指定的目錄,不同的編譯器設定方式不同,在此不表)。
如果是用角括弧括起來,則表示先搜尋編譯器自定的包含目錄,再源檔案所在目錄。為什麼要不同?只是為了防止自己起的檔案名稱正好和編譯器的包含目錄下的檔案重名而發生衝突,因為一旦找到檔案,將不再搜尋後繼目錄。
所以,一般的C++代碼中,如果要用到某個自訂類型,都將那個自訂類型的定義分別裝在兩個檔案中,對於上面結構ABC,則應該產生兩個檔案,分別為ABC.h和ABC.cpp,其中的ABC.h被稱作標頭檔,而ABC.cpp則稱作源檔案。標頭檔裡放的是聲明,而源檔案中放的是定義,則ABC.h的內容就和前面的abc一樣,而ABC.cpp的內容就和b.cpp一樣。然後每當工程中某個源檔案裡要使用結構ABC時,就在那個源檔案的開頭包含ABC.h,這樣就相當於將結構ABC的所有相關聲明都帶進了那個檔案的編譯,比如前面的a.cpp就通過在開頭包含abc以聲明了結構ABC。
為什麼還要產生一個ABC.cpp?如果將ABC::AB的定義語句也放到ABC.h中,則a.cpp要使用ABC,c.cpp也要使用ABC,所以a.cpp包含ABC.h,由於裡面的ABC::AB的定義,產生一個符號[email protected]@@[email protected](對於VC);同樣c.cpp的編譯也要產生這個符號,然後串連時,由於出現兩個相同的符號,連接器無法確定使用哪一個,報錯。因此專門定義一個ABC.cpp,將函數ABC::AB的定義放到ABC.obj中,這樣將只有一個符號產生,串連時也就不再報錯。
注意上面的struct ABC { void AB( float ){} };。如果將這個放在ABC.h中,由於在類型定義符中就已經將函數ABC::AB的定義給出,則將會同上,出現兩個相同的符號,然後串連失敗。為了避開這個問題,C++規定如上在類型定義符中直接書寫函數定義而定義的函數是inline函數,出於篇幅,下篇介紹。
C++函式宣告和定義