webentwicklung-frage-antwort-db.com.de

C: Zeiger auf Array von Zeigern auf Strukturen (Zuweisungs- / Freigabeprobleme)

Ich bin wegen etwas wieder zu C gekommen, aber ich habe Probleme, mich an vieles zu erinnern, was diese Speicherverwaltung bewirkt. Ich hätte gerne einen Zeiger auf eine Reihe von Zeigern auf Strukturen.

Angenommen, ich habe:

struct Test {
   int data;
};

Dann das Array:

struct Test **array1;

Ist das richtig? Mein Problem ist die Arbeit mit dieser Sache. Jeder Zeiger im Array zeigt also auf etwas, das separat zugewiesen wird. Aber ich denke, ich muss das zuerst tun:

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

Ich habe Probleme, das oben Genannte zu verstehen. Muss ich das tun und warum muss ich das tun? Was bedeutet es insbesondere, Speicher für Zeiger zuzuweisen, wenn ich für jedes Objekt, auf das der Zeiger verweist, Speicher zuweisen möchte?

Sagen wir jetzt, ich habe Zeiger auf ein Array von Zeigern auf Strukturen. Ich möchte jetzt, dass es auf dasselbe Array verweist, das ich zuvor erstellt habe.

struct Test **array2;

Muss ich wie oben Platz für Zeiger reservieren, oder kann ich einfach Folgendes tun:

array2 = array1
24
DillPixel

Zugewiesenes Array

Mit einem zugewiesenen Array ist es einfach genug, um zu folgen.

Deklarieren Sie Ihr Array von Zeigern. Jedes Element in diesem Array zeigt auf ein struct Test:

struct Test *array[50];

Ordnen Sie dann die Zeiger den Strukturen zu, wie Sie möchten. Die Verwendung einer Schleife wäre einfach:

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

Dann deklarieren Sie einen Zeiger auf dieses Array:

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

Dies ermöglicht es Ihnen, (*p)[n]->data; das n-te Mitglied zu verweisen.

Mach dir keine Sorgen, wenn dieses Zeug verwirrend ist. Es ist wahrscheinlich der schwierigste Aspekt von C.


Dynamisches lineares Array

Wenn Sie nur einen Block von Strukturen zuweisen möchten (effektiv ein Array von Strukturen, keine Zeiger auf Strukturen) und einen Zeiger auf den Block haben möchten, haben Sie kann es einfacher machen:

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

Sie können dann auf diesen Zeiger zeigen:

struct Test **pp = &p

Sie haben keine Zeiger mehr auf Strukturen, aber es vereinfacht das Ganze erheblich.


Dynamisches Array dynamisch allokierter Strukturen

Am flexibelsten, aber nicht oft benötigt. Es ist dem ersten Beispiel sehr ähnlich, erfordert jedoch eine zusätzliche Zuordnung. Ich habe ein komplettes Programm geschrieben, um dies zu demonstrieren, das sich gut kompilieren lässt.

#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;
}

Ausgabe:

> 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

Oder das ganze Set:

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 

Dynamic Pointer Array von Single-Dynamic Allocated Structs

Dieses letzte Beispiel ist ziemlich spezifisch. Es ist ein dynamisches Array von Zeigern, wie wir in den vorherigen Beispielen gesehen haben, aber im Gegensatz zu diesen werden die Elemente alle in einer einzelnen Zuordnung zugeordnet. Dies hat seine Verwendung, insbesondere zum Sortieren von Daten in verschiedenen Konfigurationen, wobei die ursprüngliche Zuordnung unverändert bleibt.

Wir beginnen mit der Zuweisung eines einzelnen Blockes von Elementen, wie wir es bei der grundlegendsten Einzelblockzuweisung tun:

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

Nun ordnen wir einen separaten Zeigerblock zu:

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

Anschließend füllen wir jeden Steckplatz in unserer Zeigerliste mit der Adresse eines unserer ursprünglichen Arrays. Da die Zeigerarithmetik es uns ermöglicht, von Element zu Element zu wechseln, ist dies einfach:

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

An dieser Stelle beziehen sich beide auf dasselbe Elementfeld

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

Und nach Überprüfung der obigen, ich hoffe, es ist klar , warum .

Wenn wir mit dem Zeigerarray und dem ursprünglichen Blockarray fertig sind, werden sie wie folgt freigegeben:

free(ptrs);
free(arr);

Hinweis: Wir befreien NICHT jeden Artikel im ptrs[] Array einzeln. So wurden sie nicht zugeteilt. Sie wurden als einzelner Block zugewiesen (auf den arr zeigt), und so sollten sie freigegeben werden.

Warum sollte jemand dies tun wollen? Mehrere Gründe.

