C array a crescita dinamica


126

Ho un programma che legge un elenco "grezzo" di entità in-game e intendo creare un array contenente un numero indice (int) di un numero indeterminato di entità, per elaborare varie cose. Vorrei evitare di usare troppa memoria o CPU per mantenere tali indici ...

Una soluzione rapida e sporca che uso finora è dichiarare, nella funzione di elaborazione principale (focus locale) l'array con una dimensione delle entità di gioco massime e un altro numero intero per tenere traccia di quanti sono stati aggiunti all'elenco. Questo non è soddisfacente, poiché ogni elenco contiene più di 3000 array, il che non è molto, ma sembra uno spreco, poiché potrò usare la soluzione per 6-7 elenchi per funzioni diverse.

Non ho trovato alcuna soluzione specifica C (non C ++ o C #) per raggiungere questo obiettivo. Posso usare i puntatori, ma ho un po 'paura di usarli (a meno che non sia l'unico modo possibile).

Gli array non lasciano l'ambito della funzione locale (devono essere passati a una funzione, quindi eliminati), nel caso in cui ciò cambi.

Se i puntatori sono l'unica soluzione, come posso tenerne traccia per evitare perdite?


1
Questo è un problema (molto, molto piccolo) in C, ma come hai perso tutte le soluzioni C ++ e C # per questo?
Ignacio Vazquez-Abrams,

11
"Se i puntatori sono l'unica soluzione, come posso tenerne traccia per evitare perdite?" Cura, attenzione e valgrind. Questo è esattamente il motivo per cui le persone hanno così tanta paura se C in primo luogo.
Chris Lutz,

27
Non è possibile utilizzare C in modo efficace senza utilizzare i puntatori. Non aver paura.
qrdl

senza grandi librerie solo una funzione per tutti anche per le strutture ad es: stackoverflow.com/questions/3456446/...
user411313

6
Usare C senza puntatori è come usare un'auto senza carburante.
martinkunev,

Risposte:


210

Posso usare i puntatori, ma ho un po 'paura di usarli.

Se hai bisogno di un array dinamico, non puoi sfuggire ai puntatori. Perché hai paura però? Non mordono (finché stai attento, cioè). Non esiste un array dinamico integrato in C, dovrai solo scriverne uno tu stesso. In C ++, è possibile utilizzare la std::vectorclasse integrata . C # e quasi ogni altro linguaggio di alto livello hanno anche una classe simile che gestisce array dinamici per te.

Se hai intenzione di scrivere il tuo, ecco qualcosa per iniziare: la maggior parte delle implementazioni di array dinamici funzionano iniziando con un array di alcune dimensioni (piccole) predefinite, quindi ogni volta che rimani a corto di spazio quando aggiungi un nuovo elemento, raddoppia il dimensione dell'array. Come puoi vedere nell'esempio seguente, non è affatto difficile: (ho omesso i controlli di sicurezza per brevità)

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

Usarlo è altrettanto semplice:

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
Grazie per il codice di esempio. Ho implementato la funzione specifica usando un grande array, ma implementerò altre cose simili usando questo, e dopo che lo avrò controllato, cambierò di nuovo gli altri :)
Balkania

2
Grazie mille per il codice. Un removeArraymetodo che elimina l'ultimo elemento sarebbe anche pulito. Se lo permetti, lo aggiungerò al tuo esempio di codice.
Brimborium,

5
% d e size_t ... un po 'di no-no lì. Se usi C99 o versioni successive, puoi sfruttare l'aggiunta di% z
Randy Howard,

13
Non omettere mai i controlli di sicurezza con allocazione e riallocazione della memoria.
Alex Reynolds,

3
È un compromesso prestazionale. Se raddoppi ogni volta, a volte hai un overhead del 100% e in media il 50%. 3/2 ti dà il 50% peggiore e il 25% tipico. È anche vicino alla base effettiva della sequenza di Fibionacci nel limite (phi) che viene spesso elogiato e usato per le sue caratteristiche "esponenziali ma molto meno violentemente della base-2", ma più facile da calcolare. Il +8 indica che le matrici che sono ragionevolmente piccole non finiscono per fare troppe copie. Aggiunge un termine moltiplicativo che consente all'array di crescere rapidamente se le sue dimensioni sono irrilevanti. Negli usi specialistici questo dovrebbe essere sintonizzabile.
Dan Sheppard,

10

