言語Cが武器だった場合
著者から:この記事の概要は2015年の初めに掲載されましたが、資料の出版には至りませんでした。 最後に、私のライティングデスクの引き出しには、上記の「ドラフト」のメリットはないと判断したので、元の形で注意を喚起します。 テキストで変更されているのは、2015年から2016年までの年のみです。
そして、必要な修正、説明、または苦情についてのコメントをいつでも喜んで聞きます。
だから記事...
Cでのプログラミングの最初のルールは、他のツールで問題がなければ使用しないことです。
別の方法を見つけることができないときは、プログラマーの現代の戒めを思い出してください。
Cプログラミング言語は1970年代初期から存在していました。 専門家は進化のさまざまな段階で「Cを研究」しなければならず、親しい関係者はしばしば行き止まりに至りました。 したがって、この言語のアルゴリズムを使用した最初の経験により、さまざまなプログラマーがワールドCについて独自の考えを持っていました。
Cでのプログラミングに直面して、「80年代/ 90年代に学んだ真実」のレベルで立ち往生しないことが非常に重要です。
この記事を読んだ場合、おそらく最新のプラットフォームで作業しており、現在の標準を順守しているため、古いソフトウェアに関する無限の規則を参照する必要はありません。 個々の企業がせいぜい20年前のシステムをアップグレードすることを気にしなかったため、古代の基準を永続させることは無意味です。
はじめに
標準C99(ここではC99-「1999年以降のプログラミング標準」、C11-「2011年以降のプログラミング標準」、つまり11> 99)。
clang, default
- デフォルトでは、clangはC11の拡張バージョン(
GNU C11
)を使用するため、最新のプログラム用の追加オプションは必要ありません。 -
C11
標準が必要な場合は、-std=c11;
指定します-std=c11;
C99標準を使用する場合は、std=c99
確認してください。 - clangは、
gcc
よりも高速にソースファイルをコンパイルします。 -
gcc
選択する場合、-std=c99
または-std=c11
を指定することが重要です -
gcc
はclang
よりもソースファイルの作成に時間がclang
ますが、より高速なコードを生成する場合があります。 パフォーマンスと回帰テストの結果の比較特性が示唆されます。 - デフォルトでは、
gcc-5
(clang
ような)GNU 11
モードで動作しますが、C11またはC99が必要な場合は、-std=c11
または-std=c99
を指定する必要があります。
最適化
-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仕様にあります。
最も一般的な標準データ型には次のものがあります。
-
int8_t, int16_t, int32_t, int64_t
符号付き整数。 -
uint8_t, uint16_t, uint32_t, uint64_t
符号なし整数; -
float
-32ビット浮動小数点標準。 -
double
-64ビット浮動小数点標準。
注意してください:これ以上
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のアドバイスに従って、常に適切なタイプポインターを使用してください。
このリストには以下が含まれます(もちろん、これは短い抜粋にすぎません):
- size_t-%zu
- ssize_t-%zd
- ptrdiff_t-%td
- ポインターの初期値は%pです(最新のコンパイラーでは、16進システムで表示されます。最初はポインターをvoid *に送信します)
- int64_t-"%" PRId64
- uint64_t-"%" PRIu64
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)
.
:
- alloc , ;
- calloc , ( , , 30- , ..);
- calloc(element count, size of each element) ;
- malloc() , , ;
- calloc valgrind, / , calloc 0;
- – . , , , ;
- calloc () , , malloc(), calloc(), — , , . , calloc() . , calloc(element count, size of each element).
, , , .
,
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 «».
, — , , - .