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

Exercícios

  1. 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;
    }
    
  2. 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 prefira fgets(nome, sizeof(nome), stdin).

  3. 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;
    }
    
  4. 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;
    }
    
  5. 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.