Ci sono un paio di opzioni che mi vengono in mente.

  1. Lista collegata. È possibile utilizzare un elenco collegato per creare una matrice in modo dinamico come cosa. Ma non sarai in grado di farlo array[100]senza dover 1-99prima attraversare . E potrebbe non essere utile neanche per te.
  2. Matrice di grandi dimensioni. Crea semplicemente un array con spazio più che sufficiente per tutto
  3. Array di ridimensionamento. Ricreare l'array una volta che si conoscono le dimensioni e / o creare un nuovo array ogni volta che si esaurisce lo spazio con un certo margine e si copiano tutti i dati nel nuovo array.
  4. Combinazione di array elenco collegato. Basta usare un array con una dimensione fissa e una volta esaurito lo spazio, creare un nuovo array e collegarsi a quello (sarebbe opportuno tenere traccia dell'array e del collegamento all'array successivo in uno struct).

È difficile dire quale opzione sarebbe la migliore nella tua situazione. La semplice creazione di un array di grandi dimensioni è ovviamente una delle soluzioni più semplici e non dovrebbe causare molti problemi a meno che non sia davvero di grandi dimensioni.


Come suonano sette matrici di 3264 numeri interi per un gioco 2D moderno? Se fossi solo paranoico, la soluzione sarebbe una vasta gamma.
Balcania,

3
Sia il numero 1 che il numero 4 qui richiedono comunque l'utilizzo di puntatori e allocazione dinamica della memoria. Suggerisco di usare realloccon il n. 3: allocare l'array di dimensioni normali, quindi aumentarlo ogni volta che si esaurisce. reallocgestirà la copia dei dati se necessario. Per quanto riguarda la domanda del PO sulla gestione della memoria, è sufficiente mallocuna volta all'inizio, freeuna volta alla fine e reallocogni volta che si esaurisce lo spazio. Non è poi così male.
Borealid,

1
@Balkania: sette matrici di 3264 numeri interi sono un pelo sotto i 100 KB. Non è affatto molta memoria.
Borealid,

1
@Balkania: 7 * 3264 * 32 bitsembra 91.39 kilobytes. Al giorno d'oggi non molto secondo gli standard;)
Wolph,

1
Questa particolare omissione è un peccato, perché non è del tutto ovvio cosa dovrebbe accadere quando reallocritorna NULL: a->array = (int *)realloc(a->array, a->size * sizeof(int));... Forse sarebbe stato meglio scritto come: int *temp = realloc(a->array, a->size * sizeof *a->array); a->array = temp;... In questo modo sarebbe ovvio che qualsiasi cosa accada deve accadere prima il NULLvalore è assegnato a a->array(se lo è affatto).
autistico,

10

Come per tutto ciò che sembra più spaventoso all'inizio di quanto non fosse in seguito, il modo migliore per superare la paura iniziale è immergersi nel disagio dell'ignoto ! A volte è come quello che impariamo di più, dopo tutto.

Sfortunatamente, ci sono delle limitazioni. Mentre stai ancora imparando a usare una funzione, non dovresti assumere il ruolo di insegnante, per esempio. Leggo spesso le risposte di coloro che apparentemente non sanno come usare realloc(cioè la risposta attualmente accettata! ) Dicendo agli altri come usarlo in modo errato, a volte con il pretesto di aver omesso la gestione degli errori , anche se si tratta di una trappola comune che deve essere menzionato. Ecco una risposta che spiega come utilizzare realloccorrettamente . Si noti che la risposta memorizza il valore restituito in una variabile diversa per eseguire il controllo degli errori.

Ogni volta che si chiama una funzione e ogni volta che si utilizza un array, si utilizza un puntatore. Le conversioni si stanno verificando implicitamente, cosa che dovrebbe essere ancora più spaventosa, poiché sono le cose che non vediamo che spesso causano la maggior parte dei problemi. Ad esempio, perdite di memoria ...

Gli operatori di matrice sono operatori di puntatore. array[x]è davvero una scorciatoia per *(array + x), che può essere suddivisa in: *e (array + x). È molto probabile che *sia ciò che ti confonde. Possiamo eliminare ulteriormente l'aggiunta dal problema ipotizzando xessere 0, quindi, array[0]diventa *arrayperché l'aggiunta 0non cambierà il valore ...

... e quindi possiamo vedere che *arrayè equivalente a array[0]. Puoi usarne uno dove vuoi usare l'altro e viceversa. Gli operatori di matrice sono operatori di puntatore.

malloc, reallocE gli amici non inventano il concetto di un puntatore che hai utilizzato per tutto il tempo; usano semplicemente questo per implementare qualche altra caratteristica, che è una diversa forma di durata della memoria, più adatta quando desideri cambiamenti drastici e dinamici delle dimensioni .