Erstens wird die Anzahl der Speicherzuweisungsaufrufe drastisch reduziert. Eher, als N+1 (eine für das Zeigerarray, N für einzelne Strukturen) Sie haben jetzt nur zwei: eine für den Arrayblock und eine für das Zeigerarray. Speicherzuweisungen sind eine der teuersten Operationen, die ein Programm anfordern kann, und wenn möglich, ist es wünschenswert, sie zu minimieren (Anmerkung: file IO ist eine andere, fyi).

Ein weiterer Grund: Mehrere Darstellungen desselben Basisdatenfelds. Angenommen, Sie möchten die Daten sowohl aufsteigend als auch absteigend sortieren und gleichzeitig über beide sortierten Darstellungen verfügen . Sie könnten das Datenarray duplizieren, dies würde jedoch viel Kopieren und eine erhebliche Speichernutzung erfordern. Weisen Sie stattdessen einfach ein zusätzliches Zeigerarray zu, füllen Sie es mit Adressen aus dem Basisarray und sortieren Sie das Zeigerarray. Dies hat besonders bedeutende Vorteile, wenn die zu sortierenden Daten groß sind (möglicherweise Kilobyte oder sogar größer pro Element). Die ursprünglichen Elemente verbleiben an ihren ursprünglichen Positionen im Basisarray, aber jetzt haben Sie einen sehr effizienten Mechanismus, mit dem Sie sie sortieren können ohne sie tatsächlich bewegen zu müssen . Sie sortieren das Array von Zeigern auf Elemente. Die Gegenstände werden überhaupt nicht bewegt.

Mir ist klar, dass dies eine Menge zu beachten ist, aber die Verwendung von Zeigern ist entscheidend, um die vielen leistungsstarken Funktionen zu verstehen, die Sie mit der Sprache C ausführen können. Stöbern Sie also in den Büchern und aktualisieren Sie Ihr Gedächtnis. Es wird wiederkommen.

69
teppic

Es ist vielleicht besser, ein tatsächliches Array zu deklarieren, wie andere vorgeschlagen haben, aber Ihre Frage scheint sich mehr auf die Speicherverwaltung zu beziehen, daher werde ich darauf eingehen.

struct Test **array1;

Dies ist ein Zeiger auf die Adresse eines struct Test. (Kein Zeiger auf die Struktur selbst; es ist ein Zeiger auf einen Speicherort, der die Adresse der Struktur enthält.) Die Deklaration weist dem Zeiger Speicher zu, jedoch nicht den Elementen, auf die sie zeigt. Da auf ein Array über Zeiger zugegriffen werden kann, können Sie mit *array1 Als Zeiger auf ein Array arbeiten, dessen Elemente vom Typ struct Test Sind. Es gibt jedoch noch kein Array, auf das es verweisen könnte.

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

Dadurch wird Speicher für MAX Zeiger auf Elemente des Typs struct Test Reserviert. Wiederum nicht reserviert Speicher für die Strukturen selbst; nur für eine Liste von Zeigern. Aber jetzt können Sie array als Zeiger auf ein zugewiesenes Array von Zeigern behandeln.

Um array1 Verwenden zu können, müssen Sie die eigentlichen Strukturen erstellen. Sie können dies tun, indem Sie einfach jede Struktur mit deklarieren

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

Sie können auch die Strukturen auf dem Heap zuweisen:

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

Sobald Sie Speicher zugewiesen haben, können Sie eine neue Variable erstellen, die auf dieselbe Strukturliste verweist:

struct Test **array2 = array1;

Sie müssen keinen zusätzlichen Speicher zuweisen, da array2 Auf denselben Speicher verweist, den Sie array1 Zugewiesen haben.


Manchmal möchten Sie wollen einen Zeiger auf eine Liste von Zeigern haben, aber wenn Sie nichts Besonderes tun, können Sie ihn möglicherweise verwenden

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

Dies deklariert den Zeiger array1, Weist genügend Speicher für MAX Strukturen zu und zeigt array1 Auf diesen Speicher. Jetzt können Sie wie folgt auf die Strukturen zugreifen:

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.

Was ist der Unterschied? Ein paar Dinge. Bei der ersten Methode müssen Sie natürlich den Zeigern Speicher zuweisen und dann den Strukturen selbst zusätzlichen Speicherplatz zuweisen. Mit der zweiten können Sie mit einem malloc() -Aufruf davonkommen. Was bringt dir die zusätzliche Arbeit?

Da Sie mit der ersten Methode ein Array von Zeigern auf Test Strukturen erhalten, kann jeder Zeiger auf eine beliebige Test Struktur im gesamten Speicher verweisen. Sie müssen nicht zusammenhängend sein. Darüber hinaus können Sie den Speicher für jede aktuelle Test -Struktur nach Bedarf zuweisen und freigeben und die Zeiger neu zuweisen. So können Sie beispielsweise zwei Strukturen tauschen, indem Sie einfach ihre Zeiger austauschen:

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.

