Try   HackMD

前言

有鑑於大多數的同學用的電腦是 Windows 的,有必要來教大家如何在 Windows 上面開發 C/C++ 程式,因為其中確實是有一些很常見的問題,於是我想首先帶大家走過一次架設環境的流程,再來回答大家常會問的問題。

要怎麼執行 C/C++ 程式 ?

首先,用大家最熟悉的 Hello world ! 來舉例:

#include <stdio.h>

int main(int argc, char *argv[]) {    
    printf("Hello world !\n");
    return 0;
}

相信不用我說明,大家都知道這段程式碼在幹嘛,但問題來了,我要怎麼讓這段程式碼運作呢 ?
在那之前,先來介紹兩種不同程式的運作方式:

編譯 (Compiler)

回到最根本的問題,電腦是如何執行程式的呢 ?

電腦看得懂的語言是二進位的機械碼 (Machine Code),編譯的意思就是讓原本是由英文單字和句子組成的文字轉換成機械碼的動作。

簡單來說就是這樣:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

而產生出來的 Machine Code 的檔案,我們稱之為 Binary (顧名思義裡面就是二進位機械碼),也會稱之為 Executable (執行檔),用途就是當你透過點擊或是命令呼叫這個檔案,它就會執行你當初寫在裡面的程式碼。

直譯 (Intepreter)

另一方面,直譯雖然也是讓程式碼變成二進位的機械碼來運行,但和編譯的順序不一樣。

直譯和編譯最大的差別在於,編譯是先將一個文字寫成的程式"編譯"成一個二進位的執行檔,爾後每當要執行這個程式就直接使用這個執行檔執行程式,並且每當程式有變動時就要重新編譯,不然那個執行檔只會繼續執行舊的程式;直譯則是直接將用文字寫成的程式檔案當作執行檔,也是透過點擊或命令呼叫以後,開始執行程式的內容,和編譯好的 Binary 之間的不同在於,文字寫成的程式檔在執行時是透過直譯器 (Intepreter) 及時地將文字轉換成機械碼再馬上執行。

想更了解的可以看看這部影片

回到正題

所以到底要怎麼執行 C/C++ 的程式 ?

C/C++ 是要預先編譯好才能透過 Executable 執行的語言,所以我們要做的事情就是將它編譯,帶到我們今天最大的重點:怎麼編譯 C/C++ 程式 ?

直接說結論,方法有兩種:

  1. 安裝 Virtual Studio (Microsoft 的 IDE),它都幫你包好需要的東西了,你只需要像個傻瓜一樣無腦用就可以了
  2. 學習今天要教的 GNU Tool Chain 當個聰明的工程師,以不變應萬變

Prerequisite (前置作業)

軟體下載

  1. Visual Studio Code
    文字編輯器,這個只是推薦而已,你想用什麼"編輯器"編輯你的程式都可以,如果你有種的話,Windows 的記事本甚至是 Words 都可以拿來當作文字編輯器。
    Image Not Showing Possible Reasons
    • The image was uploaded to a note which you don't have access to
    • The note which the image was originally uploaded to has been deleted
    Learn More →
  2. MSYS2
    GCC 在 Windows 系統的移植介面,提供你在 Windows 使用 GNU/Linux 命令的途徑。
    Image Not Showing Possible Reasons
    • The image was uploaded to a note which you don't have access to
    • The note which the image was originally uploaded to has been deleted
    Learn More →

只需要這兩個軟體就可以達到編譯 C/C++ 程式的最低要求了, 至於 GNU/Linux 又是什麼就是另一個故事了,改天有機會再介紹。

下載跟安裝的過程都已經 Cover 在上面的連結了,我就不再贅述,我要說明的是在安裝之後如何將兩者結合並使其運作。

初步設定

下載好軟體之後會有一些前置步驟要做,主要是為了讓文字編輯器跟編譯器可以彼此結合,成為一個整合好的開發環境。

MSYS2

先說 MSYS2:
下載好之後,要先安裝 GCC 的套件,在 MSYS2 有一個它自己的套件管理系統,叫做 Pacman,如果你有用過 Python 的 pip 套件的話用法就跟那個一樣,使用命令

$ pacman -S <name of the package> 

就可以安裝你想要的套件,只這次的例子來說我們要安裝的是 GCC,所以命令就會是

$ pacman -S gcc

