Подтвердить что ты не робот

C: указатель на массив указателей на структуры (проблемы выделения/освобождения)

Я возвращаюсь на C для чего-то, но у меня возникают проблемы с запоминанием того, как работает это управление памятью. Я хотел бы иметь указатель на массив указателей на структуры.

Скажем, у меня есть:

struct Test {
   int data;
};

Затем массив:

struct Test **array1;

Это правильно? Моя проблема работает с этим. Таким образом, каждый указатель в массиве указывает на то, что выделяется отдельно. Но я думаю, что мне нужно сделать это сначала:

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

У меня возникли проблемы с пониманием вышеизложенного. Нужно ли мне это делать и зачем мне это нужно? В частности, что означает выделение памяти для указателей, если я собираюсь выделить память для каждой вещи, на которую указывает указатель?

Скажем, теперь у меня есть указатель на массив указателей на структуры. Теперь я хочу, чтобы он указывал на тот же массив, который я создал ранее.

struct Test **array2;

Мне нужно выделить место для указателей, как я сделал выше, или я могу просто сделать:

array2 = array1
4b9b3361

Ответ 1

Выделенный массив

С выделенным массивом он достаточно прост, чтобы следовать.

Объявите свой массив указателей. Каждый элемент в этом массиве указывает на 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.


Динамический линейный массив

Если вы просто хотите выделить блок структур (эффективно массив структур, а не указатели на структуры) и иметь указатель на блок, вы можете сделать это проще:

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 

Массив динамических указателей однонаправленных распределенных структур

Этот последний пример довольно конкретный. Это динамический массив указателей, как мы видели в предыдущих примерах, но в отличие от них все элементы выделяются в одном распределении. Это имеет свои применения, наиболее заметное для сортировки данных в разных конфигурациях, оставляя исходное распределение без изменений.

Начнем с выделения одного блока элементов, как и в самом основном распределении по одному блоку:

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

Теперь мы выделяем отдельный блок указателей:

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 (один для массива указателей, N для отдельных структур) теперь у вас есть только два: один для блока массива и один для массива указателей. Выделение памяти - одна из самых дорогих операций, которую может запросить программа, и по возможности желательно минимизировать их (обратите внимание: файл IO - другой, fyi).

Другая причина: множественные представления одного и того же базового массива данных. Предположим, вы хотели сортировать данные как по восходящей, так и по нисходящей, и одновременно иметь обе отсортированные представления. Вы можете дублировать массив данных, но для этого потребуется много копировать и использовать значительное использование памяти. Вместо этого просто выделите дополнительный массив указателей и заполните его адресами из базового массива, а затем отсортируйте этот массив указателей. Это особенно важно, когда сортируемые данные являются большими (возможно, килобайтами или даже больше для каждого элемента). Исходные элементы остаются в исходном местоположении в базовом массиве, но теперь у вас есть очень эффективный механизм, в котором вы можете сортировать их без фактического перемещения их. Вы сортируете массив указателей на элементы; элементы не перемещаются вообще.

Я понимаю, что это ужасно много, но использование указателя имеет решающее значение для понимания многих мощных вещей, которые вы можете сделать с языком C, поэтому нажмите на книги и продолжайте обновлять свою память. Он вернется.

Ответ 2

Может быть, лучше объявить фактический массив, как предложили другие, но ваш вопрос, похоже, больше связан с управлением памятью, поэтому я обсужу это.

struct Test **array1;

Это указатель на адрес struct Test. (Не указатель на структуру, это указатель на ячейку памяти, которая содержит адрес структуры.) Объявление выделяет память для указателя, но не для элементов, на которые указывает. Поскольку доступ к массиву осуществляется через указатели, вы можете работать с *array1 как указатель на массив, элементы которого имеют тип struct Test. Но для этого еще нет реального массива.

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

Это выделяет память для хранения указателей MAX для элементов типа struct Test. Опять же, он не выделяет память для самих структур; только для списка указателей. Но теперь вы можете рассматривать 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.


Иногда вы хотите иметь указатель на список указателей, но если вы не делаете что-то фантастическое, вы можете использовать

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.

И какая разница? Несколько вещей. Очевидно, что первый метод требует, чтобы вы выделили память для указателей, а затем выделили дополнительное пространство для самих структур; второй позволяет вам уйти с одним вызовом malloc(). Какая дополнительная работа вы покупаете?

Поскольку первый метод дает вам фактический массив указателей на Test structs, каждый указатель может указывать на любую структуру Test, где угодно в памяти; они не должны быть смежными. Более того, вы можете выделять и освобождать память для каждой фактической структуры Test по мере необходимости, и вы можете переназначить указатели. Так, например, вы можете поменять местами две структуры, просто обменяв их указателями:

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.

С другой стороны, второй метод выделяет единый непрерывный блок памяти для всех структур Test и разбивает его на элементы MAX. И каждый элемент массива находится в фиксированном положении; единственным способом обмена двумя структурами является их копирование.

Указатели - одна из самых полезных конструкций на C, но они также могут быть среди самых трудных для понимания. Если вы планируете продолжать использовать C, вероятно, стоит потратить некоторое время на то, чтобы потратить некоторое время на игры с указателями, массивами и отладчиком, пока вам не станет удобно.

Удачи!

Ответ 3

Я предлагаю вам создать этот слой за один раз, используя typdefs для создания слоев типов. Таким образом, различные типы, которые необходимы, будут намного яснее.

Например:

typedef struct Test {
   int data;
} TestType;

typedef  TestType * PTestType;

Это создаст два новых типа: один для структуры и один для указателя на структуру.

Итак, если вам нужен массив структур, то вы будете использовать:

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 не позволяет присваивать один массив другому. Вместо этого вы должны использовать цикл для привязки каждого элемента одного массива к элементу другого.

РЕДАКТИРОВАТЬ: Еще один интересный подход

Другим подходом будет более объектно-ориентированный подход, в котором вы инкапсулируете несколько вещей. Например, используя те же слои типов, мы создаем два типа:

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

В действительно динамической среде вы также можете иметь функцию ReallocTestData(PTestData p), которая перераспределяет объект TestData, чтобы изменить размер массива, содержащегося в объекте.

При таком подходе, когда вы закончите с определенным объектом 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;
}

Ответ 4

Структуры не сильно отличаются от других объектов. Начните с символов:

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

* p - символ, поэтому sizeof *p - 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 (struct something). Мы выделили CNT struct somethings. Далее:

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

* pp - это указатель на struct, поэтому sizeof *pp - sizeof (struct something *). Мы выделили указатели CNT.