C Array que Cresce

Um array comum tem tamanho fixo: double precos[5]; guarda cinco preços, nem um a mais. Mas a lista de compras da Ana não tem tamanho fixo: num dia são três itens, no mês de festa são quarenta. Nesta aula você vai juntar o que aprendeu em malloc e free e calloc e realloc para montar um array que cresce sozinho conforme os itens chegam. É uma das peças mais usadas de todo programa em C (e é o que o list do Python e o vector do C++ fazem por baixo dos panos).

Você vai aprender
  • A diferença entre tamanho (quantos itens existem) e capacidade (quantos cabem).
  • Como fazer o array crescer com realloc quando ele lota.
  • Por que crescer dobrando a capacidade é muito melhor do que crescer de um em um.
  • O jeito seguro de chamar realloc (e o que se perde no jeito errado).
  • Como guardar tudo em duas funções, adicionar e liberar.
  • Como ler uma quantidade qualquer de números do teclado ou de um arquivo.
  • Como conferir com o valgrind que nenhum byte vazou.

Todos os programas desta aula foram compilados com o gcc 15.2 na máquina fictícia da Ana (usuária ana, computador notebook), na pasta ~/c, com o comando padrão do guia: gcc -std=c23 -Wall -Wextra arquivo.c -o programa (veja Compilar com gcc). Você pode repetir cada um no editor do guia.


A ideia: o caderno que lota

A Ana anota as compras num caderno pequeno, de 4 linhas. Enquanto há linha livre, ela só escreve. Quando o caderno lota, ela compra um caderno maior, copia as anotações para ele e joga o pequeno fora. Daí em diante, continua escrevendo no novo.

Repare em dois números diferentes:

  • quantas linhas estão escritas (o tamanho da lista);
  • quantas linhas o caderno tem (a capacidade).

Enquanto o tamanho é menor que a capacidade, escrever é fácil. Quando os dois ficam iguais, é hora de trocar de caderno.

No caderno da AnaEm CExemplo
O caderno (as linhas)um bloco de memória reservado com malloc/reallocdouble *precos
Linhas já escritaso tamanhosize_t tamanho
Linhas que o caderno tema capacidadesize_t capacidade
Trocar por um caderno maior e copiarreallocrealloc(precos, nova * sizeof(double))
Jogar o caderno fora no fimfreefree(precos)

O realloc (de re-allocate, "reservar de novo") faz exatamente a troca de caderno: reserva um bloco maior, copia o conteúdo do antigo e libera o antigo. Às vezes ele consegue até "esticar" o bloco no mesmo lugar, sem copiar nada. Você não precisa saber qual dos dois aconteceu: só precisa usar o ponteiro que ele devolve.

size_t

size_t é o tipo que o C usa para tamanhos e quantidades: um inteiro sem sinal (nunca negativo), do tamanho certo para contar bytes na sua máquina. É o tipo que o sizeof devolve e que o malloc recebe. No printf, o formato dele é %zu. Detalhes em sizeof.


1. A primeira versão: crescer de um em um

Comece com três variáveis: o ponteiro para a memória, o tamanho e a capacidade. No começo não existe memória nenhuma: o ponteiro vale NULL e os dois números valem zero. A cada item novo, se a lista estiver lotada, o programa pede espaço para mais um.

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

