多次元配列はどのようにメモリ内でフォーマットされますか?


185

Cでは、次のコードを使用して、2次元配列をヒープに動的に割り当てることができます。

int** someNumbers = malloc(arrayRows*sizeof(int*));

for (i = 0; i < arrayRows; i++) {
    someNumbers[i] = malloc(arrayColumns*sizeof(int));
}

明らかに、これは実際には、整数の個別の1次元配列の束へのポインターの1次元配列を作成します。

someNumbers[4][2];

しかし、次の行のように2D配列を静的に宣言すると...:

int someNumbers[ARRAY_ROWS][ARRAY_COLUMNS];

...同様の構造がスタック上に作成されますか、それとも完全に別の形式ですか?(つまり、ポインターの1D配列ですか?そうでない場合、それは何ですか、そしてそれへの参照はどのように理解されますか?)

また、私が「システム」と言ったとき、それを理解するために実際に責任があるのは何ですか?カーネル?または、Cコンパイラはコンパイル中にそれを分類しますか?


8
できれば+1以上をあげます。
Rob Lachlan 2010

1
警告:このコードには2D配列がありません!
このサイトには正直すぎます

@toohonestforthissite確かに。さらに詳しく言うと、ループと呼び出しmalloc()はN次元配列にはなりません。。結果として、ポインタの配列[ポインタの配列[...]への]が1次元配列を完全に分離します。参照正しく多次元配列を割り当てる割り当て方法を確認するためにTRUE N次元配列。
Andrew Henle

回答:


145

静的な2次元配列は配列の配列のように見えます-それはメモリ内で隣接して配置されているだけです。配列はポインタと同じものではありませんが、ほとんど同じ意味で使用できるため、混乱することがあります。ただし、コンパイラーは適切に追跡しているため、すべてがうまく整列します。静的2D配列については、前述のように注意する必要がありint **ます。パラメータを受け取る関数に配列を渡そうとすると、問題が発生するためです。ここに簡単な例があります:

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

メモリ内では次のようになります。

0 1 2 3 4 5

まったく同じ:

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

しかし、array1この関数に渡そうとすると:

void function1(int **a);

警告が表示されます(アプリは配列に正しくアクセスできません):

warning: passing argument 1 of function1 from incompatible pointer type

2D配列はと同じではないためint **です。配列のポインタへの自動減衰は、いわば「1レベルの深さ」だけです。関数を次のように宣言する必要があります。

void function2(int a[][2]);

または

void function2(int a[3][2]);

すべてを幸せにするため。

これと同じ概念がn次元配列にも拡張されます。ただし、アプリケーションでこの種の面白いビジネスを利用すると、一般に理解が難しくなるだけです。だから注意してください。


説明ありがとう。したがって、「void function2(int a [] [2]);」となります。静的および動的に宣言された2Dの両方を受け入れますか?そして、最初の次元が[]のままになっている場合でも、配列の長さを渡すことは、依然として良い習慣/必須であると思いますか?
Chris Cooper

1
@Chris私はそうは思いません-Cをスタックまたはグローバルに割り当てられた配列を一連のポインタにスウィズルするのは難しいでしょう。
Carl Norum、2010

6
@JasonK。- 番号。配列はポインタではありません。配列は、一部のコンテキストではポインタに「減衰」しますが、まったく同じではありません
Carl Norum、

1
明確にするために:はいChrisは別のパラメーターとして「配列の長さを渡すことは依然として良い習慣です」、そうでない場合はstd :: arrayまたはstd :: vector(これは古いCではなくC ++です)を使用します。新しいユーザーにとって概念的にも実際的にも@CarlNorumは同意するものだと思います。QuoraでAnders Kaseorgを引用します。「Cを学ぶ最初のステップは、ポインターと配列は同じものであることを理解することです。2番目のステップは、ポインターと配列が異なることを理解することです。」
Jason K.

