Lab 2-0: Program Compilation Problem -
include
directive

   
實習目標 練習使用 Visual Studio 2010 程式發展介面 (Visual Studio 2005/2008)

了解 #include 的用途, 分辨 compiler 的錯誤和 linker 的錯誤
   
步驟一 #include <stdio.h> 或是 #include "io.h" 究 竟做了什麼事?

不好意思還是要囉唆一下,也許你心裡頭正想著「引入指令 #include、標頭檔案 *.h,這麼簡單的東西我高中就看過了,還要你講,還作為實習的一部分,我是資工系的耶,你以為我混假的,早就會用了,太瞧不起人了吧,跳過跳過,不看也罷!」,你其實不知道很多同學到了大二大三、甚至畢業時都還自己以為已經會用了,弄出很多自己不知道該怎麼解釋的錯誤,還怪編譯器不好、不穩定哩。這是一個你很想跳過去但是不該這麼做的單元,忍耐一下吧,一點一點看下去一定有收穫的。(我不像有些老師會用分數來威脅利誘,這樣子你只會建立一個被動學習的心態,長久來說對你是很不好的,扼殺你的求知慾望,以後也會讓你在不知不覺中被共同開發的同事和老闆討厭)

首先我們知道 C/C++ compiler 在編譯之前會先進行前處理 (preprocessing) 的動作, 做完了以後才開始正式編譯

#include, #define, #if, #else, #endif, #ifndef, #pragma, #line 這些就是所謂的前處理器指令 (preprocessor directives)

#include 是把所指定的檔案合併到目前要編譯的檔案中一起編譯

在 Visual Studio 中你可以用下列的設定看到前處理器真的把所 include 的檔案合在一起

首先請由選單 "檔案/新增/專案" 新增一個 testInc 專案,

為什麼希望你暫時不要勾選上面那個 "為方案建立目錄", 原因是這樣的, 本來 Visual Studio 的方案 (solution) 裡可以同時包含很多個專案 (project), 例如可能你有做一個子系統是用 C# 寫的, 另外一個子系統是用 VB 做的, 你當然不希望這些功能不同的子系統通通放在一個資料匣裡面, 所以 Visual Studio 預設替每一個專案建立一個目錄 (資料匣), 我們這個課程到相當後面的時候你才會寫到需要有所區隔的程式, 所以暫時請不要勾選, 那麼照理說勾選了也無妨才對, 可是勾選了以後, 所有的程式檔會放在專案資料匣下面的一個方案資料匣裡面, 這也不是問題, 討厭的是如果你的程式需要讀取資料檔案, 你的資料檔案需要拷貝到和程式檔案相同的那個專案資料匣裡才能夠順利讀取, 如果你要在檔案總管裡面直接點選執行程式來執行的話, 你又需要把資料檔案放到和執行程式相同的資料匣才能夠執行, 否則你就需要給檔案的絕對路徑。

專案建立後, 請加入新項目

選擇 程式碼 / C++ 檔 / main.cpp

在編輯視窗內鍵入下圖 main.cpp 程式內容

請在方案總管中點選原始程式檔, 由選單 "專案/testInc 屬性(P)" 開啟屬性視窗以進行設定 (此步驟請務必操作正確, 否則你只會看到 "專案/屬性(P)" 的選單, 而不是 "專案/testInc 屬性(P)" 選單)

選取 "C/C++" "命令列" 設定編譯器的屬性, 點選 "其他選項" 欄位, 如圖手動加入 /E, 按 確定

設定完以後, 由選單選取 "建置/編譯 (Ctrl+F7", 就可以在輸出視窗中看到下圖的輸出, 可以看到 main.cpp 第一列 引入 stdio.h,

stdio.h 共有 739 列, 然後再開始編譯 程式的第二列 void main() ...

使用 /E 只是讓你了解 #include 或是 #define 的運作方法

如果你在選單選取 "建置/建置方案" 來編譯並且想要執行這個程式的話, 會產生一個 LNK1104 的錯誤, 因為編譯器加上 /E 選項以後, 編譯器沒有產生 .obj 檔案, 所以正常撰寫程式時不能加上 /E 的編譯選項

請再將 /E 選項刪除 。

步驟二 一般來說 #include 都是引入 .h 的檔案, 在 .h 的檔案中我們只放變數的宣告, 函式的宣告, 自定資料型態的宣告 (declaration), 例如:

extern int x;

void printRoutine(int x, double y);

typedef struct
{
  int x;
  char name[100];
} MyRecordType;

不放任何變數或是函式的定義 (definition), 例如:

int x;

void printRoutine(int x, double y)
{
...
}

