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:
- O conceito de subprogramação e as vantagens da decomposição modular.
- A anatomia e estrutura de uma função em C.
- Parâmetros formais vs. parâmetros atuais.
- O princípio sagrado da passagem de parâmetros por valor.
- A instrução
returne funções booleanas/predicativas. - Procedimentos e funções sem argumentos com a cláusula
void. - Otimização algorítmica com o exemplo da função
primo(). - 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
- 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.
- 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çãomain()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):
- O número de parâmetros atuais tem de ser rigorosamente igual ao número de parâmetros formais.
- A ordem dos parâmetros deve ser respeitada escrupulosamente.
- O tipo de cada parâmetro atual deve ser compatível com o tipo do parâmetro formal correspondente.
- 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:
- 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.
- 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:
- 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 } - 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 quek * 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 para1(verdadeiro) ou0(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
ienquantoi * i <= n. Se encontrar umicujo quadrado seja exatamenten, devolve1imediatamente comreturn. Se ultrapassarn, termina e devolve0.
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
longpara 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 dereturn. - Itera de
1aténe testa se o resto da divisão inteiran % 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
returndevolve um valor e o controlo ao chamador; para funções booleanas, a expressão relacional devolve diretamente1ou0. - O tipo de retorno
voiddefine procedimentos que não devolvem valor;(void)nos parâmetros declara formalmente que a função não recebe argumentos.