配列の場合、なぜa [5] == 5 [a]になるのですか?


1622

Joel がCプログラミング言語(別名:K&R)のStack Overflowポッドキャスト#34で指摘しているように、Cでは配列のこのプロパティについて言及されています。a[5] == 5[a]

ジョエルは、それはポインター演算のせいだと言いますが、私はまだ理解していません。なぜa[5] == 5[a]ですか?


48
a [+]のようなものも*(a ++)OR *(++ a)のように機能しますか?
エゴン

45
@エゴン:とてもクリエイティブですが、残念ながらそれはコンパイラーの動作方法ではありません。コンパイラはa[1]、文字列ではなく一連のトークンとして解釈します。*({整数の場所} a {演算子} + {整数} 1)は*({整数} 1 {演算子} + {整数の場所} a)と同じです*({整数の場所} a {演算子} + {演算子} +)と同じではありません
Dinah

11
この上の興味深い化合物のばらつきがで例示されている非論理的配列アクセスあなたが持っている、char bar[]; int foo[];foo[i][bar]表現として使用されています。
ジョナサンレフラー、

5
@EldritchConundrum、なぜ「コンパイラは左部分がポインタであることを確認できない」と思いますか?はい、できます。a[b]= *(a + b)が与えられたaand については真実ですbが、+すべての型に対して可換であると定義されることは言語設計者の自由な選択でした。i + p許可してp + iいる間、彼らが禁止することを妨げることはできませんでした。
2014年

13
@Andrey Oneは通常+、可換であることを期待しているので、本当の問題は、個別のオフセット演算子を設計するのではなく、ポインタ操作を算術に似せることを選択することでしょう。
Eldritch Conundrum 2014年

回答:


1924

C標準では、[]演算子を次のように定義しています。

a[b] == *(a + b)

したがって、次のa[5]ように評価されます。

*(a + 5)

次の5[a]ように評価されます:

*(5 + a)

a配列の最初の要素へのポインタです。a[5]はから5 要素離れた値ですa。これはと同じです*(a + 5)。また、小学校の数学から、それらが等しいことがわかります(加算は可換です)。


325
それは*((5 * sizeof(a))+ a)に似ているのではないかと思います。素晴らしい説明です。
John MacIntyre

92
@ダイナ:Cコンパイラの観点からは、あなたは正しいです。sizeofは必要ありません、そして私が言及したそれらの表現は同じです。ただし、コンパイラーはマシンコードの生成時にsizeofを考慮します。aがint配列の場合は、代わりa[5]に次のようmov eax, [ebx+20]にコンパイルされます[ebx+5]
Mehrdad Afshari

12
@Dinah:Aはアドレス、たとえば0x1230です。aが32ビットのint配列にある場合、a [0]は0x1230にあり、a [1]は0x1234にあり、a [2]は0x1238にあります... a [5]はx1244にあります。 0x1230、0x1235が返されますが、これは誤りです。
James Curran

36
@ sr105:これは、+演算子の特殊なケースです。ここで、オペランドの1つはポインターで、もう1つは整数です。標準では、結果はポインタのタイプになると述べています。コンパイラーは/十分にスマートでなければなりません。
aib 2008

48
「小学校の数学から、それらは等しいことがわかっています」-あなたは単純化していると理解していますが、これは単純化し過ぎていると感じている人たちと一緒です。それは初等ではありません*(10 + (int *)13) != *((int *)10 + 13)。つまり、ここでは小学校の算数よりも多くのことが行われています。可換性は、どのオペランドがポインターであるか(およびオブジェクトのサイズがどのポインターであるか)を認識するコンパイラーに大きく依存しています。別の言い方をすれば、(1 apple + 2 oranges) = (2 oranges + 1 apple)ですが(1 apple + 2 oranges) != (1 orange + 2 apples)
LarsH

288

なぜなら、配列アクセスはポインターの観点から定義されているからです。 a[i]*(a + i)可換であるを意味するように定義されています。


42
配列はポインターに関して定義されていませんが、それらへのアクセスは定義されています
軌道の軽さのレース

5
「と等しいので*(i + a)、と書くことができます」を追加しi[a]ます。
ジムBalter

