---
GA: G-RZYLL0RZGV
---
###### tags: `大一程設-下` `東華大學` `東華大學資管系` `基本程式概念` `資管經驗分享`
Header File / Implement File / Application File
===
[TOC]
## 前言
接下來的內容是學程式來說非常基本且重要的概念,「拆分檔案」,當今天程式撰寫的越來越龐大之後,不可能都只塞在一個檔案裡面,會非常難以維護,因此接下來要教基本的拆分檔案,拆分檔案有以下幾個好處。
* 增加程式易讀性
* 容易閱讀
* 增加程式重用性
* 重複使用
* 增加程式利用率
想必學了這麼久,對於把程式碼都寫在 main 裡面大家應該已經有點感到疲倦了,尤其是在學到類別之後,程式碼真的是落落長,如果一個類別還好,兩三個類別都要塞在同一個檔案,應該會瘋掉,要一直捲滑鼠。
> <span style="color:red">**實際操作的影片我放在最下面,希望大家先讀過筆記**</span>
> [name=Orange]
## 拆分檔案你需要知道的三種檔案
在 C++ 裡面,拆分檔案會把檔案分成三種,分別是。
* Header File 標頭檔
* 只定義不實作
* Implement File 實作檔
* 實作你 Header File 的內容
* Application File 執行檔
* 去真的把標頭檔定義的工具拿來用
接下來我們一一來介紹他,先從標頭檔開始。
## 拆分檔案 with function
我們把含有類別的拆分檔案放到後面,分開來講會比較清楚,不容易搞混。
### Header File 標頭檔
標頭檔,副檔名為 `h`,`person.h`、`animal.h`、`dog.h` 等等的。
**標頭檔只會定義類別、函式,不會真的撰寫實作細節**,為的是增加程式易讀性,根據開發者的程度跟需求,去看自己想要的檔案,甚麼意思呢?
有的人看到標頭檔知道有甚麼 function 可以用之後,他不在意 function 的實作細節的話,他可能不會去看 implement file,可以少一步看複雜的東西。
我們先從只在標頭檔內定義 function 開始,類別晚一點。
functionset.h
```cpp=
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 跟參數,先被定義好了。
> 非常簡單!
> [name=Orange]
定義完之後必須要實作他,因此要來撰寫 Implement File,告訴我們每個 function 的細節。
### Implement File 實作檔
實作檔,副檔名為 `cpp`,在習慣上我們會希望他與 Header File 同名,因此我們這邊把實作檔取名為 `functionset.cpp`,標頭檔跟實作檔名字不相同沒關係,可是這邊取同名的用意是,讓我們知道哪個 Header File 對應到哪個 Implement File,方便我們去做閱讀。
實作 Implement File 非常簡單,大家其實早就已經會了。
functionset.cpp
```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
```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 檔的時候,但我們這邊就先不討論,以免混淆大家。
有興趣的人下面歡迎你來挑戰 :arrow_down:
沒有興趣的話就跳過延伸閱讀,繼續往下看吧!
## [延伸補充閱讀(Optional) - 難]
這部分真的比較難,沒有興趣可以跳過,有興趣的人可以看看,其實蠻有趣的。
影片:
* [How the C++ Compiler Works](https://www.youtube.com/watch?v=3tIqpEmWMLI)
* [How the C++ Linker Works](https://www.youtube.com/watch?v=H4s55GgAg0I)
* [C++ Header Files](https://www.youtube.com/watch?v=9RJTQmK0YPI)
文章:
* [淺談 c++ 編譯到鏈結的過程](https://medium.com/@alastor0325/https-medium-com-alastor0325-compilation-to-linking-c07121e2803)
* [How does C++ know to use the Class.cpp file?](https://stackoverflow.com/questions/50116218/how-does-c-know-to-use-the-class-cpp-file)
## 定義 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
```cpp=
#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
```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
```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;
}
```
範例輸出如下 :
![](https://i.imgur.com/8tlytj7.png)
## 問題 - 重複引入相同檔案的處理
問題如標題,今天有可能重複引入已經引入過的檔案,這其實會造成資源的過度使用,比方說有一個標頭檔定義了一些 function 而且重複使用率非常高,我們在 main 內 include 他,在 `Person.cpp` 也 include 他,就會有重複引用的問題,大概是像下面這樣。
我們把上面提到的 `functionset.h` 給拉進來。所以現在你應該有五個檔案,分別是
* main.cpp
* Person.h
* Person.cpp
* functionset.h
* functionset.cpp
> 因為貼程式碼篇幅會變長,別太在意!
> [name=Orange]
main.cpp
```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
```cpp=
#include <iostream>
using namespace std;
class Person{
public:
// 上面相同省略
int get_age();
void set_age(int age);
// 下面相同省略
private:
string name;
int age;
};
```
Person.cpp
```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.cpp` 跟 `Person.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 要求的事情先審視一遍,然後才有後續的打包。
所以我們總是在程式碼頂端寫這些東西。
```cpp=
#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,通常會搭配在檔案引入的時候使用,用來避免重複引入。
我們來看看語法
```cpp=
#ifndef identifier_name
#define identifier_name
...
#endif
```
* ifndef (if non define)
* 如果沒有定義我們看到的 identifier_name,就往下執行
* define
* 定義 identifier_name
* endif
* 會搭配一個 ifdef/ifndef 指示詞
配合實例看看。
functionset.h
```cpp=
#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
```cpp=
#ifndef PERSON_H
#define PERSON_H
#include <iostream>
using namespace std;
class Person{...};
#endif
```
那我想同理可證,寫那麼多次 `#include <iostream>` 不是也重複引入 library 很多次嗎?
所以你一定可以聯想到,`iostream` 這個檔案內一定有 directive。果不其然真的有哦。
![](https://i.imgur.com/vlYKFwQ.png)
### #pragma once
另一種解法是,在要引入的檔案最前面加入這一行 `#pragma once`,他是非標準可是被廣泛運用的指示詞。用法如下。產生的結果跟上面一模一樣。
但因為他是非標準的,所以可能會發生在不同電腦上有些人不能使用會錯誤的問題。所以還是養成習慣都寫上面那種會比較好。你在看 C++ 底層原始碼也才會更熟悉。
functionset.h
```cpp=
#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
```cpp=
#pragma once
#include <iostream>
using namespace std;
class Person{...};
```
如果還有興趣,可以看下方 Reference 第二條連結。
## Reference
* [Preprocessor directives](https://www.cplusplus.com/doc/tutorial/preprocessor/)
* [深入淺出 C++:#include Directive PART 1](https://www.796t.com/content/1549876167.html)
## 總結
無論你去學哪種程式語言,都會有非常多的檔案,知道如何引入他,或是拆開來撰寫都是非常基本的能力,希望大家都學起來。
隨便貼任何一個程式語言的,檔案的結構都五花八門,透過現在把這基本的能力學走,你未來學新的更難的才不會感到害怕。