第5章 基礎——5.3. C++項目組成

來源:互聯網
上載者:User

[回到目錄]

白話C++

 

5.3. C++項目組成

首先我們知道了,寫一個C++程式,可能需要多個源檔案,比如a.cpp、b.cpp。

有沒有可能只用一個源檔案呢?似乎是可以的,比如我們之前寫的“Hello world”經典版等項目,不就只有一個main.cpp嗎。

其實,就算是“Hello world”經典版這樣一個小程式,我們也要支付連結器同志的出場費。因為,我們在代碼中使用了std::cout,std::cout來自於C++標準庫,而C++標準庫,又可能調用了C的標準庫——“標準庫”檔案,其實也是一種“目標檔案”,通常它就是多個“目標檔案”的打包——結論是,就算我們唯寫了一個“main.cpp”,編譯器只編譯出一個“main.o”,但是,仍然需要連結器將“main.o”和一些必要的標準庫檔案進行連結。

5.3.1.專案檔

專案檔並不是必須的,直接使用g++編譯器寫命令列,就可以完成C++源檔案的編譯或連結,但我們剛提過,C++的編譯器是單個單個源檔案編譯的,非常不方便,所以,需要專案檔。

在Unix下,最流行的C++專案檔,稱作“Makefile/製作檔案”。那相當於是一種“批量處理檔案”,並不需要任何IDE。在Windows下,通常每個不同的IDE都會制定自己的專案檔格式。Code::Blocks可以使用Makefile,但預設的,也是更方便的方式是使用Code::Blocks自訂的專案檔,副檔名為:“.cbp”(Code::Blocks Project)。

〖小提示〗:工作空間:管理多重專案

除了提供專案檔以外,Code::Blocks還提供了“工作空間檔案”,用於同時管理多個相關的項目,其副檔名為:“.workspace”。

 

5.3.2.源檔案、標頭檔

作為一種籠統的說法,我們往往將程式員所寫的一切代碼檔案,都稱為“源檔案”,不過如果具體到“編譯器是單個單個源檔案進行編譯”時,這裡的“源檔案”就僅限於副檔名為“.cpp”或“.c”,或“.cxx”等檔案,而“.hpp”、“.h”、“.hxx”等檔案被稱為“標頭檔”。

從副檔名來區分有些本末倒置。我們剛剛說過,如果要代碼的某處需要用到某個函數,而該函數在代碼當前位置之前還未有定義,我們可以通過“聲明”這個函數長什麼樣子,來騙過編譯器(連結器會幫我們尋找那個函數的真實位置);那麼,假設我們“a.cpp”裡實現了100個函數,而在“b.cpp”和“c.cpp”裡都要全部用上,是否意味著我們要寫上200次“函式宣告”呢?

當然不必,再往前的課程,我們提過:“函式宣告”就像函數的名片,而“標頭檔”就是含有多個聲明的名片夾,因為,我們可以將“a.cpp”中的100個函式宣告,全都寫到一個標頭檔中(通常就叫“a.hpp”),以後在任意需要用到相關函數的源檔案裡,通過:“#include a.hpp”匯入所有全部聲明。

由於“標頭檔”不是編譯單元,所以類似像C++標準庫這樣主要以“標頭檔”形式提供的庫,我們只需要讓編譯可以找到這些標頭檔即可;而對於另外一些,直接以“源檔案”形式的擴充庫,如果我們要在某個項目中使用它,就必須將它所提供“源檔案”,最好先複製一份,然後加入專案檔,參與編譯。

在《準備》章節中,我們安裝了很多擴充庫。我們注意到,幾乎所有擴充庫,都提供一個include子目錄,通常這個目錄之下(可能還會有子目錄),會存在該擴充庫的標頭檔。比如MySQL++庫的include目錄為:E:/cpp_ex_libs/MySQL++/3.0.6/include。

〖課堂作業〗:尋找MySQL的標頭檔

請進入MySQL++中的include目錄,然後尋找“mysql++.h”檔案。

 

現在,假設以我們要用到MySQL++庫,於是要包含它的一標頭檔,名為“mysql++”,原始碼中是否寫成如下?

#include “E:/cpp_ex_libs/MySQL++/3.0.6/include/mysql++.h”

用絕對路徑不僅麻煩,而且讓原始碼變得很不通用。如果不寫絕對路徑,該如何讓編譯器找到mysql++.h這個檔案呢?

原來,編譯允許我們通過在命令列中,指定參數來告訴它,如果某個標頭檔找不到了,可以上哪裡尋找。對於g++,這個參數是“-I標頭檔路徑”,本例解決方案寫出來類似:

g++.exe –I”E:/cpp_ex_libs/MySQL++/3.0.6/include/” ……

