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
- The C Programming Language (K&R), Kernighan & Ritchie — Cap. 7, Input and Output
- cppreference —
printfe a família de saída formatada — https://en.cppreference.com/w/c/io/fprintf - cppreference —
scanfe a família de entrada formatada — https://en.cppreference.com/w/c/io/fscanf - cppreference —
fgets— https://en.cppreference.com/w/c/io/fgets - CERT C — FIO série, especialmente sobre uso seguro de
scanfe limites de buffer — https://wiki.sei.cmu.edu/confluence/display/c
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.