說到這裡,你可能會有個疑問,什麼是 GCC?
簡單來說 GCC 就是我們今天會用到的編譯器本身,它就是可以將程式碼編譯成機械碼的工具本體,關於他的來歷有很多內容可以講,有興趣可以參閱我們萬用的 Wikipedia

還有一個套件也要安裝,這個是 MinGW-w64 toolchain,可以讓 VS Code 辨識到 MSYS2 底下的 GCC 的存在,不然沒有裝的話 VS Code 會抓不到 GCC,就無法使用了。

$ pacman -S --needed base-devel mingw-w64-x86_64-toolchain

下一個動作很重要,我們需要將有 GCC 執行檔的檔案路徑加入一個要做 PATH 的環境變數裡面。這邊簡單介紹一下什麼是環境變數 $PATH,當我們在 CLI 操作命令的時候,我們要怎麼界定哪些命令是通用的,哪些是特定場合才能用的,就是透過看這個命令的執行檔的所在位置是不是在環境變數的 $PATH 裡面,如果是的話才可以在任何檔案底下都可以操作這個命令。想更了解的請參閱這裡

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →
Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

透過選取系統 -> 系統資訊 -> 進階系統設定 來找到環境變數

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

或是你也可以透過 Windows 放大鏡直接搜尋找到

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

那要怎麼做呢?
首先打開你的設定 ⚙️,並進到 系統 💻 -> 系統資訊 ℹ︎,在資訊欄的底下 (Win 11) 或右邊 (Win 10) 有一個進階系統設定,點進去之後點選中間的進階,就可以在靠下方找到編輯環境變數,有分上下兩部分,上面是使用者環境變數,下面是系統環境變數,在這裡我們編輯使用者的 PATH 項目,將你的 MSYS2 的執行檔所在的檔案路徑新增進去,通常是C:\msys64\usr\bin\,這樣才可以使用放在 MSYS2 底下的執行檔。

Visual Studio Code

再來是 VS code:
首先你要先到擴充套件的商店 (雖然叫商店但你不用付錢) 去安裝 C/C++ 的套件,這個套件的功能主要是幫助你在寫 C/C++ 時有一些輔助功能,像是 Auto complete、文字上色、語法偵錯等功能,雖然不是能直接幫你編譯的工具,但可以大幅提升寫程式時的體驗。

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

再來是要進行編譯器的指定,這個是專案 wise 的動作,你需要進到你在進行的專案,並點選右上角的 Run or Debug,如果你是第一次點的話,他會跳出選項請你指定你要使用的編譯器,這時候如果你要編譯的是 C,請選擇 gcc,而如果你要編譯的是 C++ 的話,請選擇 g++。這個指定編譯器的設定會被記錄在該專案底下的 .vscode 資料夾底下的 tasks.json 裡面,以後如果你再打開這個專案就不用再次做設定,而如果你要變更編譯器的話也可直接刪掉這個檔案,重新做一次指定的動作就可以了。

檔案路徑有中文

說到這裡不得不提的一件事情是,由於 MSYS2 (或是 Mingw) 是只支援 ASCII 的編碼格式,因此在操作跟它有關的東西時如果牽涉到任何中文的字元的話,都會變成亂碼,而導致無法辨認。這對我們會有什麼影響呢?台灣人的 Windows 電腦會有兩個常見的中文檔案路徑名稱,桌面 & 使用者。

桌面

桌面的部分我們沒辦法像改一般資料夾那樣直接重新命名成 Desktop 就解決,因為『桌面』對於電腦來說是一個特殊的角色,是一個頭銜,因此我們如果對它重新命名,他只會改變顯示的名稱,而不會真正地改變它在系統裡面的名稱,所以由命令列呼叫還是會顯示中文。要能夠真正更改桌面的名稱要重新創建一個資料夾,並將『桌面』的頭銜讓渡給它,才能真正意義地讓有桌面頭銜的資料夾是你所取的名稱。

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

使用者

使用者的部分則是要進到控制台 -> 使用者帳號 -> 使用者帳號,並點選變更使用者名稱,來將你的中文使用者名稱改成英文的。

image

有一點要注意的是,只有本機帳戶才能夠更改名稱,所以如果你是用 Microsoft 帳戶登入的話是無法更改的,建議可以去帳戶設定那邊改成本機帳戶。

使用

