Entrada e Saída Formatada: printf e scanf a Fundo

[369] Entrada e Saída Formatada: printf e scanf a Fundo

Um double se lê com %lf e se imprime com %f — assimetria histórica, sem lógica nenhuma, e que continua derrubando gente. Junto dela vêm a anatomia completa do especificador, o descasamento de tipo que o printf não verifica, o & que o scanf exige, e o motivo de fgets ser a escolha segura na entrada real.
Linguagem C

11 min de leitura

Você usa printf desde a primeira aula, quase por instinto. Mas há muito mais nessa função do que %d e \n — e seu par de entrada, scanf, esconde armadilhas que derrubam iniciantes todos os dias. Agora que dominamos ponteiros e tipos, é o momento certo de olhar essas duas funções de frente e entender de verdade como o C conversa com o mundo. Esta aula abre a Fase 4, e o domínio de entrada e saída que ganhamos aqui será a base para lermos e gravarmos arquivos nas próximas.

Anatomia de um especificador de formato

Tudo em printf gira em torno dos especificadores de formato — aqueles códigos que começam com %. Você já conhece os principais, mas eles têm uma estrutura completa que vale conhecer. Um especificador pode ter, entre o % e a letra final, vários modificadores opcionais:

%[flags][largura][.precisão][comprimento]conversão

Vamos por partes, com exemplos concretos. A conversão (a letra final) diz o tipo: %d para int, %f para double, %s para string, %c para char, %p para ponteiro, %x para hexadecimal, %u para unsigned. A largura reserva um número mínimo de colunas. A precisão (após um ponto) controla casas decimais ou o tamanho de strings. Veja:

#include <stdio.h>

int main(void) {
    printf("[%5d]\n", 42);       // [   42]  — largura 5, alinhado à direita
    printf("[%-5d]\n", 42);      // [42   ]  — flag '-' alinha à esquerda
    printf("[%05d]\n", 42);      // [00042]  — flag '0' preenche com zeros
    printf("[%8.2f]\n", 3.14159);// [    3.14] — largura 8, 2 casas decimais
    printf("[%.3s]\n", "abcdef");// [abc]    — precisão limita a string a 3 chars
    printf("[%+d]\n", 42);       // [+42]    — flag '+' força o sinal
    return 0;
}

Cada modificador resolve um problema real de apresentação: alinhar colunas numa tabela (largura), controlar casas decimais em valores monetários (precisão), preencher com zeros um número de série (flag 0). Você não precisa memorizar tudo — mas saber que esses controles existem, e onde consultá-los, é o que separa uma saída amadora de uma profissional.

Os especificadores e seus tipos: uma correspondência que não perdoa

Aqui está a primeira grande armadilha, e ela merece atenção total. O printf confia cegamente no especificador que você fornece — ele não verifica se o tipo do argumento realmente corresponde. Se você usar o especificador errado, o resultado é comportamento indefinido, não um erro claro:

#include <stdio.h>

int main(void) {
    double preco = 9.99;
    printf("%d\n", preco);  // ERRADO: %d espera int, mas passamos double
    // saída: lixo imprevisível, NÃO 9

    int idade = 30;
    printf("%f\n", idade);  // ERRADO: %f espera double, mas passamos int
    // saída: lixo, NÃO 30.000000
    return 0;
}

O compilador com -Wall felizmente avisa sobre muitos desses descasamentos (format '%d' expects argument of type 'int', but argument has type 'double'), e é mais uma razão para compilar sempre com avisos ligados. A regra é rígida: o especificador tem de casar com o tipo do argumento. Os pares essenciais: %d com int, %f com double, %c com char, %s com char * (string), %p com ponteiro, %zu com size_t (o retorno de sizeof), %ld com long, %lld com long long.

scanf: lendo entrada, e a armadilha do &

O scanf é o espelho de printf: ele lê entrada formatada do teclado. Mas tem uma diferença crucial que já anticipamos lá na aula de ponteiros e que agora entendemos por completo. Como scanf precisa escrever o valor lido de volta nas suas variáveis, ele precisa dos endereços delas — daí o onipresente &:

#include <stdio.h>

int main(void) {
    int idade;
    double altura;

    printf("Digite sua idade: ");
    scanf("%d", &idade);        // &: passamos o ENDEREÇO de idade

    printf("Digite sua altura: ");
    scanf("%lf", &altura);      // note %lf (não %f) para double no scanf!

    printf("Você tem %d anos e %.2f m.\n", idade, altura);
    return 0;
}

