C динамічно зростаючий масив


126

У мене є програма, яка читає "необроблений" список ігрових об'єктів, і я маю намір скласти масив, що містить індексне число (int) невизначеної кількості сутностей для обробки різних речей. Я хотів би не використовувати занадто багато пам'яті чи процесора для зберігання таких індексів ...

Швидке і брудне рішення, яке я використовую поки що, - оголосити в основній функції обробки (локальний фокус) масив з розміром максимальних ігрових утворень та іншим цілим числом, щоб відстежувати, скільки їх було додано до списку. Це не задовільно, оскільки кожен список містить 3000+ масивів, що не так вже й багато, але здається, що це марно, оскільки я можу використовувати рішення для 6-7 списків для різних функцій.

Я не знайшов жодного конкретного рішення для C (не C ++ чи C #), щоб досягти цього. Я можу використовувати вказівники, але я трохи побоююся їх використовувати (якщо це не єдиний можливий спосіб).

Масиви не залишають локальної області функцій (вони повинні бути передані функції, а потім відкинуті) у випадку, якщо це змінить щось.

Якщо покажчики - єдине рішення, то як я можу відстежувати їх, щоб уникнути витоків?


1
Це (дуже і дуже мала) проблема в C, але як ви пропустили всі рішення C ++ і C # для цього?
Ігнасіо Васкес-Абрамс

11
"Якщо покажчики - єдине рішення, як я можу відстежувати їх, щоб уникнути витоків?" Турбота, увага та валлінг. Саме тому люди так бояться, якщо С в першу чергу.
Кріс Лутц

27
Ви не можете ефективно використовувати C без використання покажчиків. Не бійтеся.
qrdl

без великого LIBS тільки одна функції для всіх і для структур , наприклад: stackoverflow.com/questions/3456446 / ...
user411313

6
Використовувати C без покажчиків - це як використовувати автомобіль без палива.
мартінкунев

Відповіді:


210

Я можу використовувати вказівники, але я трохи побоююся їх використовувати.

Якщо вам потрібен динамічний масив, ви не можете уникнути покажчиків. Чому ти боїшся? Вони не будуть кусатись (поки ви обережні, тобто). У C немає вбудованого динамічного масиву, просто доведеться записати його самостійно. У C ++ ви можете використовувати вбудований std::vectorклас. C # і майже всі інші мови високого рівня також мають подібний клас, який управляє динамічними масивами для вас.

Якщо ви плануєте написати власне, ось що для початку: більшість динамічних реалізацій масиву працюють, починаючи з масиву певного (невеликого) розміру за замовчуванням, тоді, коли у вас додається місця, додаючи новий елемент, подвійно розмір масиву. Як ви бачите в прикладі нижче, це зовсім не складно: (Я пропустив перевірки безпеки на стислість)

typedef struct {
  int *array;
  size_t used;
  size_t size;
} Array;

void initArray(Array *a, size_t initialSize) {
  a->array = malloc(initialSize * sizeof(int));
  a->used = 0;
  a->size = initialSize;
}

void insertArray(Array *a, int element) {
  // a->used is the number of used entries, because a->array[a->used++] updates a->used only *after* the array has been accessed.
  // Therefore a->used can go up to a->size 
  if (a->used == a->size) {
    a->size *= 2;
    a->array = realloc(a->array, a->size * sizeof(int));
  }
  a->array[a->used++] = element;
}

void freeArray(Array *a) {
  free(a->array);
  a->array = NULL;
  a->used = a->size = 0;
}

Використовувати його так само просто:

Array a;
int i;

initArray(&a, 5);  // initially 5 elements
for (i = 0; i < 100; i++)
  insertArray(&a, i);  // automatically resizes as necessary
printf("%d\n", a.array[9]);  // print 10th element
printf("%d\n", a.used);  // print number of elements
freeArray(&a);

1
Дякуємо за зразок коду. Я реалізував конкретну функцію за допомогою великого масиву, але буду реалізовувати інші подібні речі, використовуючи це, і після того, як я контролюю її, змініть інші назад :)
Balkania

