負の配列インデックスが意味をなすのはなぜですか?


14

私はCプログラミングの奇妙な経験に出会いました。次のコードを検討してください。

int main(){
  int array1[6] = {0, 1, 2, 3, 4, 5};
  int array2[6] = {6, 7, 8, 9, 10, 11};

  printf("%d\n", array1[-1]);
  return 0;
}

これをコンパイルして実行しても、エラーや警告は表示されません。私の講師が言ったように、配列インデックス-1は別の変数にアクセスします。私はまだ混乱していますが、なぜプログラミング言語にはこの機能があるのですか?つまり、なぜ負の配列インデックスを許可するのですか?


2
この質問は、具体的なプログラミング言語としてのCに動機付けられていますが、ここでは(かろうじて)話題になっている概念的な質問として理解できると思います。
ラファエル

6
@Raphael私は同意できないと、それは(アレイの外側のメモリを参照)、これは教科書未定義の動作である、SOのいずれかの方法を属している必要がありますし、適切なコンパイラフラグはこれについて警告しなければならないと考えている
ラチェットフリーク

@ratchetfreakに同意します。有効なインデックス範囲は[0、5]であるため、コンパイラの欠陥のようです。外部にあるものはすべて、コンパイル/実行時エラーでなければなりません。一般的に、ベクトルは、最初の要素インデックスがユーザー次第である関数の特定のケースです。Cコントラクトは要素がインデックス0で始まることであるため、負の要素にアクセスするのはエラーです。
ヴァル

2
@Raphael Cには、ここで重要な配列を持つ典型的な言語に対して2つの特性があります。1つは、Cにはサブ配列があり、サブ-1配列の要素を参照することは、より大きな配列でその配列の前の要素を参照するための完全に有効な方法であるということです。もう1つは、インデックスが無効な場合、プログラムは無効ですが、ほとんどの実装では、範囲外のエラーではなく、サイレントな悪い動作が発生するということです。
ジル 'SO-悪であるのをやめる'

4
@Gillesそれが問題のポイントである場合、これは実際にStack Overflowにあったはずです。
ラファエル

回答:


27

配列のインデックス付け操作a[i]は、Cの次の機能からその意味を獲得します。

  1. 構文a[i]はと同等*(a + i)です。したがって5[a]、の5番目の要素に到達すると言うのは有効ですa

  2. ポインタ算術ポインタ所与と言うpと整数ip + i ポインタがp進めi * sizeof(*p)バイト

  3. 配列の名前は、a非常に迅速に、0番目の要素へのポインターに移ります。a

実際、配列のインデックス付けは、ポインターのインデックス付けの特殊なケースです。ポインタので、アレイ内の任意の場所、のように見えるが、その任意の式を指すことができますp[-1]されていない検査で間違って、コンパイラので、エラーなど、すべてのそのような表現を検討する(することはできません)していません。

あなたの例実際には配列の名前ですが、実際には無効です。IIRC、配列の0番目の要素へのポインターであることがわかっている式の結果として意味のあるポインター値がある場合、それは未定義です。したがって、賢いコンパイラーはこれを検出し、エラーとしてフラグを立てることができます。ランダムスタックスロットへのポインターを提供することで、自分の足で撃つことができる一方で、他のコンパイラーは引き続き準拠できます。a[-1]aa - 1a

コンピュータサイエンスの答えは次のとおりです。

  • Cでは、[]演算子は配列ではなくポインターで定義されます。特に、ポインター演算とポインター逆参照の観点から定義されています。

  • Cでは、ポインターは抽象的に(start, length, offset)は条件を持つタプルです0 <= offset <= length。ポインター演算は基本的にオフセットのリフト演算であり、操作の結果がポインター条件に違反する場合は未定義の値になるという注意事項があります。ポインターを逆参照すると、という追加の制約が追加されoffset < lengthます。

  • Cには、undefined behaviourコンパイラーが具体的にそのタプルを単一の数値として表すことができるという概念があり、ポインター条件の違反を検出する必要はありません。抽象セマンティクスを満たすプログラムは、具体的な(損失のある)セマンティクスで安全です。抽象セマンティクスに違反するものはすべて、コメントなしでコンパイラーに受け入れられ、コンパイラーがやりたいことは何でもできます。


特定のプログラミング言語の特異性に依存するものではなく、一般的な回答をしてください。
ラファエル

5
@Raphael、質問は明示的にCについてでした。CコンパイラがC の定義内で一見無意味な式をコンパイルできる理由の特定の質問に取り組んだと思います。-
ハリ

特にCに関する質問はここではオフトピックです。質問に対する私のコメントに注意してください。
ラファエル

4
この質問の比較言語学の側面はまだ有用だと思います。特定の実装が特定の具体的なセマンティクスを示した理由について、かなり「コンピューターサイエンス」風味の説明をしたと思います。
ハリ

15

配列は、連続したメモリチャンクとして単純にレイアウトされます。a [i]などの配列アクセスは、メモリロケーションaddressOf(a)+ i へのアクセスに変換されます。このコードa[-1]は完全に理解可能であり、単に配列の開始前のアドレスを参照しています。

これはおかしく思えるかもしれませんが、これが許可される理由はたくさんあります。

  • a [-]のインデックスiが配列の境界内にあるかどうかをチェックするのは高価です。
  • いくつかのプログラミング技術a[-1]は、有効な事実を実際に活用し ます。たとえば、それaが実際には配列の開始ではなく、配列の中央へのポインタであることがわかっている場合、ポインタa[-1]の左側にある配列の要素を取得します。

