E/S formatada em detalhe
Nesta aula, exploramos em profundidade a entrada e saída formatada em C, cobrindo printf com largura, precisão e flags, os perigos do scanf, e as funções sprintf/snprintf e sscanf para manipulação de strings. Aprenda a controlar a formatação com precisão e a evitar armadilhas comuns de segurança e buffer overflow.
Nesta aula, vamos mergulhar nos detalhes da entrada e saída formatada em C, uma das ferramentas mais poderosas e, ao mesmo tempo, perigosas da linguagem. Dominar printf, scanf e suas variantes é essencial para escrever código robusto, legível e seguro. Vamos explorar desde os modificadores mais sutis de printf até as armadilhas de scanf, além de aprender a usar sprintf, snprintf e sscanf para trabalhar com strings formatadas.
Ao final desta aula, você será capaz de formatar saídas com precisão milimétrica, ler entradas de forma controlada e evitar os erros clássicos que levam a vulnerabilidades e bugs. Prepare-se para uma aula densa, mas recompensadora.
printf avançado (largura, precisão, flags)
O printf é uma função versátil, mas muitos programadores usam apenas o básico: printf("%d", x). No entanto, o printf oferece um controle fino sobre a formatação através de especificadores de conversão com modificadores opcionais. A sintaxe completa é: %[flags][largura][.precisão][tamanho]tipo. Vamos detalhar cada parte.
As flags alteram o alinhamento e o preenchimento. A flag - alinha à esquerda (padrão é à direita). A flag 0 preenche com zeros à esquerda (para números). A flag + mostra o sinal para números positivos. A flag espaço insere um espaço antes de números positivos. A flag # altera a representação (por exemplo, prefixo 0x para hexadecimais).
A largura é um número mínimo de caracteres a serem impressos. Se o valor for menor, ele é preenchido com espaços (ou zeros, se a flag 0 estiver presente). A precisão é um ponto seguido de um número; para inteiros, define o número mínimo de dígitos (preenchendo com zeros), e para floats, define o número de casas decimais. Para strings, define o número máximo de caracteres a serem impressos.
Vejamos um exemplo prático:
#include <stdio.h>
int main() {
int num = 42;
double pi = 3.14159;
char nome[] = "Maria";
printf("[%d]\n", num);
printf("[%5d]\n", num);
printf("[%-5d]\n", num);
printf("[%05d]\n", num);
printf("[%+d]\n", num);
printf("[% .2f]\n", pi);
printf("[%.3f]\n", pi);
printf("[%10.3f]\n", pi);
printf("[%-10.3f]\n", pi);
printf("[%.3s]\n", nome);
return 0;
}
Saída:
[42]
[ 42]
[42 ]
[00042]
[+42]
[ 3.14]
[3.142]
[ 3.142]
[3.142 ]
[Mar]
Observe como a largura e a precisão afetam a saída. A flag 0 preenche com zeros, mas não funciona com alinhamento à esquerda. A precisão em floats arredonda o valor, não apenas trunca. Para strings, a precisão limita o número de caracteres impressos.
Além disso, é possível usar * para especificar largura e precisão dinamicamente, passando os valores como argumentos:
printf("%*.*f\n", 10, 2, pi); // largura 10, precisão 2
Isso é útil quando os valores são calculados em tempo de execução.
scanf e seus perigos
scanf é a função padrão para leitura formatada do stdin, mas é notoriamente perigosa se usada sem cuidado. O principal problema é que scanf não verifica o tamanho do buffer ao ler strings, podendo causar estouro de buffer. Além disso, a entrada pode conter caracteres inesperados que quebram a leitura.
Vamos ver um exemplo clássico de vulnerabilidade:
char nome[10];
scanf("%s", nome); // perigoso! se o usuário digitar mais que 9 caracteres, estoura
Para evitar isso, devemos limitar o tamanho no especificador, por exemplo, %9s (lê no máximo 9 caracteres, deixando espaço para o '\0'). Outra dica é usar %[^\n] para ler até a nova linha, mas ainda precisamos limitar o tamanho.
Outro problema é que scanf deixa o caractere de nova linha no buffer, o que pode atrapalhar leituras subsequentes. Por exemplo, após um scanf("%d"), o '\n' permanece e um próximo scanf("%c") pode ler esse caractere indesejado. Para resolver, usamos scanf(" %c") (espaço antes de %c) para pular espaços em branco, ou limpamos o buffer manualmente.
Além disso, scanf não valida se a conversão foi bem-sucedida. Devemos sempre verificar o retorno de scanf, que é o número de itens atribuídos com sucesso. Se não for o esperado, a entrada pode estar incorreta.
Exemplo seguro de leitura de um inteiro e uma string:
#include <stdio.h>
int main() {
int idade;
char nome[50];
printf("Digite sua idade: ");
if (scanf("%d", &idade) != 1) {
printf("Entrada inválida.\n");
return 1;
}
printf("Digite seu nome: ");
scanf(" %49s", nome); // espaço para pular quebras de linha, limite 49 chars
printf("Nome: %s, Idade: %d\n", nome, idade);
return 0;
}
Perceba o espaço antes de %49s: ele faz o scanf ignorar qualquer espaço em branco (incluindo a nova linha) antes de ler a string. E o limite de 49 garante que não haja estouro de buffer.
Recomendações: evite scanf para leitura de strings em programas críticos; prefira fgets. Se precisar usar scanf, sempre limite o tamanho e verifique o retorno.
sprintf/snprintf
sprintf e snprintf são funções que formatam dados em uma string (buffer), em vez de escrever na saída padrão. Elas são muito úteis para construir mensagens, gerar nomes de arquivos, etc. A diferença entre elas é que snprintf recebe o tamanho máximo do buffer, evitando estouro.
A assinatura de sprintf é: int sprintf(char *str, const char *format, ...). Ela escreve a string formatada em str e retorna o número de caracteres escritos (sem contar o '\0'). Se o buffer for muito pequeno, sprintf causará estouro de buffer, um risco de segurança grave. Por isso, sempre que possível, use snprintf.
snprintf é semelhante, mas recebe um parâmetro adicional: int snprintf(char *str, size_t size, const char *format, ...). Ela escreve no máximo size-1 caracteres, garantindo que a string seja terminada com '\0'. Retorna o número de caracteres que seriam escritos se o buffer fosse grande o suficiente, o que é útil para saber o tamanho necessário.
Exemplo de uso:
#include <stdio.h>
int main() {
char buffer[64];
int dia = 15, mes = 7, ano = 2025;
// sprintf (perigoso se buffer pequeno)
sprintf(buffer, "%02d/%02d/%04d", dia, mes, ano);
printf("Data: %s\n", buffer);
// snprintf (seguro)
char outro[16];
int n = snprintf(outro, sizeof(outro), "%02d/%02d/%04d", dia, mes, ano);
if (n >= sizeof(outro)) {
printf("Buffer pequeno demais!\n");
}
printf("Outro: %s\n", outro);
return 0;
}
No exemplo, snprintf escreve no máximo 15 caracteres mais o '\0'. Se o resultado couber, tudo certo. Se não couber, a string é truncada e o retorno indica o tamanho necessário. Isso permite detectar e tratar o erro.
É importante lembrar que sprintf não verifica o tamanho do buffer, então é uma fonte comum de vulnerabilidades. Sempre prefira snprintf.
sscanf
sscanf funciona como scanf, mas lê de uma string em vez do stdin. É útil para extrair dados de uma string formatada. A assinatura é: int sscanf(const char *str, const char *format, ...). Ela percorre a string str e tenta converter os dados conforme o formato, atribuindo aos ponteiros fornecidos. Retorna o número de conversões bem-sucedidas.
Assim como scanf, sscanf tem os mesmos problemas de validação e buffer, mas como a entrada é uma string, o risco de estouro de buffer é menor (a string já está em memória). No entanto, é preciso verificar o retorno para saber se a conversão foi completa.
Exemplo:
#include <stdio.h>
int main() {
char dados[] = "João 25 1.75";
char nome[50];
int idade;
float altura;
int itens = sscanf(dados, "%49s %d %f", nome, &idade, &altura);
if (itens == 3) {
printf("Nome: %s, Idade: %d, Altura: %.2f\n", nome, idade, altura);
} else {
printf("Falha na conversão (%d itens lidos).\n", itens);
}
return 0;
}
Saída: Nome: João, Idade: 25, Altura: 1.75
sscanf é especialmente útil para parsear linhas de arquivos, configurações, etc. Podemos usar especificadores como %[^,] para ler até uma vírgula, ou %*d para pular um campo (o asterisco suprime a atribuição).
Exemplo com supressão e leitura até vírgula:
char linha[] = "123,456,789";
int a, b, c;
sscanf(linha, "%d,%d,%d", &a, &b, &c); // a=123, b=456, c=789
// Pular o primeiro número:
int x, y;
sscanf(linha, "%*d,%d,%d", &x, &y); // x=456, y=789
Lembre-se de sempre verificar o retorno de sscanf para garantir que todos os campos foram lidos corretamente.
Boas práticas e observações finais
Ao trabalhar com E/S formatada, siga estas boas práticas:
- Prefira snprintf a sprintf sempre que possível, para evitar estouro de buffer.
- Limite o tamanho em scanf para strings, usando especificadores como %49s.
- Verifique o retorno de scanf, sscanf e snprintf para tratar erros.
- Use fgets em vez de scanf para ler strings do teclado, pois é mais seguro e lê linhas inteiras.
- Evite scanf para entrada interativa; prefira fgets + sscanf para maior controle.
- Cuidado com a precisão em floats: ela arredonda, não trunca.
Dominar essas funções é fundamental para programação em C, seja para sistemas embarcados, desenvolvimento de sistemas ou qualquer aplicação que exija controle fino de entrada e saída.
Referências
- cppreference: fprintf
- cppreference: fscanf
- man7: sprintf/snprintf
- man7: sscanf
- GNU C Library: Formatted Output
- GNU C Library: Formatted Input
Exercícios
-
Escreva um programa que imprima um número inteiro com largura mínima de 8, preenchido com zeros à esquerda, e um float com 3 casas decimais e largura mínima de 10, alinhado à esquerda.
✓ Resposta:#include <stdio.h> int main() { int num = 42; float pi = 3.14159; printf("%08d\n", num); printf("%-10.3f\n", pi); return 0; } -
Explique por que o código abaixo é perigoso e como corrigi-lo:
char nome[5]; scanf("%s", nome);✓ Resposta:O código é perigoso porque não limita o número de caracteres lidos, podendo causar estouro de buffer. Para corrigir, use
scanf("%4s", nome)(lê no máximo 4 caracteres) ou prefirafgets(nome, sizeof(nome), stdin). -
Escreva um programa que use snprintf para formatar uma string com o nome e a idade de uma pessoa, e depois imprima a string. Considere que o nome pode ter até 30 caracteres e a idade é um inteiro.
✓ Resposta:#include <stdio.h> int main() { char nome[31] = "João"; int idade = 25; char buffer[64]; snprintf(buffer, sizeof(buffer), "Nome: %s, Idade: %d", nome, idade); printf("%s\n", buffer); return 0; } -
Use sscanf para extrair o dia, mês e ano de uma string no formato "dd/mm/aaaa" e imprima-os separadamente.
✓ Resposta:#include <stdio.h> int main() { char data[] = "15/07/2025"; int dia, mes, ano; if (sscanf(data, "%d/%d/%d", &dia, &mes, &ano) == 3) { printf("Dia: %d, Mês: %d, Ano: %d\n", dia, mes, ano); } else { printf("Formato inválido.\n"); } return 0; } -
Qual é a diferença entre sprintf e snprintf? Por que snprintf é mais segura?
✓ Resposta:sprintf escreve a string formatada no buffer sem verificar o tamanho, podendo causar estouro de buffer. snprintf recebe o tamanho máximo do buffer e escreve no máximo size-1 caracteres, garantindo que a string termine com '\0'. Portanto, snprintf é mais segura porque previne estouro de buffer.