Iniciante⏱️ 35 min

04. Entrada e Saída de Dados e Pré-Processador

Domina as diretivas do pré-processador (#define, #include e bibliotecas .h), as funções fundamentais de E/S pela consola (getchar, putchar, printf, scanf), formatação avançada de campos e precisão, e exercícios práticos com soluções interativas.

Entrada e Saída de Dados e Diretivas do Pré-Processador

Nesta aula vamos explorar os mecanismos através dos quais um programa em C comunica com o mundo exterior. Com base nos tópicos da apresentação Aula 4 — Entrada e Saída de Dados, irás compreender o papel do pré-processador e das bibliotecas de cabeçalho (.h), o funcionamento de funções de caractere único (getchar e putchar) e dominar as funções de entrada e saída formatada (printf e scanf), incluindo o controlo de larguras de campo, precisão decimal e a gestão do buffer do teclado.

No final, poderás resolver vários exercícios práticos com soluções interativas ocultas, que podes revelar com um simples clique.


1. Diretivas para o Pré-Processador

Sempre que um programa em C é compilado, o processo inicia-se com duas operações fundamentais:

  1. Pré-processamento: O pré-processador lê o código-fonte e executa todas as instruções que lhe são especificamente dirigidas (as diretivas de pré-processamento).
  2. Compilação propriamente dita: O código expandido resultante é traduzido em código-máquina.

As diretivas de pré-processamento não fazem parte da gramática da linguagem C, mas ampliam as suas capacidades e conferem ao programador controlo sobre o processo de compilação.

Regras Sintáticas das Diretivas

  • Começam sempre pelo símbolo cardinal (#).
  • Nunca terminam em ponto e vírgula (;).

2. As Diretivas #define e #include

A Diretiva #define (Constantes Simbólicas e Macros)

A diretiva #define serve para definir macros. Um caso particular muito frequente é a definição de constantes simbólicas:

#define NOTA_MAX 20
#define PI 3.14159265
#define NOME_CURSO "Linguagem C"
  • Como funciona? Uma constante simbólica não existe fisicamente numa posição de memória RAM. Antes de o compilador iniciar a análise do código, o pré-processador efetua uma substituição textual direta de todas as ocorrências de NOTA_MAX pelo texto 20.
  • Vantagens: Facilita a manutenção do código (alterar o valor num único local) e torna a leitura mais expressiva e legível.

A Diretiva #include

A diretiva #include instrui o pré-processador a substituir a própria linha pelo conteúdo integral do ficheiro designado. Permite separar o protótipo de funções e tipos da sua implementação real, sendo os ficheiros habitualmente designados por header files (com extensão .h).

Existem duas formas de sintaxe:

// 1. Delimitado por < > :
#include <stdio.h>
#include <math.h>

// 2. Delimitado por " " :
#include "meu_modulo.h"
#include "utilitarios/geometria.h"
Sintaxe Local de Procura do Ficheiro Utilização Típica
<ficheiro.h> Diretórios padrão do sistema / do compilador Ficheiros de cabeçalho da biblioteca padrão do C
"ficheiro.h" Diretório de trabalho atual do projeto (ou caminho relativo/absoluto) Ficheiros de cabeçalho desenvolvidos pelo programador

Principais Ficheiros de Cabeçalho da Biblioteca Padrão

Ficheiro .h Conteúdo e Finalidade Principal
<stdio.h> Protótipos de funções de entrada/saída de dados e fluxos (stdin, stdout, stderr).
<stdlib.h> Funções utilitárias: conversões de tipos (atoi, atof), geração de números aleatórios (rand), controlo de processos (exit) e alocação dinâmica.
<string.h> Tratamento e manipulação de cadeias de caracteres (strlen, strcpy, strcat, strcmp).
<ctype.h> Teste e classificação de caracteres (isalpha, isdigit, isupper, tolower).
<limits.h> Definição de constantes com os limites numéricos da máquina (INT_MAX, INT_MIN, CHAR_BIT).
<math.h> Funções matemáticas (sqrt, pow, sin, cos, fabs, floor, ceil). (Nota: ao compilar no Linux com GCC, adiciona a flag -lm).
<malloc.h> Protótipos para alocação e gestão de memória dinâmica.

3. Entrada e Saída pela Consola

Ao contrário de outras linguagens (como Python ou Pascal), a linguagem C não possui instruções nativas de E/S na sua sintaxe. Todas as operações de entrada e saída são asseguradas por funções da biblioteca padrão declaradas em <stdio.h>:

  • getchar() e putchar(): Leitura e escrita de caracteres individuais.
  • printf() e scanf(): Saída e entrada com formatação avançada.

4. Leitura e Escrita de um Único Caractere: getchar e putchar

Quando precisamos de processar texto caractere a caractere, o uso de getchar() e putchar() é mais simples e rápido do que scanf e printf:

A Função getchar()

  • É invocada sem qualquer argumento: ch = getchar();
  • Devolve um valor inteiro correspondente ao código ASCII do caractere introduzido na consola (ou a constante EOF em caso de fim de ficheiro).

A Função putchar(ch)

  • Recebe como argumento o caractere a apresentar: putchar(ch);
  • Escreve esse caractere na saída padrão e devolve o mesmo valor escrito.
#include <stdio.h>

int main(void) {
    int ch;

    printf("Digita uma letra e pressiona [ENTER]: ");
    ch = getchar(); // Lê o primeiro caractere da consola

    printf("O caractere digitado foi: ");
    putchar(ch);    // Escreve o caractere
    putchar('\n');  // Nova linha

    return 0;
}

5. Saída Formatada: A Função printf()

A função printf() apresenta dados no ecrã de acordo com um formato especificado:

int printf(const char *string_de_formatacao, arg1, arg2, ...);
  • Valor de Retorno: Devolve um número inteiro correspondente ao total de caracteres efetivamente escritos no ecrã.
  • Primeiro Argumento (string_de_formatacao): Obrigatório. Contém o texto fixo, sequências de escape (ex.: \n, \t) e especificadores de formato (iniciados por %).
  • Deve existir uma correspondência de tipo e ordem entre cada especificador e os argumentos seguintes:
int num = 5;
printf("Mostrar %d valores: %d e %f\n", 2, num, num / 2.0);
// Saída: Mostrar 2 valores: 5 e 2.500000

Especificadores de Formato Mais Usados

Especificador Significado / Tipo de Dado
%d ou %i Inteiro em decimal com sinal (int)
%u Inteiro em decimal sem sinal (unsigned int)
%o Inteiro em formato octal (sem sinal)
%x ou %X Inteiro em formato hexadecimal (sem sinal, minúsculas ou maiúsculas)
%c Um único caractere (char)
%s Cadeia de caracteres (string terminada em \0)
%f Vírgula flutuante decimal (float ou double)
%e ou %E Vírgula flutuante em notação científica
%p Endereço de memória / apontador
%hd Inteiro curto (short int)
%ld Inteiro longo (long int)
%% Imprime o símbolo literal %

Alinhamento e Largura Mínima de Campo

Podemos indicar uma largura mínima de caracteres antes do especificador:

  • %10d: Reserva no mínimo 10 colunas, com alinhamento à direita.
  • %-10d: Reserva no mínimo 10 colunas, com alinhamento à esquerda (usando o sinal -).
  • %3d: Reserva 3 colunas (se o número precisar de mais, o campo expande automaticamente sem cortar).
#include <stdio.h>

int main(void) {
    int valor = 123;

    printf("O valor %10d fica alinhado à direita\n", valor);
    printf("O valor %-10d fica alinhado à esquerda\n", valor);
    printf("O valor %3d fica completo\n", valor);

    return 0;
}

Saída Produzida:

O valor        123 fica alinhado à direita
O valor 123        fica alinhado à esquerda
O valor 123 fica completo

Controlo de Precisão Decimal

Em números de ponto flutuante, a precisão controla a quantidade de casas decimais após a vírgula:

#include <stdio.h>
#include <math.h> // Para sqrt()

int main(void) {
    double r = sqrt(2.0); // Raiz quadrada de 2 ≈ 1.41421356...

    printf("Raiz de 2 = %.20f\n", r);
    printf("Raiz de 2 = %.20f\n", (float)r);
    printf("Raiz de 2 = %2.10f\n", r);
    printf("Raiz de 2 = %10.2f\n", r);
    printf("Raiz de 2 = %10f\n", r);
    printf("Raiz de 2 = %10.0f\n", r);

    return 0;
}

Saída Produzida:

Raiz de 2 = 1.41421356237309510000
Raiz de 2 = 1.41421353816986080000
Raiz de 2 = 1.4142135624
Raiz de 2 =       1.41
Raiz de 2 =   1.414214
Raiz de 2 =          1

6. Entrada Formatada: A Função scanf()

A função scanf() permite ler dados a partir da entrada padrão (stdin), convertendo-os automaticamente de texto para o tipo de dado indicado:

int scanf(const char *string_de_controlo, &arg1, &arg2, ...);

O Operador de Endereço &

Os argumentos do scanf() devem ser endereços de memória onde os dados lidos serão armazenados. Para variáveis primitivas, prefixamos a variável com o operador & (&num).

Exemplos de Utilização de scanf

int num, a, b;
unsigned int u;

// 1. Leitura de inteiro em base decimal
scanf("%d", &num);

// 2. Leitura de inteiro com limite máximo de 4 dígitos
scanf("%4d", &num);

// 3. Leitura de número sem sinal (unsigned)
scanf("%u", &u);

// 4. Leitura de número em base octal
scanf("%o", &num);

// 5. Leitura de número em base hexadecimal
scanf("%x", &num);

// 6. Leitura de dois inteiros separados obrigatoriamente por vírgula no terminal
scanf("%d,%d", &a, &b);

O Buffer do Teclado e a Leitura de Caracteres (" %c")

Ao ler números com %d ou %f, o scanf ignora automaticamente quebras de linha (\n) ou espaços anteriores. No entanto, ao usar %c, qualquer caractere pendente no buffer (como o \n da tecla Enter anterior) é consumido de imediato!

Para descartar caracteres brancos residuais antes de ler um caractere, coloca um espaço antes do %c:

char opcao;
printf("Deseja continuar (s/n)? ");
scanf(" %c", &opcao); // O espaço antes de %c limpa o buffer!

7. Exercícios Práticos com Soluções Interativas

Experimenta resolver os exercícios no teu computador antes de clicares para ver a solução!

Exercício 1: Eco em Maiúsculas com getchar e putchar

Enunciado: Escreve um programa que leia um caractere do utilizador através de getchar(). Se o caractere for uma letra minúscula ('a' a 'z'), converte-o para maiúscula e imprime-o com putchar(). Caso contrário, imprime o caractere inalterado.

👉 Ver Solução Proposta
#include <stdio.h>

int main(void) {
    int c;

    printf("Digita uma letra: ");
    c = getchar();

    // Se for minúscula, converte subtraindo ('a' - 'A') = 32
    if (c >= 'a' && c <= 'z') {
        c = c - ('a' - 'A');
    }

    printf("Resultado: ");
    putchar(c);
    putchar('\n');

    return 0;
}

Explicação:

  • getchar() lê o caractere diretamente do buffer de entrada.
  • A expressão c - ('a' - 'A') retira 32 unidades ao código ASCII, transformando minúsculas em maiúsculas sem necessidade de bibliotecas adicionais.
  • putchar() escreve o resultado no ecrã.

Exercício 2: Tabela de Preços e IVA com Alinhamento em Colunas

Enunciado: Define constantes simbólicas #define IVA 0.23 e #define LOJA "TechStore". Cria um programa que apresente uma fatura simples com 3 artigos (nome, preço base, taxa de IVA e preço final). Os dados devem ser alinhados em colunas perfeitas utilizando as larguras mínimas do printf.

👉 Ver Solução Proposta
#include <stdio.h>

#define IVA 0.23f
#define LOJA "TechStore Informática"

int main(void) {
    printf("==================================================\n");
    printf("             FATURA - %s\n", LOJA);
    printf("==================================================\n");
    printf("%-18s %10s %8s %10s\n", "ARTIGO", "PREÇO", "TAXA", "TOTAL");
    printf("--------------------------------------------------\n");

    // Artigo 1: Teclado Mecânico
    float p1 = 49.90f;
    printf("%-18s %10.2f %7.0f%% %10.2f\n", "Teclado Mecanico", p1, IVA * 100, p1 * (1.0f + IVA));

    // Artigo 2: Rato Óptico
    float p2 = 19.50f;
    printf("%-18s %10.2f %7.0f%% %10.2f\n", "Rato Optico", p2, IVA * 100, p2 * (1.0f + IVA));

    // Artigo 3: Monitor 27"
    float p3 = 189.00f;
    printf("%-18s %10.2f %7.0f%% %10.2f\n", "Monitor 27 pol", p3, IVA * 100, p3 * (1.0f + IVA));

    printf("==================================================\n");

    return 0;
}

Explicação:

  • %-18s: Alinha o nome do artigo à esquerda num campo reservado de 18 caracteres.
  • %10.2f: Reserva 10 posições alinhadas à direita com exatamente 2 casas decimais.
  • %%: Imprime o símbolo de percentagem % sem que o compilador o interprete como especificador.

Exercício 3: Leitura Estruturada de Data e Hora com scanf

Enunciado: Escreve um programa que solicite ao utilizador a introdução de uma data e hora no formato estrito DD/MM/AAAA HH:MM (ex.: 25/12/2026 19:30). O programa deve validar se todos os 5 campos foram introduzidos corretamente utilizando o valor de retorno de scanf e, em caso afirmativo, imprimir cada componente isolada com zeros à esquerda (%02d).

👉 Ver Solução Proposta
#include <stdio.h>

int main(void) {
    int dia, mes, ano, hora, minuto;

    printf("Introduz a data e hora (DD/MM/AAAA HH:MM): ");
    
    // scanf retorna o número de variáveis preenchidas com sucesso
    int lidos = scanf("%d/%d/%d %d:%d", &dia, &mes, &ano, &hora, &minuto);

    if (lidos == 5) {
        printf("\n✓ Entrada válida registada com sucesso:\n");
        printf("  - Dia:    %02d\n", dia);
        printf("  - Mês:    %02d\n", mes);
        printf("  - Ano:    %04d\n", ano);
        printf("  - Horas:  %02d\n", hora);
        printf("  - Minuto: %02d\n", minuto);
    } else {
        printf("\n✗ Erro: Formato inválido! Esperado DD/MM/AAAA HH:MM (lidos apenas %d campos).\n", lidos);
    }

    return 0;
}

Explicação:

  • Na string de controlo "%d/%d/%d %d:%d", os caracteres literais / e : obrigam o utilizador a digitar essas pontuações exatas como separadores.
  • scanf devolve 5 se todos os inteiros forem capturados com sucesso.
  • O especificador %02d garante o preenchimento com zero à esquerda (ex.: dia 5 é impresso como 05).

Resumo da Aula 04

  • O pré-processador executa antes da compilação: #define cria constantes simbólicas textuais e #include importa ficheiros de cabeçalho (.h).
  • <header.h> procura nos diretórios de sistema/compilador; "header.h" procura no diretório do projeto.
  • getchar() e putchar() operam sobre caracteres únicos com rapidez e simplicidade.
  • printf() devolve o total de caracteres escritos e permite calibrar larguras de campo (%10d, %-10d), bases (%d, %o, %x) e precisão (%.2f).
  • scanf() requer o operador & para variáveis primitivas e devolve a quantidade de campos lidos com sucesso.
  • Para evitar o consumo indevido de \n residual no buffer ao ler caracteres, usa-se " %c" com espaço prévio.