4
次のような標準からの引用を含めることをお勧めします。6.5.2.1:2角括弧[]内の式が後に続く後置式は、配列オブジェクトの要素の添え字付きの指定です。添字演算子[]の定義は、E1 [E2]が(*((E1)+(E2)))と同一であることです。バイナリ+演算子に適用される変換規則により、E1が配列オブジェクト(同等に、配列オブジェクトの最初の要素へのポインター)であり、E2が整数である場合、E1 [E2]は、E2番目の要素を指定しますE1(ゼロから数えます)。
Vality 2015

より正確に言うと、配列にアクセスすると、配列はポインタに分解されます。
12431234123412341234123

Nitpick:「*(a + i)可換性がある」と言っても意味がありません。ただし、加算は可換である*(a + i) = *(i + a) = i[a]ためです。
Andreas Rejbrand

231

他の回答では何かが見落とされていると思います。

はい、p[i]定義によりと同等です*(p+i)(これは加算が可換であるため)はと同等です*(i+p)(これも[]演算子の定義による)はと同等i[p]です。

(およびでarray[i]は、配列名は暗黙的に配列の最初の要素へのポインターに変換されます。)

しかし、この場合、加算の交換可能性はそれほど明白ではありません。

両方のオペランドが同じ型の場合、または共通の型に昇格された異なる数値型の場合でも、可換性は完全に理にかなっていますx + y == y + x

ただし、この場合は、1つのオペランドがポインターで、もう1つのオペランドが整数であるポインター演算について具体的に説明します。(整数+整数は別の操作であり、ポインター+ポインターは無意味です。)

+オペレーターに関するC標準の説明(N1570 6.5.6)は次のように述べています。

さらに、両方のオペランドが算術型であるか、一方のオペランドが完全なオブジェクト型へのポインタであり、もう一方が整数型である必要があります。

それは同じくらい簡単に言ったでしょう:

さらに、両方のオペランドが算術型であるか、左の オペランドが完全なオブジェクト型へのポインタであり、右のオペランド が整数型である必要があります。

その場合、i + pとの両方i[p]が違法になります。

C ++の用語では、実際には2つのオーバーロードされた+演算子のセットがあり、大まかに次のように説明できます。

pointer operator+(pointer p, integer i);

そして

pointer operator+(integer i, pointer p);

最初のものだけが本当に必要です。

では、なぜこのようになるのでしょうか。