int main(void) {
    double *precos = NULL;
    size_t tamanho = 0;
    size_t capacidade = 0;

    double novos[] = {12.50, 4.99, 7.80, 3.25, 9.90};

    for (size_t i = 0; i < 5; i++) {
        if (tamanho == capacidade) {
            size_t nova = capacidade + 1;
            double *maior = realloc(precos, nova * sizeof(double));
            if (maior == NULL) {
                printf("Sem memória!\n");
                free(precos);
                return 1;
            }
            precos = maior;
            capacidade = nova;
            printf("Cresceu: agora cabem %zu\n", capacidade);
        }
        precos[tamanho] = novos[i];
        tamanho++;
    }

    for (size_t i = 0; i < tamanho; i++) {
        printf("Item %zu: R$ %.2f\n", i + 1, precos[i]);
    }

    free(precos);
    return 0;
}
double *precos = NULL;
O "caderno". Começa sem memória nenhuma. Iniciar com NULL é importante: o realloc aceita NULL e, nesse caso, funciona igual a um malloc.
size_t tamanho = 0; size_t capacidade = 0;
Quantos itens estão guardados e quantos cabem. As três variáveis andam sempre juntas: mexeu numa, confira as outras.
if (tamanho == capacidade)
A lista lotou (ou ainda nem existe, quando os dois valem 0). Só nesse caso é preciso crescer.
size_t nova = capacidade + 1;
A nova capacidade: um lugar a mais. (Daqui a pouco você vai trocar esta linha.)
double *maior = realloc(precos, nova * sizeof(double));
Pede um bloco com espaço para nova preços. O tamanho é em bytes, por isso o * sizeof(double). O resultado vai para uma variável nova, maior, e não direto para precos: a seção 4 explica por quê.
if (maior == NULL) { … }
O realloc devolve NULL quando não há memória. Nesse caso o bloco antigo continua intacto em precos, e o programa ainda pode liberá-lo antes de sair.
precos = maior; capacidade = nova;
Deu certo: agora o caderno é o novo bloco, e a capacidade é a nova.
precos[tamanho] = novos[i]; tamanho++;
Guarda o item na primeira posição livre (a posição tamanho) e conta mais um. Depois de crescer, sempre existe essa posição livre.
free(precos);
Um único free no fim: o realloc já liberou cada bloco antigo quando trocou de caderno.
$ gcc -std=c23 -Wall -Wextra cresce1.c -o cresce1
$ ./cresce1
Cresceu: agora cabem 1
Cresceu: agora cabem 2
Cresceu: agora cabem 3
Cresceu: agora cabem 4
Cresceu: agora cabem 5
Item 1: R$ 12.50
Item 2: R$ 4.99
Item 3: R$ 7.80
Item 4: R$ 3.25
Item 5: R$ 9.90

Funciona. Mas repare: cinco itens, cinco trocas de caderno. É como comprar um caderno novo, com uma linha a mais, toda vez que você anota uma compra. Para cinco itens, tudo bem. Para mil, a conta fica feia.


2. Dobrar a capacidade

O truque usado em praticamente toda biblioteca é: quando lotar, dobre a capacidade. Começando com 4 lugares, o caderno vai para 8, depois 16, 32, 64… Cada troca dá espaço para muitos itens novos. Só uma linha muda:

            size_t nova = (capacidade == 0) ? 4 : capacidade * 2;

O operador ternário diz: "se a capacidade é zero (a lista ainda não existe), comece com 4; senão, dobre". Sem esse cuidado, 0 * 2 daria zero, e a lista nunca cresceria. Com dez preços:

$ gcc -std=c23 -Wall -Wextra dobra.c -o dobra
$ ./dobra
Cresceu: agora cabem 4
Cresceu: agora cabem 8
Cresceu: agora cabem 16
Item 1: R$ 12.50
Item 2: R$ 4.99
Item 3: R$ 7.80
Item 4: R$ 3.25
Item 5: R$ 9.90
Item 6: R$ 6.40
Item 7: R$ 2.15
Item 8: R$ 15.00
Item 9: R$ 8.75
Item 10: R$ 1.99

Dez itens, só três trocas. No fim sobram 6 lugares vazios (capacidade 16, tamanho 10): é o preço de crescer depressa, e é um preço pequeno.

A diferença com mil itens

Este programa guarda os números de 0 a 999 em duas listas, uma crescendo de um em um e outra dobrando, e conta quantas vezes cada uma chamou o realloc:

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