È un peccato che la risposta attualmente accettata vada anche contro la consistenza di alcuni altri consigli molto fondati su StackOverflow e, allo stesso tempo, manca un'opportunità per introdurre una funzione poco nota che brilla proprio per questo caso: array flessibile membri! In realtà è una risposta piuttosto sbagliata ... :(

Quando si definisce il proprio struct, dichiarare l'array alla fine della struttura, senza alcun limite superiore. Per esempio:

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

Ciò ti consentirà di unire il tuo array intnella stessa allocazione del tuo counte averli associati in questo modo può essere molto utile !

sizeof (struct int_list)agirà come se valueavesse una dimensione di 0, quindi ti dirà la dimensione della struttura con un elenco vuoto . Devi ancora aggiungere le dimensioni passate reallocper specificare le dimensioni del tuo elenco.

Un altro suggerimento utile è ricordare che realloc(NULL, x)equivale a malloc(x), e possiamo usarlo per semplificare il nostro codice. Per esempio:

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;

Il motivo che ho scelto di usare struct int_list **come primo argomento potrebbe non sembrare immediatamente ovvio, ma se pensi al secondo argomento, eventuali modifiche apportate valuedall'interno push_backnon sarebbero visibili alla funzione da cui stiamo chiamando, giusto? Lo stesso vale per il primo argomento, e dobbiamo essere in grado di modificare il nostro array, non solo qui ma probabilmente anche in qualsiasi altra funzione / i lo passiamo a ...

arrayinizia indicando il nulla; è un elenco vuoto. Inizializzarlo equivale ad aggiungerlo. Per esempio:

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

PS Ricordati difree(array); quando hai finito!


" array[x]è davvero una scorciatoia per *(array + x), [...]" Ne sei sicuro ???? Guarda un'esposizione dei loro diversi comportamenti: eli.thegreenplace.net/2009/10/21/… .
C-Star-W-Star,

1
Purtroppo, @ C-Star-Puppy, l'unico riferimento che la tua risorsa sembra non menzionare affatto è lo standard C. Questa è la specifica con la quale i compilatori devono aderire per potersi chiamare legalmente compilatori C. La tua risorsa non sembra affatto contraddire le mie informazioni. Ciò nonostante, lo standard ha in realtà alcuni esempi come questo gioiello dove è rivelato che array[index]in realtà è ptr[index]sotto mentite spoglie ... "La definizione di operatore pedice []è che E1[E2]è identico a (*((E1)+(E2)))" Non si può confutare la std
autistico

Prova questa dimostrazione, @ C-Star-Puppy: int main(void) { unsigned char lower[] = "abcdefghijklmnopqrstuvwxyz"; for (size_t x = 0; x < sizeof lower - 1; x++) { putchar(x[lower]); } }... Probabilmente dovrai #include <stdio.h>e <stddef.h>... Vedi come ho scritto x[lower](con xil tipo intero) anziché lower[x]? Al compilatore C non importa, perché ha *(lower + x)lo stesso valore di *(x + lower), ed lower[x]è il primo dove-come x[lower]è il secondo. Tutte queste espressioni sono equivalenti. Provali ... vedi di persona, se non puoi prendere la mia parola ...
autistico,

... e poi ovviamente c'è questa parte, su cui ho posto la mia enfasi, ma dovresti davvero leggere l'intera citazione senza enfasi: "Tranne quando si tratta dell'operando dell'operatore sizeof, dell'operatore _Alignof o del unario e operatore, oppure è una stringa letterale utilizzata per inizializzare una matrice, un'espressione che ha tipo '' matrice di tipo '' viene convertita in un'espressione con tipo '' puntatore a digitare '' che punta all'elemento iniziale della matrice e non è un valore . Se l'oggetto array ha una classe di archiviazione del registro, il comportamento non è definito. " Lo stesso vale per le funzioni, tra l'altro.
autistico,

Ohh e su un'ultima nota, @ C-Star-Puppy, Microsoft C ++ non è un compilatore C e non lo è da quasi 20 anni. Puoi abilitare la modalità C89, suuuure , ma ci siamo evoluti oltre la fine degli anni '80 nel campo dell'informatica. Per ulteriori informazioni su questo argomento, ti suggerisco di leggere questo articolo ... e quindi passare a un compilatore C reale come gcco clangper tutta la tua compilazione C, perché scoprirai che ci sono così tanti pacchetti che hanno adottato le funzionalità C99 ...
autistico,

3