不過,我們並不直接寫命令列,而是通過Code::Blocks調用編譯器,因此,Code::Blocks要求我在專案檔中配置該項目所需要用到的擴充庫的路徑,具體的配置方法,我們將在IDE章節詳談。

5.3.3.使用標頭檔

標頭檔就像“名片”或“名片夾”,通常包含了一些資料聲明、函式宣告、類型定義(典型的如:struct/class)等內容。一個人的名片可以分發給很多人,一個標頭檔通常也要被多個源檔案包含;再者,標頭檔之間還可以相互包含,這就帶來了一種不太好的可能性:相同標頭檔往往會在同一個項目中被重複包含。

  • 唯一包含

生活中我們不會喜歡擁有同一個人的多張重複的名片。對C++來說,重複的包含一個標頭檔,不僅造成編譯速度降低,還會帶來編譯錯誤:資料、函式宣告允許重複,但一個類型不允許重複定義。

#define可以定義一個“宏符號”,並且可以使用 #ifndef來判斷一個“符號”是否已經定義:

#define ABCD //定義一個宏符號:名為ABCD#ifdef ABCD  //判斷ABCD是否“已定義”  /*     這裡的代碼,僅當ABCD有定義     才會接受編譯,否則被直接略過  */#endif 

和本例中的 #ifdef指示符正好相反,#ifndef多出來的字母‘n’為not,它用來判斷指定的符號是否“未定義”。我們可以方便地使用以下方法,來保證一個標頭檔只被實際包含一次。假設當前標頭檔名為:“my_header.h”

001 #ifndef  _MY_HEADER_H_002 #define _MY_HEADER_H_003// 所要聲明或定義的內容,放在此處xxx #endif //_MY_HEADER_H_

我們首先假設這是一個項目中第一次包含到“my_eader.h”這個檔案:

先行編譯器在處理“myHeader.h”時,首先就碰上下面這行預先處理指令(通常是第一行):

#ifndef _MY_HEADER_H_

它判斷宏符號“_MY_HEADER_H_”是否“未定義”,因為現在是第一次包含本檔案,所以確實還沒有定義過它,於是002行立即定義這個符號。然後才是本標頭檔的實質內容(從003行,一直到xxx前一行)。

