C: Alocação automática de memória com GNU asprintf()

Palavras-chave: C, sprintf, snprintf, asprintf, formatando strings com tamanho variável

Inspirado pelo post do Kojima eu resolvi postar uma dica para um problema similar: normalmente precisamos formatar algum texto usando a função sprintf(), porém ela requer um buffer pré-alocado.

Bem, usar sprintf() é loucura, pois buffer-overflows podem acontecer. Usamos, então, snprintf() que limita o tamanho do resultado. Mas mesmo assim podemos ficar insatisfeitos, pois ele pode sair truncado.

Porém o pessoal do GNU criou a função asprintf() que calcula e aloca a memória necessária para que tudo caiba perfeitamente, incluindo o terminador final. O uso é bem simples:

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

int main(int argc, char *argv[]) {
   char *s;

   if (asprintf(&s, "argc=%d", argc) > -1) {
      printf("s=\"%s\"\\n", s);
      free(s);
   }

   return 0;
}

Lembre-se de definir _GNU_SOURCE, pois esta é uma extensão ao padrão ANSI C e POSIX. Segundo a man-page, também está disponível em sistemas BSD, vale a pena conferir.

This entry was posted in C. Bookmark the permalink.

12 Responses to C: Alocação automática de memória com GNU asprintf()

  1. Gustavo Chaves says:

    Acho que faltou uma barra antes do “n”, no printf final.

    Boa dica!

  2. Sim, faltou o ‘\’ antes do ‘n’ sim. Era para ser “\n”.

  3. cezeiro says:

    Usando ponteiro solto assim sem iniciar ?
    core core core na certa ;)

  4. Cezeiro,

    Sim, usando ponteiro solto assim sem iniciar, sem nenhum core.

    Veja que no código “s” não deve ser iniciado, pois a função asprintf() só vai atribuir “s” caso a chamada retorne sucesso. Segundo o manual, a versão do FreeBSD atribui s=NULL em caso de erro.

    É um consenso dentre os programadores mais experientes que não se deve inicializar todas as variáveis só por fazer, para evitar “cores”. Vide maiores explicações em listas na internet, na LKML (Linux Kernel Mailing List) isso é sempre mencionado. O principal motivo é evitar bugs… e viva o “valgrind”. :-)

  5. cezeiro says:

    Oi Gustavo,
    tudo bem que não tem sentido iniciar se eu não tenho certeza se será utilizada ou não. Todavia, se utilizada, ele não foi iniciada .. vai apontar pra onde ?

    Falo isso por experiência de anos no kernel space… ah não ser que no user space seja diferente isso ai vai dar core sim.

    Mas agora fiquei curioso, vou pesquisar nas fontes que vc citou.

  6. Olá, cezeiro,

    tudo bem que não tem sentido iniciar se eu não tenho certeza se será utilizada ou não. Todavia, se utilizada, ele não foi iniciada .. vai apontar pra onde ?

    Aponta para um lugar arbitrário provavelmente inválido. A questão é que onde o ponteiro aponta antes da chamada a asprintf() não importa, já que o ponteiro não é lido, mas apenas gravado por asprintf().

  7. Exatamente o que o Eduardo disse.

    Só para ficar claro:

    char *s: s é um apontador para uma região de memória que contém elementos char. O tipo aqui é usado apenas para definir o tamanho dos elementos usados em operações aritméticas de ponteiros ou indexação, que não deixa de ser uma aritmética de ponteiro.
    char **p (ou &s): p é um apontador para uma região de memória que contém apontadores para char.

    A chamada em questão, asprintf(), recebe um apontador para apontadores para char, similar à p (ou char **), sendo que dentro dela, o valor do apontador para char, similar à s (char *), será alterado.

    Um segmentation fault, ou core dump, só aconteceria se lêssemos da região apontada por s e essa fosse inválida.

    Às vezes a abstração que a linguagem C oferece não é das melhores, e nosso entendimento é prejudicado. Um conselho é sempre simplificar para uma linguagem mais simples/básica, como o assembly. Não precisa ser algo real, pode ser um pseudo-assembly, como:

    // leitura de s, ou conteúdo de p:
    //// s = p[0]
    //// s = *p
    leia s, p + 0

    // leitura do texto (primeiro char), ou conteúdo de s:
    //// c = p[0][0]
    //// c = (*p)[0]
    //// c = *(*p)
    //// c = *s
    //// c = s[0]
    leia s, p + 0
    leia t, s + 0

    // escrita de s, ou conteúdo de p:
    //// p[0] = valor-de-s
    //// *p = valor-de-s
    escreva p + 0, valor-de-s

    // escrita do texto (primeiro char), ou conteúdo de s:
    //// p[0][0] = valor-de-c
    //// (*p)[0] = valor-de-c
    //// *(*p) = valor-de-c
    //// *s = valor-de-c
    //// s[0] = valor-de-c
    leia s, p + 0
    escreva s + 0, valor-de-c

  8. cezeiro says:

    Olá Eduardo Habkost,
    então não tem problema gravar num ponteiro que aponta num lugar arbitrário e inválido ? Apenas o que dá problema(core) é ler de um lugar arbitrário e inválido ?

  9. Cezeiro,

    Exatamente isso!

  10. Olá, cezeiro,

    Se eu entendi o que você disse, é isso. Mas detalhando melhor: a diferença não é exatamente entre ler e gravar. O problema é quando gravamos ou lemos na área apontada por um ponteiro que aponta para um lugar arbritário ou inválido. Quando escrevenos no ponteiro (não na área para onde ele aponta), não importa para onde ele está apontando.

    Ler o ponteiro inválido (não a área para onde ele aponta) também não é exatamente o problema, mas faz pouco sentido ler o valor se você não vai usar para ler ou gravar a área apontada, e você sempre precisa “ler” o ponteiro se quiser ler ou gravar na área apontada. Por exemplo, se o asprintf() lesse o valor do ponteiro, o erro não seria quando ele fizesse isso, mas o problema seria se ele tentasse usar o valor lido do ponteiro para acessar (ler ou gravar) a área apontada.

    Tentando exemplificar a diferença entre as duas coisas:

    int *ponteiro = (int*)12345; /* lugar arbitrário e (provavelmente) inválido */
    int *ponteiro2;
    int x;
    *ponteiro = 10; /* isso não funciona (A) */
    x = *ponteiro; /* isso também não funciona (B) */
    ponteiro2 = ponteiro; /* mas isso funciona (porém o ponteiro2 vai continuar apontando para um lugar inválido, o que não é muito útil) (C) */
    ponteiro2 = &x; /* e isso também funciona (D) */

    A questão é que asprintf() faz com o ponteiro ‘a’ do exemplo, o equivalente ao que fizemos na linha D, e não o que fizemos na linha A.

  11. Oops. No exemplo acima, troque a linha “ponteiro2 = &x” acima por “ponteiro = &x”. O efeito é o mesmo, mas deve deixar o exemplo mais claro.

  12. Marcus says:

    Nossa, quanta explicação complicada sobre ponteiros.
    A explicação podia ser mais simples: O ‘s’ não foi inicializado porque quem vai inicializá-lo é o próprio asprintf!

Leave a Reply

Your email address will not be published. Required fields are marked *