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.
Acho que faltou uma barra antes do “n”, no printf final.
Boa dica!
Sim, faltou o ‘\’ antes do ‘n’ sim. Era para ser “\n”.
Usando ponteiro solto assim sem iniciar ?
core core core na certa ;)
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”. :-)
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.
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 porasprintf()
.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 elementoschar
. 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 parachar
.A chamada em questão,
asprintf()
, recebe um apontador para apontadores parachar
, similar àp
(ouchar **
), sendo que dentro dela, o valor do apontador parachar
, 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
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 ?
Cezeiro,
Exatamente isso!
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.
Oops. No exemplo acima, troque a linha “ponteiro2 = &x” acima por “ponteiro = &x”. O efeito é o mesmo, mas deve deixar o exemplo mais claro.
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!