C que cresce dinamicamente


126

Eu tenho um programa que lê uma lista "bruta" de entidades no jogo e pretendo criar uma matriz contendo um número de índice (int) de um número indeterminado de entidades, para processar várias coisas. Gostaria de evitar o uso de muita memória ou CPU para manter esses índices ...

Uma solução rápida e suja que utilizo até agora é declarar, na principal função de processamento (foco local), o array com o tamanho máximo de entidades do jogo e outro número inteiro para acompanhar quantas foram adicionadas à lista. Isso não é satisfatório, pois todas as listas contêm mais de 3.000 matrizes, o que não é muito, mas parece um desperdício, pois é possível usar a solução para 6-7 listas para funções variadas.

Não encontrei nenhuma solução específica em C (não em C ++ ou C #) para conseguir isso. Posso usar ponteiros, mas tenho um pouco de medo de usá-los (a menos que seja a única maneira possível).

As matrizes não deixam o escopo da função local (elas devem ser passadas para uma função e depois descartadas), caso isso mude.

Se os ponteiros são a única solução, como posso acompanhá-los para evitar vazamentos?


1
Este é um problema (muito, muito pequeno) em C, mas como você perdeu todas as soluções C ++ e C # para isso?
Ignacio Vazquez-Abrams

11
"Se os ponteiros são a única solução, como posso acompanhá-los para evitar vazamentos?" Cuidado, atenção e valgrind. É exatamente por isso que as pessoas têm tanto medo de C em primeiro lugar.
Chris Lutz

27
Você não pode usar C efetivamente sem usar ponteiros. Não tenha medo.
Qdl

sem grandes bibliotecas, apenas uma função para todos também para estruturas, por exemplo: stackoverflow.com/questions/3456446/…
user411313

6
Usar C sem ponteiros é como usar um carro sem combustível.
martinkunev

Respostas:


210

Posso usar ponteiros, mas tenho um pouco de medo de usá-los.

Se você precisar de uma matriz dinâmica, não poderá escapar dos ponteiros. Por que você está com medo? Eles não vão morder (contanto que você seja cuidadoso). Não há matriz dinâmica embutida em C, você apenas precisará escrever uma. No C ++, você pode usar a std::vectorclasse interna. C # e quase todas as outras linguagens de alto nível também têm uma classe semelhante que gerencia matrizes dinâmicas para você.

Se você planeja escrever o seu próprio, aqui está algo para começar: as implementações mais dinâmicas da matriz funcionam começando com uma matriz de algum tamanho padrão (pequeno) e, sempre que você ficar sem espaço ao adicionar um novo elemento, duplique o tamanho da matriz. Como você pode ver no exemplo abaixo, não é muito difícil: (omiti as verificações de segurança por questões de brevidade)

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

Usá-lo é tão simples:

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
Obrigado pelo código de exemplo. Eu implementei a função específica usando uma grande variedade, mas irá implementar outras coisas semelhantes usando isso, e depois de eu obtê-lo controlado, altere os outros de volta :)
Balkania

2
Muito obrigado pelo código. Um removeArraymétodo que se livre do último elemento também seria interessante. Se você permitir, eu o adicionarei ao seu exemplo de código.
Bridborium

5
% de tamanho_t ... meio que não. Se você usar C99 ou mais tarde, pode tirar vantagem da adição de% z
Randy Howard

13
Nunca omita as verificações de segurança com alocação e realocação de memória.
Alex Reynolds

3
É uma troca de desempenho. Se você dobrar a cada vez, às vezes terá uma sobrecarga de 100% e, em média, 50%. 3/2 dá 50% a pior e 25% a típica. Também está próximo da base efetiva da sequência de Fibionacci no limite (phi), que é frequentemente elogiado e usado por suas características "exponencial, mas muito menos violentamente que a base 2", mas mais fácil de calcular. O +8 significa que matrizes razoavelmente pequenas não acabam fazendo muitas cópias. Ele adiciona um termo multiplicativo, permitindo que a matriz cresça rapidamente se seu tamanho for irrelevante. Em usos especializados, isso deve ser ajustável.
Dan Sheppard

10

