18

1) 誤解:

  • 配列が C 言語で宣言されるたびに、配列の最初の要素へのポインター(配列の名前) が暗黙的に作成されます。(そうですか?私はそうは思いません!)

  • このページの最初の 2 行(情報の正確性についてはわかりませんが) は同じことを述べています。

    これまで見てきたように、配列を宣言すると、連続したメモリ ブロックが配列のセルに割り当てられ、(適切な型の) ポインター セルも割り当てられ、配列の最初のセルを指すように初期化されます。

  • しかしそのポインターに含まれるアドレスそのポインターのアドレスを出力すると、それらは同じであることがわかります。ということで、結局ポインタは作成されていないと思います。

2)私はこの質問 からこれを拾いました。

  • ほとんどの場合、配列名はポインターに変換されます。

コンパイラーが配列名をポインターに変換することを決定したときと、その理由について詳しく説明できる人はいます?

PS: functionsで同じことを説明してください。また、このリンクでは、関数の場合、 、、int square(int,int)のいずれかが 同じ関数ポインターを参照するという例が示されています。説明できますか? square&square*square**square

編集:コードスニペット

int fruits[10];
printf("Address IN constant pointer is %p\n",  fruits);
printf("Address OF constant pointer is %p\n", &fruits); 

出力:

Address IN constant pointer is 0xbff99ca8
Address OF constant pointer is 0xbff99ca8
4

4 に答える 4

35

配列型の式は、次の場合を除き、配列オブジェクトの最初の要素へのポインターに暗黙的に変換されます。

  • 単項演算子のオペランド&
  • のオペランドsizeof; また
  • 配列オブジェクトを初期化するために使用される初期化子の文字列リテラル。

3 番目のケースの例は次のとおりです。

char arr[6] = "hello";

"hello"タイプの配列式です(ターミネータchar[6]の 5 + 1 )。'\0'アドレスには変換されません。of の完全な 6 バイト値が"hello"配列 object にコピーされますarr

一方、これでは:

char *ptr = "hello";

配列式"hello"は へのポインタに「減衰」し、'h'そのポインタ値はポインタ オブジェクト の初期化に使用されますptr。(実際には のはずですがconst char *ptr、それは副次的な問題です。)

関数型 (関数名など) の式は、次の場合を除き、関数へのポインターに暗黙的に変換されます。

  • 単項演算子のオペランド&。また
  • sizeof(のオペランドsizeof function_nameは不正であり、ポインターのサイズではありません)。

それでおしまい。

どちらの場合も、ポインターオブジェクトは作成されません。式は、アドレスとも呼ばれるポインター値に変換 (「減衰」) されます。

(これらの両方の場合の「変換」は、キャスト演算子によって指定されるような通常の型変換ではありません。オペランドの値を取得して、それを使用して結果の値を計算することはありません。int変換float. むしろ、配列または関数型のは、コンパイル時にポインタ型のに「変換」されます. 私の意見では、「調整された」という言葉は「変換された」よりも明確でした.)

配列インデックス演算子[]と関数呼び出し "operator" の両方に()ポインターが必要であることに注意してください。のような通常の関数呼び出しfunc(42)では、関数名は関数funcへのポインターに「減衰」し、呼び出しで使用されます。(この変換は、関数呼び出しが正しいことを行う限り、生成されたコードで実際に実行する必要はありません。)

関数のルールには、いくつかの奇妙な結果があります。ほとんどの場合、式funcは関数へのポインタに変換されますfunc。では&funcfuncはポインターに変換されませんが&、関数のアドレス、つまりポインター値を生成します。では*funcfunc暗黙的にポインターに変換され、それを*逆参照して関数自体が生成され、(ほとんどのコンテキストで) ポインターに変換されます。では****func、これが繰り返し発生します。

(C11 標準のドラフトでは、配列には別の例外があると述べています。つまり、配列が new_Alignof演算子のオペランドである場合です。これはドラフトのエラーであり、最終的に公開された C11 標準で修正されています。_Alignof括弧内にのみ適用できます。式ではなく型名です。)

配列のアドレスとその最初のメンバーのアドレス:

int arr[10];
&arr;    /* address of entire array */
&arr[0]; /* address of first element */

メモリアドレスは同じですが、型が異なります。前者は配列オブジェクト全体のアドレスであり、型int(*)[10](10intの配列へのポインター) です。後者はタイプint*です。この 2 つの型には互換性がなく (たとえばint*、オブジェクトに値を合法的に代入することはできません)、ポインター演算は異なる動作をします。int(*)[10]

配列または関数型の宣言された関数パラメーターは、コンパイル時に (変換されずに) ポインター パラメーターに調整されるという別の規則があります。例えば:

void func(int arr[]);

とまったく同等です

void func(int *arr);

これらの規則 (配列式の変換と配列パラメーターの調整) が組み合わさって、C における配列とポインターの関係に関して多くの混乱が生じます。

comp.lang.c FAQのセクション 6 は、詳細を説明する優れた仕事をしています。

これの決定的なソースは、ISO C 標準です。 N1570 (1.6 MB PDF) は、2011 年規格の最新のドラフトです。これらの変換は、セクション 6.3.2.1 の段落 3 (配列) および 4 (関数) で指定されています。そのドラフトには、_Alignof実際には適用されない への誤った参照があります。

ちなみに、printfあなたの例の呼び出しは厳密に間違っています:

int fruits[10];
printf("Address IN constant pointer is %p\n",fruits);
printf("Address OF constant pointer is %p\n",&fruits); 

format には typeの%p引数が必要void*です。ほとんどの実装の場合と同様に、タイプint*andのポインターがandint(*)[10]と同じ表現を持ち、同じ方法で引数として渡される場合、動作する可能性がありますが、保証されません。ポインタを次void*のように明示的に変換する必要があります。void*

