web-dev-qa-db-ja.com

C:構造体へのポインターの配列へのポインター(割り当て/割り当て解除の問題)

私は何かのためにCに戻ってきましたが、このメモリ管理がどのように機能するかを思い出すのに苦労しています。構造体へのポインタの配列へのポインタが必要です。

私が持っていると言う:

struct Test {
   int data;
};

次に、配列:

struct Test **array1;

これは正しいです?私の問題はこのことで動作しています。したがって、配列内の各ポインターは、個別に割り当てられたものを指します。しかし、私は最初にこれを行う必要があると思います:

array1 = malloc(MAX * sizeof(struct Test *));

上記を理解できません。これを行う必要がありますか?なぜこれを行う必要があるのですか?特に、ポインターが指すものごとにメモリを割り当てる場合、ポインターにメモリを割り当てるとはどういう意味ですか?

ここで、構造体へのポインタの配列へのポインタがあるとします。以前に作成したのと同じ配列を指すようになりました。

struct Test **array2;

上記のように、ポインタ用のスペースを割り当てる必要がありますか、それとも単にできますか:

array2 = array1
24
DillPixel

割り当てられた配列

割り当てられた配列を使用すると、簡単に追跡できます。

ポインターの配列を宣言します。この配列の各要素は、struct Testを指します。

struct Test *array[50];

次に、必要に応じてポインタを構造体に割り当てて割り当てます。ループの使用は簡単です:

array[n] = malloc(sizeof(struct Test));

次に、この配列へのポインターを宣言します。

                               // an explicit pointer to an array 
struct Test *(*p)[] = &array;  // of pointers to structs

これにより、(*p)[n]->data;を使用できます。 n番目のメンバーを参照します。

このようなものが混乱しても心配しないでください。これはおそらくCの最も難しい側面です。


動的線形配列

構造体のブロック(実質的に構造体の配列、not構造体へのポインタ)を割り当て、ブロックへのポインタを持ちたいだけの場合、より簡単にできる:

struct Test *p = malloc(100 * sizeof(struct Test));  // allocates 100 linear
                                                     // structs

次に、このポインターをポイントできます。

struct Test **pp = &p

構造体へのポインタの配列はもうありませんが、全体をかなり単純化します。


動的に割り当てられた構造の動的配列

最も柔軟ですが、頻繁には必要ありません。最初の例と非常に似ていますが、追加の割り当てが必要です。これをデモンストレーションする完全なプログラムを作成しましたが、これは正常にコンパイルされるはずです。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

struct Test {
    int data;
};

int main(int argc, char **argv)
{
    srand(time(NULL));

    // allocate 100 pointers, effectively an array
    struct Test **t_array = malloc(100 * sizeof(struct Test *));

    // allocate 100 structs and have the array point to them
    for (int i = 0; i < 100; i++) {
        t_array[i] = malloc(sizeof(struct Test));
    }

    // lets fill each Test.data with a random number!
    for (int i = 0; i < 100; i++) {
        t_array[i]->data = Rand() % 100;
    }

    // now define a pointer to the array
    struct Test ***p = &t_array;
    printf("p points to an array of pointers.\n"
       "The third element of the array points to a structure,\n"
       "and the data member of that structure is: %d\n", (*p)[2]->data);

    return 0;
}

出力:

> p points to an array of pointers.
> The third element of the array points to a structure,
> and the data member of that structure is: 49

またはセット全体:

for (int i = 0; i < 100; i++) {
    if (i % 10 == 0)
        printf("\n");
    printf("%3d ", (*p)[i]->data);
}

 35  66  40  24  32  27  39  64  65  26 
 32  30  72  84  85  95  14  25  11  40 
 30  16  47  21  80  57  25  34  47  19 
 56  82  38  96   6  22  76  97  87  93 
 75  19  24  47  55   9  43  69  86   6 
 61  17  23   8  38  55  65  16  90  12 
 87  46  46  25  42   4  48  70  53  35 
 64  29   6  40  76  13   1  71  82  88 
 78  44  57  53   4  47   8  70  63  98 
 34  51  44  33  28  39  37  76   9  91 

