2016年のhabr記事Cでの書き方のヒントに関するセンセーショナルな記事の著者であるMatt Stancliffによるブログの投稿の翻訳を提供します。
ここで、Mattは
const
修飾子の知識を共有しています。 反抗的な見出しにもかかわらず、おそらくここで説明されているものの多くはあなたに知られているでしょうが、何か新しいものもあることを望みます。
良い読書をしてください。
const
for Cを使用するためのすべてのルールを知っていると思いますか? もう一度考えてください。
定数の基本
スカラー変数
Cの単純な
const
ルールに精通している。
const uint32_t hello = 3;
const
before
hello
は、 コンパイル中に
hello
が変更されないことを確認することを意味します。
hello
を変更またはオーバーライドしようとすると、コンパイラーはユーザーを停止します。
clang-700.1.81: error: read-only variable is not assignable hello++; ~~~~~^ error: read-only variable is not assignable hello = 92; ~~~~~ ^ gcc-5.3.0: error: increment of read-only variable 'hello' hello++; ^ error: assignment of read-only variable 'hello' hello = 92; ^
さらに、Cは
const
が識別子の前にある限り、どこにあるかについてあまり心配していないため、
const uint32_t
宣言
const uint32_t
と
uint32_t const
同一です。
const uint32_t hello = 3; uint32_t const hello = 3;
プロトタイプのスカラー変数
次の関数のプロトタイプと実装を比較します。
void printTwo(uint32_t a, uint64_t b); void printTwo(const uint32_t a, const uint64_t b) { printf("%" PRIu32 " %" PRIu64 "\n", a, b); }
const
修飾子を持つスカラーパラメーターが
printTwo()
関数の実装で指定されていて、プロトタイプに指定されていない場合、コンパイラーは誓いますか?
いや。
スカラー引数の場合、プロトタイプと関数の実装で
const
修飾子が一致しないことは完全に正常です。
なぜそれが良いのですか? すべてが非常に単純です。関数はスコープの外で
b
と
b
変更できないため、
const
は渡すものに影響を与えません。 あなたのコンパイラは、それが
a
と
b
コピーで
a
ことを理解するのに十分賢いので、この場合、
const
有無はプログラムの物理的または精神的なモデルに影響を与えません。
コンパイラは、値によって関数にコピーされ、転送された変数の元の値は常に変更されないため、ポインターまたは配列ではないパラメーターの
const
修飾子の不一致を気にしません。 。
ただし、コンパイラーはポインターまたは配列であるパラメーターの
const
不一致について文句を言います。この場合、関数は渡されたポインターによって参照されるデータを操作できるからです。
配列
配列全体に
const
を指定できます。
const uint16_t things[] = {5, 6, 7, 8, 9};
const
は、型宣言の後に指定することもできます。
uint16_t const things[] = {5, 6, 7, 8, 9};
things[]
を変更しようとすると、コンパイラは次のことを停止します。
clang-700.1.81: error: read-only variable is not assignable things[3] = 12; ~~~~~~~~~ ^ gcc-5.3.0: error: assignment of read-only location 'things[3]' things[3] = 12; ^
構造
従来の構造
構造全体に
const
を指定できます。
struct aStruct { int32_t a; uint64_t b; }; const struct aStruct someStructA = {.a = 3, .b = 4};
または:
struct const aStruct someStructA = {.a = 3, .b = 4};
someStructA
メンバーを変更しようとした場合:
someStructA.a = 9;
エラーが発生します
someStructA
const
として宣言されます。 決定後にメンバーを変更することはできません。
clang-700.1.81: error: read-only variable is not assignable someStructA.a = 9; ~~~~~~~~~~~~~ ^ gcc-5.3.0: error: assignment of member 'a' in read-only object someStructA.a = 9; ^
const内部構造
構造体の個々のメンバーに
const
を指定できます。
struct anotherStruct { int32_t a; const uint64_t b; }; struct anotherStruct someOtherStructB = {.a = 3, .b = 4};
someOtherStructB
メンバーを変更しようとした場合:
someOtherStructB.a = 9; someOtherStructB.b = 12;
b
が変更されたときにのみエラーが発生します。
b
const
として宣言されます:
clang-700.1.81: error: read-only variable is not assignable someOtherStructB.b = 12; ~~~~~~~~~~~~~~~~~~ ^ gcc-5.3.0: error: assignment of read-only member 'b' someOtherStructB.b = 12;
const
修飾子を使用して構造体のインスタンス全体を宣言することは、すべてのメンバーが
const
として定義されている構造体の特別なコピーを宣言することと同等です。 100%
const
構造が必要ない場合は、必要な場合にのみ、構造を宣言するときに特定のメンバーに対してのみ
const
を指定できます。
ポインタ
ポインターの
const
は、楽しみの始まりです。
単一のconst
例として整数ポインターを使用してみましょう。
uint64_t bob = 42; uint64_t const *aFour = &bob;
これはポインターであるため、ここには2つのリポジトリーがあります。
- データウェアハウス-
bob
-
bob
指す4aFour
ポインターを格納する
それで、
aFour
何ができるでしょうか? いくつか試してみましょう。
彼が指し示す価値は変えられると思いますか?
*aFour = 44;
clang-700.1.81: error: read-only variable is not assignable *aFour = 44; ~~~~~~ ^ gcc-5.3.0: error: assignment of read-only location '*aFour' *aFour = 44; ^
指す値を変更せずに
const
ポインターを更新するのはどうですか?
aFour = NULL;
本当に機能し、完全に有効です。 「不変データへのポインター」を意味する
uint64_t const *
を宣言し
uint64_t const *
が、ポインター自体は不変ではありません(注:
const uint64_t *
も同じ意味です)。
データとポインターの両方を同時に不変にする方法は? 会う:double
const
2つのconst
別の
const
を追加して、状況がどうなるかを見てみましょう。
uint64_t bob = 42; uint64_t const *const anotherFour = &bob; *anotherFour = 45; anotherFour = NULL;
結果は何ですか?
clang-700.1.81: error: read-only variable is not assignable *anotherFour = 45; ~~~~~~~~~~~~ ^ error: read-only variable is not assignable anotherFour = NULL; ~~~~~~~~~~~ ^ gcc-5.3.0: error: assignment of read-only location '*anotherFour' *anotherFour = 45; ^ error: assignment of read-only variable 'anotherFour' anotherFour = NULL; ^
ええ、データとポインター自体の両方を不変にすることができました。
const *const
どういう意味ですか?
ここでの意味はそれほど明白ではないようです。
実際には変数宣言を右から左へ (またはさらに悪いことに、 スパイラルで ) 読むことが推奨されているため、値は非常に不安定です。
この場合、右から左に読むと2 、この宣言は次を意味します。
uint64_t const *const anotherFour = &bob;
anotherFour
は次の
anotherFour
です。
- 不変ポインター(
*const
) - 不変変数(
uint64_t const
)
「通常の」構文を使用して、右から左に読みます。
uint64_t const *aFour = &bob;
aFour
は:
- 通常の可変ポインター(
*
は、ポインター自体が変更できることを意味します) - 不変変数(
uint64_t const
は、データを変更できないことを意味します)
今見たものは何ですか?
重要な違いがあります。人々は通常、
const uint64_t *bob
を「不変のポインター」として呼び出しますが、それはここでは起こりません。 これは、実際には「不変データへの可変ポインター」です。
間奏-const宣言の説明
しかし、待って、もっともっと!
ポインタを表現することで、
const
修飾子を宣言するための4つの異なるオプションがどのように得られるかを見ました。 できること:
- 単一の
const
を宣言しないで、ポインター自体とポインターが指すデータの両方を変更できるようにします
uint64_t *bob;
- 不変のデータのみを宣言しますが、ポインターの変更は許可します
uint64_t const *bob;
これは、データシーケンスを列挙するための一般的なパターンです。次の要素に移動し、ポインターを増やしますが、ポインターはデータを変更できません。
- 不変のポインターのみを宣言しますが、データの変更は許可します
uint64_t *const bob;
有効なポインター値は常にスカラーメモリアドレス(uintptr_t
)であるため、ここでconst
は通常の整数値と同じ効果を持ちます。 実装がconst
を使用してパラメーターを決定する場合は完全に正常ですが、 関数のプロトタイプはパラメーターを含める必要はありません。このconst
はアドレスのみを保護し、データは保護しないためです。
- ポインターとデータを不変に宣言し、初期化宣言後に変更を禁止します
uint64_t const *const bob;
これは1つのポインターと2つの
const
に対するものですが、別のポインターを追加するとどうなりますか?
3つのconst
一
ダブルポインターに
const
を追加する方法はいくつありますか?
すぐに確認してみましょう。
uint64_t const **moreFour = &aFour;
上記の発表に基づいて許可されるこれらの操作はどれですか?
**moreFour = 46; *moreFour = NULL; moreFour = NULL;
clang-700.1.81: error: read-only variable is not assignable **moreFour = 46; ~~~~~~~~~~ ^ gcc-5.3.0: error: assignment of read-only location '**moreFour' **moreFour = 46; ^
宣言を右から左に読んだ場合、最初の割り当てのみが機能しませんでした。
uint64_t const **moreFour = &aFour;
moreFour
:
- ポインター(
*
) - ポインター(
*
) - 不変変数(
uint64_t const
)
ご覧のとおり、実行できなかった唯一の操作は、保存された値を変更することでした。 ポインターとポインターをポインターに正常に変更しました。
二
別の
const
修飾子をさらに深いレベルに追加する場合はどうなりますか?
uint64_t const *const *evenMoreFour = &aFour;
2つの
const
3が与えられた場合 今何ができる?
**evenMoreFour = 46; *evenMoreFour = NULL; evenMoreFour = NULL;
clang-700.1.81: error: read-only variable is not assignable **evenMoreFour = 46; ~~~~~~~~~~~~~~ ^ error: read-only variable is not assignable *evenMoreFour = NULL; ~~~~~~~~~~~~~ ^ gcc-5.3.0: error: assignment of read-only location '**evenMoreFour' **evenMoreFour = 46; ^ error: assignment of read-only location '*evenMoreFour' *evenMoreFour = NULL; ^
広告を右から左に読むと、変更から2回保護されます。
uint64_t const *const *evenMoreFour = &aFour;
evenMoreFour
は:
- ポインター(
*
) - 不変ポインター(
*const
) - 不変変数(
uint64_t const
)
三
2つよりも少し上手くできます。 会う:3
const
ダブルポインターを宣言するときにすべての変更をブロックする場合はどうでしょうか。
uint64_t const *const *const ultimateFour = &aFour;
今、私たちは何ができますか?
**ultimateFour = 48; *ultimateFour = NULL; ultimateFour = NULL;
clang-700.1.81: error: read-only variable is not assignable **ultimateFour = 46; ~~~~~~~~~~~~~~ ^ error: read-only variable is not assignable *ultimateFour = NULL; ~~~~~~~~~~~~~ ^ error: read-only variable is not assignable ultimateFour = NULL; ~~~~~~~~~~~~ ^ gcc-5.3.0: error: assignment of read-only location '**ultimateFour' **ultimateFour = 46; ^ error: assignment of read-only location '*ultimateFour' *ultimateFour = NULL; ^ error: assignment of read-only variable 'ultimateFour' ultimateFour = NULL; ^
何も動作しません! 成功!
もう一度行きましょう。
uint64_t const *const *const ultimateFour = &aFour;
ultimateFour
は:
- 不変ポインター(
*const
) - 不変ポインター(
*const
) - 不変変数(
uint64_t const
)
追加のルール
-
const
宣言は常に安全です(値を変更する必要がない限り):
- 任意の非
const
データをconst
変数に割り当てることができます。
可変変数への不変の参照を作成する許可が許可されます。
uint32_t abc = 123; uint32_t *thatAbc = &abc; uint32_t const *const immutableAbc = thatAbc;
- 注意して、関数の
const
パラメーターをできるだけ多く宣言してください。
void trySomething(const storageStruct *const storage, const uint8_t *const ourData, const size_t len) { saveData(storage, ourData, len); }
- 任意の非
-
const
はコンパイル時にのみチェックされます。const
宣言は、プログラムの動作を変更しません。
-
const
は、人々が複雑さを扱うのを助けるために存在し、少し簡単です:
変数およびパラメーターの予想される動作を自己文書化するのに役立ちます(将来変更すべきものとすべきでないものを忘れた場合の簡単な保護として機能します)
-
const
は、明示的な型キャストまたはメモリコピーによって常に回避できます。
コンパイラは、その裁量で、読み取り専用の場所に不変変数を配置することを決定する場合があります。そのため、const
をバイパスしようとすると、未定義の動作が発生する可能性があります。
-
カーキ色
タイプキャストハック
あなたが賢く、不変のストレージへの可変ポインタを作成したらどうでしょうか?
const uint32_t hello = 3; uint32_t *getAroundHello = &hello; *getAroundHello = 92;
あなたのコンパイラは
const
を落とすと文句を言うでしょうが、ただ警告を投げるだけです4 あなたは5を無効にすることができます 。
clang-700.1.81: warning: initializing 'uint32_t *' (aka 'unsigned int *') with an expression of type 'const uint32_t *' (aka 'const unsigned int *') discards qualifiers [-Wincompatible-pointer-types-discards-qualifiers] uint32_t *getAroundHello = &hello; ^ ~~~~~~ gcc-5.3.0: warning: initialization discards 'const' qualifier from pointer target type [-Wdiscarded-qualifiers] uint32_t *getAroundHello = &hello; ^
これはCなので、明示的な型変換によってconst修飾子を破棄し、警告(および
const
初期化違反)を取り除くことができます。
uint32_t *getAroundHello = (uint32_t *)&hello;
コンパイラーに明示的にreal
&hello
型を無視し、代わりに
uint32_t *
を使用するように指示したため、コンパイルの警告はありません。
メモリーカーキ
構造体に
const
メンバーが含まれているが、宣言後に構造体に格納されているデータを変更した場合はどうなりますか?
メンバーの不変性のみが異なる2つの構造体を宣言しましょう。
struct exampleA { int64_t a; uint64_t b; }; struct exampleB { int64_t a; const uint64_t b; }; const struct exampleA someStructA = {.a = 3, .b = 4}; struct exampleB someOtherStructB = {.a = 3, .b = 4};
someOtherStructB
を
const someStructA
コピーしてみましょう。
memcpy(&someStructA, &someOtherStructB, sizeof(someStructA));
これは機能しますか?
clang-700.1.81: warning: passing 'const struct aStruct *' to parameter of type 'void *' discards qualifiers [-Wincompatible-pointer-types-discards-qualifiers] memcpy(&someStructA, &someOtherStructB, sizeof(someStructA)); ^~~~~~~~~~~~ gcc-5.3.0: In file included from /usr/include/string.h:186:0: warning: passing argument 1 of '__builtin___memcpy_chk' discards 'const' qualifier from pointer target type [-Wdiscarded-qualifiers] memcpy(&someStructA, &someOtherStructB, sizeof(someStructA)); ^ note: expected 'void *' but argument is of type 'const struct aStruct *'
いいえ、それは機能しません。プロトタイプ6
memcpy
場合、次のようになります。
void *memcpy(void *restrict dst, const void *restrict src, size_t n);
someStructA
、コピー中に
dst
が変更されるため(および
someStructA
不変であるため)、不変のポインターを
dst
引数として渡すことを許可しません。
ただし、
const
パラメーターのチェックは、関数のプロトタイプによってのみ実行されます。
dst
として別の
const
フィールドを持つ部分的に不変の構造を使用すると、コンパイラは文句を言いますか?
const someStructA
をmutableにコピーしようとしたが、
const
メンバー
someOtherStructB
を1つ含むと
someOtherStructB
ますか?
memcpy(&someOtherStructB, &someStructA, sizeof(someOtherStructB));
これで、関数のプロトタイプテストに合格し、完全に変更されていない構造の不変メンバーを上書きしても、
memcpy
に関する警告は表示されません。
おわりに
不必要に変更可能な値を作成しないでください。 計画どおりにプログラムが実際に機能するように注意してください。
自分で試してみてください
#include <stddef.h> /* NULL */ #include <stdint.h> /* */ int main(void) { uint64_t bob = 42; const uint64_t *aFour = &bob; /* uint64_t const *aFour = &bob; */ *aFour = 44; /* */ aFour = NULL; const uint64_t *const anotherFour = &bob; /* uint64_t const *const anotherFour = &bob; */ *anotherFour = 45; /* */ anotherFour = NULL; /* */ const uint64_t **moreFour = &aFour; /* uint64_t const **moreFour = &aFour; */ **moreFour = 46; /* */ *moreFour = NULL; moreFour = NULL; const uint64_t *const *evenMoreFour = &aFour; /* uint64_t const *const *evenMoreFour = &aFour; */ **evenMoreFour = 47; /* */ *evenMoreFour = NULL; /* */ evenMoreFour = NULL; const uint64_t *const *const ultimateFour = &aFour; /* uint64_t const *const *const ultimateFour = &aFour; */ **ultimateFour = 48; /* */ *ultimateFour = NULL; /* */ ultimateFour = NULL; /* */ return 0; }
1-また、スカラー変数の初期値を変更することはできないため、
const
スカラーを非
const
パラメーターとして使用する関数に渡すことは絶対に安全であることを意味します。 ^
2-そのような場合、これらの宣言は両方ともまったく同じ結果につながるため、
const uint64_t *
uint64_t const *
ではなく
uint64_t const *
を記述する方が良いかもしれませんが、
const
修飾子が型の後に続く場合は、宣言を右から左に読む方が便利です。 ^
3-また、
const
を追加すると、ポインターは前の修飾子ではなく次の修飾子に接続されるため、ポインターの正しい構文が
type *name
ではなく
type* name
あり、確かに
type *name
ではないことを無条件に確認します。 例:
間違った
uint64_t const* const* evenMoreFour; /* const */
そうだね
uint64_t const *const *evenMoreFour; /* const . */
^
4-まあ、コンパイラモデルに応じて非標準フラグを使用する必要があるため、ビルドプロセスでは、これらの警告をオフにするさまざまなコンパイラとの互換性のために多くの冗長フラグが必要になる場合があります。 ^
5-覚えています
const
コンパイル時にのみチェックされます。
const
によって課せられた制限を破ることができない場合にのみプログラムの動作を変更することはありません(他の値を変更するだけでプログラムの動作が変更されるだけです)が、期待どおりに動作しない可能性があります。 また、コンパイラーは読み取り専用コードセグメントに不変データを配置でき、これらの
const
ブロックをバイパスしようとすると、未定義の動作が発生する可能性があります。 ^
6-
memcpy()
プロトタイプの
restrict
キーワードにも注意してください。
restrict
は、「このポインターのデータが現在のスコープ内の他のデータと交差しない」ことを意味し、
memcpy()
そのパラメーターを処理する方法を決定します。
コピーするときに、宛先へのポインターがデータの送信元へのポインターに部分的に重なる場合、
memmove()
関数を使用する必要があります。そのプロトタイプには
restrict
修飾子が含ま
restrict
いません。
void *memmove(void *dst, const void *src, size_t len);
^