int fruits[10];
printf("Address IN constant pointer is %p\n", (void*)fruits);
printf("Address OF constant pointer is %p\n", (void*)&fruits);

では、なぜこのように行われるのでしょうか。問題は、ある意味で配列が C の二流市民であることです。配列を関数呼び出しの引数として値で渡すことはできず、関数の結果として返すこともできません。配列を有効にするには、さまざまな長さの配列を操作できる必要があります。for 、 for 、 forなど (これらはすべて別個の型です) の個別のstrlen関数は、非常に扱いにくくなります。その代わりに、配列はその要素へのポインターを介してアクセスおよび操作され、ポインター演算はそれらの要素をトラバースする方法を提供します。char[1]char[2]char[3]

配列式が (ほとんどのコンテキストで) ポインターに減衰しない場合、結果に対してできることはあまりありません。また、C は、配列とポインターを必ずしも区別していなかった初期の言語 (BCPL と B) から派生したものです。

他の言語は配列を第一級の型として扱うことができますが、そうするためには「C の精神」ではない追加の機能が必要であり、C は依然として比較的低レベルの言語です。

関数をこのように扱う理由についてはよくわかりません。関数型の値がないことは事実ですが、言語は関数呼び出しの接頭辞として (関数へのポインターではなく) 関数を必要とする可能性があり*、間接呼び出しには明示的な演算子が必要です: (*funcptr)(arg). を省略することができるの*は便利ですが、途方もないものではありません。これはおそらく、歴史的な慣性と配列の扱いとの一貫性の組み合わせによるものです。

于 2013-07-06T19:17:10.690 に答える
2

短い答えはイエスです...時々を除いて。通常、配列が宣言された後、その名前が使用されるたびに、配列オブジェクトの最初の要素へのポインターに変換されます。ただし、これが発生しない場合もあります。これが発生しないこれらのケースは、@KeithThompson の回答here にあります

配列と同様に、関数型もポインター値に変換されます...例外もあります。これが再び発生しないケースは、@ KeithThompson の回答で再び見つけることができます。ここに

于 2013-07-06T19:15:11.843 に答える
2

あなたの質問の最初の部分にあるリンクされたページに記載されている説明は、確かに完全に間違っています。定数かどうかに関係なく、そこにはポインターはありません。@KeithThompsonの回答で、配列/関数の動作の徹底的な説明を見つけることができます。

それに加えて、2 つの部分からなるオブジェクト (メモリの独立した名前のないブロックを指す名前付きポインター) として実装された配列は、まったくキメラではないことを (補足として) 追加することは理にかなっているかもしれません。それらは、C言語の前身であるB言語にその特定の形で存在していました。そして当初、それらは完全に変更されずに B から C に引き継がれました。これについては、Dennis Ritchie の「 The Development of the C Language」ドキュメント (「Embryonic C」セクションを参照)で読むことができます。

ただし、まさにそのドキュメントに記載されているように、この種の配列の実装は、構造体型などの C 言語のいくつかの新機能と互換性がありませんでした。構造体オブジェクト内に 2 つの部分からなる配列を持つと、そのようなオブジェクトは自明ではない構造を持つ高レベルのエンティティに変わります。また、生メモリ操作 (など) との互換性がなくなりますmemcpy。このような考慮事項が、配列が 2 部構成のオブジェクトから現在の 1 部構成のオブジェクトに再設計された理由です。そして、そのドキュメントを読むことができるように、再設計は B スタイル配列との下位互換性を考慮して実行されました。

そのため、まず、多くの人が C スタイルの配列の動作に混乱し、どこかにポインターが隠されていると信じ込んでいます。最新の C 配列の動作は、その錯覚をエミュレート/維持するように特別に設計されています。そして第二に、いくつかの古い文書には、その「初期の」時代からの残り物がまだ含まれている可能性があります (ただし、リンクした文書がそれらの 1 つであるようには見えません)。

于 2013-07-06T20:27:05.727 に答える
2

それについて考えるもっと良い方法があります。配列型の式 (配列名、配列へのポインターの逆参照、2 次元配列の添字付けなどを含む) は、まさに配列型の式です。ポインタ型の式ではありません。ただし、ポインターが必要なコンテキストで使用される場合、言語は配列型の式からポインター型の式への暗黙的な変換を提供します。

覚える必要はありません。ああ、それは "except"sizeof&などのポインターに変換されます。式のコンテキストについて考えるだけで済みます。

たとえば、関数呼び出しに配列式を渡そうとする場合を考えてみましょう。C 標準では、関数パラメーターを配列型にすることはできません。対応するパラメーターがポインター型 (コンパイルするにはポインター型でなければなりません) の場合、コンパイラーはポインターが必要であることを認識し、配列式からポインター型への変換を適用します。

*または、間接参照演算子、算術演算子+ -、または添字演算子[];を含む配列式を使用する場合。これらの演算子はすべてポインターを操作するため、コンパイラーはそれを認識して変換を適用します。

配列式を割り当てようとすると、C では配列型は割り当て可能ではないため、コンパイルできる唯一の方法は、ポインター型に割り当てられている場合です。この場合も、コンパイラーはポインターが必要であることを認識します。 、変換を適用します。

sizeof、およびで使用すると&、これらのコンテキストは配列に対して本質的に意味があるため、コンパイラはわざわざ変換を適用しません。これらが配列からポインターへの変換の「例外」と見なされる唯一の理由は、Cの他のすべての式コンテキスト(上記の例でわかるように) が配列型 (配列型) に対して意味をなさないためです。型はCで非常に不自由であり、これらのいくつかは「残っている」唯一のものです。

于 2013-07-07T19:42:13.840 に答える