Bo2SS

Bo2SS

7 工程項目開發

課程內容#

函數的聲明與定義#

  • 聲明:告訴系統有這個東西

    • 傳入參數的變量名是什麼不重要,此時不需要標明
  • 定義:具體是怎麼實現的

  • 之前函數聲明和定義都是同時進行的

  • 編譯順序:從上到下、從左到右

    • 圖片
    • 上: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 對應

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 實現的
  • 需要實現以下三個函數或宏
TESTEXPECT_EQRUN_ALL_TEST
功能代表一個測試用例測試用例裡的測試點運行所有 TEST
宏 / 函數函數或宏函數或宏
注意點沒有返回值類型;
與後面的花括號 {} 組成合法的函數定義的形式
一種斷言返回值是 0

版本一:通過編譯,顯示測試結果#

  • haizei/test.h

    • 圖片
    • 無法使用 a##.##b

      • 函數名中只能是下劃線、字母和數字,不能有 "."!
    • a##_haizei_##b

      • 使用_haizei_或之類的特殊標識是為了防止 a 與 b 直接連接產生函數重名情況
      • 如 (test, funcadd) 與 (testfunc, add)
    • attribute((constructor))

  • 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

使用 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

  • 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++ 編譯器
        • ❗ 詳見下面的報錯一和報錯二
      • 參考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
      • image-20210329094033910
      • next 指向下一個節點的地址
        • 但實際是想訪問下一個 TEST 的 func 和 str 字段
        • 可以通過訪問下一個結構體的首地址再間接訪問兩個字段來完成
      • 如何得到一個結構體的首地址
        • 通過指針 p 對應字段 name 在結構體 T 中的偏移量來計算
        • offset 宏!
          • 用空指針得到 name 字段所在的地址
            • (T *)(NULL)->name 得到的是 name 變量
            • & 得到的是 T * 類型指針,存的是地址
          • 轉 long 整型即可得到偏移量
            • long類型會根據系統位數改變其範圍,對應指針大小
        • Head 宏!
          • 將 p 指針的地址轉成 char * 類型
          • 這樣 ±1 是按照最小單位 1 字節來偏移的
          • p 是一個指針,name 是指針 p 在結構體 T 中對應的字段名
    • haizei/test.c
      • 圖片
      • 尾插法,定義一個尾節點指針 func_tail
      • 得到結構體首地址,利用 -> 間接訪問變量
      • malloc () 和 calloc () 的主要區別
        • 前者不能初始化所分配的內存空間,而後者默認將開辟的空間初始化為 0

// 在堆區動態地分配一塊指定大小 size 的內存空間,用來存放數據
void* malloc (size_t size);
// 在堆區動態地分配 num 個長度為 size 的連續空間,並將每一個字節都初始化為 0
void* calloc (size_t num, size_t size);

      • 同樣適用 strdup,複製一份字符串在新開辟的空間上,並返回其地址
      • calloc、strdup 的空間需要自己去 free
        • 圖片
        • 上圖供參考
        • ①free calloc 的 func 空間前先保存下一節點的地址
          • 利用 p->next 即可
        • ⭐②從里到外free 結構體變量
          • func->str strdup 通過 malloc 出來的
          • func calloc 出來的
        • ③free 完讓指針指向 NULL,避免成為野指針
        • 釋放 strdup 的 func->str 指向的空間時,需使用 (void *) 強轉一下
        • free 結構體要注意細節,詳見後文思考點:free 結構體的細節
        • 查看 func 裡變量的地址
          • 圖片
          • 圖片
          • 按 8 字節對齊
          • 打印 func->str 打印的是 strdup 出來的地址,打印 &(func->str) 打印的是結構體對象中成員 str 的地址

⑥函數指針變量和函數名定義時的宏優化#

附加知識點#

  • 函數聲明和主函數往上放,函數定義放後面,可以讓代碼框架、邏輯更清晰

  • 簡易工程文件結構規範

    • 使用 tree 工具
  • 圖片
  • make 的規律

    • 如果 makefile 中有依賴文件做了修改
      • 直接 make,相關文件就會自動重新編譯,而可以不需要 make clean 做清理
    • 如果只是修改了 makefile,而想重新生成對象文件
      • 一般要先 make clean,再使用 make 重新生成新的對象文件,否則只是重新生成最頂層 all 的輸出
  • 可執行程序一般放在一個固定的目錄下:bin

  • 宏內註釋

    • 單行宏:可以在後面直接使用 // 註釋
    • 多行宏:只能使用 /*...*/ 註釋
  • 頭文件只寫函數的聲明

  • 宏嵌套宏

  • attribute((constructor)),詳見實現自己的測試框架 - 版本一

  • C 語言一行太長的換行處理-CSDN

  • ❗ 宏定義中 #的細節

    • 字符串化操作符
    • 作用:將宏定義中的傳入參數名轉換成用一對雙引號括起來參數名字符串
    • 只能用於有傳入參數的宏定義中,且必須置於宏定義體中的參數名前
  • __typeof__()、__typeof ()、typeof () 的區別-CSDN

    • 推薦使用帶下劃線的

思考點#

  • 宏函數可不可以重複定義
    • 函數定義放頭文件裡,不同文件多次編譯會出現函數重複定義的情況
    • 而把定義成函數的宏扔在頭文件裡就沒事?
      • 沒事

      • 宏函數重複定義沒問題,如下

        • 圖片
        • 對於這種情況,不要發生函數重名(a##__haizei__##b)的情況

    • 但是!宏不可以重定義,即不能修改之前的定義
  • 宏定義不用考慮先後順序!&& 宏嵌套問題
#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 的中文意思是 “又名”
  • 不只是看船長秀,自己要思考怎麼優化?怎麼開發?怎麼變成自己的知識點?
  • 堅持不住,才要堅持,往往這是最有價值的
  • 編譯報錯時,從上到下看報錯信息,後面的報錯也可能源於前面

課程速記#

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。