int main(void) {
    int *a = NULL, *b = NULL;
    size_t cap_a = 0, cap_b = 0;
    int vezes_a = 0, vezes_b = 0;

    for (size_t n = 0; n < 1000; n++) {
        if (n == cap_a) {                    /* estratégia 1: +1 */
            cap_a = cap_a + 1;
            int *novo = realloc(a, cap_a * sizeof(int));
            if (novo == NULL) { free(a); free(b); return 1; }
            a = novo;
            vezes_a++;
        }
        a[n] = (int)n;

        if (n == cap_b) {                    /* estratégia 2: dobrar */
            cap_b = (cap_b == 0) ? 4 : cap_b * 2;
            int *novo = realloc(b, cap_b * sizeof(int));
            if (novo == NULL) { free(a); free(b); return 1; }
            b = novo;
            vezes_b++;
        }
        b[n] = (int)n;
    }

    printf("1000 itens com +1:     %d realocações (capacidade final %zu)\n", vezes_a, cap_a);
    printf("1000 itens dobrando:   %d realocações (capacidade final %zu)\n", vezes_b, cap_b);
    free(a);
    free(b);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra conta.c -o conta
$ ./conta
1000 itens com +1:     1000 realocações (capacidade final 1000)
1000 itens dobrando:   9 realocações (capacidade final 1024)

Mil contra nove. E cada realocação pode copiar a lista inteira: crescendo de um em um, no pior caso o computador copia 0 + 1 + 2 + … + 999 = 499.500 números; dobrando, copia no máximo 4 + 8 + 16 + … + 512 = 1.020. Com um milhão de itens, a primeira estratégia vira uma eternidade; a segunda continua instantânea.

Missão: este programa guarda 10 preços crescendo de um em um e faz 10 realocações. Troque a linha da nova capacidade para começar com 4 e dobrar, e veja cair para Realocações: 3 e Capacidade final: 16.

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

int main(void) {
    double *precos = NULL;
    size_t tamanho = 0;
    size_t capacidade = 0;
    int realocacoes = 0;

    for (int i = 1; i <= 10; i++) {
        if (tamanho == capacidade) {
            size_t nova = capacidade + 1;   /* troque esta linha */
            double *maior = realloc(precos, nova * sizeof(double));
            if (maior == NULL) {
                free(precos);
                return 1;
            }
            precos = maior;
            capacidade = nova;
            realocacoes++;
        }
        precos[tamanho] = i * 1.5;
        tamanho++;
    }

    printf("Itens: %zu\n", tamanho);
    printf("Realocações: %d\n", realocacoes);
    printf("Capacidade final: %zu\n", capacidade);
    free(precos);
    return 0;
}
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    double *precos = NULL;
    size_t tamanho = 0;
    size_t capacidade = 0;
    int realocacoes = 0;

    for (int i = 1; i <= 10; i++) {
        if (tamanho == capacidade) {
            size_t nova = (capacidade == 0) ? 4 : capacidade * 2;
            double *maior = realloc(precos, nova * sizeof(double));
            if (maior == NULL) {
                free(precos);
                return 1;
            }
            precos = maior;
            capacidade = nova;
            realocacoes++;
        }
        precos[tamanho] = i * 1.5;
        tamanho++;
    }

    printf("Itens: %zu\n", tamanho);
    printf("Realocações: %d\n", realocacoes);
    printf("Capacidade final: %zu\n", capacidade);
    free(precos);
    return 0;
}

3. Guardar tudo em funções: adicionar e liberar

Copiar aquele bloco do if em todo lugar que acrescenta um item seria pedir para errar. O certo é escrever a lógica uma vez, numa função. O problema: a função precisa mudar as três variáveis do main (o ponteiro, o tamanho e a capacidade). Como você viu em Passar por Ponteiro, para uma função mudar uma variável de fora, ela recebe o endereço da variável. E o endereço de um ponteiro é um ponteiro para ponteiro: double **.

De quebra, este programa lê os preços do teclado (ou de um arquivo) até a entrada acabar. É a situação em que o array que cresce brilha: ninguém sabe de antemão quantos preços vão chegar.

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

bool adicionar(double **precos, size_t *tamanho, size_t *capacidade, double valor) {
    if (*tamanho == *capacidade) {
        size_t nova = (*capacidade == 0) ? 4 : *capacidade * 2;
        double *maior = realloc(*precos, nova * sizeof(double));
        if (maior == NULL) {
            return false;
        }
        *precos = maior;
        *capacidade = nova;
    }
    (*precos)[*tamanho] = valor;
    (*tamanho)++;
    return true;
}

void liberar(double **precos, size_t *tamanho, size_t *capacidade) {
    free(*precos);
    *precos = NULL;
    *tamanho = 0;
    *capacidade = 0;
}