6
つまり、おそらく使用すべきではありません。限目。何、あなたの名前はドナルドクヌースであり、さらに17の指示を保存しようとしていますか?ぜひ、どうぞ。
ラファエル

返事をありがとう、しかし、私は考えを得ることができませんでした。ところで私は理解するまで何度もそれを読みます.. :)
モハメッドフォーザン

2
@Raphael:colaオブジェクトモデルの実装では、-1の位置を使用してvtableを格納します:piumarta.com/software/cola/objmodel2.pdf。したがって、フィールドはオブジェクトの正の部分に格納され、vtableは負の部分に格納されます。詳細は思い出せませんが、一貫性と関係があると思います。
デイブクラーク

@DeZéroToxin:配列は実際には単なるメモリ内の場所であり、その隣には論理的に配列の一部である場所がいくつかあります。しかし、実際には、配列は単なるポインターです。
デイブクラーク

1
@Raphaelは、のいくつかの場合にa[-1]完全に理にかなっています。aこの特定の場合、それは明らかに違法です(ただし、コンパイラーによってキャッチされません)
-vonbrand

4

他の回答が説明しているように、これはCの未定義の動作です。Cが「高レベルアセンブラ」として定義されている(そしてほとんど使用されている)ことを考慮してください。Cのユーザーは、妥協のないスピードでそれを高く評価し、実行時のチェックは(ほとんど)純粋なパフォーマンスのために問題外です。このように、他の言語から来た人にとって無意味に見える一部のCコンストラクトは、Cでは完全に理にかなっていa[-1]ます。はい、それは常に意味をなさない(


1
私はこの答えが好きです。これが問題ない理由の本当の理由を示します。
darxsys

3

このような機能を使用して、メモリに直接アクセスするメモリ割り当てメソッドを作成できます。そのような使用法の1つは、負の配列インデックスを使用して前のメモリブロックをチェックし、2つのブロックをマージできるかどうかを判断することです。不揮発性メモリマネージャーを開発するときに、この機能を使用しました。


2

Cは強く型付けされていません。標準のCコンパイラは配列の境界をチェックしません。もう1つは、Cの配列はメモリの連続ブロックに過ぎず、インデックス付けは0から始まるため、-1のインデックスは、前のビットパターンの場所ですa[0]

他の言語では、負のインデックスをうまく利用しています。Pythonではa[-1]、最後の要素、a[-2]最後から2番目の要素などを返します。


2
厳密な型指定と配列インデックスはどのように関連していますか?配列インデックスが自然でなければならない自然の型を持つ言語はありますか?
ラファエル

@Raphael私の知る限り、厳密な型指定とは、型エラーが検出されることを意味します。配列は型であり、IndexOutOfBoundsはエラーであるため、厳密に型指定された言語ではこれが報告されますが、Cではこれは報告されません。それが私が意味したことです。
saadtaame

私が知っている言語では、配列インデックスはtype inta[-5]あり、より一般的にint i; ... a[i] = ...;は正しく型付けされています。インデックスエラーは実行時にのみ検出されます。もちろん、賢いコンパイラーはいくつかの違反を検出するかもしれません。
ラファエル

@Raphael私は全体として配列データ型について話しているのであり、インデックス型についてではありません。これが、Cがユーザーにa [-5]の書き込みを許可する理由を説明しています。はい、-5は正しいインデックスタイプですが、範囲外であり、エラーです。私の答えには、コンパイルまたは実行時の型チェックに関する言及はありません。
-saadtaame

1

簡単な言葉で:

Cのすべての変数(配列を含む)はメモリに格納されます。14バイトの「メモリ」があり、次を初期化するとします。

int a=0;
int array1[6] = {0, 1, 2, 3, 4, 5};

また、intのサイズを2バイトと考えてください。次に、仮に、メモリの最初の2バイトに整数aが保存されます。次の2バイトに配列の最初の位置の整数が保存されます(つまりarray [0])。

次に、array [-1]は、メモリ内に保存されたarray [0]の直前の整数を参照するようなものです。これは、仮に、整数aです。実際には、これは変数がメモリに保存される方法とはまったく異なります。


0
//:Example of negative index:
//:A memory pool with a heap and a stack:

unsigned char memory_pool[64] = {0};

unsigned char* stack = &( memory_pool[ 64 - 1] );
unsigned char* heap  = &( memory_pool[ 0     ] );

int stack_index =    0;
int  heap_index =    0;

//:reserve 4 bytes on stack:
stack_index += 4;

//:reserve 8 bytes on heap:
heap_index  += 8;

//:Read back all reserved memory from stack:
for( int i = 0; i < stack_index; i++ ){
    unsigned char c = stack[ 0 - i ];
    //:do something with c
};;
//:Read back all reserved memory from heap:
for( int i = 0; i < heap_index; i++ ){
    unsigned char c = heap[ 0 + i ];
    //:do something with c
};;

CS.SEへようこそ!私たちは、読書の説明や説明が付いた答えを探しています。私たちはコーディングサイトではありません。また、単なるコードのブロックである回答は必要ありません。そのような情報を提供するために回答を編集できるかどうかを検討することができます。ありがとうございました!
DW
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.