其實照著上面的說明做完就可以在 Windows 電腦進行基本的 C/C++ 程式開發了,你只要在設定好之後點擊 Run or Debug,並選擇你要的選項,這裡說明一下 Run 和 Debug 的差異,Run 是直接編譯並執行;而 Debug 則是可以讓你設置斷點,並在程式執行的過程中觀察記憶體的內容,幫助你觀察程式和硬體的交互關係和狀態,有這些資訊就可以在有 Bug 的時候更容易地找到錯誤來源,善用的話是個挺方便的功能。

編譯 main.c

接續上面的步驟,其實就可以正常編譯 main.c/cpp 了,由於 GCC 本身內建標準函式庫,所以並不需要透過額外的命令參數去做標準函式庫的連結,直接點選 Run or Debug 就可以編譯了。

更進一步

除了編譯 main.c/cpp 以外,我們也會常常需要使用自己寫的 *.h,而在 IDE 裡面都已經幫你指定好你要放這些檔案的地方了,所以也沒有機會讓你認識手動連結函式庫的方法,在這邊順便說明。

目前 VS Code 好像不支援 GUI 的選擇 Local library 的 linkage,所以還是得自己手刻 CMake 去抓你要的函式庫。我先放官網的教學文件在這裡,網路上還有很多資源跟範例可以依樣畫葫蘆,如果只求可以運作的話這樣就夠了,而接下來我會用我自己的理解試圖解釋一遍。

先看範例 (來源):

Example 1

File hierarchy

Project1
├── CMakeLists.txt
├── f1.cpp
├── f1.h
├── f2.cpp
├── f2.h
└── main.cpp

CMakeLists.txt

cmake_minimum_required(VERSION 3.29) project("Project 01") # 新增一個 CMake 目標,目標型態為可執行檔。 add_executable(project-01) # 指定建置該 CMake 目標時所使用的來源檔案,不必包含標頭檔。 # PRIVATE之後列出來的檔案路徑是從 CMakeLists.txt 所在的目錄起始的相對 # 路徑。以空格區隔即可,換行加縮排是為了可讀性和方便編輯。 target_sources(project-01 PRIVATE "main.cpp" "f1.cpp" "f2.cpp" # "f3.cpp" # "f4.cpp" # ... )

main.cpp

#include "f1.h"
#include "f2.h"

int main () {
    // ...
}

Example 2

File hierarchy

Project2
├── CMakeLists.txt
├── include
│   ├── CMakeLists.txt
│   ├── f1.cpp
│   ├── f1.h
│   ├── f2.cpp
│   └── f2.h
└── main.cpp

Project2/CMakeLists.txt

cmake_minimum_required(VERSION 3.29) project("Project 02") # 新增一個 CMake 目標,目標型態為可執行檔。 add_executable(project-02) target_sources(project-02 PRIVATE "main.cpp" ) # 新增目標 project-02 的 Include 目錄 target_include_directories(project-02 PRIVATE "include/" ) # 將指定資料夾的 CMake 專案(含有 CMakeLists.txt)一起加入建置。 add_subdirectory("include/") # 新增目標 project-02 所連結的函式庫。函式庫名稱為其他 CMake 專案的目標名稱。通常來自 # find_package 或 add_subdirectory。以這個範例來說,include 函式庫是來自 # add_subdirectory 指令所加入的 CMake 專案。 target_link_libraries(project-02 PRIVATE include )

Project2/include/CMakeLists.txt

# 同等於在有 main 的檔案目錄底下的 add_executable()
add_library(include)

# 和 add_executable 一樣,需要連接源始碼檔
target_sources(inlcude f1.cpp f2.cpp)

main.cpp

#include "f1.h"
#include "f2.h"

int main () {
    // ...
}

Example 3

File hierarchy

Project3
├── CMakeLists.txt
├── include
│   ├── CMakeLists.txt
│   ├── f1.cpp
│   ├── f1.h
│   ├── f2.cpp
│   └── f2.h
└── main.cpp

Project3/CMakeLists.txt

cmake_minimum_required(VERSION 3.29) project("Project 03") # 新增一個 CMake 目標,目標型態為可執行檔。 add_executable(project-03) target_sources(project-03 PRIVATE "main.cpp" ) # 新增目標 project-03 的 Include 目錄 # target_include_directories(project-03 # PRIVATE # "include/" # ) # 將指定資料夾的 CMake 專案(含有 CMakeLists.txt)一起加入建置。 add_subdirectory("include/") # 新增目標 project-03 所連結的函式庫。函式庫名稱為其他 CMake 專案的目標名稱。通常來自 # find_package 或 add_subdirectory。以這個範例來說,include 函式庫是來自 # add_subdirectory 指令所加入的 CMake 專案。 target_link_libraries(project-03 PRIVATE include )

