Try   HackMD
tags: 大一程設-下 東華大學 東華大學資管系 基本程式概念 資管經驗分享

Header File / Implement File / Application File

前言

接下來的內容是學程式來說非常基本且重要的概念,「拆分檔案」,當今天程式撰寫的越來越龐大之後,不可能都只塞在一個檔案裡面,會非常難以維護,因此接下來要教基本的拆分檔案,拆分檔案有以下幾個好處。

  • 增加程式易讀性
    • 容易閱讀
  • 增加程式重用性
    • 重複使用
  • 增加程式利用率

想必學了這麼久,對於把程式碼都寫在 main 裡面大家應該已經有點感到疲倦了,尤其是在學到類別之後,程式碼真的是落落長,如果一個類別還好,兩三個類別都要塞在同一個檔案,應該會瘋掉,要一直捲滑鼠。

實際操作的影片我放在最下面,希望大家先讀過筆記
Orange

拆分檔案你需要知道的三種檔案

在 C++ 裡面,拆分檔案會把檔案分成三種,分別是。

  • Header File 標頭檔
    • 只定義不實作
  • Implement File 實作檔
    • 實作你 Header File 的內容
  • Application File 執行檔
    • 去真的把標頭檔定義的工具拿來用

接下來我們一一來介紹他,先從標頭檔開始。

拆分檔案 with function

我們把含有類別的拆分檔案放到後面,分開來講會比較清楚,不容易搞混。

Header File 標頭檔

標頭檔,副檔名為 hperson.hanimal.hdog.h 等等的。

標頭檔只會定義類別、函式,不會真的撰寫實作細節,為的是增加程式易讀性,根據開發者的程度跟需求,去看自己想要的檔案,甚麼意思呢?

有的人看到標頭檔知道有甚麼 function 可以用之後,他不在意 function 的實作細節的話,他可能不會去看 implement file,可以少一步看複雜的東西。

我們先從只在標頭檔內定義 function 開始,類別晚一點。

functionset.h

int add(int a, int b); int sub(int a, int b); int multi(int a, int b); int divi(int a, int b);

上面這個檔案,我們定義了四個 function,完全沒有實作他的細節,我們現在只知道規格長這樣,每個 function 的 return type 跟參數,先被定義好了。

非常簡單!
Orange

定義完之後必須要實作他,因此要來撰寫 Implement File,告訴我們每個 function 的細節。

Implement File 實作檔

實作檔,副檔名為 cpp,在習慣上我們會希望他與 Header File 同名,因此我們這邊把實作檔取名為 functionset.cpp,標頭檔跟實作檔名字不相同沒關係,可是這邊取同名的用意是,讓我們知道哪個 Header File 對應到哪個 Implement File,方便我們去做閱讀。

實作 Implement File 非常簡單,大家其實早就已經會了。

functionset.cpp

#include "functionset.h" int add(int a, int b){ return a+b; } int sub(int a, int b){ return a-b; } int multi(int a, int b){ return a*b; } int divi(int a, int b){ return a/b; }

這邊你可能發現第一行有個酷東西,因為我們的 Implement File 是去把 Header File 定義的內容拿來實作,所以想當然耳的我們必須把這個 Header File 給 include 進來。

這邊比較特別的地方是,平常我們 include 的時候都是 include<>,然而這邊卻是 include "",這是固定語法,當今天你要 include 自己撰寫的檔案的時候,必須用引號把他括起來,裡面填上要引入的 Header File。

而如果是系統預設好的檔案,要用 <> 包起來,像是 <cmath><string><vector>,想必大家非常明瞭。

Appliction File 執行檔

執行檔,副檔名 cpp,顧名思義就是要 run 起你程式的檔案,還記得最一開始教過大家 main 是程式的進入點,所以自然的這個檔案就是你 main 在的檔案,所以我們通常就是取名為 main.cpp

main.cpp

#include <iostream> #include "functionset.h" using namespace std; int main(int argc, char** argv) { cout << add(5,6) << endl << sub(1,2) << endl << multi(1,7) << endl << divi(6,3) << endl; return 0; }

你會發現第二行我們也 include "functionset.h",因為我們要讓程式知道我們即將使用的工具在哪裡,所以也必須把 Header File 給 include 進來。

那你可能會問,為什麼不是 #include "functionset.cpp" ?

這部分比較複雜,你需要有 C++ 的程式究竟是怎麼被 compiling 跟 linking 的知識,延伸補充都放在下面,但如果你不是很想知道,那你就記得通常我們不會特別引入 cpp 檔,都是引入標頭檔。而以過這堂課來說,先這樣記就可以了。

