Intermédio⏱️ 35 min

07. Funções e Programação Modular

Compreende a subprogramação e a modularidade em C: anatomia de uma função, parâmetros formais vs. atuais, passagem de parâmetros por valor, retorno com return, uso de void e exercícios resolvidos interativamente.

Funções e Programação Modular em C

Até este ponto do curso, todos os nossos programas foram escritos de forma monolítica dentro da função main(). Para programas com poucas linhas isto funciona, mas à medida que os projetos crescem, esta abordagem torna o código desorganizado, difícil de ler, quase impossível de testar e com enormes blocos de código duplicado.

Nesta aula, baseada na apresentação Aula 6 — Funções, vamos dar um salto qualitativo fundamental e aprender a criar Funções:

  1. O conceito de subprogramação e as vantagens da decomposição modular.
  2. A anatomia e estrutura de uma função em C.
  3. Parâmetros formais vs. parâmetros atuais.
  4. O princípio sagrado da passagem de parâmetros por valor.
  5. A instrução return e funções booleanas/predicativas.
  6. Procedimentos e funções sem argumentos com a cláusula void.
  7. Otimização algorítmica com o exemplo da função primo().
  8. Resolução de todos os exercícios propostos nos diapositivos com soluções interativas ocultas.

1. Subprogramação e Modularidade

O princípio fundamental da engenharia de software é a decomposição: dividir um problema inicialmente complexo num conjunto de subproblemas mais simples e autónomos (Dividir para Conquistar).

A cada subproblema faz-se corresponder um módulo independente — um subprograma.

As Duas Grandes Vantagens da Programação Modular

  1. Evita a Repetição de Código (Princípio DRY — Don’t Repeat Yourself):
    • Quando um conjunto de instruções necessita de ser executado várias vezes ao longo do programa, define-se uma função uma única vez.
    • Essa função pode ser invocada de vários pontos do programa, processando de cada vez um conjunto diferente de dados.
  2. Maior Clareza Lógica e Manutenibilidade:
    • Programas divididos em módulos são muito mais fáceis de escrever, ler e depurar (debug).
    • Se surgir um erro de cálculo, sabemos exatamente qual a função responsável por esse cálculo, sem ter de percorrer centenas de linhas de código alheio.

2. Funções na Linguagem C

Em C, a estrutura modular de um programa é implementada através de Funções.

Um programa em C bem concebido é normalmente composto por inúmeras funções pequenas e especializadas, em vez de poucas funções gigantes:

  • A Função main(): É a porta de entrada obrigatória de qualquer programa executável. Tem de existir sempre e ser única no projeto.
  • O Papel de main(): Num programa bem estruturado, o código da função main() deve funcionar como um esboço de alto nível do que o programa faz, limitando-se a orquestrar as chamadas às funções especializadas.
               ┌───────────────────────┐
               │      int main()       │  <-- Esboço da orquestração
               └──────────┬────────────┘

         ┌────────────────┼────────────────┐
         ▼                ▼                ▼
┌─────────────────┐ ┌─────────────┐ ┌──────────────┐
│  lerEntrada()   │ │  calcular() │ │  imprimir()  │  <-- Módulos autónomos
└─────────────────┘ └─────────────┘ └──────────────┘

3. Estrutura e Anatomia de uma Função

Uma função em C é composta por duas partes essenciais: o cabeçalho e o corpo.

tipo_resultado nome_da_funcao(lista_de_parametros) {
    // Corpo da função:
    declaracoes_locais;
    instrucoes;
}
CABEÇALHO ──►  float calcularMedia(float n1, float n2)
               {
CORPO     ──►      float media = (n1 + n2) / 2.0f; // Declaração local
                   return media;                   // Instrução de retorno
               }

Componentes:

  • tipo_resultado: O tipo de dado que a função devolve a quem a chamou (int, float, double, char, void, etc.).
  • nome_da_funcao: Identificador único que respeita as regras de nomenclatura de C.
  • lista_de_parametros: Variáveis declaradas no cabeçalho para receber dados do exterior.
  • Corpo { ... }: Bloco delimitado por chavetas contendo as variáveis locais (que só existem enquanto a função corre) e as instruções executáveis.

4. Parâmetros: Atuais vs. Formais

Os parâmetros oferecem um canal seguro de transferência de informação entre o ponto onde a função é chamada (chamador) e a própria função (chamada).

#include <stdio.h>

// n e y são PARÂMETROS FORMAIS (variáveis locais da função)
float fx(int n, float y) {
    return n * y;
}

int main(void) {
    int k = 4;
    float x = 3.75f;

    // k e x são PARÂMETROS ATUAIS ou ARGUMENTOS (valores reais transmitidos)
    float resultado = fx(k, x);
    printf("Resultado: %.2f\n", resultado);

    return 0;
}

