課程內容#
函數的聲明與定義#
-
聲明:告訴系統有這個東西
- 傳入參數的變量名是什麼不重要,此時不需要標明
-
定義:具體是怎麼實現的
-
之前函數聲明和定義都是同時進行的
-
編譯順序:從上到下、從左到右
-
-
上:gcc 報錯信息;下:g++ 報錯信息(也許 g++ 報錯更友好)
-
看報錯的時候從上往下看,後面的錯誤可能就是由第一個錯誤產生的連鎖反應
-
-
函數未聲明和未定義暴露在兩個時期
- 函數未聲明錯誤 —— 編譯過程(主要是語法檢查)
- g++ -c *.cpp生成編譯後的對象文件
-
- 函數未定義錯誤 —— 鏈接過程
-
g++ *.o鏈接生成可執行程序
-
-
- 上面報錯信息來自於船長的 clang 編譯器,我們使用的是 g++ 編譯器,顯示有所不同
- 函數未聲明錯誤 —— 編譯過程(主要是語法檢查)
-
函數聲明可以多次,但定義只能一次!
頭文件與源文件#
- 規範
- 頭文件放聲明,源文件放定義
- 不應該都放在頭文件裡
- 頭文件與對應的源文件名稱一致
- 頭文件放聲明,源文件放定義
- 頭文件中條件式編譯,可以避免一次編譯過程中重複包含頭文件的問題
#ifndef _HEADER1_ // 名稱最好與頭文件名對應,雖沒有硬性要求
#define _HEADER1_
...
#else // 可以沒有
#endif // 必須有
工程開發規範與靜態鏈接庫#
- 可以將 #include 後的雙引號 "" 改成尖角號 <> 嗎?
- 雙引號 "":從執行代碼所在目錄下搜索
- 尖角號 <>:從系統庫路徑下搜索
- 使用 g++/gcc -I 添加頭文件路徑到系統庫路徑下
- 向上開發時
- 給別人頭文件(include 文件夾)、源文件對應的對象文件的包(lib 文件夾) 即可
- 對象文件打包
- 靜態鏈接庫(.a)
// 打包
ar -r libxxx.a header1.o header2.o header3.o
// 鏈接 g++ *.o -L -l
g++ test.o -L./lib -lxxx
//xxx 對應
-
-
- 動態鏈接庫(.so)
- 兩者實現的功能一致,都是打包
- 靜態庫與動態庫的區別- 牛客討論
-
makefile 工具#
-
文檔編譯工具,定位類似 markdown
-
封裝編譯流程,減小程序開發時編譯的複雜度
-
示例
-
-
.PHONY 開一個虛擬的環境,避免使用 make clean 時與路徑中存在的 clean 文件衝突
-
可以有封裝變量替換操作
-
初識谷歌測試框架#
-
單元測試
- 又稱模塊測試,是對程序模塊(軟件設計的最小單位)來進行正確性檢驗的測試工作
- 在過程化編程中,一個單元就是單個程序、函數、過程等
- 框架跟著語言走:C++、Python、Java...
- 又稱模塊測試,是對程序模塊(軟件設計的最小單位)來進行正確性檢驗的測試工作
-
C++ 實現的
-
cmake 工具
- 可根據本機的環境生成 makefile 文件
- 為什麼不直接使用 makefile 呢?makefile 對環境的要求很強
- 谷歌測試框架先 cmake 再 make 即可完成編譯,注意打包庫的位置
-
代碼(main.cpp)
-
-
用的尖括號 <> 包含的 gtest.h 頭文件
-
add2 只是個標識
-
斷言是什麼?
- 用來捕捉程序員自己的錯誤:假設某情況發生,但如果沒發生則作相應處理
- ASSERT_* 版本的斷言失敗時會產生致命失敗,並結束當前函數
- EXPECT_* 版本的斷言產生非致命失敗,而不會中止當前函數
-
-
makefile
-
-
可以用 -std=xxx 指定 C++ 版標準,本機其實不需要指定
-
需要使用 - I 添加頭文件路徑./lib
-
使用 - lpthread 額外連接 pthread 庫,mac 系統會自動連接
-
🆗疑問
-
- ①根據編譯器版本設置了默認標準,c++11 是比較低的版本
- ②可能用了 make install 之類的操作,將頭文件包含到系統庫目錄裡了
-
-
-
結果
-
⭐實現自己的測試框架#
- C 實現的
- 需要實現以下三個函數或宏
TEST | EXPECT_EQ | RUN_ALL_TEST | |
---|---|---|---|
功能 | 代表一個測試用例 | 測試用例裡的測試點 | 運行所有 TEST |
宏 / 函數 | 宏 | 函數或宏 | 函數或宏 |
注意點 | 沒有返回值類型; 與後面的花括號 {} 組成合法的函數定義的形式 | 一種斷言 | 返回值是 0 |
版本一:通過編譯,顯示測試結果#
-
haizei/test.h
-
-
無法使用 a##.##b
- 函數名中只能是下劃線、字母和數字,不能有 "."!
-
a##_haizei_##b
- 使用_haizei_或之類的特殊標識是為了防止 a 與 b 直接連接產生函數重名情況
- 如 (test, funcadd) 與 (testfunc, add)
-
⭐attribute((constructor))
- 設置函數屬性,在函數聲明或定義時使用
- 讓它後面的第一個函數在主函數執行前被自動調用執行
- 否則在main.cpp中執行 RUN_ALL_TESTS () 後直接結束程序,不會經過 TEST
- 參考函數屬性__attribute__((constructor)) 和__attribute__((destructor))-CSDN
-
-
haizei/test.cc
-
-
只需要象徵性定義一下,通過編譯即可
-
-
main.cpp
-
-
三組 TEST
-
-
makefile
-
-
使用 make 可以快速編譯
-
注意 - o 的使用,讓對象文件、可執行程序自定義命名放在指定目錄下
-
注意路徑中文件所在文件夾的指明
-
-
測試結果
-
-
❓當前版本下,main () 函數裡 return 的無論是 RUN_ALL_TESTS () 還是 0 都會顯示測試結果,如何讓 RUN_ALL_TESTS () 控制顯示的輸出與否呢?
版本二:RUN_ALL_TESTS () 開關#
-
實現框架初衷 —— 開關控制
-
需記錄的點
- 有多少組測試用例
- 測試用例對應的函數名字
- 測試用例對應的函數
- 使用函數指針變量
- 用數組記錄函數指針
-
haizei/test.h
-
-
TEST 中,在 main 函數執行前使用 add_function 將函數記錄到全局變量裡
-
typedef 的第二種用法:將變量提升為類型
-
結構體的使用:封裝函數指針和函數名
-
-
haizei/test.cc
-
-
全局變量的使用
-
strdup 的使用:參考C 語言 strdup () 函數:複製字符串
-
使用 malloc () 開辟空間拷貝字符串,返回其地址,即字符串指針;
最後記得使用 free () 釋放
-
- main.cpp 與 makefile 不變
- ❗已經實現了開關控制,下面可以對顯示、斷言等進行優化!
版本三:人性化優化#
① 給輸出添加顏色#
-
參考帶顏色的 printf-Blog
-
將顏色定義封裝成宏,在頭文件 haizei/test.h 裡定義
-
-
COLOR 正常
-
COLOR_HL 高亮
-
COLOR_UL 下劃線
-
多個字符串之間用空格即可連接
-
注意!顏色控制字符中 ";" 左右不能有空格
-
正確: "\033 [1;""31""m" "% s\n"
設置無效,啥都沒有:"\033 [1;""31""m" "% s\n"
② 添加斷言宏#
-
判斷不等於、大於、大於等於、小於、小於等於
-
就事論事型:對每個宏單獨實現
-
統一管理型:類似定義顏色宏,把共同的代碼再封裝一次
-
- 掌握 #的使用
-
③ 統計每組測試的成功和失敗的測試點個數,並顯示#
-
haizei/test.h
-
-
-
定義統計的結構體,統一管理,封裝性更好
-
在斷言處進行統計
-
這裡使用 extern 聲明結構體變量,因為
- 頭文件的斷言處用了該變量,需要有該變量的聲明
-
int i 既是聲明也是定義,extern int i 只是聲明
struct FunctionInfo haizei_test_info 既是聲明也是定義
只是聲明需在前面加 extern
-
-
- 但在頭文件不能定義變量,否則易導致重定義問題
- 參考C 語言正確使用 extern 關鍵字-CSDN
-
- haizei/test.cc
-
- 定義並聲明 haizei_test_info 變量
- 1.0 提升類型,100.0 放前面可能會溢出
- 100% 情況判斷:用一個極小值和 fabs 進行浮點數判等;成功數 == 統計數
- 居中對齊效果
- % m.nf:輸出共佔 m 列,其中有 n 位小數,如數值寬度小於 m 左端補空格
- %-m.nf:輸出共佔 n 列,其中有 n 位小數,如數值寬度小於 m 右端補空格
-
④ ⭐顯示失敗測試點的詳細信息#
-
主要編寫頭文件中,斷言宏中要執行的 LOG 宏
-
haizei/test.h
-
- ⭐actual 部分的結果值類型不確定,定義泛型宏
- _Generic (a, 替換規則):根據 a 的返回類型實現對應的替換
- _Generic 是 C 語言中的關鍵字,不是宏!在預處理階段不會替換成對應類型
- ① 在與 COLOR 宏連用時要十分小心!
- 在編譯階段,字符串與一個不知道是什麼的東西 (_Generic ()) 無法拼接
- ② 不能使用 C++ 編譯器
- ❗ 詳見下面的報錯一和報錯二
- ① 在與 COLOR 宏連用時要十分小心!
- 參考cpp_reference
- 使用 typeof 定義額外變量
- 所有運算部分通過額外變量,避免 ++ 操作帶來的多次運算
- 報錯一(編譯階段 -c)
-
- 對應錯誤寫法:把 TYPE (a) 寫在 YELLOW_HL 宏裡
-
- 紅框②可以正常輸出,但是會沒有顏色
- 如果像紅框①那樣在外層套一個顏色宏,編譯會報錯
- 對 main.c 進行預處理,不會報錯
- 查看上面紅框②預處理後的代碼,如下
-
- 原因:對於宏替換後的代碼,("字符串" _Generic () "字符串") 在編譯時報錯,連接不上,因為編譯器此時並不知道_Generic () 是啥
- _Generic () 需要在運行時才能知道結果,語法檢查時字符串和莫名的東西連接,所以報錯
- 和 printf () 的原型的第一個輸入參數類型為 const char * 沒有很大關係,但是類型不匹配會報警告
- 看下面這個簡單的例子也許就明白了:
- 頭文件
-
- 源文件
-
- 編譯
-
- 同樣的報錯
- 因為在編譯檢查語法階段,編譯器不知道 s 是個啥,和字符串 "a" 連接就會出錯
- 報錯提示的是想讓你把 s 踢出去,直接在 s 前面加括號
-
- 所以通過sprintf() 將_Generic () 包裝起來的方式很巧妙,在編譯階段沒有問題,在運行階段有值了自然也正常
-
- 報錯二(編譯階段 -c)
-
-
- 關鍵信息在第二張圖的 error
-
-
error: '_Generic' was not declared in this scope
*
* _Generic只支持C語言(C11),不支持C++
* 參考[如何啟用_Generic關鍵字](https://www.thinbug.com/q/28253867)-ThinBug
* 將所有文件後綴改為C語言的
* main.cpp → main.c;test.cc→test.c
* 修改makefile,見後
-
main.c
-
- 測試 double 類型數據,驗證泛型宏作用
- 修改了函數的參數類型為 double
- 其實 double 判等不能直接用 ==,在頭文件裡要判等方式,用差值與極小值
-
-
makefile
-
-
改用 gcc
-
-
輸出
-
⑤ 存儲函數的全局變量沒有測試用例數量限制#
- 靜態數組:在運行之前就開辟好了固定的空間大小,且存儲的物理空間連續
- 鏈表:思維上是順序的,但在物理存儲上不需要順序
- 由節點組成,包含:數據域、指針域
- 占用空間動態變化
- 但是更厲害的是下面的方式:可以給任何結構體安上鏈表的外骨骼
- ⭐⭐鏈表外骨骼
- haizei/test.h
-
- 直接在一個結構體中添加節點結構體變量 node,即鏈表結構的外骨骼
- node 記錄下一個節點(下一個 TEST 的 node)的地址
- 包含鏈表節點的頭文件 haizei/linklist.h
-
- haizei/linklist.h
- next 指向下一個節點的地址
- 但實際是想訪問下一個 TEST 的 func 和 str 字段
- 可以通過訪問下一個結構體的首地址再間接訪問兩個字段來完成
- 如何得到一個結構體的首地址
- 通過指針 p 對應字段 name 在結構體 T 中的偏移量來計算
- offset 宏!
- 用空指針得到 name 字段所在的地址
- (T *)(NULL)->name 得到的是 name 變量
- & 得到的是 T * 類型指針,存的是地址
- 轉 long 整型即可得到偏移量
- long類型會根據系統位數改變其範圍,對應指針大小
- 用空指針得到 name 字段所在的地址
- Head 宏!
- 將 p 指針的地址轉成 char * 類型
- 這樣 ±1 是按照最小單位 1 字節來偏移的
- p 是一個指針,name 是指針 p 在結構體 T 中對應的字段名
- haizei/test.c
-
- 尾插法,定義一個尾節點指針 func_tail
- 得到結構體首地址,利用 -> 間接訪問變量
- malloc () 和 calloc () 的主要區別
- 前者不能初始化所分配的內存空間,而後者默認將開辟的空間初始化為 0
-
- haizei/test.h
// 在堆區動態地分配一塊指定大小 size 的內存空間,用來存放數據
void* malloc (size_t size);
// 在堆區動態地分配 num 個長度為 size 的連續空間,並將每一個字節都初始化為 0
void* calloc (size_t num, size_t size);
-
-
- 同樣適用 strdup,複製一份字符串在新開辟的空間上,並返回其地址
- strdup 中 malloc 開辟的空間很容易忘記釋放:危險的 strdup 函數
- calloc、strdup 的空間需要自己去 free
-
- 上圖供參考
- ①free calloc 的 func 空間前先保存下一節點的地址
- 利用 p->next 即可
- ⭐②從里到外free 結構體變量
- func->str strdup 通過 malloc 出來的
- func calloc 出來的
- ③free 完讓指針指向 NULL,避免成為野指針
- 釋放 strdup 的 func->str 指向的空間時,需使用 (void *) 強轉一下
- 否則會有 free const char * 的警告,參考:In C, why do some people cast the pointer before freeing it?-Stackoverflow
- 正如上面鏈接中提到的,其實 free const 類型就很奇怪
- free 結構體要注意細節,詳見後文思考點:free 結構體的細節
- 查看 func 裡變量的地址
-
-
- 按 8 字節對齊
- 打印 func->str 打印的是 strdup 出來的地址,打印 &(func->str) 打印的是結構體對象中成員 str 的地址
-
-
- 同樣適用 strdup,複製一份字符串在新開辟的空間上,並返回其地址
-
⑥函數指針變量和函數名定義時的宏優化#
- 方式一:宏替換優化 NAME、STR2
-
- 方式二:宏嵌套 NAME、STR、_STR
- STR(NAME(a, b, _))
- 但是無法使用 '.' 連接生成函數名,可以使用 '_'
- a##.##b 在預處理階段報錯如下:
-
- 把 a.b 作為參數、變量名是非法的→. 有特殊含義
- 參考error: pasting “.” and “red” does not give a valid preprocessing token-StackOverflow
-
附加知識點#
-
函數聲明和主函數往上放,函數定義放後面,可以讓代碼框架、邏輯更清晰
-
簡易工程文件結構規範
- 使用 tree 工具
-
-
make 的規律
- 如果 makefile 中有依賴文件做了修改
- 直接 make,相關文件就會自動重新編譯,而可以不需要 make clean 做清理
- 如果只是修改了 makefile,而想重新生成對象文件
- 一般要先 make clean,再使用 make 重新生成新的對象文件,否則只是重新生成最頂層 all 的輸出
- 如果 makefile 中有依賴文件做了修改
-
可執行程序一般放在一個固定的目錄下:bin
-
宏內註釋
- 單行宏:可以在後面直接使用 // 註釋
- 多行宏:只能使用 /*...*/ 註釋
-
頭文件只寫函數的聲明
-
宏嵌套宏
- 當有 #或## 的時候,宏嵌套宏不能有效的展開,此時需要多加一層宏進行轉換
- 但只是 #和## 的地方停止了展開,其它地方會繼續展開
- 參考:C/C++ 中的宏的使用技巧 (宏嵌套 / 宏展開 / 可變參數宏)-CSDN
- 當有 #或## 的時候,宏嵌套宏不能有效的展開,此時需要多加一層宏進行轉換
-
⭐attribute((constructor)),詳見實現自己的測試框架 - 版本一
-
C 語言一行太長的換行處理-CSDN
-
❗ 宏定義中 #的細節
- 字符串化操作符
- 作用:將宏定義中的傳入參數名轉換成用一對雙引號括起來參數名字符串
- 只能用於有傳入參數的宏定義中,且必須置於宏定義體中的參數名前
-
__typeof__()、__typeof ()、typeof () 的區別-CSDN
- 推薦使用帶下劃線的
思考點#
- 宏函數可不可以重複定義?
- 函數定義放頭文件裡,不同文件多次編譯會出現函數重複定義的情況
- 而把定義成函數的宏扔在頭文件裡就沒事?
-
沒事
-
宏函數重複定義沒問題,如下
-
-
對於這種情況,不要發生函數重名(a##__haizei__##b)的情況
-
-
- 但是!宏不可以重定義,即不能修改之前的定義
- 宏定義不用考慮先後順序!&& 宏嵌套問題
- 當調用了一个宏時,直接替換宏即可
- 宏嵌套情況,參考C 語言宏替換的順序-CSDN
#define _ToStr(x) #x
#define __ToStr(x) _ToStr(x)
#define EarthQuake 9.0
int main(){
printf("%s\n", _ToStr(EarthQuake); // EarthQuake
printf("%s\n", __ToStr(EarthQuake); // 9.0
return 0;
}
-
- 替換順序
- 從外層到里層,但遇到 #或者 ## 就會停止展開
- 第一種:→#EarthQuake→"EarthQuake"
- 第二種
- 先替換第一層:→_ToStr (EarthQuake)→_ToStr (9.0)
- 再替換第二層:_ToStr (9.0)→"9.0"
- 嵌套定義:#define __ToStr (x) _ToStr (x)
- 嵌套調用:__ToStr (EarthQuake)
- 替換順序
- ❗free 結構體的細節
- free (p) 不會改變 p 變量本身的值,調用 free () 後它仍然會指向相同的內存空間,但是此時該內存已無效,不能被使用
- 所有動態分配的空間都需單獨釋放,從結構體里到外釋放
- 結構體在堆空間,而結構體內也有變量在堆空間,需要先 free 成員變量,最後 free 這個結構體
Tips#
- aka 的中文意思是 “又名”
- 不只是看船長秀,自己要思考怎麼優化?怎麼開發?怎麼變成自己的知識點?
- 堅持不住,才要堅持,往往這是最有價值的
- 編譯報錯時,從上到下看報錯信息,後面的報錯也可能源於前面