當然也是有 include cpp 檔的時候,但我們這邊就先不討論,以免混淆大家。

有興趣的人下面歡迎你來挑戰

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

沒有興趣的話就跳過延伸閱讀,繼續往下看吧!

[延伸補充閱讀(Optional) - 難]

這部分真的比較難,沒有興趣可以跳過,有興趣的人可以看看,其實蠻有趣的。

影片:

文章:

定義 Header File 的原因

我們不去探討電腦底層怎麼跑這些檔案,以實務面來想想看為何要定義 Header File,記得前面有說 Header File 只需要定義,不需要實作細節,這樣的好處是,假設今天我們有一包相關的檔案,裡面有很多 Header File,我們其實可以自己去實作這些 Header File,把他撰寫進 Implement File 就可以了,Header File 只是做了一個 function 規格的定義。

因為今天不同的專案,針對不同的 function 可能有些微的差異,但是他們可能回傳型態都相同,參數也都相同,這個時候有一個常用的 Header File,其實可以省去蠻多撰寫程式的時間。

拆分檔案 with Class

Header File

有類別的 Header File,其實也很簡單,一樣不實作細節,只定義規格。
一樣 public、private 等權限的設定要寫好哦。

一個 Header File 同時包含類別跟 function 的定義也是很正常的,可以自己多方練習

如果你對於下面任何一個地方看不懂,那一定是前面的筆記沒有看熟哦,因為都講過了。
包括 friend function、operator overloading、::

Person.h

#include <iostream> using namespace std; class Person{ public: Person(); Person(string name, int age); void eat(); void run(); friend istream& operator >> (istream& in, Person& p); friend ostream& operator << (ostream& out, const Person& p); friend int add(Person& p1, Person& p2); private: string name; int age; };

Implement File

Person.cpp

#include <iostream> #include "Person.h" using namespace std; // class member function Person::Person(){ this->name = ""; this->age = 0; } Person::Person(string name, int age){ this->name = name; this->age = age; } void Person::eat(){ cout << this->name << " eating!!" << endl; } void Person::run(){ cout << this->name << " running!!" << endl; } // friend function istream& operator >> (istream& in, Person& p){ cout << "what's your name?\n"; in >> p.name; cout << "How old are you?\n"; in >> p.age; return in; } ostream& operator << (ostream& out, const Person& p){ out << "Hello! " << p.name << ", you are " << p.age << " years old!" << endl; return out; } int add(Person& p1, Person& p2){ return p1.age+p2.age; }

Application File

main.cpp

#include <iostream> #include "Person.h" using namespace std; int main(int argc, char** argv) { Person p1("Orange", 22), p2; cin >> p2; cout << p1; cout << p2; cout << add(p1, p2); return 0; }

範例輸出如下 :

問題 - 重複引入相同檔案的處理

問題如標題,今天有可能重複引入已經引入過的檔案,這其實會造成資源的過度使用,比方說有一個標頭檔定義了一些 function 而且重複使用率非常高,我們在 main 內 include 他,在 Person.cpp 也 include 他,就會有重複引用的問題,大概是像下面這樣。

我們把上面提到的 functionset.h 給拉進來。所以現在你應該有五個檔案,分別是

  • main.cpp
  • Person.h
  • Person.cpp
  • functionset.h
  • functionset.cpp

因為貼程式碼篇幅會變長,別太在意!
Orange

main.cpp

#include <iostream> #include "Person.h" #include "functionset.h" using namespace std; int main(int argc, char** argv) { Person p1("Orange", 22), p2; cin >> p2; cout << p1 << p2 << "sum of age of two people: " << add(p1, p2) << endl; p1.set_age(60); p2.set_age(30); cout << "p1.age/p2.age = " << divi(p1.get_age(), p2.get_age()); return 0; }

Person.h

#include <iostream> using namespace std; class Person{ public: // 上面相同省略 int get_age(); void set_age(int age); // 下面相同省略 private: string name; int age; };

Person.cpp

#include <iostream> #include "Person.h" #include "functionset.h" using namespace std; //上面相同省略 int Person::get_age(){ return this->age; } void Person::set_age(int age){ this->age = age; } //下面相同省略 int add(Person& p1, Person& p2){ return add(p1.age, p2.age); }

functionset.h / functionset.cpp 跟上面的例子一模一樣就不貼了。