2
@JasonK。「Cを学ぶ最初のステップは、ポインターと配列が同じものであることを理解することです。」-この引用は非常に間違っており、誤解を招きやすいです!それらが同じではないことを理解することは確かに最も重要なステップですが、その配列はほとんどの演算子の最初の要素へのポインターに変換されます!(あなたの場合を除きとプラットフォームを見つけるバイトを/ が、それは別のものです。sizeof(int[100]) != sizeof(int *)100 * sizeof(int)int
あまりにも正直、このサイトのために

85

答えは、Cには実際に 2D配列がないという考えに基づいています。C には配列の配列があります。これを宣言すると:

int someNumbers[4][2];

あなたはsomeNumbers、4つの要素の配列になることを求めています。この場合、その配列の各要素はタイプですint [2](それ自体が2 intの配列です)。

パズルの他の部分は、配列は常にメモリ内で隣接して配置されるということです。あなたが求めるなら:

sometype_t array[4];

その後、常に次のようになります。

| sometype_t | sometype_t | sometype_t | sometype_t |

(4つのsometype_tオブジェクトが隣同士に配置され、間にスペースはありません)。したがって、someNumbers配列の配列では、次のようになります。

| int [2]    | int [2]    | int [2]    | int [2]    |

そして、各int [2]要素はそれ自体が配列であり、次のようになります。

| int        | int        |

だから全体的に、あなたはこれを手に入れます:

| int | int  | int | int  | int | int  | int | int  |

1
最終的なレイアウトを見ると、int a [] []はint * ... rightとしてアクセスできると思います。
Narcisse Doudieu Siewe

2
@ user3238855:タイプに互換性はありませんがint、配列の配列の最初のポインタを取得した場合(a[0]またはを評価することによって&a[0][0])、はい、それをオフセットしてすべてのに順次アクセスできますint)。
カフェ

28
unsigned char MultiArray[5][2]={{0,1},{2,3},{4,5},{6,7},{8,9}};

メモリ内は次と等しい:

unsigned char SingleArray[10]={0,1,2,3,4,5,6,7,8,9};

5

あなたの答えにも:両方、ただし、コンパイラは重い作業のほとんどを行っています。

静的に割り当てられた配列の場合、「システム」がコンパイラになります。スタック変数と同じようにメモリを予約します。

mallocされた配列の場合、「The System」はmalloc(通常はカーネル)の実装者になります。コンパイラーが割り振るのはベース・ポインターだけです。

コンパイラーは、Carlが交換可能な使用法を理解できる例を除いて、型を宣言されたとおりに処理します。これが[] []を関数に渡す場合、静的に割り当てられたフラットであると想定する必要がある理由です。ここで、**はポインターへのポインターであると想定されます。


@Jon L. mallocはカーネルによって実装されているとは言いませんが、カーネルプリミティブ(brkなど)の上のlibcによって実装されています
Manuel Selva

@ManuelSelva:malloc実装の場所と方法は標準で指定されておらず、実装に任されています。環境。自立環境の場合、リンク機能を必要とする標準ライブラリのすべての部分と同様に、オプションです(これは、要件が実際にもたらすものであり、文字通り標準が述べているものではありません)。一部の最新のホスト環境では、stdlibとカーネルプリミティブの両方を使用して記述したように、完全なもの、または(Linuxなど)カーネル機能に依存しています。非仮想メモリのシングルプロセスシステムの場合、stdlibのみが可能です。
このサイトには正直すぎる。

2

我々が持っている、と仮定a1してa2(C99)以下のように定義して初期化:

int a1[2][2] = {{142,143}, {144,145}};
int **a2 = (int* []){ (int []){242,243}, (int []){244,245} };

a1メモリ内の単純な連続レイアウトを持つ同種の2D配列であり、式(int*)a1は最初の要素へのポインタに対して評価されます。

a1 --> 142 143 144 145

a2異種2D配列から初期化され、typeの値へのポインタですint*。つまり、逆参照式*a2はtypeの値に評価されますint*。メモリレイアウトは連続している必要はありません。

a2 --> p1 p2
       ...
p1 --> 242 243
       ...
p2 --> 244 245

完全に異なるメモリレイアウトとアクセスセマンティクスにもかかわらず、配列アクセス式のC言語の文法は、同種と異種の2D配列の両方でまったく同じに見えます。

  • a1[1][0]は配列144から値をフェッチしますa1
  • a2[1][0]は配列244から値をフェッチしますa2

コンパイラは、アクセスのための式はことを知っているa1タイプで動作するint[2][2]ためのアクセス-expressionがするとき、a2タイプで動作しますint**。生成されたアセンブリコードは、同種または異種のアクセスセマンティクスに従います。

タイプの配列が型int[N][M]キャストされ、次にtypeとしてアクセスされると、コードは通常、実行時にクラッシュします。次にint**例を示します。

((int**)a1)[1][0]   //crash on dereference of a value of type 'int'

1

特定の2D配列にアクセスするには、以下のコードに示すように、配列宣言のメモリマップを検討してください。

    0  1
a[0]0  1
a[1]2  3

各要素にアクセスするには、必要な配列をパラメーターとして関数に渡すだけで十分です。次に、列のオフセットを使用して、各要素に個別にアクセスします。

int a[2][2] ={{0,1},{2,3}};

void f1(int *ptr);

void f1(int *ptr)
{
    int a=0;
    int b=0;
    a=ptr[0];
    b=ptr[1];
    printf("%d\n",a);
    printf("%d\n",b);
}

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