2016年にCを書くためのヒント



言語Cが武器だった場合



著者から:この記事の概要は2015年の初めに掲載されましたが、資料の出版には至りませんでした。 最後に、私のライティングデスクの引き出しには、上記の「ドラフト」のメリットはないと判断したので、元の形で注意を喚起します。 テキストで変更されているのは、2015年から2016年までの年のみです。



そして、必要な修正、説明、または苦情についてのコメントをいつでも喜んで聞きます。



だから記事...



Cでのプログラミングの最初のルールは、他のツールで問題がなければ使用しないことです。



別の方法を見つけることができないときは、プログラマーの現代の戒めを思い出してください。



Cプログラミング言語は1970年代初期から存在していました。 専門家は進化のさまざまな段階で「Cを研究」しなければならず、親しい関係者はしばしば行き止まりに至りました。 したがって、この言語のアルゴリズムを使用した最初の経験により、さまざまなプログラマーがワールドCについて独自の考えを持っていました。



Cでのプログラミングに直面して、「80年代/ 90年代に学んだ真実」のレベルで立ち往生しないことが非常に重要です。



この記事を読んだ場合、おそらく最新のプラットフォームで作業しており、現在の標準を順守しているため、古いソフトウェアに関する無限の規則を参照する必要はありません。 個々の企業がせいぜい20年前のシステムをアップグレードすることを気にしなかったため、古代の基準を永続させることは無意味です。



はじめに



標準C99(ここではC99-「1999年以降のプログラミング標準」、C11-「2011年以降のプログラミング標準」、つまり11> 99)。



clang, default









最適化



-O2、-O3。


通常、 -O2



が適していますが、 -O3



必要な場合-O3



ありますので、両方のオプション(異なるコンパイラーを含む)をテストし、最も効率的な実行可能ファイルを保存します。

-オス


-Os



は、キャッシュパフォーマンスで問題が発生したときに役立ちます(これは偶然ではありません)。



警告



-Wall -Wextra -pedantic





コンパイラの最新バージョンでは-Wpedantic



オプションが-Wpedantic



ますが、必要に応じて、特に-Wpedantic



を参照して、下位互換性の可能性を広げることができます。



テスト段階で、すべてのプラットフォームに-Werror



および-Wshadow



を追加します。



異なるプラットフォーム、コンパイラ、およびライブラリが警告を発行する可能性があるため、 -Werror



-Werror



すると、プログラミングプロセスが複雑になる場合があります。 これまでに遭遇したことのないプラットフォーム上のGCC



バージョン、新規および新規の悪意のある通知による攻撃のために、顧客の開発を無視したいとは思わない。



追加の楽しいオプションには、 Wstrict-overflow -fno-strict-aliasing



ます。



-fno-strict-aliasing



を有効にするか、オブジェクトが作成された形式でのみオブジェクトを操作できます。 Cでのプログラミングにはさまざまなエイリアスが使用されるため、ソースツリー全体を制御する必要が生じない限り、 -fno-strict-aliasing



を選択することをお-fno-strict-aliasing



します。



Clang



が使用している警告を送信しないようにするには、はい、適切な構文、 -Wno-missing-field-initializers



追加するだけです。



GCC 4.7.0



以降では、この奇妙な警告は削除されました。



開発



コンパイル単位


Cでプロジェクトを開発するには、ほとんどの場合、単純に各ソースファイルにオブジェクトファイルを割り当て、結果のオブジェクトを1つの全体にコンパイルします。 このようなスキームは段階的な開発に最適ですが、パフォーマンスと最適化に関しては最適とは言えません。 このアプローチでは、コンパイラは多くのオブジェクトファイルを分析して最適化の必要性を認識しません。



LTO-リンク時間の最適化


LTO



は「コンパイル単位の問題の一部としてソースの分析と最適化」を実行し、オブジェクトファイルの注釈を中間マークの形で作成します。これにより、オブジェクトをマージするプロセスでソースデータを適切に調整できます。