你可以發現到在 main.cppPerson.cpp 我們都引用了 functionset.h,main 為了要使用 functionset.h 提供的 divi 所以引入他,而 Person.cpp 則是為了 add 而引入他。

所以可以發現當 main 執行時,會引入 functionset.h 兩次,第一次是 main 自己引入他,第二次是 main 要去引入 Person 時,Person 也會引入一次,這樣就造成了重複引入,在底層的運作其實多引入了一倍的程式碼,為了避免這樣的問題,C++ 提供了一個的解決方式 「directive 指示詞」。

directive 指示詞

這邊稍微簡短的解釋一下甚麼是指示詞。

大家應該對於 #include 這個東西非常不陌生,在最前面加上 的就是 directive,而 directive 的全名又叫做 preprocessor directive,preprocessor 的意思顧名思義,就是預處理。

指示詞會在程式一 compile 的時候先請 preprocessor 把 directive 要求的事情先審視一遍,然後才有後續的打包。

所以我們總是在程式碼頂端寫這些東西。

#include <iostream> #include <vector> #include "functionset.h" int main(){...}

這些 directive 就是告訴電腦說,請你幫我把這些程式碼引入進來,假設 iostream 這個 library 有 500 行,vector 有 300 行,functionset.h 有 4 行,那在你程式真的執行的時候,在你的 main 之前其實有 804 行來自 library 的程式碼,接著才是 main。

但其實沒有這麼簡單啦,實際上更複雜,你大概可以這樣想像就好,你真的對細節很有興趣的話可以上去看延伸閱讀的影片。

透過上面讓大家簡單的認識一下 directive 之後,我們來說說如何透過 directive 幫我們解決重複引入檔案的問題。

#ifndef, #define, #endif

如標題,這三個詞都是一種 directive,通常會搭配在檔案引入的時候使用,用來避免重複引入。

我們來看看語法

#ifndef identifier_name #define identifier_name ... #endif
  • ifndef (if non define)
    • 如果沒有定義我們看到的 identifier_name,就往下執行
  • define
    • 定義 identifier_name
  • endif
    • 會搭配一個 ifdef/ifndef 指示詞

配合實例看看。

functionset.h

#ifndef FUNCTIONSET_H #define FUNCTIONSET_H int add(int a, int b); int sub(int a, int b); int multi(int a, int b); int divi(int a, int b); #endif

這樣寫的意思是說,如果今天程式一執行,要來引入 functionset.h 的時候,會來看我們有沒有 define 過指示詞,如果有定義過的話,就不會再引入,直接跳到下面的 endif。

所以在 main 第一次引入 functionset.h 的時候,因為還沒有見過 FUNCTIONSET_H 這組詞,所以就 define 這組詞,並引入這個標頭檔,接著當 Person.cpp 也要引入 functionset.h 的時候,因為FUNCTIONSET_H 這組詞已經被定義過了,所以 #ifndef 不成立,直接跳到 #endif。透過這樣的方法,可以確保只引入一個 library 一次。

那你會說 #define 後的辨識詞一定要是檔案名稱轉大寫加上 _H 嗎?
沒有一定,你可以自訂你想要的名稱,但習慣上我們都會取全大寫而且跟標頭檔同名,你也可以取 apple。

Person.h

#ifndef PERSON_H #define PERSON_H #include <iostream> using namespace std; class Person{...}; #endif

那我想同理可證,寫那麼多次 #include <iostream> 不是也重複引入 library 很多次嗎?

所以你一定可以聯想到,iostream 這個檔案內一定有 directive。果不其然真的有哦。

#pragma once

另一種解法是,在要引入的檔案最前面加入這一行 #pragma once,他是非標準可是被廣泛運用的指示詞。用法如下。產生的結果跟上面一模一樣。

但因為他是非標準的,所以可能會發生在不同電腦上有些人不能使用會錯誤的問題。所以還是養成習慣都寫上面那種會比較好。你在看 C++ 底層原始碼也才會更熟悉。

functionset.h

#pragma once int add(int a, int b); int sub(int a, int b); int multi(int a, int b); int divi(int a, int b);

Person.h

#pragma once #include <iostream> using namespace std; class Person{...};

如果還有興趣,可以看下方 Reference 第二條連結。

Reference

總結

無論你去學哪種程式語言,都會有非常多的檔案,知道如何引入他,或是拆開來撰寫都是非常基本的能力,希望大家都學起來。

隨便貼任何一個程式語言的,檔案的結構都五花八門,透過現在把這基本的能力學走,你未來學新的更難的才不會感到害怕。