int main(void) {
    double *precos = NULL;
    size_t tamanho = 0;
    size_t capacidade = 0;

    double preco;
    while (scanf("%lf", &preco) == 1) {
        if (!adicionar(&precos, &tamanho, &capacidade, preco)) {
            printf("Sem memória!\n");
            break;
        }
    }

    double total = 0.0;
    for (size_t i = 0; i < tamanho; i++) {
        total += precos[i];
    }
    printf("%zu itens (capacidade %zu)\n", tamanho, capacidade);
    printf("Total: R$ %.2f\n", total);

    liberar(&precos, &tamanho, &capacidade);
    return 0;
}
LinhaO que faz
bool adicionar(double **precos, size_t *tamanho, size_t *capacidade, double valor)Recebe o endereço das três variáveis da lista e o valor a guardar. Devolve true se deu certo e false se faltou memória. No C23, bool, true e false são palavras da linguagem; antes, era preciso #include <stdbool.h> (veja Booleanos).
*tamanho == *capacidadeO * segue o endereço até a variável do main: compara o tamanho e a capacidade de verdade.
realloc(*precos, …)*precos é o ponteiro precos do main (o caderno atual).
return false;Sem memória: a função desiste sem estragar nada. A lista antiga continua inteira e utilizável.
*precos = maior;Muda o ponteiro do main para o bloco novo.
(*precos)[*tamanho] = valor;Primeiro (*precos) pega o ponteiro do main; depois [*tamanho] escolhe a primeira posição livre. Os parênteses são obrigatórios (veja "Quando dá errado").
(*tamanho)++;Soma 1 ao tamanho do main. Também precisa dos parênteses: *tamanho++ andaria com o ponteiro em vez de somar no número (veja Precedência).
liberar(…)Devolve a memória e deixa a lista "zerada": ponteiro NULL, tamanho e capacidade 0. Assim, se alguém chamar adicionar de novo depois, a lista recomeça do zero em vez de usar memória já liberada.
while (scanf("%lf", &preco) == 1)Lê um número por vez enquanto a leitura der certo. Quando a entrada acaba (fim do arquivo, ou Ctrl+D no teclado) ou chega algo que não é número, o scanf devolve outra coisa e o laço termina (veja Entrada com scanf).

Para testar, a Ana passa seis preços pelo printf com um pipe e depois dez preços num arquivo:

$ gcc -std=c23 -Wall -Wextra -g carrinho.c -o carrinho
$ printf '12.50\n4.99\n7.80\n3.25\n9.90\n6.40\n' | ./carrinho
6 itens (capacidade 8)
Total: R$ 44.84
$ cat compras.txt
12.50 4.99 7.80
3.25 9.90
6.40 2.15 15.00 8.75 1.99
$ ./carrinho < compras.txt
10 itens (capacidade 16)
Total: R$ 72.73

O -g no comando guarda os números das linhas dentro do programa, para o valgrind (logo abaixo) dizer em qual linha está cada problema.

Vai ficar mais bonito com struct

Três variáveis soltas que sempre andam juntas, e funções com três parâmetros cheios de *: dá para sentir que falta alguma coisa. Falta a struct, que vem logo depois desta parte do guia. Com ela, o ponteiro, o tamanho e a capacidade viram uma coisa só, e a chamada fica adicionar(&lista, 12.50). Você vai reescrever esta lista assim em Structs e Ponteiros. A lógica (dobrar, conferir o realloc, liberar no fim) continua exatamente a mesma.

Conferindo com o valgrind

O valgrind roda o programa e vigia cada byte reservado (veja Valgrind). A opção --leak-check=full pede o relatório completo de vazamentos:

$ valgrind --leak-check=full ./carrinho < compras.txt
==29259== Memcheck, a memory error detector
==29259== Copyright (C) 2002-2024, and GNU GPL'd, by Julian Seward et al.
==29259== Using Valgrind-3.26.0 and LibVEX; rerun with -h for copyright info
==29259== Command: ./carrinho
==29259==
10 itens (capacidade 16)
Total: R$ 72.73
==29259==
==29259== HEAP SUMMARY:
==29259==     in use at exit: 0 bytes in 0 blocks
==29259==   total heap usage: 5 allocs, 5 frees, 8,416 bytes allocated
==29259==
==29259== All heap blocks were freed -- no leaks are possible        « nenhum vazamento
==29259==
==29259== For lists of detected and suppressed errors, rerun with: -s
==29259== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

All heap blocks were freed -- no leaks are possible = "todos os blocos do heap foram liberados; vazamento impossível". E 0 errors: nenhum acesso fora do lugar. Os 5 allocs são as 3 vezes que a lista cresceu (4, 8 e 16 lugares) mais 2 blocos que o próprio scanf/printf reservam para a entrada e a saída (e que eles mesmos liberam no fim).

Agora, sem a linha do liberar (arquivo vaza.c):