LTO



は、合併プロセスを大幅に遅くする可能性があります。 make -j



役立ちますが、開発が独立した無関係な最終エグゼキューター(.a、.so、.dylib、テスト実行可能ファイル、実行可能アプリケーションなど)で構成されている場合のみです。



clang LTO

GCC LTO



2016年までに、 clang gcc



は補助LTO



の作成を処理しました。これは、オブジェクトをコンパイルし、最終的にライブラリ/プログラム要素をマージするときにコマンドのリストに-flto



を追加することで利用できます。 ただし、 LTO



まだ目と目が必要です。 場合によっては、プログラムが直接実行されないが追加のライブラリーを介してコードを使用する場合、 LTO



は対応する関数またはコードを除外できます。これは、一般的な分析の過程で、ユーティリティが使用されていないことを検出するためです。つまり、製品の最終バージョンでは不要です。



Arch

-march=native






コンパイラーにプロセッサーのすべての機能を使用させ、覚えておいてください:パフォーマンステストと回帰テストは重要です(異なるコンパイラーおよび/またはそれらのバージョンの結果の比較分析が後に続きます)。それらの助けを借りて、最適化要素に悪影響がないことを確認できるためです。



-msse2 -msse4.2



は、他の開発者が用意したオプションを使用する場合に必要になる場合があります。



コード生成



種類


新しいコードでchar, int, short, long unsigned



ようなものを見つけた場合、ここにエラーがあります。

最新のプログラムでは、 #include <stdint.h>



を指定してから、標準のデータ型を選択する必要があります。

詳細な説明は、 stdint.h仕様にあります。

最も一般的な標準データ型には次のものがあります。





注意してください:これ以上char



。 通常、Cプログラミング言語では、 char



コマンドは呼び出されるだけでなく、誤って使用されます。



ソフトウェア開発者は、符号なしのバイト操作が実行される場合でも、 char



コマンドを使用して「バイト」を意味し続けます。 個々の符号なしバイト/オクテット値にuint8_t



を指定し、符号なしバイト/ オクテット値のシーケンスにuint8_t



*を選択する方がはるかに正確です。



intを参照する価値がありますか


読者の中には、単にint



崇拝していることを認めている人もいます。 データ型のサイズが必要に応じて変更された場合、正しくプログラムすることは技術的に不可能であることは注目に値します。



また、 inttypes.hの説明中に表明されたJustificationをチェックしてください。固定されていない幅の型を使用することが安全でない理由を明確に説明しています。 一部のプラットフォームでの開発プロセス中にint



16ビットであり、他のプラットフォームでint



32ビットであることにすでに気付いている場合、 int



使用ごとに16ビットと32ビットの問題領域もテストしました。同じ方法で続行できます。



次のパズルを実行するときに、マルチレベル構造のプラットフォームの技術的条件の複合体全体を頭の中で保持する知恵をまだ習得していない残りの人のために、固定幅タイプで停止することをお勧めします。追加の努力が必要です。 または、説明が簡潔に述べているように、「標準整数データを促進するためのISO C規則は、完全に予期しない変更につながる可能性があります。」



幸運は不可欠です。



char



使用しない」ルールの例外


2016年にchar



コマンドにアクセスできるのは、選択したAPIがchar



(たとえば、 strncat, printf'ing "%s", ...



)を要求した場合、または読み取り専用の文字列を指定した場合(たとえば、 const char *hello = "hello";



)、Cプログラミング言語では、文字列リテラル( "hello")はchar



[]のように見えるため。

さらに、C11はネイティブUnicodeのサポートを提供し、UTF-8文字列リテラルは、 const char *abcgrr = u8"abc";



ようなマルチバイトシーケンスを操作する必要がある場合でも、引き続きchar



使用しconst char *abcgrr = u8"abc";







{int,long,etc}



使用しない」というルールの例外


結果の型またはネイティブパラメーターを使用して関数にアクセスする場合は、関数クラスまたはAPIの特性に応じて型を使用します。