Existem algumas opções em que posso pensar.

  1. Lista vinculada. Você pode usar uma lista vinculada para criar uma matriz que cresce dinamicamente. Mas você não poderá fazer isso array[100]sem ter que percorrer 1-99primeiro. E pode não ser tão útil para você usar também.
  2. Grande variedade. Basta criar uma matriz com espaço mais que suficiente para tudo
  3. Redimensionando matriz. Recrie a matriz assim que souber o tamanho e / ou crie uma nova matriz toda vez que ficar sem espaço com alguma margem e copie todos os dados para a nova matriz.
  4. Combinação de matriz de lista vinculada. Simplesmente use uma matriz com um tamanho fixo e, quando ficar sem espaço, crie uma nova matriz e vincule-a (seria aconselhável acompanhar a matriz e o link para a próxima matriz em uma estrutura).

É difícil dizer qual opção seria melhor na sua situação. Simplesmente criar uma grande variedade é, naturalmente, uma das soluções mais fáceis e não deve gerar muitos problemas, a menos que seja realmente grande.


Como sete arrays de 3264 números inteiros soam para um jogo 2D moderno? Se eu estou apenas sendo paranóico, a solução seria grande variedade.
Balkania

3
Os itens 1 e 4 aqui requerem o uso de ponteiros e alocação dinâmica de memória. Sugiro usar realloccom o nº 3 - aloque o tamanho normal da matriz e depois aumente sempre que acabar. realloctratará de copiar seus dados, se necessário. Quanto à pergunta do OP sobre gerenciamento de memória, você só precisa fazer mallocuma vez no início, freeoutra no final e realloctoda vez que ficar sem espaço. Não é tão ruim.
Borealid

1
@Balkania: sete matrizes de 3264 números inteiros são menores que 100 KB. Isso não é muita memória.
Borealid

1
@Balkania: 7 * 3264 * 32 bitparece 91.39 kilobytes.
Hoje em dia

1
Essa omissão em particular é uma pena, porque não é totalmente óbvio o que deve acontecer quando o reallocretorno NULL: a->array = (int *)realloc(a->array, a->size * sizeof(int));... Talvez tivesse sido melhor escrito como: int *temp = realloc(a->array, a->size * sizeof *a->array); a->array = temp;... Dessa forma, seria óbvio que o que quer que aconteça precisa acontecer antes o NULLvalor é atribuído a a->array(se é que existe).
Autistic

10

Como tudo o que parece mais assustador a princípio do que era depois, a melhor maneira de superar o medo inicial é mergulhar no desconforto do desconhecido ! Afinal, é em momentos como o que mais aprendemos.

Infelizmente, existem limitações. Enquanto você ainda está aprendendo a usar uma função, não deve assumir o papel de professor, por exemplo. Frequentemente leio respostas de quem aparentemente não sabe usar realloc(ou seja, a resposta atualmente aceita! ) Dizendo aos outros como usá-la incorretamente, ocasionalmente sob o pretexto de que eles omitiram o tratamento de erros , mesmo que essa seja uma armadilha comum que precisa ser mencionado. Aqui está uma resposta explicando como usar realloccorretamente . Observe que a resposta está armazenando o valor de retorno em uma variável diferente para executar a verificação de erros.

Toda vez que você chama uma função e toda vez que usa uma matriz, você está usando um ponteiro. As conversões estão ocorrendo implicitamente, o que, se algo deve ser ainda mais assustador, pois são as coisas que não vemos que geralmente causam mais problemas. Por exemplo, vazamentos de memória ...

Operadores de matriz são operadores de ponteiro. array[x]é realmente um atalho para *(array + x), que pode ser dividido em: *e (array + x). É mais provável que isso *seja o que o confunde. Podemos eliminar ainda mais a adição do problema assumindo xque 0, assim, array[0]se torna *arrayporque a adição 0não altera o valor ...

... e assim podemos ver que *arrayé equivalente a array[0]. Você pode usar um onde deseja usar o outro e vice-versa. Operadores de matriz são operadores de ponteiro.

malloc, realloce amigos não inventam o conceito de ponteiro que você usa o tempo todo; eles simplesmente usam isso para implementar algum outro recurso, que é uma forma diferente de duração do armazenamento, mais adequada quando você deseja mudanças drásticas e dinâmicas no tamanho .