$ valgrind --leak-check=full ./vaza < compras.txt
«… cabeçalho igual …»
10 itens (capacidade 16)
Total: R$ 72.73
==29331==
==29331== HEAP SUMMARY:
==29331==     in use at exit: 128 bytes in 1 blocks
==29331==   total heap usage: 5 allocs, 4 frees, 8,416 bytes allocated
==29331==
==29331== 128 bytes in 1 blocks are definitely lost in loss record 1 of 1
==29331==    at 0x485807F: realloc (vg_replace_malloc.c:1804)
==29331==    by 0x400125A: adicionar (vaza.c:7)
==29331==    by 0x4001359: main (vaza.c:33)
==29331==
==29331== LEAK SUMMARY:
==29331==    definitely lost: 128 bytes in 1 blocks
«…»
==29331== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

128 bytes in 1 blocks are definitely lost = "128 bytes num bloco estão definitivamente perdidos". São os 16 lugares × 8 bytes de cada double: o último caderno, que ninguém jogou fora. As linhas de baixo dizem onde ele nasceu: no realloc da linha 7, dentro de adicionar, chamado pela linha 33 do main. Os cadernos menores não aparecem: o próprio realloc liberou cada um deles na troca. Mais sobre isso em Vazamentos de Memória.


4. O jeito seguro de chamar realloc

Muita gente escreve assim, numa linha só:

precos = realloc(precos, nova * sizeof(double));   /* NÃO faça assim */

Funciona enquanto há memória. Mas quando o realloc falha, ele devolve NULL e não libera o bloco antigo (isso é de propósito, para você não perder os dados). Só que a linha acima guarda esse NULL em precos, por cima do único endereço que existia do bloco antigo. A lista continua lá na memória, mas ninguém mais sabe onde: não dá para usar nem para liberar.

Para ver acontecer, este programa pede, de propósito, espaço para um trilhão de preços (8 terabytes). O número vem da linha de comando (argv) só para o gcc não perceber o absurdo na hora de compilar:

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