C ++はこの定義をCから継承し、Bから取得しました(配列のインデックス付けの可換性は、1972年のユーザーズリファレンスのBで明示的に言及されています。BCPLから取得しました。以前の言語(CPL?Algol?)。

したがって、配列のインデックス付けは加算の観点から定義されており、その加算はポインタと整数であっても交換可能であり、Cの祖先言語に何十年も遡ります。

これらの言語は、現代のC言語よりも強く型付けされていません。特に、ポインタと整数の違いはしばしば無視されました。(初期のCプログラマは、unsignedキーワードが言語に追加される前に、ポインタを符号なし整数として使用することがありました。)したがって、オペランドが異なる型であるため、加算を非可換にするという考えは、おそらくそれらの言語の設計者には思い浮かばなかったでしょう。ユーザーが2つの「もの」を追加したい場合、それらの「もの」が整数、ポインター、またはその他のものであるかどうかにかかわらず、それを防ぐのは言語の責任ではありませんでした。

そして、何年にもわたって、そのルールに変更を加えると既存のコードが壊れてしまいます(1989年のANSI C規格が好機だったかもしれませんが)。

ポインターを左側に配置し、整数を右側に配置する必要があるようにCまたはC ++を変更すると、既存のコードが壊れることがありますが、実際の表現力は失われません。

つまり、後者の形式はIOCCCの外部に表示されるべきではありませんが、まったく同じことarr[3]3[arr]意味します。


12
この物件の素晴らしい説明。高レベルの観点から3[arr]は、興味深いアーティファクトだと思いますが、使用されることはまれです。しばらく前に尋ねたこの質問(< stackoverflow.com/q/1390365/356>)に対する受け入れられた回答は、構文についての私が考えていた方法を変更しました。多くの場合、技術的にこれらのことを実行する正しい方法と間違った方法はありませんが、これらの種類の機能は、実装の詳細とは別の方法で考えるようになります。この異なる考え方にはメリットがあり、実装の詳細に固執すると部分的に失われます。
Dinah 2013

3
加算は可換です。C標準がそれを定義しないのは奇妙でしょう。そのため、「さらに、両方のオペランドが算術型であるか、左のオペランドが完全なオブジェクト型へのポインタであり、右のオペランドが整数型である」と簡単に言えなかったのはそのためです。-それを追加するほとんどの人には意味がありません。
iheanyi 14

9
@iheanyi:通常、加算は可換であり、通常、同じ型の2つのオペランドを取ります。ポインターの追加では、ポインターと整数を追加できますが、2つのポインターは追加できません。ポインタが左のオペランドであることを要求することは、大きな負担ではないだろうという十分に奇妙な特別なケースである私見です。(一部の言語では、文字列の連結に「+」を使用します。これは確かに可換ではありません。)
キース・トンプソン

3
@supercat、それはさらに悪いことです。つまり、x + 1!= 1 + xになることもあります。これは、加算の結合特性に完全に違反します。
iheanyi 2014年

3
@iheanyi:あなたは可換資産を意味すると思います。ほとんどの実装では(1LL + 1U)-2!= 1LL +(1U-2)であるため、加算はすでに関連付けられていません。実際、この変更により、現在関連付けられていない状況が関連付けられます。たとえば、3U +(UINT_MAX-2L)は(3U + UINT_MAX)-2になります。ただし、言語が昇格可能な整数と代数環を「ラップする」ための新しい個別の型を追加することで、ring16_t65535を保持するaに2を追加するring16_t、のサイズに関係なくint、値1のが生成されます。
スーパーキャット2014年

196

そしてもちろん

 ("ABCD"[2] == 2["ABCD"]) && (2["ABCD"] == 'C') && ("ABCD"[2] == 'C')

これの主な理由は、Cが設計された70年代には、コンピューターに十分なメモリがなかった(64KBが多かった)ため、Cコンパイラは構文チェックをあまり行わなかったためです。したがって、「X[Y]」はやや盲目的に「*(X+Y)」に 翻訳されました

+=」と「++」の構文についても説明します。「A = B + C」という形式のすべてが同じコンパイル済み形式でした。ただし、BがAと同じオブジェクトである場合、アセンブリレベルの最適化が利用可能でした。しかし、コンパイラはそれを認識するほど明るくなかったので、開発者は(A += C)をしなければなりませんでした。同様に、もしCいた1、別のアセンブリレベルの最適化が利用可能であった、と再び開発者は、コンパイラがそれを認識していなかったので、それを明示しなければなりませんでした。(最近ではコンパイラーが実行するため、これらの構文は最近ほとんど不要です)


127
実際、これはfalseと評価されます。最初の項「ABCD」[2] == 2 ["ABCD"]は、trueまたは1、1と評価されます!= 'C':D
Jonathan Leffler

8
@ジョナサン:同じあいまいさにより、この投稿の元のタイトルが編集されます。等号は、数学的な同等性、コード構文、または疑似コードです。数学的な同等性について議論しますが、コードについて話しているので、コード構文の観点からすべてを表示していることを逃れられません。
ダイナ

19
これは神話ではありませんか?+ =および++演算子は、コンパイラーを簡素化するために作成されたということですか?一部のコードはそれらを使用するとより明確になり、コンパイラーがそれをどのように処理するかに関係なく、構文が役立つと便利です。
Thomas Padron-McCarthy

6
+ =および++には、もう1つの大きな利点があります。評価中に左側で変数を変更した場合、変更は1回だけ行われます。a = a + ...; それを2回行います。
Johannes Schaub-litb 2008

8
いいえ-"ABCD" [2] == *( "ABCD" + 2)= *( "CD")= 'C'。文字列を逆参照すると、部分文字列ではなく文字が得られます
MSalters

55

誰もダイナの問題について言及していないように見えることの1つsizeof

ポインタに整数を追加できるだけで、2つのポインタを同時に追加することはできません。このようにして、整数へのポインター、またはポインターへの整数を追加するとき、コンパイラーは、どのビットが考慮に入れられる必要があるサイズを持っているかを常に知っています。


1
受け入れられた回答のコメントには、これについてかなり徹底的な会話があります。私は編集中の会話を元の質問に言及しましたが、sizeofの非常に有効な懸念に直接対処しませんでした。SOでこれを最適に行う方法がわかりません。元のファイルをもう一度編集する必要があります。質問?
ダイナ

50

文字通り質問に答えること。常にそうであるとは限らないx == x

double zero = 0.0;
double a[] = { 0,0,0,0,0, zero/zero}; // NaN
cout << (a[5] == 5[a] ? "true" : "false") << endl;

プリント

false

27
実は「ナン」は、それ自体に等しくない:cout << (a[5] == a[5] ? "true" : "false") << endl;ですfalse
TrueY

8
@TrueY:彼は具体的にNaNの場合について(そして特にそれx == xが常に正しいとは限らない)と述べました それが彼の意図だったと思います。したがって、彼は技術的に正しいです(そしておそらく、彼らが言うように、最高の種類の正しい!)。
TimČas、2015

3
問題はCに関するもので、コードはCコードではありません。NANin もあります<math.h>。これは、が定義されていない場合はUB 0.0/0.0であるため0.0/0.0です__STDC_IEC_559__(ほとんどの実装ではは定義されていません__STDC_IEC_559__が、ほとんどの実装で0.0/0.0は機能します)
12431234123412341234123

26

この醜い構文が「有用」であるか、同じ配列への位置を参照するインデックスの配列を処理したい場合は、少なくとも非常に楽しいことがわかります。ネストされた角括弧を置き換えて、コードを読みやすくすることができます!

int a[] = { 2 , 3 , 3 , 2 , 4 };
int s = sizeof a / sizeof *a;  //  s == 5

for(int i = 0 ; i < s ; ++i) {  

           cout << a[a[a[i]]] << endl;
           // ... is equivalent to ... 
           cout << i[a][a][a] << endl;  // but I prefer this one, it's easier to increase the level of indirection (without loop)

}

もちろん、実際のコードではそのようなユースケースはないと確信していますが、とにかく面白かったです:)


