--- 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) ## 總結 無論你去學哪種程式語言,都會有非常多的檔案,知道如何引入他,或是拆開來撰寫都是非常基本的能力,希望大家都學起來。 隨便貼任何一個程式語言的,檔案的結構都五花八門,透過現在把這基本的能力學走,你未來學新的更難的才不會感到害怕。