Dois pontos merecem destaque. Primeiro, o &: esquecê-lo é o erro mais comum com scanf. Sem ele, você passaria o valor da variável (lixo, pois ela ainda não foi inicializada) em vez do endereço, e scanf escreveria num lugar imprevisível — um desastre. Agora, com ponteiros dominados, você entende exatamente por quê: é passagem por referência, feita explicitamente. Segundo, uma inconsistência histórica que confunde: no scanf, double usa %lf, enquanto no printf usa %f. É uma assimetria irritante, mas real; grave-a.

Uma exceção sobre o &: strings (vetores de char) já são endereços por natureza (decaimento de array), então você lê uma string sem o &:

char nome[50];
scanf("%49s", nome);  // sem &: 'nome' já é um endereço. E note o limite 49!

O %49s limita a leitura a 49 caracteres, deixando espaço para o '\0' num buffer de 50 — uma defesa essencial contra estouro, que veremos a seguir.

A grande armadilha de segurança: scanf sem limite

O scanf com %s puro é perigoso pela mesma razão que o strcpy cru: ele não verifica o tamanho do buffer de destino. Se o usuário digitar mais caracteres do que cabem, scanf escreve além do vetor — o buffer overflow que estudamos na aula de strings:

char nome[10];
scanf("%s", nome);  // PERIGO: se o usuário digitar 50 caracteres, estoura!

A defesa imediata é sempre especificar a largura máxima: scanf("%9s", nome) para um buffer de 10 (9 caracteres mais o '\0'). Mas há um problema maior com scanf que os programadores experientes conhecem bem: ele é traiçoeiro para ler entrada de forma robusta. Ele lida mal com erros (se o usuário digita texto onde se espera número, scanf falha de formas confusas), deixa o \n no buffer atrapalhando leituras seguintes, e mistura mal leitura de números com leitura de linhas.

A alternativa robusta: fgets

Por esses motivos, para ler entrada do usuário de forma segura, muitos programadores C preferem fgets, que lê uma linha inteira com um limite de tamanho explícito e obrigatório:

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

int main(void) {
    char linha[100];

    printf("Digite seu nome: ");
    if (fgets(linha, sizeof(linha), stdin) != NULL) {
        // fgets lê no máximo sizeof(linha)-1 caracteres — seguro por construção
        printf("Olá, %s", linha); // o '\n' já vem incluído na linha
    }

    printf("Digite um número: ");
    if (fgets(linha, sizeof(linha), stdin) != NULL) {
        int n = atoi(linha);   // converte a linha lida em inteiro
        printf("O dobro é %d\n", n * 2);
    }
    return 0;
}

O fgets recebe o buffer, seu tamanho (sizeof(linha)) e a fonte (stdin, a entrada padrão). Ele nunca escreve além do tamanho informado — a segurança é embutida no seu funcionamento. O padrão idiomático robusto em C costuma ser: ler a linha inteira com fgets e depois converter ou processar essa linha (com atoi, strtol, ou análise manual). Um detalhe: fgets inclui o \n no final da linha, o que às vezes você precisa remover. Para o aprendizado, scanf é conveniente e continuaremos a usá-lo em exemplos simples; mas para programas reais que recebem entrada de usuários, lembre-se de que fgets é a escolha mais segura.

Outras funções da família

Vale conhecer os parentes de printf e scanf, que reaparecem quando formos trabalhar com arquivos e strings. putchar e getchar lidam com um único caractere. puts imprime uma string seguida de \n. E — o mais importante para o futuro próximo — sprintf e sscanf fazem o mesmo que printf/scanf, mas escrevendo/lendo de uma string em vez do terminal:

#include <stdio.h>

int main(void) {
    char buffer[50];
    int dia = 15, mes = 6, ano = 2026;

    // sprintf: "imprime" formatado dentro de uma string
    sprintf(buffer, "%02d/%02d/%d", dia, mes, ano);
    printf("Data formatada: %s\n", buffer); // 15/06/2026
    return 0;
}

O sprintf é extremamente útil para montar strings formatadas na memória — construir nomes de arquivo, mensagens, formatar dados para depois gravar. (Como scanf, ele tem uma versão mais segura, snprintf, que recebe o tamanho do buffer e evita estouros — prefira-a em código real.) Guardaremos essas funções para a aula de arquivos, onde a família fprintf/fscanf fará a mesma mágica lendo e escrevendo em disco.

O que vem a seguir

Hoje abrimos a Fase 4 dominando a entrada e a saída formatada: a estrutura completa dos especificadores de printf (flags, largura, precisão), a correspondência inflexível entre especificador e tipo, e as armadilhas de scanf — a necessidade do &, o %lf para double, e os riscos de segurança que tornam fgets a escolha mais robusta para entrada real. Com o programa agora capaz de conversar bem com o usuário, o próximo passo é dar-lhe memória duradoura. Na próxima aula, aprenderemos a ler e gravar arquivos de texto, fazendo nossos dados sobreviverem ao encerramento do programa — o primeiro passo para construir software que realmente guarda informação.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Use printf com modificadores de largura e precisão para imprimir uma pequena "tabela" alinhada: três produtos, cada um com nome (alinhado à esquerda, largura 15) e preço (2 casas decimais, largura 8, alinhado à direita).

