(一〇九)單獨編譯(多個原始碼檔案和標頭檔案)
單獨編譯的簡單原理:
C++在記憶體中儲存資料提供了多種選擇。
可以選擇資料保留在記憶體中的時間長度(儲存持續性)以及程式的哪一部分可以訪問資料(作用域和連結)等。可以使用new來動態地分配記憶體,而定位new運算子提供了這種技術的一種變種。C++名稱空間是另一種控制訪問權的方式。
通常,大型程式都由多個原始碼檔案組成,這些檔案可能共享一些資料。這樣的程式涉及到檔案的單獨編譯。
————***一〇九談的是單獨編譯***一一〇談的是儲存***——————
和C語言一樣,C++也允許,甚至鼓勵程式設計師將元件函式放在獨立的檔案中。可以單獨編譯這些檔案,然後將他們連結成可執行的程式。(通常,
如果只是修改一個檔案,則可以只重新編譯該檔案,然後將它與其他檔案的編譯版本連結。這使得大程式的管理更便捷。
另外,大多數C++環境都提供了其他工具來幫助管理。例如,UNIX和Linux系統都具有make程式,可以跟著程式依賴的檔案以及這些檔案的最後修改時間。
執行make時,如果它檢測到上次編譯後修改了原始檔,make將記住重新構建程式所需的步驟。大多數整合開發環境(包括Embarcadero C++ Builder、Microsoft Visual C++、Apple Xcode和Freescale CodeWarrior)都在Project
假如有這樣一個程式。他包含一個結構,有main()函式和其他兩個函式,這三個函式都使用了這樣一個結構。
如果我們要把main函式和其他兩個函式分拆到兩個檔案之中,那個這個結構怎麼辦?是放在main函式還是放在其他兩個函式所在的檔案?還有,這兩個函式的原型怎麼辦?
事實上,是把這個結構放在標頭檔案之中(也可以順便將兩個函式的原型放在同一個標頭檔案之中,原因見之後,是我自己推測的),把main()函式放在一個原始碼檔案之中,把其他兩個函式放在另一個原始碼檔案之中。這兩個原始碼檔案都包含這個標頭檔案。
程式分為三部分:
①標頭檔案:包含結構宣告、以及兩個函式原型;
②原始碼檔案a
③原始碼檔案b:包含另外兩個函式。
這是一個非常有用的組織程式的策略。
例如,我們編寫另外一個程式時,也需要使用這些函式(指另外兩個函式),則只需要包含標頭檔案(裡面有結構和另外兩個函式的原型),並將函式檔案(指上面的原始碼檔案b)新增到專案列表或make列表之中即可。
另外,這種組織程式也與OOP方法一致。
一個檔案(標頭檔案)包含了使用者定義型別的定義;
另一個檔案包含操縱使用者定義型別的函式的程式碼。
這兩個檔案組成了一個軟體包,可以用於各種程式之中。
請不要將函式定義或變數宣告放在標頭檔案之中。
這樣對於簡單的檔案可能是可行的,但通常會引發問題。
例如,如果在標頭檔案之中包含一個函式定義,然後在其他兩個檔案(屬於同一個程式)中包含該標頭檔案,則同一個程式中(因為據說是程式是將多個檔案編譯後連結在一起)將包含同一個函式的兩個定義,除非函式是內聯的(why內聯就可以?),否則這將出錯。
標頭檔案中常包含的內容:
①函式原型;
②使用#define或const定義的符號常量;
③結構宣告;
④類宣告;
⑤模板宣告;(因為不生成函式定義,只有在呼叫的時候才生成)
⑥行內函數。
原因:
②被宣告為const的資料和行內函數有特殊的連結屬性,(不清楚)
③結構宣告:因為不建立變數,只是在原始碼檔案中宣告結構變數時,告訴編譯器如何建立該結構變數。
⑤模板宣告:類似結構宣告,注意,模板並不直接建立函式定義,而是呼叫的時候建立對應的函式定義。
如果要將多個原始碼檔案和標頭檔案組合在一起,應如下做:
①首先,可以將上面所說的,標頭檔案中常包含的內容,放在一個或者多個頭檔案中;
②其他原始碼檔案需要引用標頭檔案,且不能用方括號“<>”,而應該用雙引號“""”。語法為:#include"標頭檔案名"
例如,標頭檔案名為aaa.h,那麼其他原始碼檔案在需要引用這個標頭檔案時,應加入:
#include"aaa.h"
且每個需要引用標頭檔案內容的原始碼檔案,都需要有這樣一行程式碼。
③標頭檔案中如果要包含其他標頭檔案的內容,例如在結構宣告中需要string類,就如同普通原始檔那樣,新增#include<string>,然後需要記得string之前要加std::
例如:
#include<string>
struct abc
{
std::string c;
};
這樣才可以正常使用string類。
④標頭檔案中一般要包含函式原型。
這樣的話,凡是呼叫這個標頭檔案的cpp檔案(原始碼檔案)都可以省略函式原型,否則還要像正常函式呼叫那樣在main函式之前加入函式原型。
⑤在同一個原始碼檔案,若要包含一個頭檔案,那麼通常只會包含一次。
例如如果需要使用string類,那麼在這個原始檔內,我們會加入#include<string>
毫無疑問,我們不會將這行程式碼在同一個cpp檔案裡輸入兩遍。
標頭檔案可能被呼叫多次的問題:
但若我們在一個頭檔案裡引用另外一個頭檔案,就有可能涉及到將同一個標頭檔案引用多次的問題。例如:
(1)我們在標頭檔案m1.h裡面建立了一個結構宣告,然後標頭檔案m2.h裡放了一個函式原型;標頭檔案m3.h放了另外一個函式原型;
(2)由於m2.h和m3.h都需要使用這個結構,於是在兩個函式都引用了標頭檔案m1.h。即,加入了#include"m1.h"
(3)然後我們在原始碼檔案a.cpp裡面,引用了標頭檔案m2.h和m3.h(因為要使用它們提供的原型);即:
#include"nn.h"
#include"nn2.h"
(4)假如我們沒在m1.h裡面新增防多次引用的程式碼。那麼這個時候,m2.h裡面和m3.h裡面都包含了一個結構宣告(因為他們都包含了m1.h),編譯器會提示錯誤(因為它遇見了兩次結構宣告)。
防止標頭檔案被呼叫多次:
因此,我們需要一個解決辦法,方法有兩種:
(1)在標頭檔案的開始,加入#pragma once,那麼這個標頭檔案,在編譯時,由編譯器保證這個標頭檔案不會被拷貝多次;
(2)在標頭檔案的開始,加入
#ifndef 巨集名字
#define 巨集名字
..... //一堆宣告程式碼
#endif
例如:
#ifndef aa_bb_
#define aa_bb_
struct abc
{
int a;
};
#endif
第二種方法的原理是,#ifndef(if no define如果沒有巨集xxx的意思)aa_bb_,在編譯器第一次遇見這行命令的時候,編譯器發現自己沒有遇見過這個巨集名字(因為之間沒有定義aa_bb_);
於是向下執行,遇見#define aa_bb_ ,這個時候,編譯器知道巨集定義了aa_bb_(這樣的話,下次遇見定義的這個名字,#ifndef aa_bb_將不會執行一直到#endif)之間的程式碼;
因為是第一次遇見,所以在這裡,結構宣告被編譯器編譯了(且記住下次遇見就不會被編譯)。
這樣的話,第一次遇見,於是編譯結構abc,下一次遇見這幾行程式碼(比如說又包含了這個標頭檔案),這裡的結構宣告,便被編譯器自動跳過了。
⑥多個原始碼檔案,應如正常那樣包含標頭檔案,呼叫名稱空間等。
只不過包含標頭檔案的話,就可以理解為將標頭檔案的內容放在了原始碼檔案之前。即如果標頭檔案裡有一個函式原型,原始碼檔案包含這個標頭檔案,就可以視為原始碼檔案中的程式碼,有這個函式原型。
但若一個原始碼檔案中包含名稱空間(假如使用了using namespace std;),那麼並不會讓另一個原始碼檔案視為可以節約了這行程式碼。
多個檔案程式碼如下:
//標頭檔案mm.h
#pragma once //由編譯器提供保證:同一個檔案不會被包含多次。注意這裡所說的“同一個檔案”是指物理上的一個檔案,而不是指內容相同的兩個檔案。帶來的好處是,你不必再費勁想個巨集名了,當然也就不會出現巨集名碰撞引發的奇怪問題。對應的缺點就是如果某個標頭檔案有多份拷貝,本方法不能保證他們不被重複包含。當然,相比巨集名碰撞引發的“找不到宣告”的問題,重複包含更容易被發現並修正。
//#ifndef COORDIN_H_
//#define COORDIN_H_
//... ... // 一些宣告語句
//#endif
// #ifndef的方式依賴於巨集名字不能衝突,這不光可以保證同一個檔案不會被包含多次,也能保證內容完全相同的兩個檔案不會被不小心同時包含。當然,缺點就是如果不同標頭檔案的巨集名不小心“撞車”,可能就會導致標頭檔案明明存在,編譯器卻硬說找不到宣告的狀況
#include<string>
struct abc
{
int a;
int b;
std::string c;
};
//標頭檔案nn.h
#pragma once
#include"mm.h"
void plus1(abc&);
//標頭檔案nn2.h
#pragma once
#include"mm.h"
void set(abc&);
void show(abc);
//原始碼檔案:源.cpp
#include<iostream>
#include"nn.h"
#include"nn2.h"
using namespace std;
int main()
{
abc a;
string c = "abb";
set(a);
show(a);
plus1(a);
show(a);
system("pause");
return 0;
}
//原始碼檔案:源1.cpp
#include<iostream>
#include"nn.h"
#include"nn2.h"
void set(abc &a)
{
a.a = 5;
a.b = 3;
a.c = "abb";
}
void show(abc a)
{
using namespace std; //雖然源.cpp包含了using namespace std,但這個函式使用cout和cin,依然要加入這行程式碼,或者加入using std::cout和using std::cin
cout << a.a << endl;
cout << a.b << endl;
cout << a.c << endl;
}
void plus1(abc &a)
{
a.a++;
a.b++;
a.c += a.c;
}
輸出:
5
3
abb
6
4
abbabb
請按任意鍵繼續. . .
標頭檔案mm.h的程式碼,也可以修改為:
#ifndef aa_bb_
#define aa_bb_
#include<string>
struct abc
{
int a;
int b;
std::string c;
};
#endif
其效果是相同的。