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 %19s em um buffer de 20.
  • Evite ler strings com scanf; prefira fgets.
  • Se precisar ler números, considere ler a linha com fgets e depois usar sscanf para 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 snprintf a sprintf sempre que possível, para evitar estouro de buffer.
  • Evite usar scanf para ler strings; use fgets e depois sscanf.
  • Sempre verifique o retorno de scanf e sscanf para garantir que a leitura foi bem-sucedida.
  • Use largura máxima em scanf quando 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

Exercícios

  1. 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.

✓ Resposta:
#include <stdio.h>

int main() {
    int pos = 42;
    int neg = -42;
    printf("%+010d\n", pos); // +000000042
    printf("%+010d\n", neg); // -000000042
    return 0;
}
  • Explique por que o scanf com %s é perigoso e mostre uma alternativa segura usando fgets e sscanf.
  • ✓ Resposta: O scanf com %s não verifica o tamanho do buffer, podendo causar estouro. Alternativa segura:
    char linha[100];
    char nome[50];
    fgets(linha, sizeof(linha), stdin);
    sscanf(linha, "%49s", nome); // limita a 49 caracteres
    
  • Escreva um programa que use snprintf para formatar uma mensagem que inclui um nome e uma idade, garantindo que não haja estouro de buffer. Teste com um buffer pequeno.
  • ✓ Resposta:
    #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;
    }
    
  • Dada a string "João,30,Engenheiro", use sscanf para extrair os três campos e imprima-os. Considere que o nome pode conter espaços.
  • ✓ Resposta:
    #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;
    }
    
  • Escreva um programa que leia uma linha do usuário e use sscanf para verificar se ela contém um número inteiro seguido de um caractere qualquer. Se sim, imprima o número; caso contrário, informe erro.
  • ✓ Resposta:
    #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;
    }