単一動的に割り当てられた構造の動的ポインタ配列

この最後の例はかなり具体的です。前の例で見たように、これはポインターの動的配列ですが、それらとは異なり、要素はすべてsingle割り当てで割り当てられます。これには用途があり、最も顕著なのは、異なる構成でデータをソートし、元の割り当てをそのままにしておくことです。

最も基本的な単一ブロックの割り当てと同様に、要素の単一ブロックを割り当てることから始めます。

struct Test *arr = malloc(N*sizeof(*arr));

次に、ポインターのseparateブロックを割り当てます。

struct Test **ptrs = malloc(N*sizeof(*ptrs));

次に、ポインターリストの各スロットに、元の配列のいずれかのアドレスを設定します。ポインター演算により、要素から要素アドレスに移動できるため、これは簡単です。

for (int i=0;i<N;++i)
    ptrs[i] = arr+i;

この時点で、次の両方が同じ要素フィールドを参照しています

arr[1].data = 1;
ptrs[1]->data = 1;

そして、上記をレビューした後、それが明確であることを願っていますなぜ.

ポインタ配列と元のブロック配列の処理が完了すると、次のように解放されます。

free(ptrs);
free(arr);

注:ptrs[]配列の各アイテムを個別に解放するわけではありません。それは割り当てられた方法ではありません。それらは単一のブロックとして割り当てられ(arrによって指されます)、それがどのように解放されるべきかです。

では、なぜ誰かがこれをしたいのでしょうか?いくつかの理由。

まず、メモリ割り当て呼び出しの数を根本的に削減します。 N+1(1つはポインター配列、Nは個々の構造体)ではなく、2のみがあります。1つは配列ブロック用、もう1つはポインター配列用です。メモリ割り当ては、プログラムが要求できる最も高価な操作の1つであり、可能な場合は最小化することが望ましいです(注:file IO is another、fyi)。

別の理由:データの同じ基本配列の複数の表現。データを昇順と降順の両方でソートし、ソートされた両方の表現を同時に使用可能にすると仮定します。データ配列を複製することもできますが、これには大量のコピーが必要であり、かなりのメモリ使用量を消費します。代わりに、追加のポインター配列を割り当てて、基本配列からのアドレスを入力し、そのポインター配列を並べ替えます。これは、並べ替えられるデータが大きい場合(アイテムごとにキロバイト、またはさらに大きい場合)に特に大きな利点があります。元のアイテムはベース配列の元の場所に残りますが、並べ替えができる非常に効率的なメカニズムがあります。実際にmoveする必要はありません。アイテムへのポインターの配列を並べ替えます。アイテムはまったく移動しません。

これは非常に多くのことを理解する必要がありますが、ポインターを使用することは、C言語で実行できる多くの強力なことを理解するために重要なので、本を読んで記憶を更新してください。戻ってきます。

69
teppic

他の人が示唆しているように、実際の配列を宣言する方が良いかもしれませんが、あなたの質問はメモリ管理に関するものであるようですので、それについて説明します。

_struct Test **array1;
_

これは、_struct Test_のアドレスへのポインターです。 (構造体自体へのポインタではなく、構造体のアドレスを保持するメモリ位置へのポインタです。)宣言は、ポインタにメモリを割り当てますが、ポインタが指す項目には割り当てません。配列はポインターを介してアクセスできるため、_*array1_を、要素が_struct Test_型である配列へのポインターとして使用できます。しかし、それを指す実際の配列はまだありません。

_array1 = malloc(MAX * sizeof(struct Test *));
_

これは、_struct Test_型のアイテムへのMAXポインターを保持するためにメモリを割り当てます。繰り返しますが、not構造体自体にメモリを割り当てます。ポインタのリストのみ。しかし、arrayは、割り当てられたポインターの配列へのポインターとして扱うことができます。

_array1_を使用するには、実際の構造体を作成する必要があります。これを行うには、単に各構造体を次のように宣言します。