i[a][a][a]あなたが私を見ると、私は配列へのポインターか、配列または配列へのポインターの配列のいずれかでaあり、インデックスです。を見るa[a[a[i]]]と、aは配列または配列へのポインタでiあり、インデックスであると考えています。
12431234123412341234123

1
うわー!この「愚かな」機能のとてもクールな使い方です。アルゴリズムコンテストでいくつかの問題に役立つ可能性があります))
Serge Breusov

26

いい質問/答え。

Cポインターと配列は同じではないことを指摘したいだけですが、この場合、違いは本質的ではありません。

次の宣言を検討してください。

int a[10];
int* p = a;

ではa.out、シンボルaは配列の先頭のpアドレスにあり、シンボルはポインタが格納されているアドレスにあり、そのメモリ位置のポインタの値は配列の先頭です。


2
いいえ、技術的には同じではありません。いくつかのbをint * constとして定義し、それを配列を指すようにしても、それはまだポインターです。つまり、シンボルテーブルでは、bはアドレスを格納するメモリロケーションを参照します。 。
PolyThinker 2008

4
非常に良い点です。1つのモジュールでグローバルシンボルをchar s [100]として定義したときに、非常に厄介なバグがあったことを覚えています。それをextern char * sとして宣言します。別のモジュールで。すべてをリンクした後、プログラムは非常に奇妙な動作をしました。extern宣言を使用するモジュールが配列の最初のバイトをcharへのポインターとして使用していたためです。
Giorgio、

1
もともと、Cの祖父母BCPLでは、配列はポインターでした。つまり、あなたが書いたときに得たもの(私はCに音訳しました)int a[10]は 'a'と呼ばれるポインターであり、それは他の場所で10の整数のための十分なストアを指しています。したがって、a + iとj + iは同じ形式でした:いくつかのメモリ位置の内容を追加します。実際、BCPLはタイプレスだったので、まったく同じでした。また、BCPLは純粋にワード指向であったため(ワードアドレスのマシンでも)、sizeof-typeスケーリングは適用されませんでした。
デイブ、2012年

