2

信頼できる情報源を持つ言語弁護士が必要です。

gcc で問題なくコンパイルされる次のテスト プログラムを見てください。

#include <stdio.h>


void foo(int *a) {
    a[98] = 0xFEADFACE;
}

void bar(int b[]) {
    *(b+498) = 0xFEADFACE;
}

int main(int argc, char **argv) {

int a[100], b[500], *a_p;

*(a+99) = 0xDEADBEEF;
*(b+499) = *(a+99);

foo(a);
bar(b);

printf("a[98] == %X\na[99] == %X\n", a[98], a[99]);
printf("b[498] == %X\nb[499] == %X\n", b[498], b[499]);

a_p = a+98;
*a_p = 0xDEADFACE;

printf("a[98] == %X\na[99] == %X\n", a[98], a[99]);

}

それは私が期待する出力を生成します:

anon@anon:~/study/test_code$ gcc arrayType.c -o arrayType
anon@anon:~/study/test_code$ ./arrayType 
a[98] == FEADFACE
a[99] == DEADBEEF
b[498] == FEADFACE
b[499] == DEADBEEF
a[98] == DEADFACE
a[99] == DEADBEEF

a と b は同じ型ですか? コンパイラ内部で int *a同じ型として扱われる?int a[]

実用的な観点からは、int a[100], b[500], *a_p, b_a[];すべて同じタイプに見えます。上記の例のさまざまな状況で、コンパイラがこれらの型を常に調整しているとは信じがたいです。間違っていることが証明されてうれしいです。

誰かが私のためにこの質問を決定的かつ詳細に解決できますか?

4

11 に答える 11

9

a と b は同じ型ですか? int *a はコンパイラ内部で int a[] と同じ型として扱われますか?

よくあるcomp.lang.C質問から:

... 配列が式に現れるたびに、プログラマーが &a[0] を書いたかのように、コンパイラーは配列の最初の要素へのポインターを暗黙的に生成します。(例外は、配列が sizeof または & 演算子のオペランドである場合、または文字配列の文字列リテラル初期化子である場合です...)