É uma pena que a resposta atualmente aceita também seja contrária a alguns outros conselhos bem fundamentados sobre o StackOverflow e, ao mesmo tempo, perca a oportunidade de introduzir um recurso pouco conhecido que brilha exatamente nesse caso de uso: matriz flexível membros! Essa é realmente uma resposta bastante quebrada ... :(

Ao definir seu struct, declare sua matriz no final da estrutura, sem nenhum limite superior. Por exemplo:

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

Isso permitirá que você unir seu array intna mesma alocação que o seu count, e tê-los vinculados dessa maneira pode ser muito útil !

sizeof (struct int_list)agirá como se valuetivesse um tamanho 0, por isso informa o tamanho da estrutura com uma lista vazia . Você ainda precisa adicionar ao tamanho passado reallocpara especificar o tamanho da sua lista.

Outra dica útil é lembrar que isso realloc(NULL, x)é equivalente a malloc(x), e podemos usá-lo para simplificar nosso código. Por exemplo:

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;

A razão pela qual escolhi usar struct int_list **como o primeiro argumento pode não parecer imediatamente óbvio, mas se você pensar no segundo argumento, quaisquer alterações feitas no valueinterior push_backnão seriam visíveis para a função da qual estamos chamando, certo? O mesmo vale para o primeiro argumento, e precisamos ser capazes de modificar o nosso array, não apenas aqui, mas possivelmente também em qualquer outra função / s que passamos para ...

arraycomeça apontando para o nada; é uma lista vazia. Inicializar é o mesmo que adicionar a ele. Por exemplo:

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

PS Lembre-se defree(array); quando terminar!


" array[x]é realmente um atalho para *(array + x), [...]" Você tem certeza disso ???? Veja uma exposição de seus diferentes comportamentos: eli.thegreenplace.net/2009/10/21/… .
C-Star-W-Star

1
Infelizmente, @ C-Star-Puppy, a referência que seu recurso parece não mencionar é o padrão C. Essa é a especificação pela qual seus compiladores devem aderir para se chamar legalmente de compiladores C. Seu recurso não parece contradizer minhas informações. No entanto, o padrão realmente tem alguns exemplos, como esta jóia onde é revelado que array[index]é realmente ptr[index]disfarçado ... "A definição do operador subscrito []se que E1[E2]é idêntico ao (*((E1)+(E2)))" Você não pode refutar a std
autista

Experimente esta demonstração, @ C-Star-Puppy: int main(void) { unsigned char lower[] = "abcdefghijklmnopqrstuvwxyz"; for (size_t x = 0; x < sizeof lower - 1; x++) { putchar(x[lower]); } }... Você provavelmente precisará #include <stdio.h>e <stddef.h>... Você vê como eu escrevi x[lower]( xsendo o tipo inteiro) em vez de lower[x]? O compilador C não se importa, porque *(lower + x)é o mesmo valor que *(x + lower)e lower[x]é o primeiro onde-como x[lower]é o último. Todas essas expressões são equivalentes. Testá-los ... ver por si mesmo, se você não pode tomar a minha palavra ...
autista

... e é claro que há essa parte na qual enfatizei, mas você realmente deve ler a citação inteira sem ênfase: "Exceto quando é o operando do operador sizeof, o operador _Alignof ou o unary & operator, ou é uma string literal usada para inicializar uma matriz, uma expressão que tem o tipo '' matriz do tipo '' é convertida em uma expressão com o tipo '' ponteiro para o tipo '' que aponta para o elemento inicial da matriz objeto e não é um lvalue . Se o objeto da matriz tiver classe de armazenamento de registro, o comportamento será indefinido. " O mesmo vale para funções, btw.
Autism

Ah, e em uma nota final, @ C-Star-Puppy, o Microsoft C ++ não é um compilador C e não o é há quase 20 anos. Você pode ativar o modo C89, suuuure , mas evoluímos para além do final dos anos 80 na computação. Para saber mais sobre esse tópico, sugiro a leitura deste artigo ... e então mudar para um compilador real C, como gccou clangpor toda a sua compilação C, porque você vai descobrir que há tantos pacotes que adotaram C99 apresenta ...
Autista

3

Com base no design de Matteo Furlans , quando ele disse que " as implementações mais dinâmicas da matriz funcionam começando com uma matriz com algum tamanho (pequeno) padrão, sempre que você ficar sem espaço ao adicionar um novo elemento, dobre o tamanho da matriz ". A diferença no " trabalho em andamento " abaixo é que ele não dobra de tamanho, visa usar apenas o necessário. Também omiti as verificações de segurança por simplicidade ... Também com base na idéia de brimboriums , tentei adicionar uma função de exclusão ao código ...

O arquivo storage.h se parece com isso ...

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

O arquivo storage.c fica assim ...

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

O main.c fica assim ...

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

Ansiosos pelas críticas construtivas a seguir ...


1
Se você procura críticas construtivas, é melhor postar na Code Review . Dito isto, algumas sugestões: é imperativo que o código verifique o sucesso das chamadas malloc()antes de tentar usar a alocação. Na mesma linha, é um erro atribuir diretamente o resultado do realloc()ponteiro à memória original que está sendo realocada; se realloc()falhar, NULLé retornado e o código é deixado com um vazamento de memória. É muito mais eficiente dobrar a memória ao redimensionar do que adicionar um espaço por vez: menos chamadas para realloc().
Ex-nihilo

1
Eu sabia que ia ser rasgada, eu estava só brincando quando eu disse "crítica construtiva" ... Obrigado pelo conselho ...

2
Não tentando rasgar qualquer um à parte, apenas oferecer algumas críticas construtivas, que pode ter sido próxima, mesmo sem a sua luz de coração mais perto;)
ex nihilo