違いを理解する最良の方法は、と比較int*p = a;することだと思います。int b = 5; 後者では、「b」と「5」はどちらも整数ですが、「b」は変数であり、「5」は固定値です。同様に、「p」と「a」はどちらも文字のアドレスですが、「a」は固定値です。
James Curran

20

Cのポインターの場合、

a[5] == *(a + 5)

そしてまた

5[a] == *(5 + a)

したがって、それは真実です a[5] == 5[a].


15

答えではありませんが、考えるための食べ物です。クラスにオーバーロードされたインデックス/添え字演算子がある場合、式0[x]は機能しません。

class Sub
{
public:
    int operator [](size_t nIndex)
    {
        return 0;
    }   
};

int main()
{
    Sub s;
    s[0];
    0[s]; // ERROR 
}

intクラスにアクセスできないため、これは実行できません。

class int
{
   int operator[](const Sub&);
};

2
class Sub { public: int operator[](size_t nIndex) const { return 0; } friend int operator[](size_t nIndex, const Sub& This) { return 0; } };
Ben Voigt 2013

1
実際にコンパイルしてみましたか?クラスの外に(つまり、非静的関数として)実装できない演算子のセットがあります!
アジェイ2013

3
おっと、あなたは正しい。「operator[]パラメーターが1つだけの非静的メンバー関数である必要があります。」私はの制限に精通していてoperator=、に適用されるとは思わなかった[]
Ben Voigt 2013

1
もちろん、あなたがの定義変更した場合、[]オペレータが、それがあれば...再び同等になることはないa[b]ISが等しく*(a + b)、あなたはこれを変更し、あなたもオーバーロードする必要がありますint::operator[](const Sub&);int...クラスではありません
ルイス・コロラド

7
これは... Cじゃない...
MD XF

11

Ted JensenによるCのポインタと配列に関するチュートリアルで非常に良い説明があります。

テッドジェンセンは次のように説明しています。

実際、これは真実です。つまり、どこに書いa[i]*(a + i) も問題なく置き換えることができます。実際、コンパイラはどちらの場合でも同じコードを作成します。したがって、ポインタ演算は配列のインデックス付けと同じものであることがわかります。どちらの構文でも同じ結果になります。

これは、ポインタと配列が同じものであると言っているのではなく、同じではありません。配列の特定の要素を識別するには、配列のインデックスを使用する構文とポインタ演算を使用する構文の2つの構文を選択できることだけを述べています。

さて、この最後の式、その一部を見ると(a + i)、.. は、+演算子を使用した単純な追加であり、Cの規則は、そのような式は可換であると述べています。つまり、(a + i)はと同じです(i + a)。したがって、と*(i + a)同じくらい簡単に書くことができました*(a + i)。しかし*(i + a)、から来たかもしれないi[a]!これらすべてから、次のような奇妙な真実が生まれます。

char a[20];

書き込み

a[3] = 'x';

書くことと同じです

3[a] = 'x';

4
a + iはポインタ演算であるため、単純な加算ではありません。aの要素のサイズが1(char)の場合、はい、整数+と同じです。しかし、それが(例えば)整数の場合、+ 4 * iと同等になる可能性があります。
Alex Brown、

@AlexBrownはい、それはポインタ算術です。最初に 'a'を(char *)にキャストしない限り、これがまさに最後の文が間違っている理由です(intが4文字であると仮定)。ポインタ演算の実際の値の結果にそれほど多くの人が夢中になっている理由が本当にわかりません。ポインター演算の全体的な目的は、基になるポインター値を抽象化し、プログラマーに、アドレス値ではなく、操作されるオブジェクトについて考えさせることです。
jschultz410 2018年

8

質問への回答はわかっていますが、この説明を共有することに抵抗がありませんでした。

コンパイラ設計の原則を覚えています。aint配列で、サイズintが2バイトで、ベースアドレスがaが1000であるとします。

どのようa[5]に動作します->

Base Address of your Array a + (5*size of(data type for array a))
i.e. 1000 + (5*2) = 1010

そう、

同様に、cコードを3アドレスコードに分解 5[a]すると、->になります。

Base Address of your Array a + (size of(data type for array a)*5)
i.e. 1000 + (2*5) = 1010 

したがって、基本的には両方のステートメントがメモリ内の同じ場所を指しているため、a[5] = 5[a]です。