MyRecordType x;

.h 的檔案中我們只是告訴 compiler 說下面的程式中會使用到某一個名稱, 這個名稱是個具有什麼型態的變數, 或是這個名稱是一個需要傳入某些參數的函式, 以便 compiler 看到這個名稱時能夠檢查語法, 能夠加入適當的型態轉換程式碼

在我們寫的程式中, 每一個模組通常都包含 .h 和 .cpp 的檔案, .cpp 檔案中包含變數的定義以及函式的定義, 是模組的實作, .h 檔案的目的是提供給所有需要使用這個模組的程式來引入此模組內各種工具的定義用的。

簡單的說, 你現在一定知道 C/C++ 程式裡在使用一個變數之前一定要先定義/宣告過, 例如:


    int value;
    value = 1;
同樣的, 當你要使用任何函式之前, 也一定要先引入它的宣告檔案, 例如:
	#include <stdio.h>
	...
	printf("Hello World!\n");
或是
	#include "io.h"
	...
	printArray(data, ndata);

其中 stdio.h 檔案內就包含了 printf() 函式的宣告, io.h 檔案內也應該要包含 printArray() 函式的宣告

請注意:

在使用 #include 前處理指令引入 .h 檔案時, 一定只引入 "必要" 的檔案, 也就是沒有引入的話這個程式就無法編譯的那些檔案。這個規則有的同學是不太能理解的, 在先前的課程裡也許發現反正就引入一大堆的 .h 檔案, 程式就可以順利編譯執行了, 執行結果也沒有影響, 何必給自己找麻煩, 管它到底是不是必要呢?

不過在這個課程裡, 我們的目標開始是寫大的程式了, 我們的目標是希望程式裡各個部份是能夠儘量重複使用的, 其實這個規則是有相當影響的:

1. 引入很多檔案是會讓 每一個檔案的編譯時間都加長的 (不相信你可以引入 windows.h, 你大概不會用到裡面的定義, 但是它裡面又引入了幾十個檔案, 這會讓編譯器花很多的時間在分析這些檔案裡的語法), 如果你自己的專案裡有 30 個檔案, 每個檔案裡你有引入很多不相干的檔案, 那麼編譯時就有可能多花費好幾倍的時間, 會讓你有一種想換電腦的衝動, 對刺激經濟發展是有幫助啦...

2. 引入多餘的檔案會降低你這個檔案裡面的程式的可重用性: 因為別的模組只要一使用這個檔案裡的程式, 就和這個檔案所引入的 .h 檔案所對應的模組有關連性, 會使得模組迅速擴大, 維護的困難度就迅速增加, 甚至有時會遇見在某個根本不需要的模組裡使用相同名稱的函式或是變數, 使得你的程式裡不能使用某些名字

還有你以後進入職場以後, 如果上司或是同事發現你是隨便引入檔案的人, 第一個感覺就是很不專業: 要修理什麼東西就帶著需要的工具, 有看過修水電的師父帶著菜刀的嗎? 這麼不專業的人一定很雷, 一定在很多地方會弄不清楚, 簡單的事情複雜化...; 第二個感覺就是很偷懶: 可以整理一下的都不整理, 設計軟體這種東西就是不能偷懶的, 一個人偷懶, 就一定有十個人會需要替他擦屁股, 一定會有地方為了自己的方便而犧牲同事....

真心建議...別這樣做, 養成好習慣

 

步驟三 請看下列程式 , 製作一個專案, 新增下列兩個 .cpp 的檔案, 測試一下會有什麼錯誤?

檔案 printArray.cpp


#include <stdio.h>

void printArray(int data[], int numData)
{
    int i;

    for (i=0; i<numData; i++)
        printf("%d ", data[i]);
}
檔案 main.cpp

#include "printArray.cpp"

void main()
{
    int dataArray[] = {5, 4, 3, 2, 1};

    printArray(dataArray, 5);
}

1. 分別選擇 "建置/編譯" 這兩個檔案會有錯誤嗎?

2. 合在一起 "建置/建置方案"會有什麼錯誤嗎?

3. 請注意看輸出視窗裡的訊息, 這個錯誤是 編譯器(compiler) 產生的錯誤 (語法錯誤)? 還是 連結器(linker) 產生的錯誤?

步驟四