2
Дякую за код. removeArrayМетод , який дозволяє позбавитися від останнього елемента також буде акуратним. Якщо ви дозволите це, я додам його до вашого зразка коду.
бримбориум

5
% d і size_t ... там ні-ні-ні. Якщо ви використовуєте C99 або новішу версію, можете скористатися додаванням% z
Ренді Ховард

13
Ніколи не пропускайте перевірки безпеки з розподілом пам'яті та перерозподілом.
Алекс Рейнольдс

3
Це компроміс продуктивності. Якщо ви подвоюєтесь кожен раз, то іноді у вас 100% накладні витрати і в середньому 50%. 3/2 дає 50% гіршого і 25% типового. Він також близький до ефективної основи послідовності Фібіонакі в межі (фі), яку часто хвалять і використовують за свої "експоненціальні, але набагато менш жорстокі, ніж основи-2" характеристики, але простіше підрахувати. +8 означає, що масиви, які є досить маленькими, не роблять занадто багато копій. Він додає мультиплікативний термін, що дозволяє масиву швидко зростати, якщо його розмір не має значення. У спеціалізованих цілях це має бути налаштоване.
Ден Шеппард

11

Як і у всьому, що спочатку здається страшнішим, ніж пізніше, найкращий спосіб подолати початковий страх - зануритись у дискомфорт невідомого ! Це часом, як ми дізнаємося найбільше.

На жаль, існують обмеження. Хоча ви все ще вчитеся користуватися функцією, ви не повинні брати на себе роль вчителя, наприклад. Я часто читаю відповіді від тих, хто, здається, не знає, як їх використовувати realloc(тобто, прийняту на даний момент відповідь! ), Розповідаючи іншим, як ним користуватися неправильно, періодично під виглядом того, що вони пропустили поводження з помилками , хоча це звичайна помилка. про яку потрібно згадати. Ось відповідь, що пояснює, як reallocправильно користуватися . Зверніть увагу, що відповідь зберігає повернене значення в іншу змінну, щоб здійснити перевірку помилок.

Кожен раз, коли ви викликаєте функцію та кожного разу, коли ви використовуєте масив, ви використовуєте вказівник. Перетворення відбуваються неявно, що якщо що-небудь має бути ще страшніше, оскільки саме ті речі, яких ми не бачимо, часто викликають найбільші проблеми. Наприклад, витоки пам'яті ...

Оператори масиву - оператори вказівників. array[x]це дійсно ярлик для *(array + x), який можна розбити на: *і (array + x). Найімовірніше, що *саме те бентежить. Ми можемо додатково усунути додавання від проблеми, вважаючи, xщо це 0, таким чином, array[0]стає *arrayтому, що додавання0 не змінить значення ...

... і таким чином ми можемо побачити, що *arrayрівнозначно array[0]. Ви можете використовувати одне там, де ви хочете використовувати інше, і навпаки. Оператори масиву - оператори вказівників.

malloc, reallocа друзі не вигадують поняття вказівника, яким ви користуєтесь весь час; вони просто використовують це для реалізації якоїсь іншої функції, яка є різною формою тривалості зберігання, найбільш підходящою, коли ви бажаєте різких, динамічних змін розміру .

