1. 程式人生 > >(一〇九)單獨編譯(多個原始碼檔案和標頭檔案)

(一〇九)單獨編譯(多個原始碼檔案和標頭檔案)

單獨編譯的簡單原理:

C++在記憶體中儲存資料提供了多種選擇。

可以選擇資料保留在記憶體中的時間長度(儲存持續性)以及程式的哪一部分可以訪問資料(作用域和連結)等。可以使用new來動態地分配記憶體,而定位new運算子提供了這種技術的一種變種。C++名稱空間是另一種控制訪問權的方式。

通常,大型程式都由多個原始碼檔案組成,這些檔案可能共享一些資料。這樣的程式涉及到檔案的單獨編譯。

————***一〇九談的是單獨編譯***一一〇談的是儲存***——————

C語言一樣,C++也允許,甚至鼓勵程式設計師將元件函式放在獨立的檔案中。可以單獨編譯這些檔案,然後將他們連結成可執行的程式。(通常,

C++編譯器既要編譯程式,也要管理連結器)。

如果只是修改一個檔案,則可以只重新編譯該檔案,然後將它與其他檔案的編譯版本連結。這使得大程式的管理更便捷。

另外,大多數C++環境都提供了其他工具來幫助管理。例如,UNIXLinux系統都具有make程式,可以跟著程式依賴的檔案以及這些檔案的最後修改時間。

執行make時,如果它檢測到上次編譯後修改了原始檔,make將記住重新構建程式所需的步驟。大多數整合開發環境(包括Embarcadero C++ Builder、Microsoft Visual C++Apple Xcode和Freescale CodeWarrior)都在Project

選單中提供了類似的工具。

假如有這樣一個程式。他包含一個結構,有main()函式和其他兩個函式,這三個函式都使用了這樣一個結構。

如果我們要把main函式和其他兩個函式分拆到兩個檔案之中,那個這個結構怎麼辦?是放在main函式還是放在其他兩個函式所在的檔案?還有,這兩個函式的原型怎麼辦?

事實上,是把這個結構放在標頭檔案之中(也可以順便將兩個函式的原型放在同一個標頭檔案之中,原因見之後,是我自己推測的),把main()函式放在一個原始碼檔案之中,把其他兩個函式放在另一個原始碼檔案之中。這兩個原始碼檔案都包含這個標頭檔案。

程式分為三部分:

①標頭檔案:包含結構宣告、以及兩個函式原型;

②原始碼檔案a

:包含main()函式;

③原始碼檔案b:包含另外兩個函式。

這是一個非常有用的組織程式的策略。

例如,我們編寫另外一個程式時,也需要使用這些函式(指另外兩個函式),則只需要包含標頭檔案(裡面有結構和另外兩個函式的原型),並將函式檔案(指上面的原始碼檔案b)新增到專案列表或make列表之中即可。

另外,這種組織程式也與OOP方法一致。

一個檔案(標頭檔案)包含了使用者定義型別的定義;

另一個檔案包含操縱使用者定義型別的函式的程式碼。

這兩個檔案組成了一個軟體包,可以用於各種程式之中。

不要將函式定義變數宣告放在標頭檔案之中。

這樣對於簡單的檔案可能是可行的,但通常會引發問題。

例如,如果在標頭檔案之中包含一個函式定義,然後在其他兩個檔案(屬於同一個程式)中包含該標頭檔案,則同一個程式中(因為據說是程式是將多個檔案編譯後連結在一起)將包含同一個函式的兩個定義,除非函式是內聯的(why內聯就可以?),否則這將出錯。

標頭檔案中常包含的內容:

①函式原型;

②使用#defineconst定義的符號常量;

③結構宣告;

④類宣告;

⑤模板宣告;(因為不生成函式定義,只有在呼叫的時候才生成)

⑥行內函數。

原因:

②被宣告為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

其效果是相同的。