_struct Test testStruct0;  // Declare a struct.
struct Test testStruct1;
array1[0] = &testStruct0;  // Point to the struct.
array1[1] = &testStruct1;
_

ヒープに構造体を割り当てることもできます。

_for (int i=0; i<MAX; ++i) {
  array1[i] = malloc(sizeof(struct Test));
}
_

メモリを割り当てたら、同じ構造体のリストを指す新しい変数を作成できます。

_struct Test **array2 = array1;
_

_array2_は_array1_に割り当てたのと同じメモリを指すため、追加のメモリを割り当てる必要はありません。


時々wantポインタのリストへのポインタを持ちたいのですが、何かおかしなことをしていない限り、

_struct Test *array1 = malloc(MAX * sizeof(struct Test));  // Pointer to MAX structs
_

これは、ポインター_array1_を宣言し、MAX構造体に十分なメモリを割り当て、_array1_をそのメモリにポイントします。これで、次のような構造体にアクセスできます。

_struct Test testStruct0 = array1[0];     // Copies the 0th struct.
struct Test testStruct0a= *array1;       // Copies the 0th struct, as above.
struct Test *ptrStruct0 = array1;        // Points to the 0th struct.

struct Test testStruct1 = array1[1];     // Copies the 1st struct.
struct Test testStruct1a= *(array1 + 1); // Copies the 1st struct, as above.
struct Test *ptrStruct1 = array1 + 1;    // Points to the 1st struct.
struct Test *ptrStruct1 = &array1[1];    // Points to the 1st struct, as above.
_

それで、違いは何ですか?いくつかのこと。明らかに、最初の方法では、ポインターにメモリを割り当ててから、構造体自体に追加のスペースを割り当てる必要があります。 2番目は、1回のmalloc()呼び出しで逃げることができます。余分な仕事で何が買えますか?

最初のメソッドはTest構造体へのポインターの実際の配列を提供するため、各ポインターはメモリ内の任意のTest構造体を指すことができます。連続している必要はありません。さらに、必要に応じて実際のTest構造体ごとにメモリを割り当てて解放し、ポインタを再割り当てできます。したがって、たとえば、ポインタを交換するだけで2つの構造を交換できます。

_struct Test *tmp = array1[2];  // Save the pointer to one struct.
array1[2] = array1[5];         // Aim the pointer at a different struct.
array1[5] = tmp;               // Aim the other pointer at the original struct.
_

一方、2番目の方法は、すべてのTest構造体に単一の連続したメモリブロックを割り当て、それをMAXアイテムに分割します。そして、配列内の各要素は固定位置にあります。 2つの構造を交換する唯一の方法は、それらをコピーすることです。

ポインターはCで最も有用な構成要素の1つですが、最も理解しにくいものの1つでもあります。 Cの使用を継続する予定がある場合、ポインター、配列、デバッガーを使い慣れるまで時間をかけて遊ぶのは、おそらく価値のある投資になるでしょう。

がんばろう!

4
Adam Liss

タイプのレイヤーを作成するために、typdefsを使用して一度にレイヤーを作成することをお勧めします。そうすることにより、必要なさまざまなタイプがより明確になります。

例えば:

_typedef struct Test {
   int data;
} TestType;

typedef  TestType * PTestType;
_

これにより、構造体用と構造体へのポインタ用の2つの新しい型が作成されます。

したがって、次に構造体の配列が必要な場合は、次を使用します。

_TestType array[20];  // creates an array of 20 of the structs
_

構造体へのポインタの配列が必要な場合は、次を使用します。

_PTestType array2[20];  // creates an array of 20 of pointers to the struct
_

次に、配列に構造体を割り当てたい場合、次のようにします。

_PTestType  array2[20];  // creates an array of 20 of pointers to the struct
// allocate memory for the structs and put their addresses into the array of pointers.
for (int i = 0; i < 20; i++) {
    array2 [i] = malloc (sizeof(TestType));
}
_

Cでは、ある配列を別の配列に割り当てることはできません。代わりに、ループを使用して、ある配列の各要素を他の配列の要素に割り当てる必要があります。

編集:別の興味深いアプローチ