Basandosi sul design di Matteo Furlans , quando ha affermato che "la maggior parte delle implementazioni di array dinamici funzionano iniziando con un array di alcune dimensioni (piccole) predefinite, quindi ogni volta che si esaurisce lo spazio quando si aggiunge un nuovo elemento, raddoppiare le dimensioni dell'array ". La differenza nel " lavoro in corso " di seguito è che non raddoppia in termini di dimensioni, mira a utilizzare solo ciò che è richiesto. Ho anche omesso i controlli di sicurezza per semplicità ... Basandomi anche sull'idea dei brimbori , ho provato ad aggiungere una funzione di cancellazione al codice ...

Il file storage.h è simile al seguente ...

#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 */

Il file storage.c assomiglia a questo ...

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

Il main.c assomiglia a questo ...

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

Attendo con ansia le critiche costruttive da seguire ...


1
Se cerchi critiche costruttive, meglio pubblicare su Code Review . Detto questo, un paio di suggerimenti: è indispensabile che il codice controlli la riuscita delle chiamate malloc()prima di tentare di utilizzare l'allocazione. Allo stesso modo, è un errore assegnare direttamente il risultato del realloc()puntatore alla memoria originale che viene riallocata; se realloc()fallisce, NULLviene restituito e il codice viene lasciato con una perdita di memoria. È molto più efficiente raddoppiare la memoria durante il ridimensionamento piuttosto che aggiungere 1 spazio alla volta: meno chiamate a realloc().
ex nihilo,

1
Sapevo che mi sarei fatto a pezzi, stavo solo scherzando quando ho detto "critiche costruttive" ... Grazie per il consiglio ...

2
Non cercare di fare a pezzi nessuno, solo offrendo alcune critiche costruttive, che potrebbero essere state realizzate anche senza il tuo spensierato più vicino;)
ex nihilo

1
David, ho pensato al tuo commento "È molto più efficiente raddoppiare la memoria durante il ridimensionamento piuttosto che aggiungere 1 spazio alla volta: meno chiamate a realloc ()". Potresti approfondire questo per me, per favore, perché è meglio allocare il doppio della quantità di memoria e possibilmente non usarla, quindi sprecare memoria, che assegnare solo la quantità richiesta per l'attività? Ho capito cosa stai dicendo sulle chiamate a realloc (), ma perché chiamare realloc () ogni volta che è un problema? Non è quello che serve per riallocare la memoria?

1
Sebbene il raddoppio rigoroso possa non essere ottimale, è certamente meglio che aumentare la memoria di un byte (o uno int, ecc.) Alla volta. Il raddoppio è una soluzione tipica, ma non credo che esista una soluzione ottimale adatta a tutte le circostanze. Ecco perché il raddoppio è una buona idea (anche alcuni altri fattori come 1.5 andrebbero bene): se inizi con un'allocazione ragionevole, potrebbe non essere necessario riallocare affatto. Quando è necessaria più memoria, l'allocazione ragionevole viene raddoppiata e così via. In questo modo probabilmente avrai bisogno solo di una o due chiamate a realloc().
ex nihilo,

2

Quando dici

crea un array contenente un numero indice (int) di un numero indeterminato di entità

in pratica stai dicendo che stai usando "puntatori", ma uno che è un puntatore locale a livello di array anziché un puntatore a livello di memoria. Dal momento che concettualmente stai già utilizzando i "puntatori" (ovvero i numeri ID che si riferiscono a un elemento in un array), perché non usi semplicemente i puntatori regolari (ovvero i numeri ID che si riferiscono a un elemento nell'array più grande: l'intera memoria ).

Invece che i tuoi oggetti memorizzino i numeri ID di una risorsa, puoi invece far loro memorizzare un puntatore. Praticamente la stessa cosa, ma molto più efficiente poiché evitiamo di trasformare "array + index" in un "puntatore".

I puntatori non fanno paura se li consideri come indice di array per l'intera memoria (che è quello che sono realmente)


2

Per creare un array di oggetti illimitati di qualsiasi tipo:

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

e come usarlo:

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

Questo vettore / array può contenere qualsiasi tipo di elemento ed è di dimensioni completamente dinamiche.


0

Bene, immagino che se hai bisogno di rimuovere un elemento, farai una copia dell'array che disprezza l'elemento da escludere.

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

Supponi che getElement2BRemove(), copy2TempVector( void* ...)e fillFromTempVector(...)sono metodi ausiliari per gestire il vettore temp.


Non è chiaro se questa sia effettivamente una risposta alla domanda posta o se sia un commento.

È un'opinione su "come fare" e chiedo conferma (sbaglio?) SE qualcuno ha un'idea migliore. ;)
JOSMAR BARBOSA - M4NOV3Y,

Immagino di non capire la tua ultima frase. Poiché SO non è un forum thread, le domande aperte come questa nelle risposte sembrano strane.

1
Ho fissato la tua ultima frase a quello che penso tu voglia dire.
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.