Função de impressão em uma string com alocação automática - Programação - Mente Binária
Função de impressão em uma string com alocação automática
Por fredericopissarra, Maio 29, 2018 em Programação
O primeiro programinha que alguém aprende, em C, é o famoso hello world, usando a função printf(). O "correto" seria usar puts(), já que a string não tem "marcas de formatação" e, por isso, a última função tende a ser mais rápida... No entanto, você pode querer "imprimir" uma string formatada em outra string, ao invés do stream stdout (que é o padrão de printf)... Para isso existem as funções sprintf e vsprintf. O problema é que elas não se importam com o tamanho do buffer onde a string será colocada...
Suponha que você faça isso:
sprintf(s, "Valor = %d", n );
Se o valor contido em n (que, no contexto acime, é do tipo int) seja -2000000000 (perfeitamente representável num int). Neste caso a string final terá 20 bytes de tamanho.
s: { 'V', 'a', 'l', 'o', 'r', ' ', '=', ' ', '-', '2', '0', '0', '0', '0', '0', '0', '0', '0', '0','\0' }
Mas o buffer s só tem 16! O resultado é um buffer overflow, que pode ter consequências desastrosas na pilha, provavelmente causando um segmentation fault.
A resposta a esse problema, é claro, é usar um buffer maior. Aqui isso é simples de prever, já que um int pode assumir valores entre -2147483648 e 2147483647, ou seja, ocupar até 11 bytes na string. Adicionando os bytes restantes (8 bytes de "Valor = " e 1 byte extra para o NUL char no final da string) podemos usar um buffer de 20 bytes sem que o buffer overflow ocorra. Mas, e no caso de uma string que recebe um ponteiro para outra string como argumento?
extern char *varname;
extern int varvalue;
sprintf(s, "Valor de '%s' = %d", varname, varvalue);
Dependendo do conteúdo de varname, não há como prever o tamanho mínimo do buffer s.
Alguns tendem a usar um buffer grande para isso. Por exemplo, definindo o buffer s como um array de 8 KiB. Mas, mesmo assim, lembre-se que um buffer pode ter, em teoria, 256 TiB de tamanho, na arquitetura x86-64! E, acho meio exagerado usar um "bufferzão" para, na maioria das vezes, lidar com pequenas strings!
Nesse caso, a única solução é alocar o espaço do buffer dinamicamente, de acordo com o tamanho final da string...
A função asprintf, que não existe nas implementações de compiladores para WINDOWS, faz justamente isso (mais abaixo, explico como escrevê-la!)... O primeiro argumento, ao invés do ponteiro para o buffer, é um ponteiro para um ponteiro. Por quê? asprintf alocará um buffer para nós e devolverá o ponteiro desse buffer através desse primeiro argumento. Eis um exemplo de uso:
// Precisa desse símbolo para adicionar asprintf e vasprintf...
// Se asprintf() retornar -1, deu erro!
if ((size = asprintf(&s, "Valor de '%s' = %d", varname, varvalue)) != -1)
// ... usa s aqui ...
free(s);  // libera o espaço alocado por asprintf.
Note que, assim como todas as variantes de printf, asprintf retorna a quantidade de bytes "impressos" ou -1, em caso de erro. O erro aqui é, provavelmente, o de alocação de memória. Aliás, o GCC emitirá um aviso se nenhuma verificação for feita com relação ao retorno de asprintf ou vasprintf. Aviso: A não ser por asprintf e sua irmã, vasprintf, as demais funções derivadas de printf devolvem valores negativos em caso de erros.
Repare, também, que é necessário liberar, via free, o bloco alocado por asprintf, da mesma forma que você faria se tivesse alocado com malloc ou derivados...
Mas, e quanto ao WINDOWS? A libc do GCC para Windows ou a MSVCRT#.DLL não possui tal função disponível... Felizmente a função snprintf nos avuda um bocado... Essas funções funcionam como sprintf, mas só preenchem n bytes do buffer (incluindo o '\0' final). A descrição ISO da função nos diz:
...if the output was truncated due to this limit then the return value is the number of characters (excluding the terminating null byte) which would have been written to the final string if enough space had been available....
O "...which would have been writen to the final string if..." na frase acima faz toda a diferença... Significa que, mesmo que a string final não caiba no buffer, snprintf retornará a quantidade de chars que seria impressa! Isso nos permite criar a nossa própria função asprintf, assim:
// Se quiser, adicione o atributo format(printf,2,3) para a função, no GCC */
int asprintf(char **s, char *fmt, ...)
/* Tenta pegar o tamanho da string final. */
size = vsnprintf(tmp, 2, fmt, argp);
/* Aloca o espaço para a string final. */
if (!(*s = (char *)malloc(size+1)))
/* Finalmente, cria a string final. */
vsnprintf(*s, size+1, fmt, argp);
A função acima se comporta exatamente igual ao asprintf que consta da glibc...
Quanto ao "número de caracteres", em charsets multibyte (como o UTF-8, por exemplo) as funções printf devolvem a quantidade de bytes... 'ç', por exemplo, em UTF-8, tem 2 bytes ("\xc3\a7")... No caso de charsets com tamanho de elementos maiores (UTF-16 e UTF-32, por exemplo) existem as variações para wchar_t, mas a variante wasprintf não existe e o contexto de swprintf não é o mesmo que o de snprintf...
Um outro ponto de cuidado: As funções printf e derivadas devolvem sempre um int, nunca um ssize_t (já que podem ser valores negativos). Assim, os buffers são limitados ao tamanho máximo de 2 GiB - 1 byte, mesmo no modo x86-64.