署名


コードでunsigned



を使用しようとしunsigned



でください。 これで、コンテンツを読みにくくするだけでなく、完成品を使用する効率性に疑問を投げかける多数のデータ型を使用して、厄介なCの規則なしにまともなコードを記述する方法がわかりました。 単純なuint64_t



制限できる場合、誰がunsigned long long int



を入力しますか? タイプ<stdint.h>のファイルは、より具体的かつ正確な意味を持ち、作者の意図をよりよく伝え、コンパクトです。これは、操作と読みやすさの両方にとって重要です。



整数ポインター


おそらくあなたの一人は、「しかし、ポインターなしでlong



、どうすればすべての数学がカバーされるでしょう!」と反対するでしょう。



もちろん、あなたはそのようなことを言うことができますが、だれが声明が真実であると言いますか?



この場合のポインターの正しいタイプはuintptr_t



で、ファイル<stdint.h>



によって設定されます。 同時に、非常に有用なptrdiff_t



stddef.h



によって定義されていることに注意することが重要です。



代わりに:

long diff = (long)ptrOld - (long)ptrNew;







使用:

ptrdiff_t diff = (uintptr_t)ptrOld - (uintptr_t)ptrNew;







そしてまた:

printf("%p is unaligned by %" PRIuPTR " bytes.\n", (void *)p, ((uintptr_t)somePtr & (sizeof(void *) - 1)));







システム依存のデータ型



「32ビットプラットフォームでは32ビットlong



が必要であり、64番目のプラットフォームでは64ビットが必要です!」



プラットフォームに応じてコードで2つの異なるサイズを使用する理由を説明するのが難しいと明確に判断する推論を省略した場合、最終的には、システム依存のデータ型に向けられたlong



に焦点を当てたくないと思います。



このような状況では、プラットフォームのポインター値を格納する整数データ型であるintptr_t



を参照するのが合理的です。



最新の32ビットプラットフォームでintptr_t



intptr_t



int32_t



変換されます。



最新の64ビットプラットフォームでは、 intptr_t



int64_t



の形式を取ります。



また、 intptr_t



uintptr_t



バリアントにあります。



ポインターオフセットに関する情報を保存するには、 ptrdiff_t



使用しptrdiff_t



-減算されたポインターのパラメーターを記憶できるのはこのデータ型です。



最大値



システム上の整数値を処理できる整数データ型を探していますか?



原則として、プログラマーは最もよく知られている代替手段、特に気取らないuint64_t



、あらゆる種類の値を格納するために任意の変数を使用できるおかげで、より効率的な技術的解決策があります。 整数データの安全な保存は、 intmax_t



(またはuintmax_t



)によって保証されます。 データの精度が影響を受けないことを確認して、任意の符号値intmax_t



委任できます。 同様に、 uintmax_t



によって委任された符号なし整数をuintmax_t



ます。



別のデータ型



広範囲にわたるシステム依存のデータ型について話している場合、 stddef.h



によって保証されているsize_t



がお気に入りのリストの1位になります。



実際、 size_t



は「巨大な配列インデックスを格納できる整数」のようなものです。つまり、作成中のプログラムの印象的な変位インジケーターを修正できます。



実際には、 size_t



は、sizeof演算子の結果タイプとして機能します。



いずれの場合でも、最新のプラットフォームでは、 size_t



uintptr_t



と実質的に同じ特性を持つため、32ビットバージョンでsize_t



size_t



uint32_t



、64ビットuint64_t



uint64_t



ます。



ssize_t



もありssize_t



。これは、ライブラリ関数の結果の型として使用される符号付きのsize_t



です。エラーの場合、1を取得します(注: ssize_t



はPOSIXパッケージに属し、Windowsには適していません)。



独自の関数のパラメーターを設定することで、システムに依存する任意のサイズにsize_t



を使用する価値はありますか? 技術的には、 size_t



sizeof