Regras Obrigatórias de Invocação (Slide 8):

  1. O número de parâmetros atuais tem de ser rigorosamente igual ao número de parâmetros formais.
  2. A ordem dos parâmetros deve ser respeitada escrupulosamente.
  3. O tipo de cada parâmetro atual deve ser compatível com o tipo do parâmetro formal correspondente.
  4. Um parâmetro atual pode ser uma constante (fx(4, 3.75)), uma variável (fx(k, x)) ou uma expressão (fx(k + 2, x * 1.5)).

5. Passagem de Parâmetros por Valor

🔒 Regra Sagrada em C:
A linguagem C utiliza exclusivamente a passagem de parâmetros por valor para tipos primitivos!

Quando invocas uma função:

  1. O valor do argumento atual é duplicado / copiado para uma nova posição de memória na pilha (stack) reservada ao parâmetro formal da função.
  2. Qualquer alteração efetuada sobre o parâmetro formal dentro da função não se reflete na variável original que esteve na origem da chamada!

Exemplo Prático de Verificação de Múltiplos (Slide 9):

#include <stdio.h>

void multiplo(int m, int n) {
    if (m % n == 0) {
        printf("%d e' multiplo de %d\n", m, n);
    } else {
        printf("%d nao e' multiplo de %d\n", m, n);
    }
}

int main(void) {
    int a = 20, b = 10;

    // m recebe uma cópia do valor de a (20)
    // n recebe uma cópia do valor de b (10)
    multiplo(a, b);

    return 0;
}

6. A Instrução return e Funções Booleanas

A instrução return termina a execução da função e devolve o valor de uma expressão a quem a chamou:

return expressao;

Exemplo de Função Booleana: Verificar se um Número é Par (Slide 11)

Muitos programadores iniciantes escrevem:

// Forma deselegante e redundante:
int par(int n) {
    if (n % 2 == 0)
        return 1;
    else
        return 0;
}

Como vimos na Aula 3, as expressões relacionais em C já produzem o valor 1 (se verdadeiro) ou 0 (se falso). Por isso, a solução canónica e elegante dispensa o if:

// Solução correta e idiomática em C:
int par(int n) {
    return (n % 2 == 0);
}

Uma função booleana pode ser utilizada diretamente como condição de um if:

int k = 42;
if (par(k)) {
    printf("%d e' um numero par!\n", k);
}

7. Funções Matemáticas: Potência com Expoente Inteiro (Slide 12)

Podemos definir funções que implementam fórmulas iterativas, como o cálculo de $x^n$ para $n \ge 0$:

#include <stdio.h>

double potencia(double x, int n) {
    double pot = 1.0;

    for (int k = 1; k <= n; k++) {
        pot *= x;
    }

    return pot;
}

int main(void) {
    double base = 3.14;
    int exp = 3;

    double res = potencia(base, exp);
    printf("%.2lf elevado a %d = %.4lf\n", base, exp, res);

    return 0;
}

8. A Palavra-Chave void

A palavra-chave void assume dois papéis distintos na declaração de funções:

  1. Quando uma função não devolve nenhum valor (Procedimento):
    void exibirMensagem(int vezes) {
        for (int i = 0; i < vezes; i++) {
            printf("Olá!\n");
        }
        // return; é opcional aqui
    }
  2. Quando uma função não aceita parâmetros de entrada:
    void limparEcra(void) {
        // O void entre parênteses sinaliza explicitamente a ausência de argumentos
        printf("\033[H\033[J");
    }

9. Algoritmos e Otimização: A Função primo() (Slide 14)

Um número natural $n > 1$ é primo se apenas for divisível por $1$ e por si próprio.

Abordagem Básica vs. Otimizada

// 1. Abordagem Básica (testa todos os números até n-1, mesmo que já tenha encontrado divisores):
int primo_basico(int n) {
    if (n <= 1) return 0;

    int eprimo = 1;
    for (int k = 2; k <= n - 1; k++) {
        if (n % k == 0) {
            eprimo = 0;
        }
    }
    return eprimo;
}

// 2. Abordagem Otimizada com Curto-Circuito (Slide 14):
int primo_otimizado(int n) {
    if (n <= 1) return 0;

    int eprimo = 1;
    // O ciclo para imediatamente assim que encontrar o primeiro divisor (eprimo passa a 0)
    for (int k = 2; k <= n - 1 && eprimo; k++) {
        if (n % k == 0) {
            eprimo = 0;
        }
    }
    return eprimo;
}

💡 Dica de Performance Pro:
Na realidade, nenhum fator primo de $n$ pode ser estritamente superior a $\sqrt{n}$. Logo, podemos parar o teste logo que k * k > n, transformando um algoritmo linear $O(n)$ num algoritmo de raiz quadrada $O(\sqrt{n})$!