int main(int argc, char *argv[]) {
    size_t capacidade = 4;
    double *precos = malloc(capacidade * sizeof(double));
    if (precos == NULL) {
        return 1;
    }
    precos[0] = 12.50;

    /* um pedido grande demais, de propósito, para o realloc falhar */
    capacidade = (argc > 1) ? strtoull(argv[1], NULL, 10) : 8;

    precos = realloc(precos, capacidade * sizeof(double));
    if (precos == NULL) {
        printf("O realloc falhou. E a lista antiga?\n");
        return 1;
    }
    printf("Primeiro item: R$ %.2f\n", precos[0]);
    free(precos);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra -g perdeu.c -o perdeu
$ ./perdeu
Primeiro item: R$ 12.50
$ valgrind --leak-check=full ./perdeu 1000000000000
«… cabeçalho …»
O realloc falhou. E a lista antiga?
==31796==
==31796== HEAP SUMMARY:
==31796==     in use at exit: 32 bytes in 1 blocks
==31796==   total heap usage: 3 allocs, 2 frees, 8,000,000,004,128 bytes allocated
==31796==
==31796== 32 bytes in 1 blocks are definitely lost in loss record 1 of 1
==31796==    at 0x4850858: malloc (vg_replace_malloc.c:447)
==31796==    by 0x4001213: main (perdeu.c:6)
«…»

Os 32 bytes (4 preços de 8 bytes) do malloc da linha 6 ficaram perdidos: o NULL apagou o endereço deles. A forma segura usa uma variável a mais, como todos os exemplos desta aula:

double *maior = realloc(precos, nova * sizeof(double));
if (maior == NULL) {
    /* precos ainda aponta para a lista antiga, inteira:
       dá para avisar, salvar, liberar ou continuar sem o item novo */
    free(precos);
    return 1;
}
precos = maior;          /* só troca quando deu certo */
capacidade = nova;
Depois do realloc, o endereço antigo morreu

Quando o realloc muda o bloco de lugar, o bloco antigo é liberado. Qualquer ponteiro que você guardou para dentro dele (por exemplo, double *primeiro = &precos[0];) passa a apontar para memória que não é mais sua. Depois de crescer, use sempre precos, e pegue endereços de dentro da lista de novo, se precisar. O cenário completo está em "Quando dá errado".


Formas de crescer: qual usar

EstratégiaNova capacidadeVantagemDesvantagem
De um em umcapacidade + 1Nunca sobra lugar vazio.Um realloc por item: lento com muitos itens.
De tantos em tantoscapacidade + 100Simples; bom quando você sabe mais ou menos o tamanho.Com muitos itens, volta a ficar lento (10 mil itens = 100 trocas).
Dobrarcapacidade * 2 (começando em 4, 8 ou 16)Pouquíssimas trocas, sempre. É a escolha padrão.Pode sobrar até metade da capacidade vazia.
Multiplicar por 1,5capacidade + capacidade / 2Desperdiça menos que dobrar; usado por algumas bibliotecas.Um pouco mais de trocas. Cuidado: começando em 1, 1 + 1/2 é 1 e a lista nunca cresce.

Qual usar: dobre. Se você já sabe quantos itens vêm (por exemplo, leu a quantidade antes), reserve tudo de uma vez com malloc e nem precisa crescer. E se a lista nunca passa de um tamanho pequeno e conhecido, um array comum (double precos[50];) continua sendo o mais simples.

Encolher no fim (opcional)

Se a lista terminou com 10 itens numa capacidade de 16 e vai ficar muito tempo na memória, dá para devolver a sobra com realloc(precos, tamanho * sizeof(double)), também com a variável auxiliar. Para listas pequenas, não vale o trabalho. Cuidado com tamanho igual a 0: realloc com tamanho zero tem comportamento que muda de biblioteca para biblioteca (no C23 passou a ser comportamento indefinido); nesse caso, use free.


Quando dá errado

Esqueceu o sizeof: "heap-buffer-overflow"
        double *maior = realloc(*precos, nova);   /* faltou * sizeof(double) */

O realloc conta em bytes. Sem o sizeof, a "capacidade 4" vira 4 bytes, metade de um único double. O gcc não avisa nada, e o programa escreve fora do bloco desde o primeiro item. O sanitizer de endereços (-fsanitize=address, veja Sanitizers) pega na hora:

$ gcc -std=c23 -Wall -Wextra -g -fsanitize=address semsizeof.c -o semsizeof
$ ./semsizeof < compras.txt
=================================================================
==30440==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x73853f7e0010 at pc 0x5e54d7d4c47e bp 0x7ffe2eaa3ea0 sp 0x7ffe2eaa3e90
WRITE of size 8 at 0x73853f7e0010 thread T0
    #0 0x5e54d7d4c47d in adicionar /home/ana/c/semsizeof.c:14
    #1 0x5e54d7d4c6e8 in main /home/ana/c/semsizeof.c:33
«…»
0x73853f7e0014 is located 0 bytes after 4-byte region [0x73853f7e0010,0x73853f7e0014)
allocated by thread T0 here:
    #0 0x77654092a6cf in realloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:81
    #1 0x5e54d7d4c399 in adicionar /home/ana/c/semsizeof.c:7
«…»

O que significa: heap-buffer-overflow = "estouro de um bloco do heap"; WRITE of size 8 = uma escrita de 8 bytes (um double) na linha 14 ((*precos)[*tamanho] = valor;), num bloco de só 4 bytes (4-byte region) criado pelo realloc da linha 7.

No editor do guia, esse programa roda calado e dá o total errado (R$ 69.48 em vez de R$ 72.73): os preços escritos fora do bloco atropelaram uns aos outros. Por isso erros assim são tão traiçoeiros.

Como resolver: sempre quantidade * sizeof(tipo). Uma forma que não deixa errar o tipo é nova * sizeof **precos (ou nova * sizeof *precos quando precos é um double * comum): o tamanho vem do próprio ponteiro.

Guardou um ponteiro de antes do realloc: "may be used after ‘realloc’"
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    size_t capacidade = 4;
    double *precos = malloc(capacidade * sizeof(double));
    if (precos == NULL) {
        return 1;
    }
    precos[0] = 12.50;

    double *primeiro = &precos[0];

    capacidade = capacidade * 2;
    double *maior = realloc(precos, capacidade * sizeof(double));
    if (maior == NULL) {
        free(precos);
        return 1;
    }
    precos = maior;

    printf("Primeiro item: R$ %.2f\n", *primeiro);
    free(precos);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra -g antigo.c -o antigo
antigo.c: In function ‘main’:
antigo.c:22:5: warning: pointer ‘primeiro’ may be used after ‘realloc’ [-Wuse-after-free]
   22 |     printf("Primeiro item: R$ %.2f\n", *primeiro);
      |     ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
antigo.c:15:21: note: call to ‘realloc’ here
   15 |     double *maior = realloc(precos, capacidade * sizeof(double));
      |                     ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
$ ./antigo
Primeiro item: R$ 12.50        « parece certo, mas foi sorte
$ valgrind ./antigo
«… cabeçalho …»
==29753== Invalid read of size 8
==29753==    at 0x4001243: main (antigo.c:22)
==29753==  Address 0x4aa6040 is 0 bytes inside a block of size 32 free'd
==29753==    at 0x485807F: realloc (vg_replace_malloc.c:1804)
==29753==    by 0x4001218: main (antigo.c:15)
==29753==  Block was alloc'd at
==29753==    at 0x4850858: malloc (vg_replace_malloc.c:447)
==29753==    by 0x40011CC: main (antigo.c:6)
==29753==
Primeiro item: R$ 12.50
«…»
==29753== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

O que significa: o gcc avisou: pointer ‘primeiro’ may be used after ‘realloc’ = "o ponteiro primeiro pode ser usado depois do realloc". O programa ainda mostrou o valor certo, por sorte. O valgrind confirma o problema: Invalid read ("leitura inválida") de um endereço dentro de um bloco de 32 bytes free'd ("liberado") pelo realloc da linha 15.

Como resolver: depois que a lista pode ter crescido, pegue o endereço de novo (&precos[0]) ou, melhor, guarde a posição (o índice 0) em vez do endereço. Índices continuam valendo depois do realloc; endereços, não.

Sem os parênteses em (*precos)[*tamanho]: "Falha de segmentação"
    *precos[*tamanho] = valor;        /* deveria ser (*precos)[*tamanho] */
$ gcc -std=c23 -Wall -Wextra -g parenteses.c -o parenteses
$ ./parenteses < compras.txt
Falha de segmentação     (imagem do núcleo gravada) ./parenteses < compras.txt

O que aconteceu: compilou sem nenhum aviso e morreu. O [] é mais forte que o *, então *precos[*tamanho] quer dizer *(precos[*tamanho]): trata precos (o endereço da variável do main) como se fosse um array de ponteiros e pega o "ponteiro" da posição tamanho. Na posição 0 dá certo por coincidência; na posição 1 já é lixo, e escrever lá derruba o programa. No editor do guia acontece o mesmo: Falha de segmentação (imagem do núcleo gravada).

Como resolver: (*precos)[*tamanho] = valor;. Primeiro siga o endereço até o ponteiro do main, depois escolha a posição. (Com uma struct, como você verá em Structs e Ponteiros, isso vira lista->dados[lista->tamanho], bem mais legível.)

A lista nunca cresce: capacidade * 2 começando em zero

O que aconteceu: com size_t nova = capacidade * 2; e a capacidade começando em 0, a nova capacidade é 0 * 2 = 0. O realloc recebe tamanho zero e o programa escreve num bloco que não tem lugar para nada.

Como resolver: trate o começo à parte: (capacidade == 0) ? 4 : capacidade * 2.

"definitely lost" no valgrind

O que aconteceu: faltou o free (ou o liberar) no fim, ou você usou precos = realloc(precos, …) e o realloc falhou.

Como resolver: todo array dinâmico termina com um free do ponteiro atual; e o realloc sempre vai para uma variável auxiliar.


Referência rápida

EscritaSignificado
double *dados = NULL; size_t tamanho = 0, capacidade = 0;Uma lista vazia, ainda sem memória.
if (tamanho == capacidade)Lotou: é hora de crescer.
size_t nova = (capacidade == 0) ? 4 : capacidade * 2;Começa com 4 lugares e depois dobra.
double *maior = realloc(dados, nova * sizeof(double));Pede um bloco maior (em bytes), copiando o conteúdo antigo.
if (maior == NULL) { … }Falhou: dados continua válido e inteiro.
dados = maior; capacidade = nova;Deu certo: troca para o bloco novo.
dados[tamanho] = valor; tamanho++;Guarda na primeira posição livre.
realloc(NULL, n)O mesmo que malloc(n).
free(dados); dados = NULL; tamanho = capacidade = 0;Libera e deixa a lista vazia de novo.
(*dados)[*tamanho]Dentro de uma função que recebeu double **dados: a posição livre da lista de fora.
valgrind --leak-check=full ./programaConfere vazamentos e acessos inválidos (compile com -g).

Exercícios

1. Lotou?

Complete a condição que diz que a lista está cheia e precisa crescer:

if () { /* crescer */ }

2. Dobrar

Complete a nova capacidade, que começa em 8 quando a lista está vazia e dobra nas outras vezes:

size_t nova = (capacidade == 0) ? ;

3. Em bytes

Complete o tamanho pedido ao realloc para uma lista de int com nova lugares:

int *maior = realloc(numeros, );

4. Uma lista começa com capacidade 4 e dobra quando lota. Quantas vezes o realloc é chamado para guardar 20 itens?

5. Por que dados = realloc(dados, n); é perigoso?

6. Depois que a lista cresceu com realloc, o que continua valendo com certeza?

Missão final: o carrinho da Ana

Missão: a função adicionar ainda não sabe crescer: quando a lista lota, ela desiste. Complete o bloco do if: calcule a nova capacidade (começando em 4 e dobrando), chame o realloc numa variável auxiliar, devolva false se ele falhar e atualize *precos e *capacidade se der certo. Os seis preços já estão na entrada; o resultado esperado é 6 itens (capacidade 8) e Total: R$ 44.84.

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

bool adicionar(double **precos, size_t *tamanho, size_t *capacidade, double valor) {
    if (*tamanho == *capacidade) {
        /* lotou: faça a lista crescer aqui */
        return false;
    }
    (*precos)[*tamanho] = valor;
    (*tamanho)++;
    return true;
}

void liberar(double **precos, size_t *tamanho, size_t *capacidade) {
    free(*precos);
    *precos = NULL;
    *tamanho = 0;
    *capacidade = 0;
}

int main(void) {
    double *precos = NULL;
    size_t tamanho = 0;
    size_t capacidade = 0;

    double preco;
    while (scanf("%lf", &preco) == 1) {
        if (!adicionar(&precos, &tamanho, &capacidade, preco)) {
            printf("Não coube o item de R$ %.2f\n", preco);
        }
    }

    double total = 0.0;
    for (size_t i = 0; i < tamanho; i++) {
        total += precos[i];
    }
    printf("%zu itens (capacidade %zu)\n", tamanho, capacidade);
    printf("Total: R$ %.2f\n", total);

    liberar(&precos, &tamanho, &capacidade);
    return 0;
}
#include <stdio.h>
#include <stdlib.h>

bool adicionar(double **precos, size_t *tamanho, size_t *capacidade, double valor) {
    if (*tamanho == *capacidade) {
        size_t nova = (*capacidade == 0) ? 4 : *capacidade * 2;
        double *maior = realloc(*precos, nova * sizeof(double));
        if (maior == NULL) {
            return false;
        }
        *precos = maior;
        *capacidade = nova;
    }
    (*precos)[*tamanho] = valor;
    (*tamanho)++;
    return true;
}

void liberar(double **precos, size_t *tamanho, size_t *capacidade) {
    free(*precos);
    *precos = NULL;
    *tamanho = 0;
    *capacidade = 0;
}

int main(void) {
    double *precos = NULL;
    size_t tamanho = 0;
    size_t capacidade = 0;

    double preco;
    while (scanf("%lf", &preco) == 1) {
        if (!adicionar(&precos, &tamanho, &capacidade, preco)) {
            printf("Não coube o item de R$ %.2f\n", preco);
        }
    }

    double total = 0.0;
    for (size_t i = 0; i < tamanho; i++) {
        total += precos[i];
    }
    printf("%zu itens (capacidade %zu)\n", tamanho, capacidade);
    printf("Total: R$ %.2f\n", total);

    liberar(&precos, &tamanho, &capacidade);
    return 0;
}

Resumo

  • Um array que cresce precisa de três coisas: o ponteiro para a memória, o tamanho (itens guardados) e a capacidade (itens que cabem).
  • Quando tamanho == capacidade, cresça com realloc, sempre em bytes: nova * sizeof(double).
  • Dobre a capacidade (começando em 4, por exemplo): 1000 itens custam 9 realocações em vez de 1000.
  • Guarde o resultado do realloc numa variável auxiliar e confira o NULL antes de trocar o ponteiro.
  • Depois do realloc, endereços antigos de dentro da lista morrem; índices continuam valendo.
  • Coloque a lógica em funções (adicionar, liberar) e termine sempre com free. Confira com valgrind --leak-check=full.
  • Com structs, as três variáveis viram uma só, e o código fica bem mais limpo.