Andererseits reserviert die zweite Methode einen einzelnen zusammenhängenden Speicherblock für alle Test -Strukturen und partitioniert ihn in MAX -Elemente. Und jedes Element im Array befindet sich an einer festen Position. Die einzige Möglichkeit, zwei Strukturen auszutauschen, besteht darin, sie zu kopieren.

Zeiger sind eines der nützlichsten Konstrukte in C, aber sie können auch zu den am schwierigsten zu verstehenden gehören. Wenn Sie C weiterhin verwenden möchten, ist es wahrscheinlich eine lohnende Investition, einige Zeit mit Zeigern, Arrays und einem Debugger zu verbringen, bis Sie mit ihnen vertraut sind.

Viel Glück!

4
Adam Liss

Ich schlage vor, dass Sie jeweils eine Ebene mit typdefs erstellen, um Typenebenen zu erstellen. Auf diese Weise werden die verschiedenen benötigten Typen viel klarer.

Zum Beispiel:

typedef struct Test {
   int data;
} TestType;

typedef  TestType * PTestType;

Dadurch werden zwei neue Typen erstellt, einer für die Struktur und einer für einen Zeiger auf die Struktur.

Wenn Sie also als Nächstes ein Array der Strukturen wünschen, würden Sie Folgendes verwenden:

TestType array[20];  // creates an array of 20 of the structs

Wenn Sie ein Array von Zeigern auf die Strukturen benötigen, verwenden Sie:

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

Wenn Sie dann dem Array Strukturen zuweisen möchten, gehen Sie wie folgt vor:

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));
}

Mit C können Sie kein Array einem anderen zuweisen. Sie müssen stattdessen eine Schleife verwenden, um jedes Element eines Arrays einem Element des anderen zuzuweisen.

BEARBEITEN: Ein weiterer interessanter Ansatz

Ein anderer Ansatz wäre ein eher objektorientierter Ansatz, bei dem Sie einige Dinge zusammenfassen. Wenn wir zum Beispiel dieselben Typenebenen verwenden, erstellen wir zwei Typen:

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;

Als nächstes haben wir eine Hilfsfunktion, die wir verwenden, um das Objekt zu erstellen. Sie heißt entsprechend 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;
}

Jetzt können wir unser neues Objekt wie im folgenden Quellcodesegment verwenden. Es sollte überprüft werden, ob der von CreateTestData () zurückgegebene Zeiger gültig ist. Dies soll jedoch nur zeigen, was getan werden kann.

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

In einer wirklich dynamischen Umgebung möchten Sie möglicherweise auch eine ReallocTestData(PTestData p) -Funktion, die ein TestData -Objekt neu zuordnet, um die Größe des im Objekt enthaltenen Arrays zu ändern.

Wenn Sie mit einem bestimmten TestData-Objekt fertig sind, können Sie mit dieser Methode das Objekt wie in free (go) freigeben, und das Objekt und sein Array werden gleichzeitig freigegeben.

Bearbeiten: Weiter erweitern

Mit diesem gekapselten Typ können wir jetzt ein paar andere interessante Dinge tun. Zum Beispiel können wir eine Kopierfunktion haben, PTestType CreateCopyTestData (PTestType pSrc), die eine neue Instanz erzeugt und dann das Argument in ein neues Objekt kopiert. Im folgenden Beispiel verwenden wir die Funktion PTestType CreateTestData (int nCount), mit der eine Instanz unseres Typs mit der Größe des zu kopierenden Objekts erstellt wird. Nach dem Erstellen des neuen Objekts erstellen wir eine Kopie der Daten aus dem Quellobjekt. Der letzte Schritt besteht darin, den Zeiger zu reparieren, der im Quellobjekt auf seinen Datenbereich zeigt, so dass der Zeiger im neuen Objekt jetzt auf den Datenbereich von sich selbst und nicht auf den Datenbereich des alten Objekts zeigt.

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

Strukturen unterscheiden sich nicht sehr von anderen Objekten. Beginnen wir mit den Zeichen:

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

* p ist ein Zeichen, also sizeof *p ist sizeof (char) == 1; Wir haben CNT-Zeichen zugewiesen. Nächster:

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

* p ist ein Zeiger auf ein Zeichen, also sizeof *pp ist sizeof (char *). Wir haben CNT-Zeiger zugewiesen. Nächster:

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

* p ist eine Struktur, also sizeof *p ist sizeof (Struktur etwas). Wir haben CNT-Strukturen zugewiesen. Nächster:

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

* pp ist ein Zeiger auf struct, also sizeof *pp ist sizeof (struct something *). Wir haben CNT-Zeiger zugewiesen.

2
wildplasser