接著,我們假設某一處代碼,再次包含了這個標頭檔,但這回先行編譯器發現符號_MY_HEADER_H_ 已經定義過了,於它直接跳到xxx行(#endif)之後。

竅門顯然在於:我們應該為每一個檔案都取一個唯一的宏符號——通常也稱為:“保護符”。 要讓每一個標頭檔都“掛著”一個唯一的保護符,方法是讓這個符號的名字和標頭檔名字有一個映射關係,習慣上是:將所有字母都改成大寫,再把副檔名之前的‘.’改成‘_’,如果還想再酷一點,可以在前後分別再加一個底線。

〖小提示〗:同名標頭檔怎麼辦?

有時候,位於不同目錄下的兩個標頭檔,確實有可能同名,這時我們需要使用更長一些的保護符名稱。

 

  • 使用自訂的標頭檔

#include 接受兩種形式的標頭檔指示

#include <library_header.h>   #include "my_header.h"

通常我們對標準庫標頭檔,使用角括弧(<>)形式,對當前項目我們自己所寫的標頭檔,使用雙引號("")形式。至於第三方庫,如果我們已經在IDE中配置它的全域路徑,也可以使用角括弧的形式,典型的如wxWidgets或boost庫。

Code::Blocks提供了方便的“檔案嚮導”用於產生標頭檔或源檔案,但本節我們將學習如何“純手工”地項目添加標頭檔及源檔案。

先用嚮導建立一個控制台項目(命名為IncludeDemo1),一開始它只有一個檔案:main.cpp。

點擊主菜單“檔案”-> “建立” ->“空白檔案”(或熱鍵:Ctrl + Shift + N),出現提問框:

“是否將新檔案加入到當前項目(加入項目前,須先儲存)?”

選擇“是”,然後將檔案存為:“my_file.hpp”。由於一個項目預設會有兩個構建目標:Debug和Release版,所以接下來IDE會詢問建立的檔案要加入到哪些構建目標,請選擇全部目標。

再建立一個檔案,儲存為“my_file.cpp”,同樣加入全部目標。現在,專案檔樹如下:

圖 5-4 添加了my_file.cpp/.hpp之後的項目樹

然後,我們將——

  • 在my_file.hpp中,定義MyStruct類,聲明my_function函數;
  • 在my_file.cpp中,實現MyStruct類,實現my_function函數;
  • 在main.cpp中,使用MyStruct類,使用my_function函數。

 

請分別完成以下代碼,為了方便排除輸入代碼不小心造成的錯誤,請每完成一個檔案的內容之後,就按Ctrl + F9 進行編譯,確實編譯無誤後,再進行下一步。

第一、my_file.hpp中的代碼——聲明、定義:

#ifndef _MY_FILE_HPP_#define _MY_FILE_HPP_//定義一個類struct MyStruct{    MyStruct();    ~MyStruct();};//聲明一個函數:void my_function(int year, int month, int day);#endif //my_file.hpp

第二、my_file.cpp中的代碼——實現

#include "my_file.hpp" //包含自訂的標頭檔#include <iostream> //包含標準庫檔案using namespace std;MyStruct::MyStruct(){    cout << "MyStruct Construct." << endl;}MyStruct::~MyStruct(){    cout << "MyStruct Destruct." << endl;}void my_function(int year, int month, int day){    cout << year << '-' << month << '-' << day << endl;}

第三、main.cpp中的代碼——使用

#include <iostream>#include "my_file.hpp" //引入MyStruct和my_function using namespace std;int main(){    MyStruct myStruct;        my_function(1974, 4, 20);        return 0;}

5.3.4.庫檔案

C++擴充庫的提代方式,可以是普通源檔案,也可以是已經編譯成目標檔案的“庫檔案”。對於前者,使用起來和我們自己寫的源檔案沒有兩樣,需要加入項目,參加編譯;對於後者,只需要參加連結。連結形式上,又分成兩種方式:靜態連結庫、動態連結程式庫。之前我們在《準備》章節已經介紹過二者的區別。

  • 靜態連結庫

在“構建期”(也經常籠統地稱為“編譯期”)完成連結。即,當源檔案編譯完成之後,庫檔案和其它中間檔案統一參加連結。因此,庫檔案也被合并到可執行檔(程式)。

靜態連結庫的副檔名,通常是“.lib”或“.a”。

  • 動態連結程式庫

動態連結程式庫的副檔名,通常是“.dll”或“.so”、“.o”。

這類庫採用特定技術,允許在程式運行時,才將庫與程式在記憶體中實現合并。合并所完成的主要任務,是“定位”。比如在主程式k.exe中,需要用到動態庫m.dll中的一個簽名為“void foo()”的函數,自然的,k.exe就需要知道“foo()”函數在m.dll中“地址”。由於程式(包括動態庫)運行時,需要載入到記憶體中,因此這個地址,是一個“記憶體位址”。

如何定位函數(或其它記憶體對象,下面僅以函數為例)在動態庫中的地址,又分為兩種方法:

第一、自動匯入

對於C++編程,當我們寫一個複雜的程式,往往會將程式分成一個主專案(用於產生可執行檔)和好些子項目(用於產生動態庫)。此時常用的方法是由可執行程式在啟動時自動載入所需要的動態庫,並完成定址。

程式如何知道需要載入哪些函數?又如何知道這些函數的地址?這就需要第三種庫出現:“匯入庫/import library”(全稱符號匯入庫)。“匯入庫”儲存了某一動態庫中全部(需要匯出的)函數等符號的位移地址。

位移地址不是“記憶體位址”,它是一種各個函數在DLL檔案中的地址,而當動態庫被載入到記憶體時,整個動態庫有一個起始地址。函數記憶體位址=DLL起始地址+函數位移地址。

通常我們在編譯一個動態庫項目時,除了產生動態庫檔案以外,還會同時產生“匯入庫”。而當我們需要在一個執行檔案使用這個動態庫,並且想採用“自動匯入”的方法,則需要將“匯入庫”以“靜態連結”的方式,加入項目。對於g++,匯入庫通常以“.a”為副檔名。

C++對動態庫自動匯入實現沒有統一標準,因此,這項技術通常無法在不同編譯器之間使用。比如Borland C++ 或Visual C++編譯出來的動態庫,則g++編譯出來的可執行檔無法以自動匯入的方式調用。

第二、手工匯入

手工匯入動態庫中函數等資料,其實是C語言的一項標準。C++因為相容而獲得了這項功能。要使用這項技術,要求必須以C語言的相關標準來匯出一個函數或資料,通常被稱為“C 語言介面”。C語言介面是作業系統暴露其編程介面時的事實標準。

程式在運行時,僅當在需要時,才通過一些特定的語句,將指定動態庫載入到記憶體,再尋找到所需的函數,然後調用這個函數。用完之後,還可以從記憶體中卸載掉這個動態庫。這就是手工匯入動態庫這項技術最大的特色。

 

C++庫的形式及用法有哪些,是時候重新看一眼了:

圖 5-5 C++庫形式及使用方法

 

不管採用什麼形式,提供“標頭檔”,以免使用者自己去寫聲明,這是最基本的要求了。對於C++標準庫,及boost中的很多子庫,由於使用了“泛型”技術,所以採用的是“純標頭檔”的形式。

最後,我們一直沒有提到的,但對於庫的使用非常重要的內容是:庫的說明文檔。通常開源的庫都可以在共官方網站上找到說明文檔的連結

 

[回到目錄]

白話C++

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.