Ver resposta

✓ Resposta:

#include <stdio.h>
int main(void) {
    printf("%-15s%8.2f\n", "Caderno", 12.90);
    printf("%-15s%8.2f\n", "Caneta", 2.50);
    printf("%-15s%8.2f\n", "Mochila", 149.99);
    return 0;
}

O %-15s alinha o nome à esquerda em 15 colunas; o %8.2f reserva 8 colunas para o preço com 2 casas decimais, alinhado à direita. O resultado são colunas verticalmente alinhadas.

Exercício 2

Escreva um programa que leia um inteiro e um double do usuário com scanf e imprima a soma. Preste atenção em usar %d, %lf (na leitura) e & corretamente. Teste com valores como 5 e 2.5.

Ver resposta

✓ Resposta:

#include <stdio.h>
int main(void) {
    int a;
    double b;
    printf("Inteiro: ");
    scanf("%d", &a);
    printf("Decimal: ");
    scanf("%lf", &b);      // %lf para double na LEITURA
    printf("Soma: %.2f\n", a + b);
    return 0;
}

Pontos-chave: &a e &b passam os endereços (para scanf escrever neles), e %lf (não %f) é o especificador correto de double no scanf.

Exercício 3

Explique o que há de errado no trecho int x; scanf("%d", x); e por que ele provoca comportamento indefinido. Qual é a correção?

Ver resposta

✓ Resposta: O erro é a ausência do &: scanf("%d", x) passa o valor de x em vez do seu endereço. Como x não foi inicializado, esse valor é lixo, e scanf o interpreta como um endereço de memória onde deveria escrever o inteiro lido — escrevendo, portanto, num local imprevisível e inválido. Isso é comportamento indefinido e costuma travar o programa (segmentation fault). A correção é passar o endereço: scanf("%d", &x);. O scanf precisa do endereço porque vai escrever o resultado de volta na variável.

Exercício 4

Reescreva o programa do exercício 2 usando fgets + atoi/atof em vez de scanf. Explique por que essa versão é mais segura contra entradas malformadas.

Ver resposta

✓ Resposta:

#include <stdio.h>
#include <stdlib.h>
int main(void) {
    char linha[100];
    int a;
    double b;

    printf("Inteiro: ");
    fgets(linha, sizeof(linha), stdin);
    a = atoi(linha);

    printf("Decimal: ");
    fgets(linha, sizeof(linha), stdin);
    b = atof(linha);

    printf("Soma: %.2f\n", a + b);
    return 0;
}

Esta versão é mais segura porque fgets lê a linha inteira com um limite de tamanho explícito (sizeof(linha)), nunca estourando o buffer, e consome a linha completa de uma vez — evitando o problema clássico do \n deixado no buffer pelo scanf, que atrapalha leituras subsequentes. entradas malformadas não deixam o fluxo de entrada num estado confuso: você lê texto bruto e decide como interpretá-lo (atoi/atof simplesmente retornam 0 se a linha não for um número, em vez de travar a sequência de leituras).

Exercício 5

Use sprintf (ou, melhor, snprintf) para montar uma string no formato "Aluno #007: media 8.50" a partir de um número de matrícula inteiro (7) e uma média double (8.5). Imprima a string resultante. Dica: %03d para o número com zeros à esquerda.

Ver resposta

✓ Resposta:

#include <stdio.h>
int main(void) {
    int matricula = 7;
    double media = 8.5;
    char buffer[50];

    snprintf(buffer, sizeof(buffer), "Aluno #%03d: media %.2f", matricula, media);
    printf("%s\n", buffer); // Aluno #007: media 8.50
    return 0;
}

O %03d formata a matrícula com três dígitos, preenchendo com zeros à esquerda (007), e o %.2f dá duas casas à média (8.50). Usamos snprintf (em vez de sprintf) por segurança: ao receber sizeof(buffer), ele nunca escreve além do buffer, mesmo que a string formatada fosse maior que o esperado.

Comentários

Mais em Linguagem C

Ponteiros: O que É um Endereço de Memória
Ponteiros: O que É um Endereço de Memória

A memória como uma rua de casas numeradas, e o ponteiro como a variável que…

Testes Automatizados em C
Testes Automatizados em C

Testar só o caminho feliz dá segurança falsa: o bug mora no zero, no negativo…

typedef e o Design de Tipos Legíveis
typedef e o Design de Tipos Legíveis

Esconder um asterisco dentro de um apelido faz PInt a, b; declarar dois…