10. Exercícios Práticos com Soluções Interativas (Slide 15)

Eis os quatro exercícios oficiais propostos na apresentação de Funções. Resolve-os antes de clicares para validar a tua resposta!

Exercício 1: Verificar se um Caractere é Dígito

Enunciado: Escreve uma função int ehDigito(char c) que receba um caractere e devolva 1 se for um algarismo de '0' a '9', ou 0 caso contrário.

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

int ehDigito(char c) {
    return (c >= '0' && c <= '9');
}

int main(void) {
    char teste1 = '7';
    char teste2 = 'a';

    printf("'%c' e' digito? %d\n", teste1, ehDigito(teste1)); // 1
    printf("'%c' e' digito? %d\n", teste2, ehDigito(teste2)); // 0

    return 0;
}

Explicação:

  • Em C, os caracteres '0' a '9' têm códigos ASCII consecutivos (48 a 57).
  • A expressão (c >= '0' && c <= '9') avalia para 1 (verdadeiro) ou 0 (falso) de forma direta e concisa.

Exercício 2: Verificar se um Inteiro é Quadrado Perfeito

Enunciado: Escreve uma função int ehQuadradoPerfeito(int n) que receba um número inteiro positivo e devolva 1 se o número for o quadrado exato de um inteiro (ex.: 0, 1, 4, 9, 16, 25…), ou 0 caso contrário.

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

int ehQuadradoPerfeito(int n) {
    if (n < 0) return 0; // Números negativos não têm raiz real

    int i = 0;
    while (i * i <= n) {
        if (i * i == n) {
            return 1; // Encontrou a raiz exata
        }
        i++;
    }

    return 0; // Passou do valor sem encontrar igualdade
}

int main(void) {
    int valores[] = {16, 25, 26, 49, 50};

    for (int idx = 0; idx < 5; idx++) {
        int v = valores[idx];
        printf("%d e' quadrado perfeito? %s\n", v, ehQuadradoPerfeito(v) ? "SIM" : "NAO");
    }

    return 0;
}

Explicação:

  • O ciclo itera incrementando i enquanto i * i <= n. Se encontrar um i cujo quadrado seja exatamente n, devolve 1 imediatamente com return. Se ultrapassar n, termina e devolve 0.

Exercício 3: Converter Tempo para Total de Segundos

Enunciado: Escreve uma função long totalSegundos(int h, int m, int s) que receba um horário expresso em horas, minutos e segundos, e calcule o número total de segundos correspondente.

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

long totalSegundos(int h, int m, int s) {
    return ((long)h * 3600) + ((long)m * 60) + s;
}

int main(void) {
    int horas = 2, minutos = 45, segundos = 30;

    long total = totalSegundos(horas, minutos, segundos);
    printf("%02dh %02dm %02ds correspondem a %ld segundos.\n", 
           horas, minutos, segundos, total);

    return 0;
}

Explicação:

  • 1 hora contém $60 \times 60 = 3600$ segundos e 1 minuto contém 60 segundos.
  • Usamos o tipo long para evitar estouro de capacidade de inteiros normais quando se processam muitas horas.

Exercício 4: Procedimento para Listar os Divisores de um Número

Enunciado: Escreve um procedimento void mostrarDivisores(int n) que receba um número inteiro positivo e imprima no terminal todos os seus divisores inteiros.

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

void mostrarDivisores(int n) {
    if (n <= 0) {
        printf("O numero deve ser estritamente positivo!\n");
        return;
    }

    printf("Divisores de %d: ", n);
    for (int k = 1; k <= n; k++) {
        if (n % k == 0) {
            printf("%d ", k);
        }
    }
    printf("\n");
}

int main(void) {
    mostrarDivisores(12); // Divisores: 1, 2, 3, 4, 6, 12
    mostrarDivisores(28); // Divisores: 1, 2, 4, 7, 14, 28
    mostrarDivisores(17); // Divisores: 1, 17 (é primo!)

    return 0;
}

Explicação:

  • Por ser do tipo void, a função não necessita de devolver valores através de return.
  • Itera de 1 até n e testa se o resto da divisão inteira n % k é zero.

Resumo da Aula 07

  • A modularidade divide problemas em subprogramas autónomos, promovendo reutilização e clareza.
  • Em C, a passagem de parâmetros primitivos é sempre por valor (os argumentos originais não são alterados pela função).
  • Os parâmetros formais são variáveis locais da função inicializadas com cópias dos parâmetros atuais.
  • A instrução return devolve um valor e o controlo ao chamador; para funções booleanas, a expressão relacional devolve diretamente 1 ou 0.
  • O tipo de retorno void define procedimentos que não devolvem valor; (void) nos parâmetros declara formalmente que a função não recebe argumentos.