Прикро, що прийнята в даний час відповідь також суперечить зерну деяких інших дуже обґрунтованих порад щодо StackOverflow , і в той же час пропускає можливість запровадити маловідому функцію, яка світить саме для цього використання: гнучкий масив члени! Це насправді досить зламана відповідь ... :(

Коли ви визначаєте свою struct, оголошуйте масив наприкінці структури, без верхньої межі. Наприклад:

struct int_list {
    size_t size;
    int value[];
};

Це дозволить вам об'єднати масив intу те саме виділення count, що і ваше , і пов'язати їх так, як це може бути дуже зручно !

sizeof (struct int_list)буде діяти так, ніби valueмає розмір 0, тому він розповість вам розмір структури з порожнім списком . Ще потрібно додати розмір, переданий доrealloc щоб вказати розмір списку.

Ще одна підказка - пам’ятати, що realloc(NULL, x)це рівнозначно malloc(x), і ми можемо використовувати це для спрощення нашого коду. Наприклад:

int push_back(struct int_list **fubar, int value) {
    size_t x = *fubar ? fubar[0]->size : 0
         , y = x + 1;

    if ((x & y) == 0) {
        void *temp = realloc(*fubar, sizeof **fubar
                                   + (x + y) * sizeof fubar[0]->value[0]);
        if (!temp) { return 1; }
        *fubar = temp; // or, if you like, `fubar[0] = temp;`
    }

    fubar[0]->value[x] = value;
    fubar[0]->size = y;
    return 0;
}

struct int_list *array = NULL;

Причина, яку я вирішив використовувати struct int_list **як перший аргумент, може здатися не очевидною відразу, але якщо ви подумаєте про другий аргумент, будь-які зміни, внесені valueзсередини push_back, не будуть видні функції, з якої ми викликаємо, правда? Те ж саме стосується першого аргументу, і нам потрібно вміти змінювати наші array, не тільки тут, але, можливо, і в будь-якій іншій функції / функції, на яку ми передаємо його ...

arrayпочинає вказувати на ніщо; це порожній список. Ініціалізація це те саме, що і додавання до неї. Наприклад:

struct int_list *array = NULL;
if (!push_back(&array, 42)) {
    // success!
}

PS Пам'ятайте,free(array); коли ви закінчите з цим!


" array[x]- це дійсно ярлик для *(array + x), [...]" Ви впевнені в цьому ???? Дивіться виклад їх різних поводжень: eli.thegreenplace.net/2009/10/21 / ... .
C-Star-W-Star

1
На жаль, @ C-Star-Puppy, одна з посилань, яку ваш ресурс, як видається, зовсім не згадує, це стандарт C. Це специфікація, згідно з якою ваші компілятори повинні дотримуватися юридичних назв себе компіляторами C. Схоже, ваш ресурс зовсім не суперечить моїй інформації. Проте, стандарт на самому справі має кілька прикладів , такі як цей дорогоцінний камінь , де він показав , що array[index]насправді ptr[index]переодягнений ... «визначення оператора індексу []є те , що E1[E2]є ідентичним (*((E1)+(E2)))» Ви не можете спростувати Зої
аутист

Спробуйте цю демонстрацію, @ C-Star-Puppy: int main(void) { unsigned char lower[] = "abcdefghijklmnopqrstuvwxyz"; for (size_t x = 0; x < sizeof lower - 1; x++) { putchar(x[lower]); } }... Вам, мабуть, знадобиться #include <stdio.h>і <stddef.h>... Ви бачите, як я писав x[lower]xтим, що це цілий тип), а не lower[x]? Компілятор C не хвилює, тому що *(lower + x)це те саме значення *(x + lower), що і lower[x]колишнє де, як x[lower]і друге. Усі ці вирази рівнозначні. Спробуйте їх ... подивіться самі, якщо ви не можете прийняти моє слово ...
аутист

... і тоді, звичайно, є ця частина, на яку я зробив власний акцент, але ви дійсно повинні прочитати всю цитату без наголосу: "За винятком випадків, коли це операнд оператора sizeof, оператора _Alignof або unary & operator, або це рядковий літерал, який використовується для ініціалізації масиву, вираз, який має тип '' масив типу '', перетворюється у вираз із типом '' покажчик на тип '', який вказує на початковий елемент масиву об'єкт і не є значенням . Якщо об'єкт масиву має клас зберігання реєстру, поведінка не визначена. " Те саме стосується функцій, btw.
аутист

О, і, нарешті, остання записка @ C-Star-Puppy, Microsoft C ++ не є компілятором на C і не була такою вже майже 20 років. Ви можете ввімкнути режим C89, suuuure , але ми розвинулися за межами кінця 1980-х в обчислювальних технологіях. Докладніше про цю тему я пропоную прочитати цю статтю ... а потім перейти до фактичного компілятора C, такого як gccабо clangдля всієї вашої компіляції C, оскільки ви знайдете стільки пакетів, які прийняли функції C99 ...
аутист

10

Є кілька варіантів, про які я можу придумати.

  1. Пов'язаний список. Ви можете використовувати пов'язаний список, щоб створити подібний масив, що динамічно зростає. Але вам не вдасться обійтися, array[100]не 1-99спершу пройшовшись . І вам це може бути не так зручно.
  2. Великий масив. Просто створіть масив з більш ніж достатньо місця для всього
  3. Змінення розміру масиву. Відтворіть масив, як тільки ви дізнаєтесь розмір та / або створіть новий масив кожного разу, коли у вас вичерпається місце з деяким запасом і скопіюйте всі дані в новий масив.
  4. Комбінація масиву пов'язаних списків. Просто використовуйте масив із фіксованим розміром, і як тільки у вас не залишиться місця, створіть новий масив і покладіть на нього (було б розумно відслідковувати масив та посилання на наступний масив у структурі).

Важко сказати, який варіант був би найкращим у вашій ситуації. Просто створення великого масиву - це, звичайно, одне з найпростіших рішень і не повинно створювати великих проблем, якщо він дійсно не великий.


Як звучить сім масивів з 3264 цілих чисел для сучасної 2-грі? Якби я просто був параноїком, рішення було б великим масивом.
Балканія

3
І №1, і №4 тут вимагають використання покажчиків та динамічного розподілу пам'яті. Я пропоную використовувати reallocз №3 - виділіть масив нормального розміру, а потім збільшуйте його, коли ви закінчитеся. reallocпри необхідності буде обробляти копіювання ваших даних. Що стосується питання ОП щодо управління пам’яттю, то потрібно просто mallocодин раз на старті, freeраз в кінці, і reallocкожен раз, коли у вас не вистачає місця. Це не дуже погано.
Borealid

1
@Balkania: сім масивів з 3264 цілих чисел - це волосся менше 100 КБ. Це взагалі не дуже багато пам’яті.
Borealid

1
@Balkania: 7 * 3264 * 32 bitсхоже 91.39 kilobytes. Не так вже й багато за будь-якими стандартами в ці дні;)
Вольф