の結果タイプであるため、数量のサイズを特定のバイト数として決定する関数は、 size_t



の形式を取ることができます。



その他のアプリケーション: size_t



はmalloc関数の引数型であり、 ssize_t



read()



およびwrite()



の結果型ですread()



Windowsインターフェイスを除き、 ssize_t



提供されず、intのみが結果値に使用されます)。



印刷タイプ





印刷中にデータ型を参照しないでください。 inttypes.hのアドバイスに従って、常に適切なタイプポインターを使用してください。



このリストには以下が含まれます(もちろん、これは短い抜粋にすぎません):





PRI [udixXo] 64スタイルマクロのみを使用して、64ビットデータ型を印刷します。

なんで?



一部のプラットフォームでは、64ビット値はlong



関数で表され、他のプラットフォームではlong



表されますこれらのマクロは、さまざまなプラットフォームに最適な基本フォーマット特性を提供します。



これらのフォーマットマクロがないと、すべてのプラットフォームに適したフォーマット文字列を同時に作成することは事実上不可能です。データタイプはアクションに関係なく変化するためです(そして、印刷前に上記の値を設定することは安全であるだけでなく、論理的でないことも忘れないでください)。



intptr_t



-「%」PRIdPTR

uintptr_t



-「%」PRIuPTR

intmax_t



-「%」PRIdMAX

uintmax_t



-「%」PRIuMAX



PRI *形式指定子への追加:これらはマクロであり、特定のプラットフォームに応じて、適切なprintfクラス指定子に展開されます。 したがって、次を指定することはできません。



printf("Local number: %PRIdPTR\n\n", someIntPtr);







代わりに、マクロを扱っていることを知って、次のように記述します。



printf("Local number: %" PRIdPTR "\n\n", someIntPtr);







注:隣接するすべての行は1つの最終的な結合文字列リテラルでプリプロセッサによって結合されるため、%は書式文字列リテラルの本体に含まれますが、タイプポインターはその外側に留まります。



C99では、どこでも変数の説明を使用できます。



これは行いません:

 void test(uint8_t input) { uint32_t b; if (input > 3) { return; } b = input; }
      
      







代わりに、次のように記述します。

 void test(uint8_t input) { if (input > 3) { return; } uint32_t b = input; }
      
      







警告:プログラムのサイクルが制限されている場合は、イニシャライザーの位置を確認してください。 体系化されていない記述は、予期しない速度低下につながる場合があります。 加速化されていない通常のコード(実際にはほとんどの場合に使用されます)の場合、明確性に焦点を合わせることが最善です。 したがって、イニシャライザの作業を完了した直後にデータ型を定義することにより、読みやすさが著しく向上します。



C99では、forループを使用してインラインカウンターの説明を作成できます。



決して書かない:

 uint32_t i; for (i = 0; i < 10; i++)
      
      







正しいでしょう:

 for (uint32_t i = 0; i < 10; i++)
      
      







1つの例外:ループの終了後にカウンターの値を保持する場合は、もちろん、ループの本文に対応する説明を挿入しないでください。



最新のコンパイラは#pragma onceをサポートしています。



間違ったオプション:

 #ifndef PROJECT_HEADERNAME #define PROJECT_HEADERNAME . . . #endif /* PROJECT_HEADERNAME */
      
      







代わりに、使用します

#pragma once







#pragma once



、ヘッダーを1回だけ要求する必要があることをコンパイラーに通知するため、ヘッダーを保護するために余分な行を記述する必要はありません。 この関数は、すべてのコンパイラーおよび異なるプラットフォームでサポートされており、ヘッダーコードを手動で入力するよりもはるかに効率的なメカニズムです。

プラグマを1回サポートするコンパイラーのリストに、オプションの詳細な説明があります。



Cプログラミング言語では、自動的に作成された配列を静的に初期化できます。



だから私たちは書きません:

  uint32_t numbers[64]; memset(numbers, 0, sizeof(numbers));
      
      