... 配列 a とポインタ p が与えられた場合、a[i] という形式の式は、上記の規則に従って配列をポインタに分解し、式 p のポインタ変数と同じように添字を付けます。 [i] (最終的なメモリアクセスは異なりますが...

与えられた宣言

char a[] = "hello";
char *p = "world";

... コンパイラが式a[3]を検出すると、コードを出力して、位置 から開始し、そのa3 つ後ろに移動して、そこに文字をフェッチします。式を検出すると、位置 から開始し、そこにポインタ値をフェッチし、ポインタに 3 を加算し、最後にポイントされた文字をフェッチp[3]するコードを出力します。言い換えると、は という名前のオブジェクト (の開始点) の3 つ後ろの場所であり、は が指すオブジェクトの 3 つ後ろの場所です。pa[3]ap[3]p

強調は私です。最大の違いは、ポインターの場合はポインターがフェッチされるのに対し、配列の場合はフェッチするポインターがないことです。

于 2009-09-03T11:33:41.180 に答える
3

abは両方ともintの配列です。a [0]は、メモリアドレスを含むメモリ位置ではなく、intを含むメモリ位置です。

配列とポインタは同一でも互換性もありません。式に現れるarray-of-T型の左辺値が(3つの例外を除いて)最初の要素へのポインターに減衰する場合、配列はポインターと同等です。結果のポインターのタイプはpointer-to-Tです。これは、関連するコードのアセンブリ出力を見ると明らかになります。3つの例外fyiは、配列がsizeofまたはのオペランド、あるいは文字配列のリテラル文字列初期化子である場合です。

あなたがこれを想像するならば:

char a[] = "hello";
char *p = "world";

次のように表すことができるデータ構造になります。

   +---+---+---+---+---+---+
a: | h | e | l | l | o |\0 |
   +---+---+---+---+---+---+

   +-----+     +---+---+---+---+---+---+
p: |  *======> | w | o | r | l | d |\0 |
   +-----+     +---+---+---+---+---+---+

x [3]のような参照は、xがポインターであるか配列であるかに応じて、異なるコードを生成することを理解してください。コンパイラのa[3]は、次のことを意味します。場所aから開始し、それを3つ超えて移動し、そこでcharをフェッチします。p [3]は、場所pに移動し、そこで値を逆参照し、その値を3つ超えて移動し、そこでcharをフェッチすることを意味します。

于 2009-09-03T12:16:15.567 に答える
3

違いの 1 つ -int a[x][y]int **aは互換性がありません。

http://www.lysator.liu.se/c/c-faq/c-2.html

2.10:

配列の配列 (つまり、C の 2 次元配列) は、ポインターへのポインターではなく、配列へのポインターに崩壊します。

于 2009-09-03T11:56:40.470 に答える
3

C言語標準から:

6.3.2.1.3 sizeof 演算子のオペランドまたは
          単項 & 演算子、または初期化に使用される文字列リテラル
          配列、タイプ ''array of type'' を持つ式は
          ''型へのポインタ'' 型の式に変換され、
          配列オブジェクトの最初の要素を指しており、そうではありません
          左辺値。配列オブジェクトが register ストレージ クラスを持っている場合、
          動作は未定義です。

次のコードを想定します。

#include <stdio.h>
#include <string.h>
int main(void)
{
  char foo[10] = {0};
  char *p = foo;
  foo[0] = 'b';
  *(foo + 1) = 'a';
  strcat(foo, "t");
  printf("foo = %s, &foo = %p, &p = %p, sizeof foo = %lu, sizeof p = %lu\n", 
    foo, &foo, &p, (unsigned long) sizeof foo, (unsigned long) sizeof p);
  return 0;
}

foo は、すべての要素が 0 に初期化された char の 10 要素の配列として宣言されます。p は、char へのポインターとして宣言され、foo を指すように初期化されます。

ラインで

char *p = foo;

式 foo の型は「char の 10 要素配列」です。foo は sizeof または & のオペランドではなく、配列の初期化に使用される文字列リテラルでもないため、その型は暗黙的に「char へのポインター」に変換され、配列の最初の要素を指すように設定されます。このポインタ値は p にコピーされます。

ラインで

foo[0] = 'b';
*(foo + 1) = 'a';

式 foo の型は「char の 10 要素配列」です。foo は sizeof または & のオペランドではなく、配列の初期化に使用される文字列リテラルでもないため、その型は暗黙的に「char へのポインター」に変換され、配列の最初の要素を指すように設定されます。添字式は「`*(foo + 0)」と解釈されます。

ラインで

strcat(foo, "t");

foo の型は「char の 10 要素配列」であり、文字列リテラル「t」の型は「char の 2 要素配列」です。どちらも sizeof または & のオペランドではないため、"t" は文字列リテラルですが、配列の初期化には使用されず、両方とも "pointer to char" 型に暗黙的に変換され、ポインター値はに渡されますstrcat()。

ラインで

  printf("foo = %s, &foo = %p, &p = %p, sizeof foo = %lu, sizeof p = %lu\n", 
    foo, &foo, &p, (unsigned long) sizeof foo, (unsigned long) sizeof p);

foo の最初のインスタンスは、上記のように char へのポインターに変換されます。foo の 2 番目のインスタンスは & 演算子のオペランドであるため、その型は「char へのポインター」に変換され、式「&foo」の型は「char の 10 要素配列へのポインター」または「char」です。 ( *)[10]". これを、「char へのポインターへのポインター」または「char **」である式「&p」の型 type と比較してください。foo の 3 番目のインスタンスは sizeof 演算子のオペランドであるため、その型は変換され、sizeof は配列に割り当てられたバイト数を返します。これを、ポインターに割り当てられたバイト数を返す sizeof p の結果と比較します。

誰かがあなたに「配列はただのポインタだ」と言うときはいつでも、彼らは上で引用した標準のセクションを混乱させています。配列はポインターではなく、ポインターは配列ではありません。ただし、多くの場合、配列をポインターのように扱い、ポインターを配列のように扱うことができます。6 行目、7 行目、8 行目の "foo" を "p" に置き換えることもできます。ただし、sizeof または & のオペランドとしては交換できません。

編集:ところで、関数パラメータとして、

void foo(int *a);

void foo(int a[]);

同等です。「a[]」は「*a」と解釈されます。これは、関数パラメーターに のみ当てはまることに注意してください。

于 2009-09-03T15:23:56.100 に答える
2

sepp2k の回答と Mark Rushakoff の comp.lang.c FAQ の引用に同意します。2 つの宣言と一般的なトラップの間にいくつかの重要な違いを追加させてください。

  1. 配列として定義する場合a(特殊なケースである関数の引数以外のコンテキストで)、a = 0; と書くことはできません。または++; aは左辺値 (代入演算子の左側に表示される値) ではないためです。

  2. 配列定義はスペースを予約しますが、ポインターは予約しません。したがって、sizeof(array)は配列のすべての要素を格納するために必要なメモリ空間を返します (たとえば、32 ビット アーキテクチャで 10 個の整数の配列の場合、4 バイトの 10 倍) sizeof(pointer)。 64 ビット アーキテクチャでは 8 バイト)。

  3. ポインターを先頭に追加したり、配列宣言を追加したりすると、状況は明らかに異なります。たとえばint **a、整数へのポインターへのポインターです。ポインターの配列を行に割り当て、それぞれが整数を格納するためのメモリを指すようにすることで、2 次元配列 (さまざまなサイズの行を含む) として使用できます。a[2][3]コンパイラにアクセスするには、ポインタをフェッチしa[2]てから、値にアクセスするためにポインタが指す場所を 3 つ過ぎた場所に移動します。これb[10][20]を 10 個の要素の配列と比較してください。各要素は 20 個の整数の配列です。コンパイラにアクセスb[2][3]すると、2 に 20 個の整数のサイズを掛けて、さらに 3 個の整数のサイズを加算することにより、配列のメモリ領域の先頭がオフセットされます。

最後に、このトラップについて考えてみましょう。1つのCファイルにある場合

int a[10];

そして別の

extern int *a;
a[0] = 42;

ファイルはエラーなしでコンパイルおよびリンクされますが、コードは期待どおりに動作しません。おそらく null ポインターの割り当てでクラッシュします。その理由は、2 番目のファイルの a はポインタであり、その値は最初のファイルの内容a[0]、つまり最初は 0 であるためです。

于 2009-09-03T11:47:40.053 に答える
2

ここを見て:

2.2: でも char a[] は char *a と同じだと聞きました。

http://www.lysator.liu.se/c/c-faq/c-2.html

于 2009-09-03T11:57:22.043 に答える
1

あなたの例には2つのaと2つのbがあります。

パラメータとして

void foo(int *a) {
    a[98] = 0xFEADFACE;
}

void bar(int b[]) {
    *(b+498) = 0xFEADFACE;
}

a と b は同じ型 (int へのポインター) です。

変数として

int *a;
int b[10];

同時期ではありません。1 つ目はポインタ、2 つ目は配列です。

配列の動作

配列 (変数であるかどうかに関係なく) は、ほとんどのコンテキストで、その最初の要素へのポインターで暗黙的に変換されます。それが行われていない C の 2 つのコンテキストは、sizeof の引数としてと&;の引数としてです。C++ では、参照パラメーターとテンプレートに関連するものがいくつかあります。

私は、変数に対してのみ変換が行われないため、変数かどうか、いくつかの例を書きました:

int foo[10][10];
int (*bar)[10];
  • foo10 個の整数の 10 個の配列の配列です。ほとんどのコンテキストでは、10 int の配列へのポインター型の最初の要素へのポインターに変換されます。

  • foo[10]10 個の int の配列です。ほとんどのコンテキストでは、int へのポインター型の最初の要素へのポインターに変換されます。

  • *bar10 個の int の配列です。ほとんどのコンテキストでは、int へのポインター型の最初の要素へのポインターに変換されます。

いくつかの歴史

C の直接の祖先である B では、

int x[10];

現在の C で何を書くかという効果がありました

int _x[10];
int *x = &_x;

つまり、メモリを割り当て、それへのポインタを初期化しました。一部の人々は、それが C でも真実であると誤解しているようです。

注: C が B ではなく、まだ C と呼ばれていないとき、ポインターが宣言されていた時期がありました。

int x[];

しかし

int foo[10];

現在の意味になります。機能パラメータの調整は当時の名残です。

于 2009-09-03T13:18:35.190 に答える
0

いいえ、それらは同じではありません!1 つは int へのポインターで、もう 1 つは 100 個の int の配列です。そうです、彼らは同じです!

わかりました、私はこの愚かさを説明しようとします。

*a と a[100] は基本的に同じです。しかし、コンパイラのメモリ処理ロジックを詳しく見ると、次のようになります。

  • *a コンパイラ、メモリが必要ですが、それについては後で説明しますので、今は落ち着いてください。
  • a[100] コンパイラ、私はメモリが必要です、そして私は100が必要であることを知っているので、それがあることを確認してください!

どちらもポインタです。そして、コードはそれらを同じように扱い、それらのポインターの近くのメモリを必要なだけ踏みにじることができます。しかし、a[100]コンパイル時に割り当てられたポインタからの連続メモリですが、 *a はいつメモリが必要になるかわからないためポインタを割り当てるだけです(実行時のメモリの悪夢)。

それで、誰が気にしますか?まあ、sizeof()ケアのような特定の機能。 とに対してsizeof(a)異なる答えを返します。そして、これは機能も異なります。この関数の場合、コンパイラは違いを認識しているため、ループや memcpy などのコードでもこれを有利に使用できます。続けて試してください。*aa[100]

これは大きな質問ですが、ここで私が与える答えはこれです。コンパイラは微妙な違いを認識しており、ほとんどの場合は同じように見えますが、重要な場合には異なるコードを生成します。cimpiler にとって *a または a[100] が何を意味し、どこでそれを異なる方法で扱うかは、あなた次第です。それらは事実上同じである可能性がありますが、同じではありません。さらに悪いことに、あなたが持っているような関数を呼び出すことでゲーム全体を変えることができます.

ふぅ…今、C# のようなマネージ コードがとてもホットなのも不思議ではありませんか?!

編集: あなたができることも追加する必要がありますが*a_p = X、あなたの配列の1つでそれを試してみてください! 配列はポインターと同じようにメモリを操作しますが、移動したりサイズを変更したりすることはできません。のようなポインター*a_pは、さまざまなものを指すことができます。

于 2009-09-03T11:50:00.220 に答える
0

a と b は同じ型ですか?

はい。[編集: 明確にする必要があります: 関数 foo のパラメーター a は、関数 bar のパラメーター b と同じ型です。どちらも int へのポインタです。main のローカル変数 a は、int のローカル変数 b と同じ型です。どちらも int の配列です (サイズが同じではないため、実際には同じ型ではありません。ただし、どちらも配列です)。]

int *a はコンパイラ内部で int a[] と同じ型として扱われますか?

通常はありません。例外はfoo bar[]、関数にパラメーターとして書き込む場合 (ここで行ったように)、自動的に になりfoo *barます。

ただし、非パラメータ変数を宣言する場合、大きな違いがあります。

int * a; /* pointer to int. points nowhere in paticular right now */
int b[10]; /* array of int. Memory for 10 ints has been allocated on the stack */
foo(a); /* calls foo with parameter `int*` */
foo(b); /* also calls foo with parameter `int*` because here the name b basically
           is a pointer to the first elment of the array */
于 2009-09-03T11:25:21.230 に答える
0

文字配列へのポインターがある (そしてその配列のサイズを取得したい) 場合、sizeof(ptr) は使用できず、代わりに strlen(ptr)+1 を使用する必要があります。

于 2009-09-03T13:08:29.313 に答える
0

これを簡単に説明するために、リングに帽子を投げます。

  • 配列は、同じタイプの一連の連続したストレージの場所です

  • ポインタは、単一の格納場所のアドレスです

  • 配列のアドレスを取得すると、その最初の要素のアドレス (つまり、ポインタ) が得られます。

  • 配列の要素には、配列の最初の要素へのポインタを介してアクセスできます。これが機能するのは、添字演算子 [] がこれを容易にするように設計された方法でポインターに定義されているためです。

  • ポインター パラメーターが必要な場所に配列を渡すことができ、最初の要素へのポインターに自動的に変換されます (ただし、これは複数レベルのポインターまたは多次元配列の場合は再帰的ではありません)。繰り返しますが、これは仕様によるものです。

そのため、多くの場合、配列とその最初の要素へのポインターとの間に意図的に特別な関係があるため、配列として割り当てられていない配列と連続するメモリ ブロックに対して同じコードを使用できます。ただし、それらは異なる型であり、状況によっては異なる動作をします。たとえば、配列へのポインターは、ポインターからポインターとはまったく同じではありません。

これは、ポインターから配列へのポインターとポインターからポインターへの問題に触れる最近の SO の質問です: Whats the difference between "abc" and {"abc"} in C?

于 2009-09-03T12:34:19.190 に答える