Project3/include/CMakeLists.txt

# 同等於在有 main 的檔案目錄底下的 add_executable()
add_library(include)

# 和 add_executable 一樣,需要連接源始碼檔
target_sources(inlcude f1.cpp f2.cpp)

main.cpp

#include "include/f1.h"
#include "include/f2.h"

int main () {
    // ...
}

說明

我們來看看出現的幾道指令
根檔案目錄的 CMakeLists.txt

# 指定 CMake 的版本,通常以你當下所使用的版本為基準
cmake_minimum_require(3.XX)

# 建立一個CMake專案,並給予專案名稱
project("project_name")

# 新增一個 CMake 目標,目標型態為可執行檔
# 語法:add_executable(執行檔名稱)
add_executable(project)
# 連接源始碼的檔案到執行檔,用於 main 所在的檔案目錄
# 語法:target_sources(執行檔名稱 源始碼檔案)
target_sources(project main.cpp f1.cpp)

# 有一點可以注意一下,add_executable() + target_sources() 
# 兩件事其實可以合併成一件事
add_executable(project main.cpp f1.cpp) # 這樣寫也可以

到這邊為止,討論了在同一個檔案目錄底下所會遇到的所有情境,緊接著有子檔案目錄的情境

# 目前為止已知
cmake_minimum_require(3.XX)
project("project_name")
add_executable(project)
target_sources(project main.cpp)

# 將指定資料夾的 CMake 專案(含有 CMakeLists.txt)一起加入建置
# 這個是必要條件,必須要讓 CMake 知道這是有函式庫的子檔案目錄
add_subdirectory("include/")

# 連結函式庫的物件,會從 add_subdirectory() 中尋找該物件
target_link_libraries(project include)

# (optional) 這個取決於一個細節
# main.cpp 當中的 #include 部分
# 若 #include "include/f1.h",則不需要此項
# 若 #include "f1.h" 則需要此項
# 可以理解為是否為 main 專案定義函式庫的檔案目錄位置
target_include_directories(project "include/")

子檔案目錄底下的 CMakeLists.txt

# 同等於在有 main 的檔案目錄底下的 add_executable()
add_library(include)

# 和 add_executable 一樣,需要連接源始碼檔
target_sources(inlcude f1.cpp f2.cpp)

# 和 add_executable() 一樣 
# add_library() + target_sources()
# 這兩著也可以合併成一個動作
add_library(include f1.cpp f2.cpp)

基本上上述案例就涵蓋了大多的專案的形式,用於寫作業或組建一些專案應該是夠用了,有任何問題可以再提出來一起討論

Build

除了預先寫好 CMakeLists.txt ,還要透過命令列才可以執行。
按照慣例,我們在組建專案執行檔的時候會將所有過程生成的檔案跟執行檔都放在一個 build 資料夾裡面,以方便劃分源始碼跟組建資料,讓組建過程在跨平台相容性上更簡單方便。

前情提要:現在的 File hierarchy 是

Project
├── CMakeLists.txt
├── include
│   ├── CMakeLists.txt
│   ├── f1.cpp
│   ├── f1.h
│   ├── f2.cpp
│   └── f2.h
└── main.cpp

而你正處於 Project/

# 創建 build 資料夾並移至其中
$ mkdir build && cd build

# 你現在正在 .../Project/build/
# 執行上一層檔案目錄的 CMakeLists.txt,也就是 Project/CMakeLists.txt
$ cmake ..

# 上一行命令會生成正式的組建文件,利用這些文件進行組建的通用命令
$ cmake --build .

經過上述步驟之後,新的 File hierarchy 會變成

Project
├── CMakeLists.txt
├── build   # build 資料夾,拿來裝自動生成的組建資料
│   ├── CMakeFiles
│   ├── src # executable 的所在位置
│   │   ├── ....      # 其他雜七雜八的檔案,不重要
│   │   └── project*  # 本專案的 executable
│   ├── cmake_install.cmake
│   └── CMakeCache.txt
├── include
│   ├── CMakeLists.txt
│   ├── f1.cpp
│   ├── f1.h
│   ├── f2.cpp
│   └── f2.h
└── main.cpp

而你要做的就是進到 .../Project/build/src/ 中,執行當中的 executable
以現在來說就是 project

執行的方式也是透過命令列操作:

$ ./project

然後程式就會執行了。

Exercise