我開始備這門課的時間是瑞典時間半夜
精神狀態很差
有錯誤請幫我主動提出
這種東西有什麼好教的 --- cjtsai
IDE
Editor + Compiler
OS
一般來說,我們在打競賽或是 APCS 寫的 C / C++ 程式,都只會有一個檔案
main.c
main.cpp
#include <stdio.h>
int main(void) {
printf("Hello, World!\n");
return 0;
}
#include <iostream>
int main() {
std::cout << "Hello, World!" << std::endl;
return 0;
}
有人可以出來完整解釋整個 hello world 嗎?
只有 main.c ,仍然不足以執行!
只有 main.c ,仍然不足以執行!
#include <stdio.h>
來看看這句
stdio.h
stdio.h
,我們才有 printf 可以用
在 stdio.h 裡面定義了 printf!
main.c
stdio.h
int printf(const char* restrict format, ...);
#include <stdio.h>
int main(void) {
printf("Hello, World!\n");
return 0;
}
只有 main.c ,仍然不足以執行!
#include <stdio.h>
來看看這句
stdio.h
stdio.h
,我們才有 printf 可以用
在 stdio.h 裡面定義了 printf!
main.c
int printf(const char* restrict format, ...);
int main(void) {
printf("Hello, World!\n");
return 0;
}
#include <iostream>
#include "person.h"
其中,用<>的話代表是系統標頭檔
例如電腦
的/usr/bin/include中會有一個iostream.h
用 "" 的則是你自己寫的標頭檔
,要放在同一個目錄底下,或是用 gcc 的特殊參數 -I但這樣直接編譯,「理論上」仍然不足以執行!
main.c
int printf(const char* restrict format, ...);
int main(void) {
printf("Hello, World!\n");
return 0;
}
為何?
誰幫你實作了 printf?
系統另一個地方寫好了 printf 的實作,變成一個 library !
換個例子
int add(int a, int b) {
return a + b;
}
int main() {
int a = 10;
int b = 5;
int c = add(a, b);
return 0;
}
main.c
換個例子
#include "add.h"
int main() {
int a = 10;
int b = 5;
int c = add(a, b);
return 0;
}
main.c
add.h
int add(int a, int b) {
return a + b;
}
換個例子
#include "add.h"
int main() {
int a = 10;
int b = 5;
int c = add(a, b);
return 0;
}
main.c
add.h
int add(int, int);
add.c
int add(int a, int b) {
return a + b;
}
How to compile?
gcc main.c add.c
How to compile?
gcc main.c add.c
但是實務上不推薦這樣做
Why?
So How?
(Static library)
我不會編譯,反正 atom 亂按一通就好 --- RepKiroNcA
沒有說過
#define
)#ifdef
)#include
的頭檔內容直接插入來源檔。指令:
gcc -E main.c -o main.i
# 新增 include 檔案搜尋路徑 (#include "header.h" 這種)
gcc -E main.c -o main.i -I ./path/to/include
# 定義 Macro
gcc -E main.c -o main.i -DHAHA # 相當於在程式裡面加上 #define HAHA
-std=<c89|c99|c11|c++98|c++11|c++17> 可以
指定語言標準
指令:
gcc -S main.c -o main.s
gcc -S main.c -o main.s -g # -g 代表產生除錯符號
gcc -S main.c -o main.s -O2 # -O1 -O2 -O3 分別為不同優化,-O0 不優化
把組語(.s
)轉成「目標檔」(object file,通常是 .o
或 .obj
),也就是二進位的機器碼塊。
object file 的檔案格式為 「elf file」
指令:
gcc -c main.c -o main.o # from .c
as main.s -o main.o # from .s
目標:製作靜態函式庫
在 Unix/Linux 下最常用的工具是 ar
,可以把多個 .o
檔案打包成一個 .a
檔
指令:
ar rcs libadd.a add.o
要注意的是,在 linux 裡面,靜態函式庫一定要是 lib 開頭,.a 結尾,中間為函式庫名稱
把多個目標檔(.o
)和函式庫合併,解析跨檔符號依賴,最終產生可執行檔或動態函式庫。
指令:
# -l<name>:指定連結函式庫(lib<name>.so / .a)
# -L<path>:指定函式庫搜尋路徑
# -static: 強制全部用靜態連結,執行時完全不需動態函式庫
gcc main.o add.o -o myapp -ladd -L./lib
# 連結成動態函式庫
gcc -fPIC -shared add.c -o libadddy.so
gcc main.o add.o -o myapp -ladddy -L./lib
# 檢查執行檔的動態函式庫依賴
ldd myapp
# 執行依賴動態函式庫的程式
export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
./run_shared
-I
-D
-E
預處理階段
連結階段
編譯階段
其他
-S
-c
-std
-g
-l
-L
-shared
-o
# 1. 靜態函式庫範例
gcc -Wall -O2 -std=c99 -c add.c -o add.o
ar rcs libadd.a add.o
# 2. 連結可執行檔
gcc main.c -L. -ladd -o run_static
# 3. 動態函式庫範例
gcc -fPIC -shared add.c -o libadd.so
gcc main.c -L. -ladd -o run_shared
# 4. 高階最佳化測試
gcc main.c -O0 -o app_O0
gcc main.c -O3 -march=native -o app_O3_native
# 比較執行時間
time ./app_O0
time ./app_O3_native
Try It Out
我們現在已經學會所有 C / C++ 編譯的方法了
理論上可以完全編譯出任何程式了
嗎?
gcc 一次只能編譯一個檔案
當檔案一多,會變得很麻煩
所以我們要有自動的工具,能夠用簡易的指令,按照預先定義好的規則,去編譯
最常用 / 古老的工具稱為
GNU Make
他的設計理念:依賴項
我們先來創一個新的專案
add 和 sub 定義了加法和減法函數
utils 定義了 print_result,可以列印一個算式
plugin 裡面有一個一些函數,會呼叫 math 的各種公式進行運算,然後用 print_result 輸出
main.c 會呼叫 plugin 和 math 的程式
何謂依賴?
a 的程式會用到 b
→ a 依賴 b
我們先來創一個新的專案
add 和 sub 定義了加法和減法函數
utils 定義了 print_result,可以列印一個算式
plugin 裡面有一個一些函數,會呼叫 math 的各種公式進行運算,然後用 print_result 輸出
main.c 會呼叫 plugin 和 math 的程式
誰依賴誰?
add.c, sub.c, utils.c 不依賴別人
plugin.c 依賴 add.c, sub.c, utils.c
main.c 依賴 add.c, sub.c, utils.c, plugin.c
我依賴蔡政廷之前的簡報
看完蔡政廷簡報,我們知道 makefile 怎麼寫了
現在來試著寫剛剛那個專案的
我希望將 add 和 sub 合併成 math 靜態函式庫
utils 也是靜態函式庫,而 plugin 弄成動態函式庫
我需要生成哪些東西?
5 個 object file, each for 1 c file
兩個靜態函式庫、一個動態函式庫
一個最終執行檔
5 個 object file, each for 1 c file
兩個靜態函式庫、一個動態函式庫
一個最終執行檔
.PHONY: all clean
all: myapp
# 編譯 object files
math/add.o:
math/sub.o:
utils.o:
plugin.o:
main.o:
# 靜態函式庫
libmath.a:
libutils.a:
# 動態函式庫
libplugin.so:
# 最終執行檔
myapp:
# 清理所有產出
clean:
來按照以上框架一步一步完成它吧
實作時間!
完整答案
.PHONY: all clean
all: myapp
# 編譯 object files
math/add.o: math/add.c math/add.h
gcc -Wall -std=c99 -O2 -c math/add.c -o math/add.o -I.
math/sub.o: math/sub.c math/sub.h
gcc -Wall -std=c99 -O2 -c math/sub.c -o math/sub.o -I.
utils.o: utils.c utils.h
gcc -Wall -std=c99 -O2 -c utils.c -o utils.o
plugin.o: plugin.c plugin.h math/add.h utils.h
gcc -Wall -std=c99 -O2 -fPIC -c plugin.c -o plugin.o
main.o: main.c math/add.h math/sub.h utils.h plugin.h
gcc -Wall -std=c99 -O2 -c main.c -o main.o
# 靜態函式庫
libmath.a: math/add.o math/sub.o
ar rcs libmath.a math/add.o math/sub.o
libutils.a: utils.o
ar rcs libutils.a utils.o
# 動態函式庫
libplugin.so: plugin.o libmath.a libutils.a
gcc -shared -o libplugin.so plugin.o libmath.a libutils.a
# 最終執行檔
myapp: libmath.a libutils.a libplugin.so main.o
gcc -Wall -std=c99 -O2 -o myapp main.o -L. -lplugin -lutils -lmath
# 清理所有產出
clean:
rm -f math/*.o *.o *.a *.so myapp
更多 make 技巧
這什麼垃圾東西 --- cjtsai
Make 很棒,但是有一個致命的缺點
還記得他的全名嗎?
GNU Make
看到 GNU ,就知道這東西是為了 GNU/Linux 開發的
所以跟 gcc 一樣,windows 上面原生沒有
對的,所以 windows 要用 gcc 或是 make,都需要另外裝 mingw 這種怪怪方式
那有沒有一種 Make,能夠針對不同平台,用不同方式建置呢
CMake!
我懶得偷了
來看個 U 質的影片
(抱歉是簡中,但這我找到最好的說明了)
最常用:在 linux 底下,變成 makefile
mkdir build && cd build
cmake ..
make
# 1. 指定 CMake 最低版本 & 專案名稱
cmake_minimum_required(VERSION 3.10)
project(MyApp LANGUAGES C)
# 2. 設定 C 標準
set(CMAKE_C_STANDARD 99)
set(CMAKE_C_STANDARD_REQUIRED ON)
# 3. 全域編譯旗標:啟用所有警告並最佳化到 O2
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -O2")
# 4. 結構化多子目錄
add_subdirectory(foo_dir)
add_subdirectory(bar_dir)
# 5. 定義函式庫
## 靜態庫
add_library(mylib STATIC lib.c)
## 動態庫
add_library(plugin SHARED plugin.c)
# 6. 定義可執行檔
## myapp 為執行檔名稱,後面列出要編譯的原始檔
add_executable(myapp main.c foo.c)
# 7. 設定 include 路徑
target_include_directories(myapp
PRIVATE
${CMAKE_SOURCE_DIR}/include
)
# 8. 連結函式庫
target_link_libraries(myapp
PRIVATE
mylib
plugin
)
CMAKE_SOURCE_DIR:
指向最上層(root)CMakeLists.txt 所在的原始碼目錄絕對路徑。
CMAKE_BINARY_DIR:
指向頂層 build 目錄(也就是你執行 cmake 時所在的資料夾)。
CMAKE_CURRENT_SOURCE_DIR:
指向正在處理的那一層 CMakeLists.txt 所在的原始碼資料夾。通常搭配 add_subdirectory() 後在子目錄裡使用,用以組合相對路徑。
CMAKE_CURRENT_BINARY_DIR:
指向與 CMAKE_CURRENT_SOURCE_DIR 相對應的 build 目錄(子目錄對應的 build 資料夾),方便在子目錄中輸出檔案。
CMAKE_INSTALL_PREFIX:
設定 make install 時安裝的根目錄(預設通常是 /usr/local)。
CMAKE_PREFIX_PATH:
用於輔助 find_package() 搜尋第三方庫的根路徑;指定後 CMake 會在這些路徑底下找「include」和「lib」。
cmake_minimum_required(VERSION 3.10)
project(MyApp C)
set(CMAKE_C_STANDARD 99)
set(CMAKE_C_STANDARD_REQUIRED ON)
# 把 include/ 加到搜尋路徑
include_directories(${PROJECT_SOURCE_DIR}/include)
# math 模組 → 靜態庫 libmath.a
add_library(math STATIC
src/math/add.c
src/math/sub.c
)
# utils 模組 → 靜態庫 libutils.a
add_library(utils STATIC
src/utils.c
)
# plugin 模組 → 共享庫 libplugin.so,link math + utils
add_library(plugin SHARED
src/plugin.c
)
target_link_libraries(plugin PRIVATE math utils)
# 最終執行檔 myapp,link plugin + utils + math
add_executable(myapp
src/main.c
)
target_link_libraries(myapp PRIVATE plugin utils math)
在實際讓你們開始實作 CMake 之前,想要先講講 C 的專案結構
C / C++ 的專案結構很自由,但是大部分大專案都是用以下三種架構的變形
定義一下名詞:
Module 指的是在大專案當中,可以獨立編譯的小部分(可以想成一個 library 之類的)
1. Source files 和 header files 依照 module 放在一起
2. Source files 和 header files 分開,再依照 module 放在一起
3. 在 module 底下分成 include 和 src
到目前為止我們已經可以
但是,在實際開發專案的時候,還會有別的問題要處理
還沒學會如何抄別人程式!
我是說 整合
根據前面的我們可以知道,如果我要使用別人的函式庫,在編譯時重點有三個
要知道他的 header 路徑在哪裡 (才能用 -I 參數)
要知道他的 Library 路徑在哪裡 (才能用 -L 參數)
要知道他的 Library 名字(才能用 -l 參數)
前面的坑
gcc 怎麼找到系統的預設函式的 library 和 header 在哪裡呢?
在 Linux 底下,系統 library 和 header 通常放在 /usr 路徑下
裡面有幾個比較需要知道的
bin:一堆執行檔
lib:一堆函式庫
include:一堆標頭檔
local:和 /usr 一樣的結構,但是是給使用者手動放東西的
所以在 Unix 世界當中,有個很簡單的法則
當你要安裝一個執行檔時,只要把它放進 /usr/bin 或是 /usr/local/bin 裡面就好
(雖然一般來說我們不會直接放進去,會用 symbolic link 的,但這不是我們今天要談的事)
但如果我今天想要將某個資料夾底下的所有檔案都可以直接呼叫怎麼辦呢?
例如在 mac 上,我用 brew 裝的東西都會在 /opt/homebrew/bin 裡面
所以實際上,系統會搜尋的路徑不單單只有 /usr/bin 或是 /usr/local/bin,而是一個列表
怎麼新增路徑進去呢?
一次性指令:export PATH=/path/to/bin:$PATH
但重新開一個 session 就會消失
多次:把 export 寫在 ~/.bashrc 或 ~/.zshrc 當中
所以到底
gcc 怎麼找到系統的預設函式的 library 和 header 在哪裡呢?
有幾個跟 $PATH 類似的變數
還有幾個寫死在 gcc 裡面的路徑
看到 gcc 搜尋路徑:
gcc -v -E -x c /dev/null
環境變數
CPATH
LIBRARY_PATH
LD_LIBRARY_PATH
有了這些之後,我們就知道怎麼引用別人函式庫了
下載或是編譯出 header 和 library
放到 /usr 目錄下,或是把編譯完的資料夾放入環境變數
開心編譯
怎麼聽起來還是那麼麻煩啊?
有沒有簡單一點的做法?
有...但不多
系統套件管理器(例如 apt)
brew 這種其他另外裝的套件管理器
vcpkg、conan(但都不太好用)
將原始碼用 cmake 整合進專案內
vcpkg 一點基本的使用
(因為我也不會)
1. 手動編譯並安裝
git clone https://github.com/microsoft/vcpkg.git ~/vcpkg
cd ~/vcpkg
./bootstrap-vcpkg.sh
2. 在 .bashrc 或是 .zshrc 加入這幾行
export VCPKG_ROOT="$HOME/vcpkg"
export PATH="$VCPKG_ROOT:$PATH"
export CMAKE_TOOLCHAIN_FILE="$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake"
3. 用 vcpkg install 來安裝東西,就可以正常用 cmake 編譯了
來嘗試安裝 fmt 並成功執行以下程式
#include <fmt/core.h>
int main() {
fmt::print("Hello, vcpkg + CMake!\n");
return 0;
}
來試著寫一個小專案吧
要求:
使用 CMake 建置
專案當中要包含至少一個靜態函式庫、一個動態函式庫、一個執行檔
需要安裝並使用至少一項以下這幾個工具,並在 readme 中寫下步驟
專案要整理乾淨、整潔、推到 github 上
希望你們還沒睡著 XD