E/S formatada em detalhe
Nesta aula, você vai dominar a entrada e saída formatada em C, indo além do básico. Vamos explorar os modificadores avançados do printf, os perigos do scanf, e as funções seguras sprintf/snprintf e sscanf, com exemplos práticos e exercícios para fixar o conteúdo.
Nesta aula, vamos mergulhar fundo na entrada e saída formatada em C, uma das áreas mais importantes para qualquer programador que deseja criar programas robustos e eficientes. Dominar printf e scanf vai além de simplesmente exibir ou ler valores: envolve controle preciso de formatação, tratamento de erros e segurança. Você aprenderá a usar modificadores avançados, evitar armadilhas comuns e aplicar funções como sprintf, snprintf e sscanf para manipular strings formatadas de maneira segura.
Vamos começar explorando o printf em detalhes, depois veremos os perigos do scanf e, em seguida, as variantes que trabalham com strings. Ao final, você terá uma base sólida para escrever código de entrada e saída profissional, com exemplos prontos para usar e exercícios para testar seus conhecimentos.
printf avançado (largura, precisão, flags)
O printf é a função padrão de saída formatada em C. Sua sintaxe básica é printf("formato", argumentos), onde o formato pode conter texto literal e especificadores de conversão que começam com %. Mas o que muitos não sabem é que entre o % e a letra de conversão há espaço para uma série de modificadores que controlam a largura mínima, a precisão e flags de alinhamento, sinal, zeros à esquerda, entre outros.
Vamos ver cada um desses modificadores com exemplos práticos. A sintaxe completa de um especificador de conversão é: %[flags][largura][.precisão][comprimento]tipo. Vamos focar nos três primeiros, que são os mais comuns.
Flags: podem ser:
-: alinha à esquerda (padrão é à direita).+: exibe o sinal para valores positivos e negativos.0: preenche com zeros à esquerda (para números).espaço: deixa um espaço em branco para números positivos.#: usa forma alternativa (por exemplo, 0x para hexadecimais).
Largura: número mínimo de caracteres a serem impressos. Se o valor tiver menos caracteres, ele é preenchido com espaços (ou zeros, com a flag 0).
Precisão: para inteiros, é o número mínimo de dígitos; para ponto flutuante, é o número de casas decimais; para strings, é o número máximo de caracteres a imprimir.
Vamos a um exemplo completo:
#include <stdio.h>
int main() {
int num = 42;
double pi = 3.14159;
char texto[] = "Olá";
printf("[%d]\n", num); // [42]
printf("[%5d]\n", num); // [ 42] (largura mínima 5)
printf("[%-5d]\n", num); // [42 ] (alinhado à esquerda)
printf("[%05d]\n", num); // [00042] (zeros à esquerda)
printf("[%+d]\n", num); // [+42] (sinal sempre)
printf("[% d]\n", num); // [ 42] (espaço para positivos)
printf("[%.2f]\n", pi); // [3.14] (2 casas decimais)
printf("[%8.2f]\n", pi); // [ 3.14] (largura 8, 2 decimais)
printf("[%08.2f]\n", pi); // [00003.14] (zeros à esquerda)
printf("[%.3s]\n", texto); // [Olá] (máximo 3 caracteres)
printf("[%10.3s]\n", texto); // [ Olá] (largura 10, máx 3)
return 0;
}
No exemplo acima, observe como a largura e a precisão se combinam. Para inteiros, a precisão indica o número mínimo de dígitos (preenchendo com zeros se necessário), e para strings, o número máximo de caracteres a serem exibidos. Já para ponto flutuante, a precisão controla as casas decimais.
Existem também os modificadores de comprimento, como l para long, ll para long long, h para short, e z para size_t. Por exemplo, para imprimir um long long, use %lld. E para size_t, use %zu.
scanf e seus perigos
O scanf é a função padrão de entrada formatada em C. Sua sintaxe é scanf("formato", &variavel) — note o & para passar o endereço da variável. Ela lê dados da entrada padrão (geralmente o teclado) e os armazena nas variáveis fornecidas.
No entanto, o scanf é notório por seus perigos. O principal é que ele não verifica os limites do buffer ao ler strings com %s, podendo causar estouro de buffer e vulnerabilidades de segurança. Veja um exemplo clássico:
#include <stdio.h>
int main() {
char nome[10];
printf("Digite seu nome: ");
scanf("%s", nome); // PERIGO: se o usuário digitar mais de 9 caracteres, estoura o buffer
printf("Olá, %s!\n", nome);
return 0;
}
Nesse código, se o usuário digitar um nome com mais de 9 caracteres, o scanf escreverá além do array, causando comportamento indefinido. Para mitigar isso, podemos usar a largura máxima com %9s (deixa espaço para o terminador nulo):
scanf("%9s", nome); // lê no máximo 9 caracteres
Outro perigo é a leitura de números: se o usuário digitar algo que não seja um número, o scanf falha e deixa o caractere inválido no buffer de entrada, o que pode causar loops infinitos em programas interativos. Por exemplo:
int idade;
printf("Digite sua idade: ");
scanf("%d", &idade); // se o usuário digitar "abc", o scanf retorna 0 e 'a' permanece no buffer
Além disso, o scanf não lida bem com entradas que contêm espaços em strings, pois %s para no primeiro espaço em branco. Para ler uma linha inteira, é melhor usar fgets e depois sscanf para interpretar.
Para usar scanf com segurança, siga estas recomendações:
- Sempre verifique o retorno do scanf (número de itens lidos com sucesso).
- Use largura máxima para strings, como
%19sem um buffer de 20. - Evite ler strings com scanf; prefira
fgets. - Se precisar ler números, considere ler a linha com
fgetse depois usarsscanfpara extrair os valores.
Vamos ver um exemplo mais seguro usando fgets e sscanf (que veremos em detalhes adiante):
#include <stdio.h>
#include <string.h>
int main() {
char linha[100];
int idade;
printf("Digite sua idade: ");
if (fgets(linha, sizeof(linha), stdin) != NULL) {
if (sscanf(linha, "%d", &idade) == 1) {
printf("Você tem %d anos.\n", idade);
} else {
printf("Entrada inválida.\n");
}
}
return 0;
}
Esse padrão é muito mais robusto, pois lê a linha inteira e depois tenta interpretá-la, sem deixar lixo no buffer.
sprintf/snprintf
As funções sprintf e snprintf são semelhantes ao printf, mas em vez de escrever na saída padrão, elas escrevem em uma string (buffer). A sintaxe é:
int sprintf(char *str, const char *format, ...);
int snprintf(char *str, size_t size, const char *format, ...);
A diferença crucial é que snprintf recebe o tamanho do buffer e garante que não escreva além dele, prevenindo estouro de buffer. O retorno de ambas é o número de caracteres que seriam escritos (sem contar o terminador nulo), o que é útil para detectar truncamento.
Vamos ver um exemplo:
#include <stdio.h>
int main() {
char buffer[50];
int idade = 30;
double altura = 1.75;
// sprintf sem segurança
sprintf(buffer, "Idade: %d, Altura: %.2f", idade, altura);
printf("%s\n", buffer);
// snprintf com limite de tamanho
int n = snprintf(buffer, sizeof(buffer), "Idade: %d, Altura: %.2f, Sobrenome: %s", idade, altura, "Silva");
if (n >= sizeof(buffer)) {
printf("A string foi truncada!\n");
}
printf("%s\n", buffer);
return 0;
}
No exemplo acima, o snprintf limita a escrita a 49 caracteres mais o terminador nulo. Se o conteúdo exceder, o buffer conterá os primeiros 49 caracteres e o retorno indicará o tamanho total que seria necessário. Isso é essencial para evitar estouro de buffer.
Um uso comum de snprintf é construir strings dinamicamente, como em mensagens de log ou formatação de dados. Por exemplo, para criar um nome de arquivo com um número:
#include <stdio.h>
int main() {
char filename[64];
for (int i = 0; i < 10; i++) {
snprintf(filename, sizeof(filename), "dados_%02d.txt", i);
printf("%s\n", filename);
}
return 0;
}
Isso gera arquivos como dados_00.txt, dados_01.txt, etc., com zero à esquerda graças ao modificador %02d.
sscanf
A função sscanf é a versão do scanf que lê de uma string em vez da entrada padrão. Sua sintaxe é:
int sscanf(const char *str, const char *format, ...);
Ela é extremamente útil para interpretar dados estruturados em strings, como linhas de arquivo, entradas do usuário ou protocolos de comunicação. O retorno é o número de itens atribuídos com sucesso.
Vamos ver um exemplo de parsing de uma data no formato "dd/mm/aaaa":
#include <stdio.h>
int main() {
char data[] = "25/12/2024";
int dia, mes, ano;
int lidos = sscanf(data, "%d/%d/%d", &dia, &mes, &ano);
if (lidos == 3) {
printf("Dia: %d, Mês: %d, Ano: %d\n", dia, mes, ano);
} else {
printf("Formato inválido.\n");
}
return 0;
}
Um ponto importante é que sscanf pode ser usado com %s para extrair palavras, mas assim como scanf, ele para em espaços. Para ler até o fim da string ou até um delimitador, podemos usar %[^
] para ler até o fim da linha, ou %[^,] para ler até uma vírgula, por exemplo.
Um exemplo de extração de campos separados por vírgula (CSV):
#include <stdio.h>
int main() {
char linha[] = "João,30,Engenheiro";
char nome[50];
int idade;
char profissao[50];
int lidos = sscanf(linha, "%[^,],%d,%[^\n]", nome, &idade, profissao);
if (lidos == 3) {
printf("Nome: %s, Idade: %d, Profissão: %s\n", nome, idade, profissao);
} else {
printf("Erro ao ler a linha.\n");
}
return 0;
}
Aqui, %[^,] lê todos os caracteres até uma vírgula, e %[^\n] lê até o fim da linha. Isso permite extrair campos com espaços, como o nome "João".
Outro uso comum é verificar se uma string é um número válido, tentando ler com %d e verificando o retorno:
#include <stdio.h>
int main() {
char entrada[] = "123abc";
int numero;
if (sscanf(entrada, "%d", &numero) == 1) {
printf("Número válido: %d\n", numero);
} else {
printf("Não é um número.\n");
}
return 0;
}
Nesse caso, sscanf lê 123 e retorna 1, ignorando o restante "abc". Isso pode ser útil ou perigoso, dependendo do contexto.
Boas práticas e observações finais
Ao trabalhar com entrada e saída formatada, siga estas boas práticas:
- Prefira
snprintfasprintfsempre que possível, para evitar estouro de buffer. - Evite usar
scanfpara ler strings; usefgetse depoissscanf. - Sempre verifique o retorno de
scanfesscanfpara garantir que a leitura foi bem-sucedida. - Use largura máxima em
scanfquando realmente precisar usar%s. - Para formatar saídas, explore os flags e precisões para obter o layout desejado.
Essas técnicas são fundamentais para escrever código robusto, seguro e profissional em C. Pratique com os exercícios abaixo para consolidar o que aprendeu.
Referências
- cppreference: fprintf
- cppreference: fscanf
- cppreference: snprintf
- cppreference: sscanf
- man7: printf(3)
- man7: scanf(3)
Exercícios
- Escreva um programa que use printf para exibir um número inteiro com largura mínima de 10, preenchido com zeros à esquerda e com sinal. Teste com valores positivos e negativos.
#include <stdio.h>
int main() {
int pos = 42;
int neg = -42;
printf("%+010d\n", pos); // +000000042
printf("%+010d\n", neg); // -000000042
return 0;
}
char linha[100];
char nome[50];
fgets(linha, sizeof(linha), stdin);
sscanf(linha, "%49s", nome); // limita a 49 caracteres
#include <stdio.h>
int main() {
char buffer[20];
int n = snprintf(buffer, sizeof(buffer), "Nome: %s, Idade: %d", "Maria", 25);
if (n >= sizeof(buffer)) {
printf("Truncado!\n");
}
printf("%s\n", buffer); // "Nome: Maria, Idade:" (truncado)
return 0;
}
#include <stdio.h>
int main() {
char linha[] = "João,30,Engenheiro";
char nome[50];
int idade;
char profissao[50];
sscanf(linha, "%[^,],%d,%[^\n]", nome, &idade, profissao);
printf("Nome: %s, Idade: %d, Profissão: %s\n", nome, idade, profissao);
return 0;
}
#include <stdio.h>
int main() {
char linha[100];
int numero;
char resto[100];
printf("Digite um número e depois qualquer coisa: ");
fgets(linha, sizeof(linha), stdin);
if (sscanf(linha, "%d %99s", &numero, resto) == 2) {
printf("Número: %d\n", numero);
} else {
printf("Entrada inválida.\n");
}
return 0;
}