Bo2SS

Bo2SS

7 プロジェクト開発

コース内容#

関数の宣言と定義#

  • 宣言:システムにこのものがあることを知らせる

    • 引数の変数名は重要ではなく、この時点では明示する必要はない
  • 定義:具体的にどのように実装されているか

  • 以前は関数の宣言と定義は同時に行われていた

  • コンパイル順序:上から下、左から右

    • 画像
    • 上:gcc のエラーメッセージ;下:g++ のエラーメッセージ(g++ のエラーメッセージはより親切かもしれない)

    • エラーメッセージを見るときは上から下に見る、後のエラーは最初のエラーから生じた連鎖反応かもしれない

  • 関数が未宣言および未定義であることが2 つの時期に露呈する

    • 関数未宣言エラー —— コンパイルプロセス(主に構文チェック)
      • g++ -c *.cppでコンパイル後のオブジェクトファイルを生成
      • 画像
    • 関数未定義エラー —— リンクプロセス
      • g++ *.oでリンクして実行可能プログラムを生成

      • 画像
    • 上記のエラーメッセージは船長の clang コンパイラからのもので、私たちが使用しているのは g++ コンパイラで、表示が異なる
  • 関数の宣言は複数回行うことができるが、定義は一度だけ行える!

ヘッダーファイルとソースファイル#

  • 規範
    • ヘッダーファイルには宣言を、ソースファイルには定義を置く
      • すべてをヘッダーファイルに置くべきではない
    • ヘッダーファイルと対応するソースファイルの名前は一致する
  • ヘッダーファイル内の条件付きコンパイルは、1 回のコンパイルプロセス中のヘッダーファイルの重複包含の問題を回避できる
#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 ファイルとの衝突を避ける

    • 変数置換操作をカプセル化することができる

Google テストフレームワークの初歩#

  • ユニットテスト

    • モジュールテストとも呼ばれ、プログラムモジュール(ソフトウェア設計の最小単位)の正確性を検証するテスト作業
      • プロセス指向プログラミングでは、1 つのユニットは単一のプログラム、関数、プロセスなどである
    • フレームワークは言語に従う: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 で実装された
  • 次の 3 つの関数またはマクロを実装する必要がある
TESTEXPECT_EQRUN_ALL_TEST
機能テストケースを表すテストケース内のテストポイントすべての TEST を実行
マクロ / 関数マクロ関数またはマクロ関数またはマクロ
注意点戻り値の型はない;
後の波括弧 {} と合法的な関数定義の形式を構成する
一種のアサーション戻り値は 0

バージョン 1:コンパイルを通過し、テスト結果を表示#

  • haizei/test.h

    • 画像
    • a##.##b は使用できない

      • 関数名にはアンダースコア、文字、数字のみが含まれ、"." を含めることはできない!
    • a##_haizei_##b

      • _haizei_のような特殊識別子を使用するのは、a と b が直接接続されて関数名の重複が発生するのを防ぐためである
      • 例 (test, funcadd) と (testfunc, add)
    • attribute((constructor))

  • haizei/test.cc

    • 画像
    • 象徴的に定義するだけで、コンパイルを通過できる

  • main.cpp

    • 画像
    • 3 つの 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

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 を付ける

  • 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 を参照
      • 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)
      • 画像
      • 画像
      • 重要な情報は 2 枚目の画像のエラー

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 フィールドにアクセスしたい
        • 次の構造体の先頭アドレスにアクセスしてから、2 つのフィールドに間接的にアクセスすることで実現できる
      • 構造体の先頭アドレスを取得する方法
        • ポインタ p が構造体 T 内のフィールド name のオフセットを計算する
        • offset マクロ!
          • NULL ポインタを使用して 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 に初期化する

// ヒープ領域に指定サイズのメモリ空間を動的に確保し、データを格納する
void* malloc (size_t size);
// ヒープ領域に num 個のサイズの連続した空間を動的に確保し、各バイトを 0 に初期化する
void* calloc (size_t num, size_t size);

      • 同様に strdup を使用して文字列を新たに確保したスペースにコピーし、そのアドレスを返す
      • calloc、strdup のスペースは自分で free する必要がある
        • 画像
        • 上の図を参考
        • ①free calloc の func 空間を解放する前に次のノードのアドレスを保存
          • p->next を利用する
        • ⭐②内側から外側へ構造体変数を free する
          • func->str は strdup を通じて得られた
          • func は calloc を通じて得られた
        • ③free が完了したらポインタを NULL に指すようにし、野良ポインタを避ける
        • strdup の func->str が指す空間を解放する際は、(void *) にキャストする必要がある
        • 構造体を解放する際は細部に注意し、後の考察点:構造体を解放する際の細部を参照
        • func 内の変数のアドレスを確認
          • 画像
          • 画像
          • 8 バイトに揃える
          • func->str を印刷すると strdup から得たアドレスが表示され、&(func->str) を印刷すると構造体オブジェクト内のメンバー str のアドレスが表示される

⑥ 関数ポインタ変数と関数名の定義時のマクロ最適化#

  • 方法 1:マクロ置換最適化 NAME、STR2
    • 画像
  • 方法 2:マクロネスト NAME、STR、_STR
    • STR(NAME(a, b, _))
    • しかし、'.' を使用して関数名を生成することはできず、'_' を使用することができる
    • a##.##b は前処理段階で以下のようなエラーを引き起こす:

付加知識点#

  • 関数の宣言とメイン関数を上に置き、関数の定義を後に置くことで、コードのフレームワークとロジックをより明確にすることができる

  • 簡易プロジェクトファイル構造規範

    • tree ツールを使用
  • 画像
  • make の規則

    • makefile 内に依存ファイルが変更された場合
      • 直接 make を実行すると、関連ファイルが自動的に再コンパイルされ、make clean を行う必要はない
    • ただ makefile を変更しただけで、新しいオブジェクトファイルを再生成したい場合
      • 一般的にはまず make clean を実行し、その後 make を使用して新しいオブジェクトファイルを再生成する、さもなければ最上位の all の出力だけが再生成される
  • 実行可能プログラムは一般的に固定のディレクトリに配置される:bin

  • マクロ内のコメント

    • 単行マクロ:後ろに直接 // コメントを使用できる
    • 複数行マクロ:/*...*/ コメントのみ使用できる
  • ヘッダーファイルには関数の宣言のみを書く

  • マクロネストマクロ

  • attribute((constructor))、詳細は自分のテストフレームワークの実現 - バージョン 1 を参照

  • C 言語の 1 行が長すぎる場合の改行処理-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 の日本語の意味は「別名」
  • 船長のパフォーマンスを見るだけでなく、自分がどう最適化するか、どう開発するか、どう自分の知識点にするかを考えるべきである
  • 持続できない場合は、続けるべきである、往々にしてこれが最も価値のあるものである
  • コンパイルエラーが発生した場合は、上から下にエラーメッセージを確認し、後のエラーも前のエラーに起因する可能性がある

コース速記#

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。