別のアプローチは、いくつかのことをカプセル化する、よりオブジェクト指向のアプローチです。たとえば、同じタイプのレイヤーを使用して、2つのタイプを作成します。

_typedef struct _TestData {
    struct {
        int myData;   // one or more data elements for each element of the pBlob array
    } *pBlob;
    int nStructs;         // count of number of elements in the pBlob array
} TestData;

typedef TestData *PTestData;
_

次に、適切なCreateTestData (int nArrayCount)という名前のオブジェクトを作成するために使用するヘルパー関数があります。

_PTestData  CreateTestData (int nCount)
{
    PTestData ret;

    // allocate the memory for the object. we allocate in a single piece of memory
    // the management area as well as the array itself.  We get the sizeof () the
    // struct that is referenced through the pBlob member of TestData and multiply
    // the size of the struct by the number of array elements we want to have.
    ret = malloc (sizeof(TestData) + sizeof(*(ret->pBlob)) * nCount);
    if (ret) {   // make sure the malloc () worked.
            // the actual array will begin after the end of the TestData struct
        ret->pBlob = (void *)(ret + 1);   // set the beginning of the array
        ret->nStructs = nCount;           // set the number of array elements
    }

    return ret;
}
_

これで、以下のソースコードセグメントのように、新しいオブジェクトを使用できます。 CreateTestData()から返されたポインターが有効であることを確認する必要がありますが、これは実際に何ができるかを示すためのものです。

_PTestData  go = CreateTestData (20);
{
    int i = 0;
    for (i = 0; i < go->nStructs; i++) {
        go->pBlob[i].myData = i;
    }
}
_

真に動的な環境では、オブジェクトに含まれる配列のサイズを変更するためにTestDataオブジェクトを再割り当てするReallocTestData(PTestData p)関数も必要になる場合があります。

このアプローチでは、特定のTestDataオブジェクトの処理が完了したら、free (go)のようにオブジェクトを解放するだけで、オブジェクトとその配列の両方が同時に解放されます。

編集:さらに拡張する

このカプセル化された型を使用すると、他のいくつかの興味深いことができます。たとえば、コピーインスタンスPTestType CreateCopyTestData (PTestType pSrc)を使用して、新しいインスタンスを作成し、引数を新しいオブジェクトにコピーできます。次の例では、コピーするオブジェクトのサイズを使用して、型のインスタンスを作成する関数PTestType CreateTestData (int nCount)を再利用します。新しいオブジェクトの作成を行った後、ソースオブジェクトからデータのコピーを作成します。最後の手順は、ソースオブジェクト内のデータ領域を指すポインターを修正して、新しいオブジェクト内のポインターが古いオブジェクトのデータ領域ではなく、それ自体のデータ領域を指すようにすることです。

_PTestType CreateCopyTestData (PTestType pSrc)
{
    PTestType pReturn = 0;

    if (pSrc) {
        pReturn = CreateTestData (pSrc->nStructs);

        if (pReturn) {
            memcpy (pReturn, pSrc, sizeof(pTestType) + pSrc->nStructs * sizeof(*(pSrc->pBlob)));
            pReturn->pBlob = (void *)(pReturn + 1);   // set the beginning of the array
        }
    }

    return pReturn;
}
_
2

構造体は、他のオブジェクトと大差ありません。文字から始めましょう:

char *p;
p = malloc (CNT * sizeof *p);

* pは文字なので、sizeof *p is sizeof(char)== 1; CNT文字を割り当てました。次:

char **pp;
pp = malloc (CNT * sizeof *pp);

* pは文字へのポインタなので、sizeof *ppはsizeof(char *)です。 CNTポインターを割り当てました。次:

struct something *p;
p = malloc (CNT * sizeof *p);

* pは構造体なので、sizeof *pはsizeofです(何かを構築します)。 CNT構造体を割り当てました。次:

struct something **pp;
pp = malloc (CNT * sizeof *pp);

* ppは構造体へのポインタなので、sizeof *ppはsizeof(何かを構築する*)です。 CNTポインターを割り当てました。

2
wildplasser