二次元配列を割り当てる奇抜な方法?


110

プロジェクトでは、誰かがこの行をプッシュしました:

double (*e)[n+1] = malloc((n+1) * sizeof(*e));

これはおそらく(n + 1)*(n + 1)doubleの2次元配列を作成します。

おそらく、これまでのところ、これが何をするのか、正確には、どこから来たのか、なぜ機能するのかを教えてくれなかったので、私は尋ねました。

多分私は明白な何かを見逃しているかもしれませんが、誰かが私に上記の行を説明していただければ幸いです。個人的には、私たちが実際に理解しているものを使ったほうがずっと気持ちがいいからです。


15
動作してもプッシャーを発射します。
マーティンジェームズ

22
@MartinJamesなんで?それ以外の場合、隣接するメモリに2D配列をどのように割り当てますか?マングルされた1D配列?それが1990年代のプログラミングです。現在、VLAがあります。
ランディン

43
記録のために、それがある一方と動的に実際の2次元アレイを割り当てる唯一の方法。
クエンティン

15
@Kninnugいいえ、2D配列ではなくポインタの配列を宣言します。2D配列が必要な場合、ポインターの配列を割り当てる必要がある理由はありません。ヒープの断片化とキャッシュメモリの利用率が低いために低速で、配列(memcpyなど)として使用できないため安全ではありません。さらに、コードは肥大化しています。複数のfree()呼び出しが必要になり、メモリリークが発生しやすくなります。そのようなコードが広まっているかもしれないので、それは明らかに悪いです。
ランディン

15
この問題は、サンプルが同じ値のディメンションを使用せずn+1、代わりに使用した場合の説明/回答がより明確でしたdouble (*e)[rows] = malloc(columns * sizeof *e);
chux-Reinstate Monica

回答:


87

変数eは、n + 1型の要素の配列へのポインタdoubleです。

で逆参照演算子を使用すると、「型の要素の配列」eeある基本型が得られます。n + 1double

mallocコールは、単にの塩基型をとるe(上記で説明)、それにより、乗算を、そのサイズを取得しn + 1、そしてするために、そのサイズを渡すmalloc機能。基本的n + 1にのn + 1要素の配列の配列を割り当てますdouble


3
@MartinJamesはとsizeof(*e)同等sizeof(double [n + 1])です。それを掛けれn + 1ば十分です。
一部のプログラマーの男

24
@MartinJames:何が問題なのですか?それは目がくらんでいるわけではなく、割り当てられた行が連続していることを保証し、他の2D配列と同じようにインデックスを付けることができます。私は自分のコードでこのイディオムをよく使用しています。
John Bode

3
当たり前のように思えるかもしれませんが、これは正方配列(同じ次元)でのみ機能します。
イェンス、

18
@イェンス:n+1両方の次元に置くと、結果は正方形になるという意味でのみ。指定した場合double (*e)[cols] = malloc(rows * sizeof(*e));、結果には指定した行数と列数が含まれます。
user2357112はモニカ

9
@ user2357112かなり見たくなりました。それが意味している場合でも、あなたが追加する必要がありますint rows = n+1int cols = n+1。神は賢いコードから私たちを救ってくださいます。
candied_orange

56

これは、2D配列を動的に割り当てる一般的な方法です。

  • e型の配列への配列ポインタdouble [n+1]です。
  • sizeof(*e)したがって、1つのdouble [n+1]配列のサイズである、ポイントされた型の型を示します。
  • n+1そのようなアレイにスペースを割り当てます。
  • eこの配列の配列の最初の配列を指すように配列ポインターを設定します。
  • これによりe、as を使用e[i][j]して2D配列内の個々のアイテムにアクセスできます。

個人的には、このスタイルはずっと読みやすいと思います:

double (*e)[n+1] = malloc( sizeof(double[n+1][n+1]) );

12
私はあなたの提案されたスタイルに同意せず、ptr = malloc(sizeof *ptr * count)スタイルを好むことを除いて良い答え です。
chux-モニカを2016年

いい答えで、あなたの好みのスタイルが好きです。考慮すべき行の間にパディングがある可能性があるため、この方法で行う必要があることを指摘することで、若干の改善が見られる場合があります。(少なくとも、それがこの方法で行う必要がある理由だと思います。)(私が間違っているかどうかを知らせてください。)
davidbak

2
@davidbakそれは同じことです。配列構文は、自己文書化されたコードにすぎません。「2D配列用のスペースを割り当てる」とソースコード自体で書かれています。
ランディン