正しいでしょう:

  uint32_t numbers[64] = {0};
      
      







Cで作業する場合、自動生成された構造を静的に初期化できます。



従来のエラー:

  struct thing { uint64_t index; uint32_t counter; }; struct thing localThing; void initThing(void) { memset(&localThing, 0, sizeof(localThing)); }
      
      







正しく:

  struct thing { uint64_t index; uint32_t counter; }; struct thing localThing = {0};
      
      







重要:構造体が内部アライメントを提供する場合、{0}メソッドはこの目的のために追加のバイトをゼロにしません。 そのため、たとえば、構造が1ワード単位で埋められるため、構造物のカウンターの後(64ビットプラットフォーム上)に4バイトのインデントがある場合に発生します。 未使用のインデントバイトを含む構造全体を無効にする必要がある場合は、8 + 4 = 12バイトしか使用できないにもかかわらず、sizeof(localThing)== 16バイトであるためmemset(&localThing, 0, sizeof(localThing))



指定します。



以前に割り当てられた構造を再初期化する必要がある場合は、以降の値の決定に共通のゼロ構造を使用します。

  struct thing { uint64_t index; uint32_t counter; }; static const struct thing localThingNull = {0}; . . . struct thing localThing = {.counter = 3}; . . . localThing = localThingNull;
      
      







C99(またはそれ以降)で作業する幸運があれば、基本的な「ゼロ構造」をいじる代わりに複合リテラルを選択できます( The New C:Compound Literals、2001を参照)。



複合リテラルを使用すると、コンパイラは一時的な匿名構造を自動的に作成し、対応する値フィールドにコピーできます。

localThing = (struct thing){0};







可変長の配列はC99で登場しました(C11では、必要に応じて選択できます)。



したがって、次のように記述しないでください(ミニチュア配列を扱う場合、または単に高速テストを行う場合)。

 uintmax_t arrayLength = strtoumax(argv[1], NULL, 10); void *array[]; array = malloc(sizeof(*array) * arrayLength); / *    ()     * /
      
      







代わりに、以下を示します。

  uintmax_t arrayLength = strtoumax(argv[1], NULL, 10); void *array[arrayLength]; /*     */
      
      







重要:通常の配列と同様に、可変長の配列が(規則として)スタック上に作成されます。 300万要素の通常の配列を静的に作成できない場合、この構文を使用して同じサイズの動的配列を生成しようとしないでください。 これらはスケーラブルなPython / Ruby自動リストではありません。 プログラムの起動中に配列の長さを設定し、スタックに対して大きすぎると判明した場合、混乱が始まります(機能不全、セキュリティ上の問題)。 可変長配列は、特定のタスクを実行するように設計された個々の状況に理想的ですが、すべてのタイプのソフトウェアを開発するために使用しないでください。 一度3つの要素の配列を生成する必要があり、もう1つは300万に達すると、可変長の配列を使用することにほとんど価値がありません。



はい、VLA構文を理解しておくと便利です(または、製品の1回限りの迅速なテストを行う必要がある場合)。 同時に、プログラム全体がクラッシュすると、こうした取り組みは悲劇になります。要素サイズをチェックするための正確なパラメーターを忘れるか、追加のスタックスペースがない不慣れなターゲットプラットフォームに直面しているという事実を見失うだけです。



: , arrayLength



– ( ; 4 ). ( ), , , , 99 VLA, malloc



.



: , , , VLA. - VLA , , , , , .



C99 .





, .



:

 void processAddBytesOverflow(uint8_t *bytes, uint32_t len) { for (uint32_t i = 0; i < len; i++) { bytes[0] += bytes[i]; } }
      
      





:

 void processAddBytesOverflow(void *input, uint32_t len) { uint8_t *bytes = input; for (uint32_t i = 0; i < len; i++) { bytes[0] += bytes[i]; } }
      
      







, . « », uint8_t



. , , , char *



, - . , void *



, , , , , .



