int *nums = {5, 2, 1, 4};
printf("%d\n", nums[0]);
セグメンテーション違反を引き起こしますが、
int nums[] = {5, 2, 1, 4};
printf("%d\n", nums[0]);
しません。今:
int *nums = {5, 2, 1, 4};
printf("%d\n", nums);
プリント5。
これに基づいて、配列初期化表記{}は、このデータを左側の変数に盲目的にロードすると推測しました。 int []の場合、配列は必要に応じて埋められます。 int *の場合、ポインタは5で埋められ、ポインタが格納された後のメモリ位置は2、1、および4で埋められます。したがって、nums [0]は5の逆参照を試行し、セグメンテーション違反が発生します。
私が間違っている場合、私を修正してください。そして、正しい場合は、配列初期化子がそのように機能する理由がわからないため、詳しく説明してください。
Cには、配列であるかのように、ブレースで囲まれた初期化子リストでプレーン変数を初期化できるという(愚かな)ルールがあります。
たとえば、int x = {0};
と完全に同等のint x = 0;
を記述できます。
したがって、int *nums = {5, 2, 1, 4};
を記述すると、実際には単一のポインタ変数に初期化子リストが与えられます。ただし、これは1つの変数であるため、最初の値5のみが割り当てられ、リストの残りの部分は無視されます(実際、過剰な初期化子を含むコードは厳密なコンパイラでコンパイルする必要はないと思います)-まったくメモリに書き込まれます。コードはint *nums = 5;
と同等です。つまり、nums
はaddress5
を指す必要があります。
この時点で、すでに2つのコンパイラの警告/エラーが発生しているはずです。
そしてもちろん、5
はnums[0]
で逆参照することを許可されている有効なアドレスではない可能性が高いため、コードはクラッシュして書き込みます。
補足として、printf
ポインターアドレスは%p
指定子を使用する必要があります。そうでない場合は、未定義の動作を呼び出します。
ここで何をしようとしているのかわかりませんが、配列を指すようにポインタを設定する場合は、次のようにする必要があります。
int nums[] = {5, 2, 1, 4};
int* ptr = nums;
// or equivalent:
int* ptr = (int[]){5, 2, 1, 4};
または、ポインターの配列を作成する場合:
int* ptr[] = { /* whatever makes sense here */ };
[〜#〜] edit [〜#〜]
いくつかの調査の後、「余分な要素の初期化リスト」は実際には有効なCではないと言うことができます。これは GCC拡張 です。
標準6.7.9 Initializationは(emphasis mine)と言います:
2 No initializer shall attempt to provide a value for an object not contained within the entity being initialized.
/-/
11スカラーの初期化子は、オプションで中括弧で囲まれた単一の式でなければなりません。オブジェクトの初期値は、式の初期値です(変換後) ;単純な割り当てと同じ型の制約と変換が適用され、スカラーの型は宣言された型の非修飾バージョンになります。
「スカラー型」は、配列、構造体、またはユニオン型ではない単一の変数を指す標準用語です(これらは「集約型」と呼ばれます)。
そのため、英語の標準では、「変数を初期化するときは、可能だからといって、イニシャライザ式の周りに余分な括弧を入れて自由に投げてください。」と書かれています。
シナリオ1
int *nums = {5, 2, 1, 4}; // <-- assign multiple values to a pointer variable printf("%d\n", nums[0]); // segfault
なぜこれがセグメンテーション違反なのでしょうか?
nums
をintへのポインタとして宣言しました。つまり、nums
はメモリ内でone integerのアドレスを保持することになっています。
次に、nums
を複数値の配列に初期化しようとしました。したがって、詳細を掘り下げることなく、これは概念的に間違っていますです。1つの値を保持するはずの変数に複数の値を割り当てることは意味がありません。この点で、これを行うとまったく同じ効果が得られます。
int nums = {5, 2, 1, 4}; // <-- assign multiple values to an int variable
printf("%d\n", nums); // also print 5
いずれの場合(ポインターまたはint変数に複数の値を割り当てる)、その後に起こることは、変数が5
である最初の値を取得し、残りの値は無視されることです。このコードは準拠していますが、割り当てに含まれていない追加の値ごとに警告が表示されます。
warning: excess elements in scalar initializer
。
ポインター変数に複数の値を割り当てる場合、nums[0]
にアクセスするとプログラムはセグメンテーション違反になります。つまり、アドレス5に文字通り格納されているものはすべて延期されます。この場合、ポインタnums
に有効なメモリを割り当てませんでした。
Int変数に複数の値を割り当てる場合、セグメンテーション違反がないことに注意する価値があります(ここでは無効なポインターを間接参照していません)。
シナリオ2
int nums[] = {5, 2, 1, 4};
スタック内の4つのintの配列を合法的に割り当てているため、これはセグメンテーション違反ではありません。
シナリオ
int *nums = {5, 2, 1, 4};
printf("%d\n", nums); // print 5
これは期待どおりにセグメンテーション違反になりません。これは、ポインタ自体の値-参照解除するもの(無効なメモリアクセス)ではないためです。
その他
あなたがいつでもセグメンテーションフォールトする運命にある ポインターの値をハードコードする このように(どのプロセスがどのメモリ位置にアクセスできるかを決定するのはオペレーティングシステムのタスクだからです)。
int *nums = 5; // <-- segfault
そのため、経験則として、次のようなallocated変数のアドレスへのポインターを常に初期化することです。
int a;
int *nums = &a;
または、
int a[] = {5, 2, 1, 4};
int *nums = a;
int *nums = {5, 2, 1, 4};
は不正な形式のコードです。このコードを次と同じように扱うGCC拡張機能があります。
int *nums = (int *)5;
メモリアドレス5へのポインターを作成しようとしています(これは、私にとって便利な拡張機能ではないようですが、開発者ベースがそれを望んでいると思います)。
この動作を回避する(または少なくとも警告を表示する)には、標準モードでコンパイルできます。 -std=c11 -pedantic
。
有効なコードの代替形式は次のとおりです。
int *nums = (int[]){5, 2, 1, 4};
nums
と同じ保存期間の可変リテラルを指します。しかし int nums[]
バージョンは、使用するストレージが少ないため、一般的に優れています。また、sizeof
を使用して、配列の長さを検出できます。
int *nums = {5, 2, 1, 4};
nums
はint
型のポインターです。したがって、このポイントを有効なメモリの場所に設定する必要があります。 num[0]
ランダムなメモリの場所を間接参照しようとしているため、セグメンテーションエラーが発生しています。
はい、ポインタは値5を保持しており、システムで定義されていない動作である間接参照を試みています。 (5
はシステム上の有効なメモリ位置ではありません)
一方、
int nums[] = {1,2,3,4};
nums
はint
型の配列であり、初期化中に渡された要素の数に基づいてメモリが割り当てられると言っている有効な宣言です。
{5, 2, 1, 4}
を割り当てることにより
int *nums = {5, 2, 1, 4};
5をnums
に割り当てます(intからintへのポインターへの暗黙の型キャストの後)。参照を解除すると、0x5
のメモリ位置へのアクセス呼び出しが行われます。それはあなたのプログラムがアクセスすることを許可されないかもしれません。
試してみる
printf("%p", (void *)nums);