1
@davidbak注:オーバーフロー時にコメントの マイナーな欠点が malloc(row*col*sizeof(double))発生しますが、row*col*sizeof()オーバーフローは発生sizeof()*row*colしません。(例:row、col are int
chux-モニカを復活

7
@davidbak:sizeof *e * (n+1)メンテナンスが簡単です。あなたは今まで(から基本タイプを変更することを決定した場合doublelong double、たとえば、)、そして、あなただけの宣言を変更する必要がありますe。呼び出しのsizeof式を変更する必要はありませんmalloc(これにより、時間を節約し、1つの場所でしか変更できないようにしますが、他の場所では変更できません)。 sizeof *e常に適切なサイズを提供します。
John Bode

39

このイディオムは当然1D配列の割り当てから外れます。まず、任意のタイプの1D配列を割り当てることから始めましょうT

T *p = malloc( sizeof *p * N );

シンプルでしょ?式は *p型を持つTので、sizeof *p同じ結果になりますsizeof (T)ので、我々は十分なスペースを割り当てている、Nの-element配列T。これはどのタイプT当てはまります。

では、のTような配列型に置き換えましょうR [10]。次に、割り当ては

R (*p)[10] = malloc( sizeof *p * N);

ここでのセマンティクスは、1D割り当て方法とまったく同じです。変更されるのはのタイプだけですp。の代わりにT *、今R (*)[10]です。式*pはtype Tである型を持っているR [10]のでsizeof *psizeof (T)whichはwhichと同等sizeof (R [10])です。したがってN、の10要素ごとの配列に十分なスペースを割り当てていますR

必要に応じて、これをさらに進めることができます。Rそれ自体が配列型だとしましょうint [5]。それを代用するとR

int (*p)[10][5] = malloc( sizeof *p * N);

同じ契約は- sizeof *pと同じでありsizeof (int [10][5])、我々は保持するのにメモリ十分な大きさの連続したチャンクを割り当てる羽目になるNによって105の配列int

これが割り当ての側面です。アクセス側はどうですか?

[]添え字演算はポインタ演算の観点から定義されていることを覚えておいてください:a[i]*(a + i)1として定義されています。したがって、添え字演算子はポインターを[] 暗黙的に逆参照します。pがへのポインタの場合T、単項演算*子を使用して明示的に逆参照することにより、ポイントされた値にアクセスできます。

T x = *p;

または[]添え字演算子を使用して:

T x = p[0]; // identical to *p

したがって、が配列のp最初の要素を指している場合、ポインターに添え字を使用することにより、その配列の任意の要素にアクセスできます。p

T arr[N];
T *p = arr; // expression arr "decays" from type T [N] to T *
...
T x = p[i]; // access the i'th element of arr through pointer p

ここで、置換操作を再度実行しTて、配列型に置き換えR [10]ます。

R arr[N][10];
R (*p)[10] = arr; // expression arr "decays" from type R [N][10] to R (*)[10]
...
R x = (*p)[i];

すぐに明らかな違いの1つ。p下付き演算子を適用する前に明示的に逆参照しています。に添え字を付けるのではなくp、何をp 指すのかを添え字にしたい(この場合は配列 arr[0])。単項*は下付き[]演算子よりも優先順位が低いため、括弧を使用して明示的にでグループ化pする必要があり*ます。ただし、上から*pはと同じp[0]であることを覚えておいてください。

R x = (p[0])[i];

あるいは単に

R x = p[0][i];

したがって、p2D配列を指す場合、次のようにしてその配列にインデックスを付けることができますp

R x = p[i][j]; // access the i'th element of arr through pointer p;
               // each arr[i] is a 10-element array of R

上記と同じ結論にこれを撮影し、置き換えRint [5]

int arr[N][10][5];
int (*p)[10][5]; // expression arr "decays" from type int [N][5][10] to int (*)[10][5]
...
int x = p[i][j][k];

これは、通常の配列を指す場合も、を介して割り当てられたメモリを指す場合も同じように機能pmallocます。

このイディオムには次の利点があります。

  1. シンプルです-断片的な割り当て方法とは異なり、コードは1行だけです
    T **arr = malloc( sizeof *arr * N );
    if ( arr )
    {
      for ( size_t i = 0; i < N; i++ )
      {
        arr[i] = malloc( sizeof *arr[i] * M );
      }
    }
    
  2. 割り当てられた配列のすべての行は*連続*ですが、これは上記の段階的な割り当て方法の場合とは異なります。
  3. 配列の割り当て解除は、を1回呼び出すだけで簡単freeです。繰り返しますが、断片的な割り当て方法では当てはまりません。この方法では、割り当てを解除するarr[i]前に、それぞれの割り当てを解除する必要がありますarr

ヒープの断片化が激しく、メモリを連続したチャンクとして割り当てることができない場合や、各行の長さが異なる可能性のある「ギザギザ」の配列を割り当てる場合など、断片的な割り当て方法が望ましい場合があります。しかし、一般的には、これがより良い方法です。


1.配列ポインターではないことに注意しください。代わりに、配列は必要に応じてポインター式に変換されます。


4
+1私はあなたが概念を提示する方法が好きです:一連の要素を割り当てることは、それらの要素自体が配列であっても、どのタイプでも可能です。
logo_writer

1
あなたの説明は本当に良いですが、あなたが本当にそれを必要とするまで、連続したメモリの割り当ては利益ではないことに注意してください。連続メモリは、非連続メモリよりも高価です。単純な2D配列の場合、メモリレイアウトに違いはないため(割り当てと割り当て解除の行数を除く)、連続していないメモリを使用することをお勧めします。
Oleg Lokshyn 16
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.