, , , . , , . - Unaligned Memory Access (: , , , ).





C99 <stdbool.h>



, true 1, false — 0.

/ true or false, int32_t



, 1 0 (, , 1 -1; : 0 – success, 1 — failure? 0 – success, -1 — failure?).



, , , API , , . , , , « , ».



:

 void *growthOptional(void *grow, size_t currentLen, size_t newLen) { if (newLen > currentLen) { void *newGrow = realloc(grow, newLen); if (newGrow) { /*    */ grow = newGrow; } else { /*    ,     ,      */ free(grow); grow = NULL; } } return grow; }
      
      









:

 /*  : * - 'true'  newLen > currentLen     * -    'true'     ,      '*_grow' * - 'false'  newLen <= currentLen */ bool growthOptional(void **_grow, size_t currentLen, size_t newLen) { void *grow = *_grow; if (newLen > currentLen) { void *newGrow = realloc(grow, newLen); if (newGrow) { /*    */ *_grow = newGrow; return true; } /*     */ free(grow); *_grow = NULL; /*   , * 'true'     ,       */ return true; } return false; }
      
      







, , :

 typedef enum growthResult { GROWTH_RESULT_SUCCESS = 1, GROWTH_RESULT_FAILURE_GROW_NOT_NECESSARY, GROWTH_RESULT_FAILURE_ALLOCATION_FAILED } growthResult; growthResult growthOptional(void **_grow, size_t currentLen, size_t newLen) { void *grow = *_grow; if (newLen > currentLen) { void *newGrow = realloc(grow, newLen); if (newGrow) { /*    */ *_grow = newGrow; return GROWTH_RESULT_SUCCESS; } /*    ,   ,          */ return GROWTH_RESULT_FAILURE_ALLOCATION_FAILED; } return GROWTH_RESULT_FAILURE_GROW_NOT_NECESSARY; }
      
      







書式設定



, .



50 , - . , .



– .



, 2016 , , — clang-format. clang-format C-. , .



clang-format:

#!/usr/bin/env bash







clang-format -style="{BasedOnStyle: llvm, IndentWidth: 4, AllowShortFunctionsOnASingleLine: None, KeepEmptyLinesAtTheStartOfBlocks: false}" "$@"







( cleanup-format



):

matt@foo:~/repos/badcode% cleanup-format -i *.{c,h,cc,cpp,hpp,cxx}







-i , .



, :

 #!/usr/bin/env bash #  : clang-tidy      ,       #    . find . \( -name \*.c -or -name \*.cpp -or -name \*.cc \) |xargs -n1 -P4 cleanup-tidy # clang-format     ,      12 #  ()    . find . \( -name \*.c -or -name \*.cpp -or -name \*.cc -or -name \*.h \) |xargs -n12 -P4 cleanup-format -i
      
      







, cleanup-tidy. , , :

 #!/usr/bin/env bash clang-tidy \ -fix \ -fix-errors \ -header-filter=.* \ --checks=readability-braces-around-statements,misc-macro-parentheses \ $1 \ -- -I.
      
      







clang-tidy



— . :



readability-braces-around-statements



– if/while/for ;



, « » . . , , « !», – , - . , , , , , .



misc-macro-parentheses



– , .



clang-tidy



– , , , , . , clang-tidy



— , clang-format



, .





, , …



コメント



.





1000 (1500 ). ( ..), .





malloc


calloc



. . calloc(object count, size per object)



, #define mycalloc(N) calloc(1, N)



.



:







, , , .



, calloc()



, :



Benchmarking fun with calloc() and zero pages (2007)

Copy-on-write in virtual memory management



2016 - calloc()



( , 64 , , , ). « » , « », .



: calloc()



– , . calloc()



realloc()



, . . realloc()



, memset()



.



memset



( )


memset



(ptr, 0, len, () ( , ).

memset()



— , , ( {0} , ).



おわりに



, , . , , , , , RAM «».



, — , , - .



All Articles