この説明は、配列の負のインデックスがCで機能する理由でもあります。

つまり、アクセスa[-5]すると、

Base Address of your Array a + (-5 * size of(data type for array a))
i.e. 1000 + (-5*2) = 990

場所990にあるオブジェクトを返します。


6

Cアレイarr[3]及び3[arr]同じであり、その等価ポインタ表記である*(arr + 3)*(3 + arr)。しかし、逆に[arr]3[3]arr正しくないと、構文エラーになります、など(arr + 3)*(3 + arr)*有効な式ではありません。その理由は、逆参照演算子は、アドレスの後にではなく、式によって生成されたアドレスの前に配置する必要があるためです。


6

Cコンパイラ

a[i]
i[a]
*(a+i)

配列の要素を参照するさまざまな方法があります!(まったくではない)


5

少し歴史があります。他の言語の中でも、BCPLはCの初期の開発にかなり大きな影響を与えました。BCPLで次のような配列を宣言した場合:

let V = vec 10

実際には、10ではなく11ワードのメモリが割り当てられました。通常、Vが最初で、直後のワードのアドレスが含まれていました。したがって、Cとは異なり、Vの名前はその場所に移動し、配列の0番目の要素のアドレスを取得しました。したがって、次のように表されるBCPLの配列間接指定

let J = V!5

J = !(V + 5)配列のベースアドレスを取得するためにVをフェッチする必要があったため、実際に(BCPL構文を使用して)実行する必要がありました。したがってV!55!V同義でした。事例観察として、WAFL(ワーウィック関数型言語)はBCPLで記述されており、私の記憶の限りでは、データストレージとして使用されるノードへのアクセスには、前者ではなく後者の構文を使用する傾向がありました。これは35年から40年前のどこかからのものであるため、私の記憶は少し錆びています。:)

ストレージの余分なワードを省き、名前が付けられたときにコンパイラーに配列のベースアドレスを挿入させるという革新は後に登場しました。Cの歴史の論文によると、これは構造がCに追加された頃に起こりました。

!BCPLでは、単項前置演算子と二項中置演算子の両方があり、どちらの場合も間接指定を行うことに注意してください。バイナリ形式には、間接指定を行う前に2つのオペランドが追加されているだけです。BCPL(およびB)の単語指向の性質を考えると、これは実際には非常に理にかなっています。Cではデータ型を取得する際に「ポインタと整数」の制限が必要にsizeofなり、モノになりました。


1

まあ、これは言語サポートのためにのみ可能な機能です。

コンパイラはa[i]として解釈し*(a+i)、式はに5[a]評価され*(5+a)ます。加算は可換であるため、両方が等しいことがわかります。したがって、式はに評価されtrueます。


冗長ですが、これは明確で簡潔です。
ビルK

0

Cで

 int a[]={10,20,30,40,50};
 int *p=a;
 printf("%d\n",*p++);//output will be 10
 printf("%d\n",*a++);//will give an error

ポインタは「変数」です

配列名は「ニーモニック」または「同義語」

p++;有効ですがa++無効です

a[2] これは2 [a]と同じです。これは、両方の内部演算が

「ポインター演算」は内部的に次のように計算されます

*(a+3) 等しい *(3+a)


-4

ポインタ型

1)データへのポインタ

int *ptr;

2)データへのconstポインター

int const *ptr;

3)constデータへのconstポインター

int const *const ptr;

そして、配列は、私たちのリストから(2)のタイプです
あなたはときに配列を定義一度に一つのアドレスが初期化されているポインタで
、我々はそれをスローしています原因私たちは私たちのプログラムでconstの値を変更したり、修正することができないことを知っているようにERRORをコンパイル時に時間

主な違い私が見つけたのです...

ポインターをアドレスで再初期化できますが、配列の場合とは異なります。

======
そしてあなたの質問に戻って...それ
a[5]は何でも*(a + 5)
簡単に理解できます
a -私たちのリストの(2)タイプのポインタのようにアドレス(人々はそれをベースアドレスと呼ぶ)を含む
[]-その演算子はポインタと交換可能*

最後に...

a[5] == *(a +5) == *(5 + a) == 5[a] 
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.