1
Цей конкретний упущення прикро, тому що не зовсім очевидно, що має відбуватися при reallocповерненні NULL: a->array = (int *)realloc(a->array, a->size * sizeof(int));... Можливо, це було б найкраще написано так: int *temp = realloc(a->array, a->size * sizeof *a->array); a->array = temp;... Таким чином, було б очевидно, що все, що відбувається, має відбуватися ранішеNULL присвоюється значення a->array(якщо воно взагалі).
аутист

3

Спираючись на дизайн Matteo Furlans , коли він сказав, що " найбільш динамічні реалізації масиву працюють, починаючи з масиву деякого (невеликого) розміру за замовчуванням, то коли ви не вистачаєте місця при додаванні нового елемента, подвоюйте розмір масиву ". Відмінність " незавершеного виробництва " нижче полягає в тому, що він не подвоюється за розміром, він має на меті використовувати лише те, що потрібно. Я також опустив перевірки безпеки для простоти ... Також, спираючись на ідею brimboriums , я спробував додати функцію видалення до коду ...

Файл storage.h виглядає приблизно так ...

#ifndef STORAGE_H
#define STORAGE_H

#ifdef __cplusplus
extern "C" {
#endif

    typedef struct 
    {
        int *array;
        size_t size;
    } Array;

    void Array_Init(Array *array);
    void Array_Add(Array *array, int item);
    void Array_Delete(Array *array, int index);
    void Array_Free(Array *array);

#ifdef __cplusplus
}
#endif

#endif /* STORAGE_H */

Файл storage.c виглядає приблизно так ...

#include <stdio.h>
#include <stdlib.h>
#include "storage.h"

