web-dev-qa-db-ja.com

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

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コンパイラはコンパイル中にそれを整理しますか?

159
Chris Cooper

静的な2次元配列は、配列の配列のように見えます。メモリ内に連続して配置されているだけです。配列はポインターと同じものではありませんが、配列をほとんど同じ意味で使用できるため、混乱する場合があります。ただし、コンパイラーは適切に追跡し続けるため、すべてがうまく整列します。 int **パラメータを取る関数に渡すと、悪いことが起こるので、あなたが言及したように静的な2D配列には注意する必要があります。以下に簡単な例を示します。

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-次元配列にも拡張されます。ただし、アプリケーションでこの種の面白いビジネスを利用することは、一般的に理解するのを難しくするだけです。だからそこに注意してください。

125
Carl Norum

答えは、Cは実際にはhave 2D配列ではなく、配列の配列を持っているという考えに基づいています。これを宣言するとき:

int someNumbers[4][2];

someNumbersが4つの要素の配列であり、その配列の各要素がint [2]型(それ自体が2 intsの配列)であることを求めています。

パズルの他の部分は、配列が常にメモリ内で連続して配置されることです。あなたが求める場合:

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  |
76
caf
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};
26
kanghai

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

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

Mallocされた配列の場合、「システム」はmalloc(通常はカーネル)の実装者になります。コンパイラが割り当てるのは、ベースポインタのみです。

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

5
Jon L

特定の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;
}
1