請注意上述程式並沒有語法的錯誤 (編號 C####), 發生的錯誤是 連結器(linker) 連結時的錯誤 (編號 LNK####), linker 告訴你有函式重複定義了,

請回憶上一次實習時 .h 檔案的用法, 製作一個使用兩個檔案, 但是可以一起編譯的專案

步驟五 想一想為什麼會發生那樣的錯誤, 為什麼你的方法可以解決那樣的錯誤, Demo 時告訴助教

如果把 printArray.cpp 改成 printArray.h 就可以了嗎? (.h 檔案裡放函式的定義的話也會有同樣的錯誤嗎?)

是檔案名稱的問題嗎? 還是檔案內容的問題? .h 檔案該放什麼東西?

請依照上一次實習時 分開檔案的作法, 將適當的東西放入 .h 檔案, main.cpp 只引入使用到的 .h 檔案

步驟六 請助教檢查後, 將所完成的 專案 (只需保留 .cpp, .h, .sln 以及 .vcxproj 檔案即可; 刪除掉 .sdf, .filters, .users, debug\ 資料匣, 以及 ipch\ 資料匣下的所有內容) 以 zip/rar/7zip 程式將整個資料匣壓縮起來, 登入上傳網頁, 選擇 Lab2-0 上傳
 

簡單整理一下

標頭檔 (*.h 檔案) 裡該放些什麼東西呢?

  1. 型態定義: class, struct, typedef, extern int, ...
  2. 常數及巨集: #define
  3. 函式原型宣告: bool isEmpty(int);
  4. template 函式及類別的完整定義

每次在檔案裡定義一個全域變數時,編譯器會替它準備存放資料的記憶體,所以不能在很多檔案裡都定義同樣名稱的全域變數,也就是不能在 .h 檔案裡放變數的定義,否則被很多 .cpp 檔案引入時就變成一個全域變數定義很多次了。

型態和 template 的定義就和變數的定義不一樣了,編譯器看到一個型態的定義時,只有在編譯那個檔案時用到那個定義,也不像全域變數一樣需要替它準備存放資料的記憶體空間。

我們希望自己定義的型態在各個檔案裡是一致的,所以我們「要求」型態的定義需要放在 .h 檔案裡,如果你不這麼做的話,可能會遇見另外一個很不容易 debug 的錯誤,這個錯誤很多人都不知道,例子如下:

---- main.cpp ----
#include <iostream>
struct Data 
{
   int x, y, z;
};
void func(struct Data&);
int main()
{
    struct Data d={1,2,3};
    std::cout << d.x << ' ' << d.y << ' ' << d.z << std::endl;
    func(d);
    std::cout << d.x << ' ' << d.y << ' ' << d.z << std::endl;
    return 0;
}

---- func.cpp ----
struct Data 
{
    float x,y,z;
};
void func(struct Data &r)
{
    r.x = 11;
    r.y = 22;
    r.z = 0;
}

這是可以編譯執行的兩個檔案,執行起來結果如下:

1 2 3
1093664768 1102053376 0

不太對吧,稍微看一下程式可能你就發現問題了, 你應該會問說為什麼編譯器沒有辦法發現呢? 編譯器竟然還會很高興地幫你翻譯出執行起來一定會爆的執行碼,真是很 OOXX 啊!!

想像一下這個問題如果發生在一個多人合作、有 200 個程式檔案的專案裡,一定會讓很多人瘋掉。

編譯器其實也很為難,又被要求獨立編譯每一個檔案, 然後寫程式的人又故意在各個檔案裡放不太一致的型態定義來整它... 哈哈哈!!! 應該「突然覺得編譯器好可憐吧!!」,「當個使用者 還蠻幸福的!!」

對,別整它,結構的定義放在 .h 檔案裡面,不要在各個獨立程式檔案裡有重複的 struct 的自訂型態定義,這樣子應該可以維持自己的模組裡面型態定義的一致性。如果不希望和專案裡面其他夥伴的自訂型態有衝突,就要善用 C++ 的命名空間 namespace,每一個人自己有自己的 namespace。另外我們也可以想像到寫編譯器的人一定更可憐,哈哈!!! 所以編譯器才會排在大三下作為核心選修課程,你們有機會的話應該要修一下,才知道為什麼該建立好自己良好的習慣,對於編譯器,不要「不理它」、也不要過份「依賴它」、更不要「戲弄它」,然後看到它有跨不過去的極限以後就「不相信它」,這不是對待你的合作夥伴該有的態度,哈哈!!!

另外也應該會發現「標頭檔裡面該放哪些東西其實還蠻重要的欸??????」

 

再給你一個精神上類似,更少人知道的錯誤範例:

---- mainTemp.cpp ----
#include <iostream>
template <class T>
T func(T data)
{
    return data+1;
}
void misbehave();
int main()
{
    int d=123, result;
    result = func(d);
    std::cout << "In main(), after func(), result=" << result << std::endl;
    misbehave();
    return 0;
}

---- misbehaveTemp.cpp ----
#include <iostream>
template <class T>
T func(T data)
{
    return data-1;
}
void misbehave()
{
    int result, d=123;
    result = func(d);
    std::cout << "In misbehave(), after func(), result=" << result << std::endl;

}

當然還是可以編譯執行的兩個檔案,猜猜看結果是什麼? 執行結果如下

In main(), after func(), result=124
In misbehave(), after func(), result=124

這樣子可以? 程式有聽話嗎? 沒有吧!!

再一次想像如果這個問題發生在一個多人合作、有 200 個程式檔案的專案裡,是不是會讓很多人瘋掉?? 如果你負責的是 misbehaveTemp.cpp 你壓根兒沒有看過 mainTemp.cpp 的內容,你是不是會發現你有一個 func() 樣板函式,編譯器很高興地翻譯完,也可以執行,但是完全沒有做你要求的「減 1」的動作??!! 還幫你「加 1」勒?!! 什麼跟什麼? 這是工程嗎? 這是可以託付一生的行業嗎????

寶傑你怎麼說??? 寫這個程式的人犯了什麼錯誤?! 還是, 編譯器太爛?! 都是 M$ 的錯吧,真的該考慮轉行了嗎?

這是有經驗的團隊 可以/應該 避開的問題嗎???

如果你還沒學到 C++ 的樣板,別著急,等學到了再來看下面的說明:

會發生這個狀況的原因還是因為 C/C++ 要求各個程式檔案需要能夠獨立編譯,編譯 mainTemp.cpp 時,編譯器看到了 T func(T&) 樣板函式的定義,幫你寫出一個 int func(int) 的函式,如果你還有其它 double func(double) 函式的使用,編譯器也會幫你寫 double func(double) 函式出來,編譯器那麼聽話,真是太美妙了;同樣地在編譯 misbehaveTemp.cpp 時,編譯器也看到了 T func(T&) 樣板函式的定義,再幫你寫出一個 int func(int) 的函式,這真的是太美妙了,直到你看到剛才那個程式輸出之前,應該都是滿懷感謝、滿懷欣喜的啊!!!

因為你讓編譯器在編譯每一個檔案的時候都根據 T func(T) 樣板幫你寫了一個 int func(int) 函式,這實在像是巨集的表現,這時候連結器有點看不下去了,會幫你做一件本來也是理所當然的事情,就是只留下一個 int func(int) 函式,反正都是編譯器產生出來的,大家就呼叫同一個好了,不要讓程式碼大爆炸,放一大堆「一模一樣」的 int func(int) 函式,所以你就看到程式有上面的表現了。你也許抱怨說,怎麼編譯器不幫你檢查一下每一個 T func(T) 樣板是不是完全一樣的呢??? 可是編譯器是獨立編譯每一個 cpp 程式檔案的,所以編譯 mainTemp.cpp 時根本不知道 misbehaveTemp.cpp 裡面有不太一樣的 T func(T) 樣板啊,所以不能說是編譯器的錯啦,那就都是連結器的錯囉? 沒事何必節省程式碼呢? 所以你會希望每次編譯器根據 T func(T) 所寫的 int func(int) 一定是不一樣的函式囉? 那樣子函式內部一定不可以有狀態囉!!! 這真的有點兩難吧!!! 那麼連結器難道不能檢查樣板 T func(T) 是不是一樣的嗎? 這... 當然不行,連結器根本不會看到任何 C++ 原始程式檔案。

其實這是有「相當」經驗的 C++ 程式設計者自己要想辦法避開的問題,如果你有一個專案計畫裡所有成員都需要使用的函式樣板,當然要放在 .h 檔案裡面,而且要讓所有專案的成員都知道,樣板函式的簽章不要重複;如果你的樣板只是自己負責的檔案模組裡要用的,還是需要放在 .h 檔案裡,這樣子你自己的模組裡面不同的檔案才能夠用到一致的樣板,你應該要放在自己的 namespace 裡面,樣板函式的簽章不可以重複。如果你堅持使用共同的預設 namespace, 只是改用靜態的函式

template <class T>
static T func(T data)
{
    return data+1;
}

的話,這時候其它合作夥伴的模組就連結不到你這個檔案產生的樣板函式了,但是你自己的模組裡面多個檔案還是變成自己擁有自己的 static int func(int),函式就不能有自己的狀態了。

C++ 物件導向程式設計課程 首頁

製作日期: 03/16/2013 by 丁培毅 (Pei-yih Ting)
E-mail: pyting@mail.ntou.edu.tw TEL: 02 24622192x6615
海洋大學 電機資訊學院 資訊工程學系 Lagoon