/* Initialise an empty array */
void Array_Init(Array *array) 
{
    int *int_pointer;

    int_pointer = (int *)malloc(sizeof(int));

    if (int_pointer == NULL)
    {       
        printf("Unable to allocate memory, exiting.\n");
        free(int_pointer);
        exit(0);
    }
    else
    {
        array->array = int_pointer; 
        array->size = 0;
    }
}

/* Dynamically add to end of an array */
void Array_Add(Array *array, int item) 
{
    int *int_pointer;

    array->size += 1;

    int_pointer = (int *)realloc(array->array, array->size * sizeof(int));

    if (int_pointer == NULL)
    {       
        printf("Unable to reallocate memory, exiting.\n");
        free(int_pointer);
        exit(0);
    }
    else
    {
        array->array = int_pointer;
        array->array[array->size-1] = item;
    }
}

/* Delete from a dynamic array */
void Array_Delete(Array *array, int index) 
{
    int i;
    Array temp;
    int *int_pointer;

    Array_Init(&temp);

    for(i=index; i<array->size; i++)
    {
        array->array[i] = array->array[i + 1];
    }

    array->size -= 1;

    for (i = 0; i < array->size; i++)
    {
        Array_Add(&temp, array->array[i]);
    }

    int_pointer = (int *)realloc(temp.array, temp.size * sizeof(int));

    if (int_pointer == NULL)
    {       
        printf("Unable to reallocate memory, exiting.\n");
        free(int_pointer);
        exit(0);
    }
    else
    {
        array->array = int_pointer; 
    } 
}

/* Free an array */
void Array_Free(Array *array) 
{
  free(array->array);
  array->array = NULL;
  array->size = 0;  
}

Main.c виглядає приблизно так ...

#include <stdio.h>
#include <stdlib.h>
#include "storage.h"

int main(int argc, char** argv) 
{
    Array pointers;
    int i;

    Array_Init(&pointers);

    for (i = 0; i < 60; i++)
    {
        Array_Add(&pointers, i);        
    }

    Array_Delete(&pointers, 3);

    Array_Delete(&pointers, 6);

    Array_Delete(&pointers, 30);

    for (i = 0; i < pointers.size; i++)
    {        
        printf("Value: %d Size:%d \n", pointers.array[i], pointers.size);
    }

    Array_Free(&pointers);

    return (EXIT_SUCCESS);
}

З нетерпінням чекайте наступної конструктивної критики ...


1
Якщо ви шукаєте конструктивну критику, краще опублікуйте її в Code Review . У зв'язку з цим, пара пропозицій: обов'язково потрібно перевірити код на успішність викликів, malloc()перш ніж намагатися використовувати розподіл. У цьому ж ключі - помилка безпосередньо присвоїти результат realloc()вказівника перерозподіленій оригінальній пам'яті; якщо realloc()не вдається, NULLповертається, і код залишається з витоком пам'яті. Набагато ефективніше подвоїти пам'ять під час зміни розміру, ніж додати 1 простір за один раз: менше викликів realloc().
ex nihilo

1
Я знав, що зірвусь, я просто жартував, коли сказав "конструктивну критику" ... Дякую за пораду ...

2
Не намагаючись когось зірвати, просто запропонувавши якусь конструктивну критику, яка, можливо, виникла навіть без вашого легкого серця ближче;)
ex nihilo

1
Девіде, я думав над вашим коментарем "Набагато ефективніше подвоїти пам'ять під час зміни розміру, ніж додати 1 простір за один раз: менше викликів в realloc ()". Чи хотіли б ви детальніше зупинитися на цьому для мене? Чому краще виділити вдвічі більше пам’яті і, можливо, не використовувати її, тому витрачаючи пам’ять, ніж призначати лише необхідну кількість для виконання завдання? Я розумію, що ви говорите про дзвінки до realloc (), але чому викликає realloc () щоразу проблему? Хіба це не те, для чого потрібно перерозподілити пам'ять?

