コース内容#
関数の宣言と定義#
-
宣言:システムにこのものが存在することを知らせる
- 引数の変数名は重要ではなく、この時点では明示する必要はない
-
定義:具体的にどのように実装されているか
-
以前は関数の宣言と定義は同時に行われていた
-
コンパイル順序:上から下、左から右
-
-
上: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 ファイルとの衝突を避ける
-
カプセル化された変数置換操作が可能
-
Google テストフレームワークの初歩#
-
ユニットテスト
- モジュールテストとも呼ばれ、プログラムモジュール(ソフトウェア設計の最小単位)の正確性を検証するテスト作業
- プロセス指向プログラミングにおいて、単位は単一のプログラム、関数、プロセスなどである
- フレームワークは言語に従う:C++、Python、Java...
- モジュールテストとも呼ばれ、プログラムモジュール(ソフトウェア設計の最小単位)の正確性を検証するテスト作業
-
C++ で実装された
-
cmake ツール
- 自分の環境に基づいて makefile ファイルを生成できる
- なぜ直接 makefile を使用しないのか?makefile は環境に対する要求が厳しい
- Google テストフレームワークは 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 |
バージョン 1:コンパイルを通過し、テスト結果を表示#
-
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 () が出力の表示を制御するにはどうすればよいか?
バージョン 2:RUN_ALL_TESTS () スイッチ#
-
フレームワークの初期の目的 —— スイッチ制御
-
記録すべき点
- テストケースの数
- テストケースに対応する関数名
- テストケースに対応する関数
- 関数ポインタ変数を使用
- 配列で関数ポインタを記録
-
haizei/test.h
-
-
TEST 内で、メイン関数の実行前に add_function を使用して関数をグローバル変数に記録する
-
typedef の第二の使用法:変数を型に昇格させる
-
構造体の使用:関数ポインタと関数名をカプセル化
-
-
haizei/test.cc
-
-
グローバル変数の使用
-
strdup の使用:参考C 言語 strdup () 関数:文字列をコピー
-
malloc () を使用してメモリを確保し、文字列のポインタを返す;
最後に free () を使用して解放することを忘れない
-
- main.cpp と makefile は変更なし
- ❗スイッチ制御が実現され、次に表示、アサーションなどを最適化できる!
バージョン 3:人間工学的な最適化#
① 出力に色を追加#
-
色付き 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++ コンパイラを使用できない
- ❗ 下記のエラー 1とエラー 2 を参照
- ① COLOR マクロと連携する際は非常に注意が必要!
- cpp_referenceを参照
- typeof を使用して追加の変数を定義する
- すべての演算部分は追加の変数を通じて行い、++ 操作による多重計算を回避する
- エラー 1(コンパイル段階 -c)
-
- 対応する誤った書き方:TYPE (a) を YELLOW_HL マクロ内に書く
-
- 赤枠②は正常に出力されるが、色が付かない
- 赤枠①のように外側に色マクロを包むと、コンパイルエラーが発生する
- main.c を前処理するとエラーは発生しない
- 上記の赤枠②の前処理後のコードを確認すると、以下のようになる
-
- 理由:マクロ置換後のコードで、("文字列" _Generic () "文字列") はコンパイル時にエラーが発生し、結合できない。なぜなら、コンパイラはこの時点で_Generic () が何であるかわからないからである
- _Generic () は実行時に結果を知ることができ、構文チェック時に文字列と不明なものが結合されるため、エラーが発生する
- printf () のプロトタイプの最初の入力パラメータの型が const char * であることとはあまり関係がないが、型が一致しないと警告が表示される
- 以下の簡単な例を見れば理解できるかもしれない:
- ヘッダーファイル
-
- ソースファイル
-
- コンパイル
-
- 同様のエラー
- コンパイル時に構文チェック段階で、コンパイラは s が何であるかわからず、文字列 "a" と結合するとエラーが発生する
- エラーメッセージは s を取り除くように求めており、s の前に括弧を追加するように指示している
-
- したがって、sprintf() を使用して_Generic () をラップする方法は非常に巧妙で、コンパイル段階では問題がなく、実行段階で値があれば自然に正常に動作する
-
- エラー 2(コンパイル段階 -c)
-
-
- 重要な情報は第二の画像のエラーにある
-
-
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 が構造体 T 内のフィールド name のオフセットを計算する
- 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
// ヒープ領域に指定サイズのメモリ空間を動的に割り当て、データを格納する
void* malloc (size_t size);
// ヒープ領域に num 個のサイズの連続空間を動的に割り当て、各バイトを 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 型は非常に奇妙である
- 構造体を解放する際には細部に注意が必要であり、後の考察点:構造体を解放する際の細部を参照
- func 内の変数のアドレスを確認する
-
-
- 8 バイトに揃えられている
- func->str を印刷すると strdup されたアドレスが表示され、&(func->str) を印刷すると構造体オブジェクト内のメンバー str のアドレスが表示される
-
-
- 同様に strdup を使用して、文字列を新たに確保した空間にコピーし、そのアドレスを返す
-
⑥ 関数ポインタ変数と関数名の定義時のマクロ最適化#
- 方法 1:マクロ置換最適化 NAME、STR2
-
- 方法 2:マクロネスト 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))、詳細は自分のテストフレームワークの実装 - バージョン 1 を参照
-
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 する必要がある
ヒント#
- aka の日本語の意味は「別名」
- 船長のパフォーマンスを見るだけでなく、自分でどう最適化するか?どう開発するか?どう自分の知識点にするかを考えるべきである
- 持続できない場合は、続けるべきであり、往々にしてそれが最も価値のあるものである
- コンパイルエラーが発生した場合は、上から下にエラーメッセージを確認し、後のエラーも前のエラーに起因する可能性がある