この非常に簡単なテスト関数を使用して、const修飾子で何が起こっているのかを把握しています。
int test(const int* dummy)
{
*dummy = 1;
return 0;
}
これにより、GCC 4.8.3でエラーが発生します。まだこれはコンパイルします:
int test(const int* dummy)
{
*(char*)dummy = 1;
return 0;
}
したがって、const修飾子は、他の型にキャストせずに引数を使用した場合にのみ機能するようです。
最近使用したコードを見ました
test(const void* vpointer, ...)
少なくとも私にとっては、void *を使用した場合、スタック内のポインター演算またはトレースのためにchar *にキャストする傾向があります。 const void *サブルーチン関数がvpointerが指しているデータを変更できないようにする方法
const int *var;
const
は契約です。 const int *
パラメータを使用すると、ポインタ(呼び出し先の関数)がポインタが指すオブジェクトを変更しないことを呼び出し元に「伝える」ことができます。
2番目の例は、明示的にそのコントラクトを壊します const修飾子をキャストしてから、受け取ったポインターが指すオブジェクトを変更します。これを絶対にしないでください。
この「契約」はコンパイラーによって実施されます。 *dummy = 1
はコンパイルされません。キャストはそれを回避する方法であり、コンパイラーに自分が何をしているかを本当に知っていることを伝え、それをさせます。残念ながら、「自分が何をしているのか本当にわかっている」というのは、通常そうではありません。
const
は、他の方法では不可能だった最適化を実行するためにコンパイラーによって使用されることもあります。
未定義の動作に関する注意:
キャスト自体は技術的には正当ですが、const
として宣言された値の変更は未定義の動作です。技術的には、元の関数は、渡されたポインターが可変と宣言されたデータを指している限り、問題ありません。それ以外の場合は、未定義の動作です。
ポストの最後でこれについての詳細
動機付けと使用に関しては、strcpy
およびmemcpy
関数の引数を取ることができます。
char* strcpy( char* dest, const char* src );
void* memcpy( void* dest, const void* src, std::size_t count );
strcpy
は文字列で動作し、memcpy
は汎用データで動作します。 strcpyを例として使用していますが、以下の説明は両方でまったく同じですが、char *
およびconst char *
for strcpy
およびvoid *
およびconst void *
for memcpy
:
dest
はchar *
バッファにdest
関数がコピーを配置するため。この関数はこのバッファーの内容を変更するため、constではありません。
src
はconst char *
は、関数がバッファsrc
の内容のみを読み取るためです。変更しません。
関数の宣言を見るだけで、呼び出し元は上記のすべてをアサートできます。契約により、strcpy
は、引数として渡された2番目のバッファーの内容を変更しません。
const
とvoid
は直交しています。 const
に関する上記の説明はすべて、すべてのタイプに適用されます(int
、char
、void
、...)
void *
はCで「汎用」データに使用されます。
未定義の動作の詳細:
事例1:
int a = 24;
const int *cp_a = &a; // mutabale to const is perfectly legal. This is in effect
// a constant view (reference) into a mutable object
*(int *)cp_a = 10; // Legal, because the object referenced (a)
// is declared as mutable
ケース2:
const int cb = 42;
const int *cp_cb = &cb;
*(int *)cp_cb = 10; // Undefined Behavior.
// the write into a const object (cb here) is illegal.
これらの例は、理解しやすいため、始めました。ここから、関数引数への1つのステップのみがあります。
void foo(const int *cp) {
*(int *)cp = 10; // Legal in case 1. Undefined Behavior in case 2
}
事例1:
int a = 0;
foo(&a); // the write inside foo is legal
ケース2:
int const b = 0;
foo(&b); // the write inside foo causes Undefined Behavior
繰り返しますが、あなたが何をしているかを本当に知っていて、現在および将来コードで作業するすべての人が専門家であり、これを理解していない限り、上記のすべてが満たされない限り、あなたは良い動機を持っています- 決してconstnessを捨てないでください!!
int test(const int* dummy) { *(char*)dummy = 1; return 0; }
いいえ、これは機能しません。 (真のconst
データを使用した)constnessのキャストはundefined behaviorであり、たとえば、実装がconst
ROMのデータ。 「機能する」という事実は、コードの形式が正しくないという事実を変えるものではありません。
少なくとも私にとっては、void *を使用した場合、スタック内のポインター演算またはトレースのためにchar *にキャストする傾向があります。 const void *は、vpointerが指しているデータをサブルーチン関数が変更するのを防ぐことができますか?
const void*
は、変更できないデータへのポインタを意味します。読むには、はい、char
などの具体的な型にキャストする必要があります。しかし、readingではなく、writingではなく、UB 。
これについては、さらに詳しく説明します こちら 。 Cを使用すると、タイプセーフを完全にバイパスできます。それを防ぐのはあなたの仕事です。
特定のOS上の特定のコンパイラが、そのconst
データの一部を読み取り専用メモリページに置く可能性があります。その場合、その場所への書き込みを試みると、一般保護違反が発生するなど、ハードウェアで失敗します。
const
修飾子は、書き込みが未定義の動作であることを意味します。これは、言語標準により、プログラム(またはその他)がクラッシュした場合にプログラムをクラッシュさせることを意味します。それにもかかわらず、Cは、自分が何をしているのかを知っていると思う場合、自分で足を撃つことができます。
サブルーチンが、必要に応じて与えたビットを再解釈し、必要なマシン命令を実行することを止めることはできません。呼び出すライブラリ関数は、アセンブラーで記述されている場合もあります。しかし、それをconst
ポインターに対して行うのは未定義の動作であり、実際に未定義の動作を呼び出したくないのです。
私の頭上で、それが理にかなっている可能性のあるまれな例:ハンドルパラメーターを渡すライブラリがあるとします。それらをどのように生成して使用しますか?内部的には、データ構造へのポインタである場合があります。それで、あなたはtypedef const void* my_handle;
そのため、クライアントが間接参照しようとしたり、誤って算術を行ったりした場合、コンパイラはエラーをスローし、ライブラリ関数内のデータ構造へのポインタにキャストバックします。これは最も安全な実装ではなく、ライブラリに任意の値を渡すことができる攻撃者に注意する必要がありますが、オーバーヘッドは非常に低くなります。