# C++ 開發環境與專案設定指南 在軟體開發的過程中,建置專案是一個必須的步驟。然而,當專案的規模逐漸擴大、原始碼的檔案越來越多時,如何有效地管理每一個檔案的編譯順序、相依關係與目標設定,就成為一個工程上需要嚴謹思考的問題。 當專案規模較小時,開發者可以會手動使用編譯器(如 g++)羅列檔案,輸入冗長的指令來建構整個程式。這種方式不僅繁瑣,且容易出錯,更不利於團隊協作與跨平台部署。為了解決這個問題,各種建構系統(Build System)來自動化與管理編譯流程。而 CMake,正是其中最廣泛使用的一種。 >[!Note] 引言 > 本文章開發環境基於 VS Build、Visual Studio Code 與 CMake,並使用 LLVM 相關的專案來設定、編譯專案,相較於平常學校撰寫的幾個小檔案的作業,我們將要設定一些稍微大型的專案。除此之外,還會補充分離式編譯、搜尋路徑等概念,因為這些知識須等到大三同學們修習編譯器時才會有更深入體會,這裡我會盡可能地用簡單的方式來說明。 >[!Important] 誰適合閱讀此文章 > 本文章撰寫的時候,預設會使用 Windows 進行說明,因為原意是寫給大一修習「物件導向程式設計」的學生。如果你是**非**Windows用戶,其實更加單純,安裝的時候可以不用安裝VS BuildTools,然後其他的全部下載`.tar*`格式,解壓縮後丟到 `/usr/local`。macOS 作法接近,但需注意 macOS 為非 GNU/Linux 系統,可能在動態連結庫格式、CPU 架構與執行簽章限制上與 Linux 有所不同,請確認 binary 相容後再安裝。 > > 本文章用意是讓學生學習跨平台的C++環境架設,並學習專案架構與CMake的簡單用法。 ## 生態系比較 | 套件 | 風格 | 優缺點 | | ------------------------------- | ------------------ | ------------------------------------------ | | 🔵 Visual Studio 全家桶 | 微軟生態 | 一切都有,但很稍顯肥大 | | 🟢 WSL2 + VSCode | 類 Linux | 環境相對簡單、可跨平台 | | 🔴 Windows SDK + LLVM + CMake | LLVM 生態 | 靈活但複雜 | | 🟡 MSYS2 / MinGW | 類 UNIX in Windows | 可用 GCC/libstdc++,編譯快但和 MSVC 不兼容 | >[!Tip] 其實以筆者個人經驗,如果你學會配置 Linux 環境,並且沒有需求要撰寫 Windows 專用的程式( 如 *WinForm* , *.NET* ) 之類的最簡單的環境可能是直接安裝 WSL2 然後設定一下 CMake 跟 Ninja,然後安裝一下編譯器就好。 「Visual Studio 稍顯肥大」的意思是: Visual Studio 全家桶 ≒ 新安裝的 Linux 作業系統 + CMake + 編譯器 + VSCode 但也不否認 Visual Studio 整個環境配置完善是真的很強,該有的套件應有盡有,也有好好整合CMake的工具鏈以及 vcpkg 等輔助工具。 ## 安裝套件 ### Visual Studio Buildtools >[!Warning] 如果已經安裝了 VS2019 或是 VS2022,可以跳過該章節 [Visual Studio Buildtools](https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2022) 是微軟提供的基於命令列的VS工具包,是一個精簡版的 Visual Studio,只包含編譯器工具鏈與建構系統,沒有完整的圖形化 IDE、編輯器、設計工具、範本等工具。對於 C++ 專案的基本開發,只需要安裝以下套件: ![image](https://hackmd.io/_uploads/B17OAUH1gx.png) 它包含了 MSVC 標頭與 lib(C++ STL, runtime, Windows headers)與 msbuild 等工具。 ### Visual Studio Code [Official Download](https://code.visualstudio.com/) VSCode 是由微軟開發的免費開源跨平台程式碼編輯器。它支援多種編程語言,並提供許多強大的功能,如語法高亮、自動補全、調試支持以及豐富的擴展插件系統。用戶可以安裝各種插件來增強 VSCode 的功能,比如 Git 支援、Python 語言支援等。 ### CMake 3.28 以上 [Official Download](https://cmake.org/download/) CMake 是一個跨平台的開源建構系統工具,主要用來自動化編譯過程。它將源碼和編譯選項轉換為平台特定的編譯命令(如 Makefile 或 Visual Studio 工程),從而幫助開發者簡化不同平台上編譯過程的管理。請確保安裝的版本在 3.28 以上最好 ![image](https://hackmd.io/_uploads/H1xMKwaAke.png) ### LLVM Projects v19 以上 [Github Release Page](https://github.com/llvm/llvm-project/releases) [Debian Packages](https://apt.llvm.org/) LLVM 是一個開源的編譯器基礎設施,設計上旨在為各種編程語言提供後端支持。LLVM 其實是一組工具和庫,幫助開發者編寫編譯器、靜態分析工具等。它支持生成高效的機器碼,並提供了優化工具、分析工具等功能。 ![image](https://hackmd.io/_uploads/B1cfKvpCyx.png) ### Ninja [Github Release Page](https://github.com/ninja-build/ninja/releases) Ninja 是一個輕量級的建構系統,設計目的是用來進行快速、高效的編譯和構建過程。它通常與其他工具(如 CMake)搭配使用,生成用於構建的指令檔案(如 build.ninja),並依此執行構建過程。 ![image](https://hackmd.io/_uploads/SyW7KDaAJx.png) 安裝完成後,可以先打開終端機,Ex. 安裝 Git 附帶的 `Bash`,並嘗試執行幾個指令: ```sh cmake --version ninja --version clang -v ``` 如果每個指令都有正確輸出版本資訊,就是安裝成功了。若沒有正確輸出,確保所有的執行檔在環境變數中: ![image](https://hackmd.io/_uploads/SJ3HKwTAkl.png) 以筆者的例子來說 - D:\LLVM - D:\CMake 分別是安裝 LLVM 與 CMake 的路徑,那就把 `D:\LLVM\bin` 以及 `D:\CMake\bin` 放到環境變數即可。如果你使用的是 Linux,套件管理工具應該會幫你設定好。 >[!Tip] >在 Linux 的開發者,不少人都喜歡自己管理工具的位置,或是你有需要管理不同的工具版本,建議的做法是下載編譯好的 archive 檔案,比方說 .zip 或是 .tar 檔案,然後直接解壓縮到 `$HOME/.local` 底下即可。 >若希望這些工具可以讓所有該主機的用戶使用,可以選擇解壓縮到 `/usr/local` 底下 Ninja 下載後應該是單獨一個執行檔,可以丟到 CMake 安裝路徑底下的 bin 資料夾下即可 ## Visual Studio Code 整合 ### 相關擴充 以下幾個插件請同學們進行安裝,它們分別是針對語法提示、程式碼風格化、偵錯器進行設定的工具 接下的專案,將會先請先在你的 Visual Studio Code 安裝以下的延伸模組: ![image](https://hackmd.io/_uploads/Hk_GYPS1eg.png) 另外也推薦你安裝 `Editorconfig`,用來整合整個團隊的編輯器設定: ![image](https://hackmd.io/_uploads/HylLj3A0kl.png) 接下來,請隨便開啟一個 C++ 檔案,他可能會跳出提示,跟你說找不到 clangd。省麻煩的可以直接按 install,Visual Studio 會嘗試幫你安裝 ![image](https://hackmd.io/_uploads/H1Qx2nRC1g.png) 另一種做法是,在 Visual Studio 的頁籤中:找到設定,然後搜尋 `clangd` 的設定,手動把 clangd 的執行檔位置填入(此範例是 `/usr/local/bin/clangd`) ![image](https://hackmd.io/_uploads/Hy7nqnR0yx.png) ![image](https://hackmd.io/_uploads/BkGa32C0Jl.png) 把滑鼠移動到系統的標頭上 ![image](https://hackmd.io/_uploads/S1uIphARkg.png) 如果有出現提示,就是 clangd 設定完成。 也可以打開終端機,切換到「輸出」面板,右側選擇 clangd 輸出 ![image](https://hackmd.io/_uploads/Hy-h620C1g.png) 有看到 clangd 的運作輸出,就是正常執行中。其中 LSP 的意義是 `Language Server Protocol` > 語言伺服器協定(Language Server Protocol,LSP)是一個開放的、基於JSON-RPC的網路傳輸協定,原始碼編輯器或整合式開發環境(IDE)與提供特定程式語言特性的伺服器之間互動時會用到這個協定。該協定的目標是讓編輯器或整合式開發環境能支援更多的程式語言。 ### 建議設定 ![image](https://hackmd.io/_uploads/SJES0nARJx.png) 在設定右邊,有一個「*開啟設定(JSON)*」,會使用JSON格式來顯示目前系統的設定,舉例來說: ![image](https://hackmd.io/_uploads/S1QgJpCR1l.png) ![image](https://hackmd.io/_uploads/S1ZbkTAAyx.png) 以上兩張圖都是關於`Editor`的相關設定,一個使用 GUI 呈現;你也可以選擇使用 JSON 格式來編寫你的系統設定。 這裡推薦幾個設定: - 使用 Clang-format 進行格式化 在 C++ 檔案點選右鍵 -> 文件格式化方式 -> 選擇預設格式器 -> Clang-Format ![image](https://hackmd.io/_uploads/S1-PJa0AJe.png) ![image](https://hackmd.io/_uploads/HkKdyaCRJx.png) 對應的 JSON 格式設定: ```json "[c]": { "editor.defaultFormatter": "xaver.clang-format" }, "[cpp]": { "editor.defaultFormatter": "xaver.clang-format" }, ``` - Clangd 的相關參數 Clangd 找到 Arguments,填入對應的資訊: ![image](https://hackmd.io/_uploads/S14blpRCJg.png) 對應的 JSON 格式設定: ```json "clangd.arguments": [ "--all-scopes-completion", "--background-index", "--clang-tidy", "--compile-commands-dir=${workspaceFolder}/build", "--completion-parse=always", "--completion-style=detailed", "--fallback-style=google", "--function-arg-placeholders=0", "--header-insertion=never", "--pch-storage=memory", "--import-insertions", "--enable-config" ] ``` - 內嵌提示 inlayHints ![image](https://hackmd.io/_uploads/rkmIlpRA1x.png) 這個可以根據自己的喜好來設定,有分成開、關、「按下 Ctrl + Alt 時關閉」、「按下 Ctrl + Alt 時開啟」 ![image](https://hackmd.io/_uploads/ryIfWaAC1x.png) 開啟的話會顯示參數的名稱、自動推導的型別等,像我個人覺得很礙眼,所以設定是「按下 Ctrl + Alt 時開啟」 ### 個人化設定 Visual Studio Code 除了可以設定整個系統的設定,也可以設定工作區的個人化設定。 工作區就是目前開啟的資料夾位置,可以在 `.vscode` 中設定個人化的設定,這些設定通常不會同步到雲端上,細節可以查看 VSCode 的[官方說明](https://code.visualstudio.com/docs/configure/settings)。 接下來我們就要正式進入專案設定的說明,但是在進入專案設定之前,會簡單補充一下**分離式編譯**的概念,讀者可以選擇性跳過,直接到最後的專案設定教學。 ## 分離式編譯 在正式說明如何設定專案之前,首先簡單的講一下分離式編譯與查找路徑。分離式編譯會在 OOP 的課程上提到,但是更正式一點必須得要到編譯器的課程才會提及。通常學生只會被 OOP 課程的一句話快速帶過: > 分離式編譯可以讓我們在撰寫程式的時候,把聲明與實作給分開 畢竟 OOP 是給大一學生的課程,諸如符號表、靜態鏈結、動態鏈結等觀念自然不會深入說明,所以同學們會一直抱著一個疑問:「為什麼我一定得要把`.hpp` 檔案跟 `.cpp` 檔案分開來?」鑒於各位到大三編譯器應該也不會認真上課,這裡筆者就簡單解釋一下。 ![image](https://hackmd.io/_uploads/r1VdRP60ye.png) [該網站](https://hackingcpp.com/cpp/lang/separate_compilation.html)的寫的其實很清楚,但同學們現階段應該無法獨立理解整個過程,因此助教簡單的導讀一下 ### 編譯執行檔 假設一開始我們只有一個簡單的程式: ```cpp= // main.cpp #include <iostream> int main() { std::cout << "Hello, world!" << std::endl; } ``` 通常會用一個簡單的指令進行編譯: ```sh clang++ ./main.cpp -o hello #會輸出 "Hello, world!" ./hello ``` 在這個情境下,很單純的就是生成一個可執行檔 `hello` ### 加入標頭檔 現在情境稍微複雜一點,比方說有以下三個檔案 ```cpp= // main.cc #include <iostream> #include "sum.hh" int main() { std::cout << sum(100, 200) << '\n'; } ``` ```cpp= // sum.hh #ifndef _SUM_H_ #define _SUM_H_ int sum(int a, int b); #endif ``` ```cpp= // sum.cc #include "sum.hh" int sum(int a, int b) { return a + b; } ``` 那我們可以使用幾種方法進行編譯: 第一種是把所有的**實作原始碼**當作輸入,直接進行編譯 ``` clang++ main.cc sum.cc -o sum ``` 第二種是先把原始碼編成**目標檔**,然後再產生**可執行檔** ``` clang++ -c sum.cc -o sum.o clang++ main.cc sum.o -o sum ``` 第三種是先把原始碼編成**函式庫**,然後再產生**可執行檔** ``` clang++ -fPIC -shared sum.cc -o libsum.so clang++ main.cc -L. -lsum -o sum ``` 以下分別是他們的檔案大小與描述 ![image](https://hackmd.io/_uploads/SJr4NupAyx.png) ![image](https://hackmd.io/_uploads/SkAL4_pC1e.png) 而第二種與第三種作法,是本段的主旨:分離式編譯 - `.o` 檔的意義是 `Object File`,是編譯的中介產物 - `.so` 檔的意義是 `Shared Object`,是允許共享的函示庫 `.so` 檔案與 Windows 的 `.dll`(Dynamic Link Library) 意義相同,就是先編好的一組函式庫。 ### 現實中的比喻 這裡用熟悉的生活經驗來理解抽象概念,分離式編譯到底在幹嘛: 把編譯的過程想像成製作一道菜,比方說番茄肉醬義大利麵 - 第一種方法就是一次全煮: - 自己切番茄、洋蔥 - 炒絞肉 - 煮醬料 - 煮麵 - 擺盤 - 第三種方法就是先去買調好的肉醬(`.so檔案`): - 煮麵 - 倒醬料 - 擺盤 那 `.o` 檔案這個東西,因為他是編譯的中間產物,約等於以下物品 - 切好的番茄 - 炒好的絞肉 那你可以把多個 `.o` 檔案先編成 `.so` 檔案,那就是你先做一罐醬料,等以後使用; 也可以把多個 `.o` 檔案直接編成執行檔,該情境就是直接下鍋炒出成品出來。 - `.cc` 檔案是生鮮食材 - `.o` 檔案是你備好的料,可以直接下鍋後出盤,也可以煮成醬料用 - `.so` 檔案是預先做好的調味粉or醬料包,需要就加進去 ![image](https://hackmd.io/_uploads/HkIOuuT0Je.png) 這裡感謝 chatgpt 把筆者的比喻繪製成圖片 其實還有一種叫做 `.a` 的檔案,意義是 *Archive File*,其實也是一堆 `.o` 檔案的集合 他跟 `.so` 檔案主要的差異就是動態鏈結與靜態鏈結,這個主題就跳過不提 ### 搜尋路徑 -I 在上個情境中,程式檔案目錄如下: ``` example1 ├── main.cc ├── sum.cc └── sum.hh ``` 並且所有的指令都在 `example1` 資料夾底下執行,情境二的檔案目錄如下: ``` example2 ├── include │ └── sum.hh ├── main.cc └── sum.cc ``` 我們用同樣的編譯指令執行,會出現錯誤: ![image](https://hackmd.io/_uploads/B1hH5OpCkl.png) 原因是因為 `sum.hh` 不在編譯器搜尋標頭檔的目錄,有兩種解法: 1. 分別修改 `sum.cc` 跟 `main.cc` - `main.cc` 把 `#include "sum.hh"` 改成 `#include "include/sum.hh"` - `sum.cc` 把 `#include "sum.hh"` 改成 `#include "include/sum.hh"` 2. 在編譯器中,使用 `-I` 參數 ```sh clang++ -Iinclude main.cc sum.cc -o sum ``` >[!Important] > 上面範例並沒有錯誤,是 `-Iinclude` 而不是 `-I include`,不需要空白隔開 > 直接使用 `-I<Path>` 就好 方案一方法顯然不是很聰明,如果你的專案很大,你會需要修改非常多的原始碼,且還要考慮他們的檔案結構。 所以建置大型專案都會採用方案二,直接在編譯器的參數加上搜尋路徑。如果是 Linux 系統,有更加簡單的作法,後面會提到。 ### 鏈結路徑 -L 同樣的,如果是採用先編譯出函式庫的做法,就需要使用`-L`指定鏈結路徑: ![image](https://hackmd.io/_uploads/HJO23dTA1l.png) 如果你沒有加上 `-L` 參數: ![image](https://hackmd.io/_uploads/r1x0ndaRJx.png) 會顯示「不知道 `-l` 是要鏈結哪個函式庫」 >[!Important] > 共享函式庫必須得要是 lib 開頭的檔案,比方說 `libsum.so` 或是 `libsum.a` > 如果沒有 `lib` 開頭,一樣會跟你說不知道要鏈結誰 > `-L`用來指定要鏈結的搜尋路徑,`-l` 指定要鏈結哪些函式庫 ![image](https://hackmd.io/_uploads/Sk68T_pA1l.png) 這樣同學們應該知道為什麼 Linux 上,函式庫幾乎都是以 `lib` 開頭 `.so` 結尾 在 Linux 上,有個環境變數叫做 `LD_LIBRARY_PATH`,編譯器也會根據其內容去嘗試鏈結函式庫,比方說 Nvidia 的 CUDA,安裝後會出現以下提示: ![image](https://hackmd.io/_uploads/BkSu5YT0ye.png) 開發者通常會在如 `.bashrc` 或是 `.zshrc` 等設定檔案,加入以下資訊: ![image](https://hackmd.io/_uploads/HyHqitTAyg.png) 讓套件的函式庫可以被編譯器與鏈結器搜尋到。還有一種做法是 `rpath`,但這裡也跳過不提 ### Linux 更好做為開發平台嗎 根據筆者認識的同學與業界前輩,大部分人都同意這個觀點。 除非你是撰寫一些平台特定的程式,比方說寫 WinForm 當然就用 Windows,寫 Windows 上可以玩的遊戲也是用 Windows 等;或者要寫 Swift / Objective-C 會使用 macOS,否則 Linux 或者 Unix-Like 平台確實有其優點: Linux 除了有不少對開發者來說很好用的妙妙工具,再來就是檔案系統規劃的比較清楚。 在 Linux 的根目錄,存在一個 `/usr` 資料夾,`usr` 一種說法是 **U**nix **S**ystem **R**esources,筆者也覺得挺合理的。 ![image](https://hackmd.io/_uploads/Sy4ZyYaC1x.png) 其實我們只要關注 `include` 跟 `lib` 開頭的資料夾就好,在 Linux 中,只需要把標頭檔丟進去 `/usr/include`,編譯器就能搜尋到;同時只要把 `.so` 檔案丟到 `/usr/lib`,就可以被鏈結到。 `g++` 或是 `clang++` 都可以列出搜尋路徑,編譯器會告訴你以下資訊: ![image](https://hackmd.io/_uploads/rybkgY6CJx.png) 以下兩個是比較重要的路徑: - `/usr/local/include` - `/usr/include` 已經明確跟你說 `#include <...> search starts here:` ,使用角括號來 include 的時候,會嘗試尋找那些路徑。 同時也注意到,`#include "..." search starts here:`,預設是沒有搜尋路徑的,所以通常會使用`#include<...>`引用 std library;使用`#include "..."` 引用自己的標頭檔。 而 Linux 的套件管理工具,比方說`apt`,會直接幫你把下載好的套件解壓縮到 `/usr`,我們看看第一節提到的 LLVM 跟 CMake,裝在你的 Windows 上長怎樣 ![image](https://hackmd.io/_uploads/SyJZ-Ya0kx.png) ![image](https://hackmd.io/_uploads/SyvZZKpRJe.png) 太苦了,Windows 的系統,卻用著 Linux 的檔案結構風格。對於 Linux 來說,只要把這些資料夾丟到 `/usr` 或是 `/usr/local` 底下就可以直接使用大多數的軟體,而 Windows 還要設定一堆玩意,所以單純論開發環境,Linux 是比 Windows 有不少優勢的。微軟這幾年可能也注意到這個趨勢,所以整合了不少 Linux 開發相關的功能,比方說 WSL。而套件管理工具,之前有 `chocolatey` 跟 `scoop`,近幾年還有內建的 `winget`,但老實說還是滿難用的。 比方說我需要使用 `SDL`、`OpenGL` 跟 `curl` ![image](https://hackmd.io/_uploads/SkQmMtpAJl.png) 只需要使用: `lib<套件名稱>-dev`,就可以直接裝好對應的開發包。對Linux開發人員,使用原始碼建置也是常見的作法,比方說同學們用來做UI的 `qt`: ![image](https://hackmd.io/_uploads/BkVz4t6Akx.png) 也是使用 `CMake` 跟 `Ninja` 作為專案建置的工具。那通常在使用 `cmake --build .` 之後,會接續使用 `ninja install` 或是 `make install` 直接安裝套件到 `/usr/local` ### 比較一下 Windows 與 Linux #### Linux 開發相關的路徑 | 層級 | include 路徑 | lib 路徑 | 意義與用途 | - | - | - | - | 系統層級 | `/usr/include` | `/usr/lib` | 由發行版安裝的核心開發檔(如 glibc、OpenSSL) | 本機全域 | `/usr/local/include` | `/usr/local/lib` | 手動安裝的第三方函式庫(如自行編譯的 OpenGL) | 使用者私有 | `$HOME/.local/include` | `$HOME/.local/lib` | 單一使用者安裝的函式庫(無需 root 權限) >[!Tip] > `/usr` 通常由套件管理工具處理,檔案來源是系統的套件庫(例如 apt / dnf / pacman 等) > `/usr/local` 這裡存放的是你自己手動安裝、自行編譯或非套件管理安裝的軟體。 > 以上面的例子來說,就會把 CMake 跟 LLVM 套件丟到 `/usr/local`。如果你沒有系統權限,就丟到 `$HOME/.local`,代表安裝的這些東西僅限你本人使用 > 其實 Windows 在安裝的時候,也會問你應該 `Add PATH to all User` 還是 `Add PATH to current User`,就是這個區別 > CMake 編譯出來的檔案,預設安裝路徑也是 `/usr/local` | 動機 | 行為 | - | - | 你需要新版的 CMake,但套件庫太舊 | 自己編譯 CMake 安裝到 /usr/local | 你測試一個開源函式庫但不想影響系統環境 | 安裝到 /usr/local/lib + /usr/local/include | 開發一個新函式庫,但還未正式發佈 | 裝在 /usr/local,不干擾原本的套件 #### Windows 開發相關的路徑 > 沒有😅 與其說沒有,不如說沒有統一的標準: 在 Windows 中: > - 編譯器(如 MSVC、MinGW)並不會主動搜尋標準 include/lib 路徑,開發者必須在建構系統中手動指定。 > - 安裝的開發工具包通常會透過 環境變數 或 CMake 的 find_package 機制來設定 include/lib 路徑。 Windows 開發環境中常見的做法: > - 透過 Visual Studio 安裝的 SDK 路徑 自動管理(例如 Windows SDK、MSVC 工具集) > - 第三方函式庫則多透過 vcpkg、conan、NuGet 等套件管理器安裝與管理 這裡講那麼多其實也是跟你說學一下怎麼用 Linux,如果你是軟體開發相關人員就更該了解一下。 ### 為什麼需要分離式編譯 分離式編譯(Separate Compilation) 是一種將程式碼分散到多個原始碼檔案中,並各自獨立編譯,最後再 **統一連結(link)** 成一個完整可執行檔的做法。這涉及到[增量編譯](https://en.wikipedia.org/wiki/Incremental_compiler)的概念,當整個軟體的一小部分進行修改時,不需要把整個專案重新編譯一次,只需要把修改過的部分重新編譯成函式庫,再透過靜態鏈結或是動態鏈結生成執行檔即可。 一個實際的例子就是 Windows 或 macOS,某天系統跳出提示:「發現更新,請重新啟動以套用更新」。請問更新內容是什麼? 大多數情況下,不會下載新的原始碼,並重新編譯整個作業系統,而是僅下載某些動態函式庫(例如 `.dll` 或 `.dylib`,Linux上可能是 `.so` 檔),也就是: :::success **只替換有更新的模組,而不是整個程式。** 分離式編譯讓你局部處理變化、有效控管複雜度、加速開發、優化維護與部署。 是這個時代的必要技術。 ::: 另外一個例子就是 Steam 或是一些遊戲的更新,只更新了幾個模組(`.dll` / `.so`),而不是重新下載整個遊戲的主程式。 ## 專案規劃 當我們在學習程式語言時,經常是以一支單獨的檔案來撰寫、編譯與執行。這樣的方式適合學習語法、觀念與演算法,但 當程式碼逐漸變得龐大或需要多人協作時,良好的專案規劃就變得不可或缺。本小節會很簡易的提及一些專案設計的基本概念。 ### 軟體設計原則 在討論怎麼寫程式之前,先了解一下軟體設計的原則。在軟體設計中,有一些非常重要的基本原則 - 內聚性:模組內部功能的關聯度 - 耦合度:模組之間的相依程度 - 抽象化:隱藏實作的細節,僅提供基本的功能 - 關注點:每個部份應該解決的問題是什麼 這裡引用[維基百科](https://en.wikipedia.org/wiki/Cohesion_(computer_science))的說明與圖片 ![choesion](https://upload.wikimedia.org/wikipedia/commons/thumb/0/09/CouplingVsCohesion.svg/1280px-CouplingVsCohesion.svg.png) 其中**內聚性**與**耦合度**是一個重要的指標 > 一般會希望程式的模組有高內聚性,因為高內聚性一般和許多理想的軟體特性有關,包括強健性、可靠度、可復用性及易懂性(understandability)等特性,而低內聚性一般也代表不易維護、不易測試、不易復用以及難以理解。 通俗的來說,就是模組裡的所有成員是不是都在做同一件事? 內聚性關注的是「模組內」,舉例來說,如果我們需要讀取與解析XML檔案,可能會撰寫以下程式碼: ```cpp= class XmlReader { public: bool loadFile(const std::string& filename); std::string getAttribute(const std::string& path, const std::string& attr) const; std::string getText(const std::string& path) const; std::vector<std::string> children(const std::string& path) const; private: std::string rawXml; struct XmlNode; std::unique_ptr<XmlNode> root; void parse(); const XmlNode* findNode(const std::string& path) const; }; ``` 該類別中,所有的功能都是為了**讀取與解析XML**,另一個反例是: 如果我們需要控制學生的行為,比方說規劃以下的程式: ```cpp= class Student { public: void addScore(int); void printSchedule(); void playMusic(); void sendEmail(); void submitMoodle(); }; ``` 比方說寄送 Email、列出課表、交作業到Moodle,可能都是學生會進行的操作,但是其實每件事情的關聯度很低,這個類別就是低內聚性的。 耦合度則是討論「模組之間」,耦合度的例子要說明比較困難,因為同學們對於介面隔離並沒有太深入的理解。但是以現實的例子來說,其實就是 Mac筆電跟自己裝桌機的區別: - MacBook:高耦合系統 - 所有零件焊在主機板上(RAM、SSD、電池都不能換) - 整體設計精緻一致,效率高、整合佳 - 但是如果你有以下需求: - 換記憶體 - 找原廠 or 不能換 - 換顯卡 - 可能不能加裝 - 壞一個零件 - 使用者很難自行更換 這就是高耦合的系統: > 各模組彼此依賴緊密,整合性強,但彈性與維護性差 - 自己組桌機:低耦合系統 - 每個零件獨立(主機板、CPU、RAM、硬碟、顯卡) - 用 **標準化介面(如 PCIe、SATA、ATX)** 彼此連接 - 你如果有以下需求: - 換記憶體 - 買了裝上,記得買對代數就好 - 換顯卡 - 買了裝上,甚至有內顯可以不用裝 - 壞一個零件 - 使用者自行更換相對簡單 這就是低耦合的系統: > 各模組責任分明、彼此獨立,容易維護與擴充 | 硬體概念 | 軟體概念 | - | - | 每個零件可替換 | 每個模組只依賴抽象介面 | 標準化插槽 | 明確定義 API / interface | 可選組件(你想裝什麼都行) | 多型(polymorphism)、依賴反轉(DI) | 單一元件壞了不會影響整台 | 單元測試、可重構 介面是一個很好的概念,其意義就是大家都遵守一樣的規格,例如現在的手機幾乎都使用 TypeC 的充電口,你買哪個廠商的線材都可以直接插上去充電。 >[!Tip] 對應到 C++,就是 `<algorithm>` 的內部實作,比方說 `std::all_of` 的參數不是直接傳入 `vector`, `list`, `array`,他要求的是傳入 `container.begin()`,所有的算法都是基於迭代器(`iterator`)這個介面去設計的。 ### 檔案結構規劃 在我們談論架構規劃之前,會先決定怎麼如何存放原始碼與資源,這就是專案的檔案結構,或者說是 Code Structure,就是討論「該怎麼組織我的專案檔案與資料夾?」 不少網誌都有討論到這些,這裡主要以 [Redux](https://redux.js.org/faq/code-structure) 的例子中說明 #### 按檔案類型分類(By Type) ``` StudentManager/ ├── CMakeLists.txt # 主建構腳本 ├── include/ # 所有對外公開的標頭檔 │ ├── Student.hpp │ ├── StudentDatabase.hpp │ └── Utility.hpp ├── impl/ # 所有實作原始碼 │ ├── Student.cpp │ ├── StudentDatabase.cpp │ └── Utility.cpp └── data/ # 測試用資料、輸出資料 └── sample_students.txt ``` 以該例子來說, `include` 都是存放 .h 標頭檔, `impl` 都是存放 .cpp 實作檔案 #### 按照功能分類(By Feature) ``` StudentManager/ ├── CMakeLists.txt # 主建構腳本 ├── student/ │ ├── Student.hpp │ └── Student.cpp ├── database/ │ ├── StudentDatabase.hpp │ └── StudentDatabase.cpp ├── common/ │ ├── Utility.hpp │ └── Utility.cpp └── data/ └── sample_students.txt ``` 以該例子來說,不同的資料夾區分不同的模組,也僅存放對應模組的相關檔案 | 比較項目 | 按功能模組分類 | 按檔案類型分類 | - | - | - | 資料夾組織 | 每個功能是一個資料夾,內含其 UI、邏輯、測試等 | 每種型別(如 models、views、controllers)一個資料夾 | 設計理念 | 高內聚:功能模組自成單元 | 分工清楚:每種職責集中管理 | 模組邊界 | 明確:每個功能集中在一起 | 分散:功能實作橫跨多個資料夾 | 擴充新功能 | 建一個資料夾即可(例:features/chat/) | 需在多個資料夾新增檔案(例:model/chat, view/chat) | 團隊協作 | 每個人可以負責一個完整模組 | 每個人處理一種檔案型別(前端/後端/DB) | 模組耦合 | 低:各模組獨立,依賴明確 | 易耦合:模組之間交錯依賴 | 學習曲線 | 初期稍複雜,但結構清晰、擴充性強 | 初學者容易理解,但隨規模成長會變雜亂 | 適用專案規模 | 中大型專案、多人協作、強調模組化 | 小型專案、原型快速開發、單人開發 | 代表架構例子 | Redux Toolkit、Vue SFC、C++ feature-based CMake | 傳統 MVC、C++ 類別集中於 include/, src/ 在專案的規模小一點時,依照 Type 去分類是一個不錯的選擇,但是這裡會建議你都依照功能分類,這會讓你的專案結構更加清晰。不過這並不是一種強制性的規範,無論何種選擇都沒有正確與錯誤的區別,比方說以下兩個例子: - 例子一:在每個資料夾底下新增 `test.cpp`,專門用來測試該模組的程式碼 ``` StudentManager/ ├── CMakeLists.txt ├── student/ │ ├── test.cpp │ ├── Student.hpp │ └── Student.cpp ├── database/ │ ├── test.cpp │ ├── StudentDatabase.hpp │ └── StudentDatabase.cpp ├── common/ │ ├── test.cpp │ ├── Utility.hpp │ └── Utility.cpp └── data/ └── sample_students.txt ``` - 例子二:資料夾的深度追加一層,並把所有測試代碼集中 ``` StudentManager/ ├── CMakeLists.txt ├── features │ ├── student/ │ │ ├── Student.hpp │ │ └── Student.cpp │ ├── database/ │ │ ├── StudentDatabase.hpp │ │ └── StudentDatabase.cpp │ └── common/ │ ├── Utility.hpp │ └── Utility.cpp ├── tests │ ├── student.test.cpp │ ├── database.test.hpp │ └── common.test.cpp └── data/ └── sample_students.txt ``` >[!Note] > 實務上兩種做法都很常見,只要團隊統一作法即可,但我的個人經驗是: > **大方向先根據類別區分** > - assets 存放圖片、影片等素材 > - src 存放原始碼 > - config 放置一些專案的設定檔案 > > 如果你是遊戲開發人員,你正在撰寫 `Archer` 這個職業的代碼,你會希望弓箭的素材(Ex.弓的圖片)直接放在 `src/roles/archer.png`,還是置放在 `assets/archer.png`? > 兩種做法好像都可行,但實務上會放在 `assets` 底下,因為還有其他因素,如安裝遊戲時,只需要複製`assets`到安裝目錄底下就好。 影響你怎麼規劃檔案結構的因素其實很多,無法一概而論。這裡只是提出一些常見的觀點。 ## CMake 基礎介紹 因為 CMake 其實是一個非常知名的專案,網路上有很多介紹的資源,比方說:[cmake-example](https://github.com/ttroy50/cmake-examples?tab=readme-ov-file)或者 CMake 的官網,但對於初學者來說,筆者稍微減少範例,只講最簡單的幾個用法 >[!Tip] > 以下說明的範例程式碼,將會放在:https://github.com/silent4v/ntust-cmake-example 1. 如何設定 CMake 專案 2. 如何編譯執行檔跟函式庫 3. 如何新增標頭檔的搜尋位置 4. 如何新增函式庫的搜尋位置 5. 和 clangd 整合 6. CMake 擴充使用教學 那我們先從經典案例 `Hello, World` 開始 ![image](https://hackmd.io/_uploads/HJIIoDB1gg.png) - 建立一個`main.cc`,然後寫一個小範例 - 建立一個`CMakeLists.txt`,然後寫入以下內容 ```cmake # CMakeLists.txt cmake_minimum_required(VERSION 3.28) project(hello) add_executable(main main.cc) ``` 這就是一個 CMake 的最小範例了,先來解釋用到的三個語法 - `cmake_minimum_required` 定義該專案用到的 CMake 最小版本 - `project` 該專案的名稱 - `add_executable` 新增一個叫做 `main` 的執行檔案,使用 `main.cc` 進行編譯 ![image](https://hackmd.io/_uploads/H1CM6vHkge.png) 對於 CMake 來說,最基本的可以拆解成兩個階段: 1. Configure - 解析 CMakeLists.txt - 決定編譯器與工具鏈(例如 gcc、clang、MSVC) - 根據平台與選項,產生適合的 原生建置系統檔案(如 Makefile、Visual Studio .sln、Ninja 檔案等) 2. Build - 執行編譯器 - 連結程式碼 以上方的例子來說,Configure 階段使用 `cmake -S . -B build`,編譯階段則使用 `cmake --build build`。 而 `-S` 跟 `-B` 是非常重要的兩個參數,它會讓原始碼還有建置過程分離: 參數 | 意義 | 預設行為 | 實務建議 |-|-|-|- -S | Source directory 原始碼目錄 | 預設是當前目錄 | 包含 CMakeLists.txt 的資料夾 -B | Binary directory 編譯產物目錄 | 預設也是當前目錄 | 建議指定 build 或 out 這個例子中,會生成以下的資料夾: ![image](https://hackmd.io/_uploads/H1p_CPSyxx.png) 如果使用 `cmake -S . -B .`,或是直接使用 `cmake`,資料夾目錄會變成這樣: - 使用 Visual Studio 工具鏈 ![image](https://hackmd.io/_uploads/ryjaCwS1ge.png) - 使用 LLVM + Ninja ![image](https://hackmd.io/_uploads/ByRZkOHJxl.png) >[!Important] 讓原始碼與建置分離 把原始碼跟編譯目錄區分是很重要的,可以避免把編譯產物塞進原始碼資料夾,保持乾淨。也允許同時建立 Debug 和 Release 等多版本。遇到問題時,只需刪除 build 資料夾就能重建。是現代 CMake 專案的標準做法。 同時也會注意到:**使用 Visual Studio 工具鏈**的案例,他是生成一個 `hello.sln` 專案。是的,CMake的作法不是直接編譯程式,而是幫助你「負責產生 Makefile、Visual Studio 專案等原生建置腳本」,這也是為什麼還需要安裝 Visual Studio Buildtools。 當然,如果你不熟悉使用終端機的話,也可以用剛剛請你安裝的 CMake 延伸模組,點選「設定」後,會請你選擇套件。請先開啟Visual Studio Code 的設定,然後找到`cmake.buildDirectory` 填入 `${workspaceFolder}/build`,這樣預設編譯的目錄就會在當前專案的 `build` 資料夾下。 ![image](https://hackmd.io/_uploads/HJz2GOSyex.png) ![image](https://hackmd.io/_uploads/HycjedBygl.png) 然後在上方「專案狀態」的位置,會有一個選項是「刪除快取並重新設定」(齒輪旁邊) ![image](https://hackmd.io/_uploads/rk2ez_Hkee.png) 此時會看到專案大綱出現`main`,這個就是會幫你建置的執行檔案。在「專案大綱」Debug符號的右邊,就是建置所有專案的按鈕。 ### 加入搜尋路徑 接下來,稍微把例子變得複雜一點: - 建立一個 `project` 資料夾,放置原始碼 - `project/include` 放置所有的標頭檔案 - `project/implement` 放置所有的實作檔案 ![image](https://hackmd.io/_uploads/HJUyVdSkxl.png) ```cpp= /** * @file project/implement/add.cc */ #include "add.hh" int add(int a, int b) { return a + b; } ``` 現在 `main.cc` 會跳錯誤提示:「add.hh not found」,我們回到 `CMakeLists.txt`,修改成以下內容: ```cmake cmake_minimum_required(VERSION 3.28) project(hello) message(STATUS "PROJECT_SOURCE_DIR = ${PROJECT_SOURCE_DIR}") message(STATUS "CMAKE_CURRENT_SOURCE_DIR = ${CMAKE_CURRENT_SOURCE_DIR}") message(STATUS "CMAKE_CURRENT_BINARY_DIR = ${CMAKE_CURRENT_SOURCE_DIR}") # 加入搜尋路徑的檔案 include_directories(${CMAKE_CURRENT_SOURCE_DIR}/project/include) add_executable(main ${CMAKE_CURRENT_SOURCE_DIR}/project/main.cc ${CMAKE_CURRENT_SOURCE_DIR}/project/implement/add.cc ) ``` `include_directories`,就如同其字義:新增 `include` 路徑的資料夾。 而 `message` 不是必要的指令,他只是輸出 `configure` 階段的資訊,初學者只要知道 3 個變數就好: * `PROJECT_SOURCE_DIR` - 整個專案的原始碼根目錄 * `CMAKE_CURRENT_SOURCE_DIR` - **目前這個** CMakeLists.txt 所在的資料夾 * `CMAKE_CURRENT_BINARY_DIR` - 編譯時使用的路徑 因為在 `CMakeLists.txt` 中,`project` 是必要的指令,你也可以把 `PROJECT_SOURCE_DIR` 想成 `project()` 指令所在的檔案的資料夾。而 `CMAKE_CURRENT_SOURCE_DIR` 為什麼要強調「**目前這個**」,我們會在後面說明。 - 加入前: ![image](https://hackmd.io/_uploads/r1w2Sur1ex.png) - 加入後: ![image](https://hackmd.io/_uploads/SyAvHOBJxe.png) ### 修理 intellisense 你可能會注意到一個很詭異的事情:雖然編譯可以通過,但是語法提示還是會顯示找不到檔案。這是因為語法提示不是由 CMake 處理的,而是clangd提供的,如果你在上一節 VSCode 設定沒有弄好,就會出現該問題。你有幾個簡單的補救方式是: - 建立一個 `.clangd` 檔案,寫入以下內容: ```cpp CompileFlags: CompilationDatabase: build ``` 比較建議的做法,意義和上一節設定的 clangd 參數是一樣的: ``` --compile-commands-dir=${workspaceFolder}/build ``` 也就是嘗試搜尋專案資料夾的`build`資料夾,看看有沒有 `compile_commands.json` 檔案。`compile_commands.json`是實際上編譯器會調用的參數,其內容可能為: ![image](https://hackmd.io/_uploads/S1SFNFr1gx.png) clangd 的做法是解析編譯器的指令,然後再回推給語法提示。如果使用 clangd 當作語法提示引擎,CMake的設定檔一定要是 **clang-cl**。 - 或者,在 `.vscode/settings` 新增你的編譯路徑: ```json "clangd.fallbackFlags": [ "-I${workspaceFolder}/project/include2", "-I${workspaceFolder}/project/include" ] ``` 其原理是:`clangd.fallbackFlags` 的意義為「當 `clangd` 檢測 `compile_commands.json` 失敗時,會嘗試加入這些 Flag 來進行檢查」。那以該例子來說,就是把當前專案資料夾的`project/include2`跟`project/include1` 加入搜尋目錄。 ### 模組化 假定專案採用「以功能性區分檔案結構」,並設計了加減乘除四種模組: ![image](https://hackmd.io/_uploads/BJXDvtS1le.png) 當我們的專案越來越大,`include_directories` 和 `add_executable` 會越來越大。 我們必須得要有幾個處理方式 #### 蒐集所有檔案 ```cmake cmake_minimum_required(VERSION 3.28) project(hello) message(STATUS "PROJECT_SOURCE_DIR = ${PROJECT_SOURCE_DIR}") message(STATUS "CMAKE_CURRENT_SOURCE_DIR = ${CMAKE_CURRENT_SOURCE_DIR}") message(STATUS "CMAKE_CURRENT_BINARY_DIR = ${CMAKE_CURRENT_SOURCE_DIR}") # 加入搜尋路徑的檔案 include_directories( ${CMAKE_CURRENT_SOURCE_DIR}/project/sum ${CMAKE_CURRENT_SOURCE_DIR}/project/sub ${CMAKE_CURRENT_SOURCE_DIR}/project/mul ${CMAKE_CURRENT_SOURCE_DIR}/project/div ) # 加入這行 file(GLOB_RECURSE SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/project/*.cc") foreach(file IN LISTS SOURCES) message(STATUS "Find ${file}") endforeach() add_executable(main ${SOURCES} ) ``` 可以使用 `file(GLOB_RECURSE <變數名稱> <搜尋模式>)` 來匹配多個檔案,以該例子來說,就是匹配所有在 `project` 底下,以 `*.cc` 結尾的檔案。`foreach` 語法也只是列印出找到了那些檔案,並非必要的。 ![image](https://hackmd.io/_uploads/HkDnKKr1xg.png) #### 模組化建置與target-based 接下來要講的最重要的一個章節:模組化建置 在分離式編譯中,我們大概可以猜測到,當專案越大型,就會切割成多個模組進行編譯,此時合理作法是: ```cmake cmake_minimum_required(VERSION 3.28) project(hello) add_library(div ${CMAKE_CURRENT_SOURCE_DIR}/project/div/div.cc ) add_library(mul ${CMAKE_CURRENT_SOURCE_DIR}/project/mul/mul.cc ) add_library(sub ${CMAKE_CURRENT_SOURCE_DIR}/project/sub/sub.cc ) add_library(sum ${CMAKE_CURRENT_SOURCE_DIR}/project/sum/sum.cc ) add_executable(main ${CMAKE_CURRENT_SOURCE_DIR}/project/main.cc) target_link_libraries( main div mul sub sum ) target_include_directories( main PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/project/div ${CMAKE_CURRENT_SOURCE_DIR}/project/mul ${CMAKE_CURRENT_SOURCE_DIR}/project/sub ${CMAKE_CURRENT_SOURCE_DIR}/project/sum ) ``` 注意到我們使用了 `add_library`, `target_link_libraries`, `target_include_directories` 三個新參數 - `add_library`:建立一個函式庫 - `target_link_libraries`:當編譯該目標時,應該鏈結哪些函式庫 - `target_include_directories`:當編譯這個 target 時,要加上哪些 -I 目錄」,而且會根據 PRIVATE / PUBLIC / INTERFACE 控制傳遞性 這裡要先提及 cmake 中,絕大多數的方法,首個參數都是**target**。以該例子,會分別建立 5 個 **target** - main - div - mul - sub - sum 這些也同時會在你延伸模組的「專案大綱」顯示: ![image](https://hackmd.io/_uploads/SJ1pntr1xx.png) 這就是**目標導向**的編譯方式,這個範例很粗糙,因為 main 處理太多東西了,我們修改一下 ### 優化後的模組化 最後一個章節,將會把現代大型專案如何建構與模組化拆分作個範例: 首先把四個子模組加入到 `math` 資料夾 ![image](https://hackmd.io/_uploads/SyVZycHyel.png) 此外,分別建立 - `project/CMakeLists.txt`: ```txt cmake_minimum_required(VERSION 3.28) project(hello) add_subdirectory(math) add_executable(main ${CMAKE_CURRENT_SOURCE_DIR}/main.cc) target_link_libraries( main math ) ``` - `project/math/CMakeLists.txt`: ```cmake add_library(div ${CMAKE_CURRENT_SOURCE_DIR}/div/div.cc ) target_include_directories(div PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/div) add_library(mul ${CMAKE_CURRENT_SOURCE_DIR}/mul/mul.cc ) target_include_directories(mul PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/mul) add_library(sub ${CMAKE_CURRENT_SOURCE_DIR}/sub/sub.cc ) target_include_directories(sub PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/sub) add_library(sum ${CMAKE_CURRENT_SOURCE_DIR}/sum/sum.cc ) target_include_directories(sum PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/sum) add_library(math INTERFACE) target_link_libraries(math INTERFACE div mul sub sum) ``` #### `project/math/CMakeLists.txt` 的用途說明 1. 個別建立四個函式庫(div, mul, sub, sum) 每個子模組各自建立為 add_library() target, 並設定對應的 include 目錄讓外部可以使用 header(如 div.hh)。 2. 建立虛擬的整合函式庫 math math 本身不含任何程式碼,僅是個「轉接器」, 幫助 main 一次引入所有功能模組,也會自動繼承 include 路徑。 #### `project/CMakeLists.txt` 做了什麼 1. **加入子模組(數學函式庫)** ```cmake add_subdirectory(math) ``` > 將 math 子目錄下的 `CMakeLists.txt` 加入,載入 `div`, `mul`, `sub`, `sum` 等 library。 2. **設定主程式(可執行檔)** ```cmake add_executable(main ${CMAKE_CURRENT_SOURCE_DIR}/main.cc) ``` > 建立主程式 `main` 的執行檔。 3. **將主程式與 math 函式庫連結** ```cmake target_link_libraries(main math) ``` > 將 `main` 與整合後的 `math` 函式庫連結,包含所有 `*.hh` 的 include path 也會自動傳入。 ### 如何共用 在 CMake 中,我們可以用 `target_include_directories()` 加上 `PUBLIC`、`PRIVATE`、`INTERFACE`,來控制 **header 搜尋路徑** 要給誰用。 #### `target_link_libraries 的 PRIVATE 是:自己用就好` ```cmake target_include_directories(mylib PRIVATE include/) ``` 表示: > `mylib` 自己在編譯的時候會用到 `include/`, > 但別人 `target_link_libraries` 到它的時候不會自動得到這個路徑。 --- #### `target_link_libraries 的 PUBLIC 是:自己用,也給別人用` ```cmake target_include_directories(mylib PUBLIC include/) ``` 這表示: > `mylib` 編譯時會用到 `include/`, > 而且任何 `target_link_libraries` 到 `mylib` 的人,也會自動得到這個 include 路徑。 --- #### `target_link_libraries 的 INTERFACE 是:自己不用,只給別人用` ```cmake target_include_directories(mylib INTERFACE include/) ``` 這表示: > `mylib` 本身沒有用到 `include/`,但它會「幫忙轉交」這個路徑給使用者。 ## 總結 本篇教學從零開始,帶領學生建立一個跨平台的 C++ 開發環境,並逐步引導至模組化的專案架構設計。主要涵蓋以下幾個核心主題: ### 1. 開發環境的建置與工具鏈整合 - **工具選擇與安裝**:介紹了多種開發環境,包括 Visual Studio、WSL2 + VSCode、LLVM + CMake 等,並比較其優缺點。 - **必要套件安裝**:說明了 Visual Studio BuildTools、CMake、LLVM、Ninja 等工具的安裝與配置。 ### 2. 分離式編譯與模組化概念 - **分離式編譯的介紹**:透過簡單的程式範例,說明了分離式編譯的概念,並比較了不同編譯方式(如直接編譯、使用目標檔、建立函式庫)的差異。 - **模組化設計的實踐**:以數學函式庫為例,展示了如何將程式碼分割為多個模組,並透過 CMake 的 `add_library`、`target_include_directories` 等指令進行管理。 ### 3. CMake 的進階使用與實踐 - **CMake 指令的應用**:探討了 CMake 的指令使用,包括 `add_subdirectory`、`target_link_libraries` 等,並說明了 `PUBLIC`、`PRIVATE`、`INTERFACE` 的差異與使用時機。 - **專案結構的規劃**:強調了良好的專案結構對於維護與擴充的重要性,並提供了範例專案的目錄結構,作為學生參考。 在本篇教學中,我們不僅學習了如何建立一個 C++ 開發環境,更重要的是理解了專案規劃與模組化設計的核心理念。這些知識將為同學們未來的軟體開發之路打下堅實的基礎。 學習程式設計不僅是掌握語法,更是培養解決問題的能力。希望您能夠將所學應用於實際專案中,逐步成為一位具備專業素養的軟體工程師。