1
Хоча суворе подвоєння може бути не оптимальним, це, безумовно, краще, ніж збільшити обсяг пам’яті на один байт (або один intтощо). Подвоєння - типове рішення, але я не думаю, що існує оптимальне рішення, яке відповідає всім обставинам. Ось чому подвоєння є хорошою ідеєю (якийсь інший фактор, такий як 1.5, також буде добре): якщо ви почнете з розумного розподілу, вам може не знадобитися перерозподіляти взагалі. Коли потрібно більше пам’яті, розумний розподіл подвоюється тощо. Таким чином, вам, ймовірно, потрібно лише один або два дзвінки realloc().
ex nihilo

2

Коли ти кажеш

зробити масив, що містить індексний номер (int) невизначеної кількості сутностей

ви в основному говорите, що використовуєте "покажчики", але той, який є локальним покажчиком, а не локальним вказівником. Оскільки ви концептуально вже використовуєте "покажчики" (тобто ідентифікаційні номери, які посилаються на елемент у масиві), чому б вам просто не використовувати звичайні покажчики (тобто ідентифікаційні номери, що посилаються на елемент у найбільшому масиві: вся пам'ять ).

Замість того, щоб ваші об'єкти зберігали номери ідентифікаторів ресурсів, ви можете змусити їх зберігати покажчик. В основному те саме, але набагато ефективніше, оскільки ми уникаємо перетворення "масив + індекс" в "покажчик".

Покажчики не страшні, якщо ви вважаєте їх індексом масиву для всієї пам'яті (це те, що вони є насправді)


2

Щоб створити масив необмежених елементів будь-якого типу:

typedef struct STRUCT_SS_VECTOR {
    size_t size;
    void** items;
} ss_vector;


ss_vector* ss_init_vector(size_t item_size) {
    ss_vector* vector;
    vector = malloc(sizeof(ss_vector));
    vector->size = 0;
    vector->items = calloc(0, item_size);

    return vector;
}

void ss_vector_append(ss_vector* vec, void* item) {
    vec->size++;
    vec->items = realloc(vec->items, vec->size * sizeof(item));
    vec->items[vec->size - 1] = item;
};

void ss_vector_free(ss_vector* vec) {
    for (int i = 0; i < vec->size; i++)
        free(vec->items[i]);

    free(vec->items);
    free(vec);
}

і як ним користуватися:

// defining some sort of struct, can be anything really
typedef struct APPLE_STRUCT {
    int id;
} apple;

apple* init_apple(int id) {
    apple* a;
    a = malloc(sizeof(apple));
    a-> id = id;
    return a;
};


int main(int argc, char* argv[]) {
    ss_vector* vector = ss_init_vector(sizeof(apple));

    // inserting some items
    for (int i = 0; i < 10; i++)
        ss_vector_append(vector, init_apple(i));


    // dont forget to free it
    ss_vector_free(vector);

    return 0;
}

Цей вектор / масив може вміщувати будь-який тип предмета, і він має абсолютно динамічний розмір.


0

Ну, я думаю, якщо вам потрібно видалити елемент, ви зробите копію масиву, що зневажає елемент, який буде виключений.

// inserting some items
void* element_2_remove = getElement2BRemove();

for (int i = 0; i < vector->size; i++){
       if(vector[i]!=element_2_remove) copy2TempVector(vector[i]);
       }

free(vector->items);
free(vector);
fillFromTempVector(vector);
//

Припустимо , що getElement2BRemove(), copy2TempVector( void* ...)і fillFromTempVector(...)допоміжні методи для обробки вектора Темп.


Незрозуміло, чи це насправді відповідь на поставлене питання, чи це коментар.

Це думка щодо "як це зробити", і я прошу підтвердження (чи я помиляюся?), Якщо хтось має кращу ідею. ;)
JOSMAR BARBOSA - M4NOV3Y

Я думаю, я не розумію вашого останнього речення. Оскільки SO не є потоковим форумом, відкриті запитання на зразок цього у відповідях виглядають дивним.

1
Я зафіксував ваше останнє речення до того, що, на вашу думку, ви хочете сказати.
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.