1
David, estive pensando no seu comentário "É muito mais eficiente duplicar a memória ao redimensionar do que adicionar um espaço por vez: menos chamadas para realloc ()". Você poderia elaborar isso para mim, por que é melhor alocar o dobro da quantidade de memória e possivelmente não usá-la, desperdiçando memória, do que atribuir apenas a quantidade necessária para a tarefa? Entendi o que você está dizendo sobre chamadas para realloc (), mas por que chamar realloc () toda vez que é um problema? Não é para isso que é necessário realocar memória?

1
Embora a duplicação estrita possa não ser ideal, é certamente melhor do que aumentar a memória, um byte (ou um int, etc.) de cada vez. Dobrar é uma solução típica, mas não acho que exista uma solução ideal que atenda a todas as circunstâncias. Eis por que duplicar é uma boa idéia (algum outro fator, como 1,5, também seria bom): se você começar com uma alocação razoável, talvez não seja necessário realocar. Quando mais memória é necessária, a alocação razoável é dobrada e assim por diante. Dessa forma, você provavelmente precisará de apenas uma ou duas chamadas realloc().
Ex-nihilo

2

Quando você está dizendo

criar uma matriz contendo um número de índice (int) de um número indeterminado de entidades

você está basicamente dizendo que está usando "ponteiros", mas um que é um ponteiro local para toda a matriz em vez de ponteiro para toda a memória. Como você já está conceitualmente usando "ponteiros" (ou seja, números de identificação que se referem a um elemento em uma matriz), por que você não usa ponteiros regulares (ou seja, números de identificação que se referem a um elemento na maior matriz: toda a memória )

Em vez de seus objetos armazenarem números de identificação de recurso, você pode fazê-los armazenar um ponteiro. Basicamente a mesma coisa, mas muito mais eficiente, pois evitamos transformar "array + index" em um "ponteiro".

Os ponteiros não são assustadores se você os considera como índice de matriz para toda a memória (que é o que realmente são)


2

Para criar uma matriz de itens ilimitados de qualquer 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 como usá-lo:

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

Esse vetor / matriz pode conter qualquer tipo de item e é totalmente dinâmico em tamanho.


0

Bem, acho que se você precisar remover um elemento, fará uma cópia da matriz desprezando o elemento a ser excluído.

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

Assume-se que getElement2BRemove(), copy2TempVector( void* ...)e fillFromTempVector(...)são métodos auxiliares para lidar com o vector de temperatura.


Não está claro se essa é realmente uma resposta à pergunta ou se é um comentário.

É uma opinião sobre "como fazer" e estou pedindo confirmação (estou errado?) SE alguém tiver uma idéia melhor. ;)
JOSMAR BARBOSA - M4NOV3Y

Acho que não entendi sua última frase. Como o SO não é um fórum encadeado, perguntas abertas como esta nas respostas parecem estranhas.

1
Eu consertei sua última frase com o que acho que você quer dizer.
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.