Funções: declaração, definição e protótipos

1. Introdução às Funções em C

1.1. O que são funções e por que utilizá-las

Funções são blocos de código nomeados que executam uma tarefa específica. Em Linguagem C, elas são o principal mecanismo para estruturar programas de forma modular. Utilizar funções traz benefícios como:

  • Modularidade: dividir problemas complexos em partes menores e gerenciáveis
  • Reuso: escrever código uma vez e utilizá-lo em diferentes contextos
  • Legibilidade: nomear operações torna o código mais expressivo e fácil de entender
  • Manutenção: alterações localizadas em funções não afetam o restante do programa

1.2. Estrutura básica de uma função

Toda função em C possui quatro elementos fundamentais:

tipo_de_retorno nome_da_funcao(tipo param1, tipo param2) {
    // corpo da função
    return valor;
}
  • Tipo de retorno: especifica o tipo do valor que a função devolve (ou void se não retornar nada)
  • Nome: identificador único para chamar a função
  • Parâmetros: lista de variáveis que recebem dados externos (opcional)
  • Corpo: bloco de código entre chaves {} que implementa a lógica

1.3. Diferença conceitual entre declaração, definição e protótipo

  • Declaração (protótipo): anuncia a existência da função ao compilador, informando sua assinatura
  • Definição: implementa o corpo da função, com todo o código executável
  • Protótipo: termo sinônimo à declaração em C, usado para garantir verificação de tipos

2. Declaração de Funções (Protótipos)

2.1. Sintaxe de um protótipo

O protótipo é uma declaração que termina com ponto e vírgula, sem corpo:

int somar(int a, int b);
float calcular_media(float notas[], int tamanho);
void exibir_mensagem(char *texto);

2.2. Onde colocar protótipos

Os protótipos devem ser colocados antes do primeiro uso da função, geralmente:

  • No início do arquivo fonte (antes da função main)
  • Em arquivos de cabeçalho .h para compartilhar entre múltiplos arquivos
#include <stdio.h>

// Protótipos
int somar(int a, int b);
void exibir_resultado(int valor);

int main() {
    int resultado = somar(5, 3);
    exibir_resultado(resultado);
    return 0;
}

// Definições
int somar(int a, int b) {
    return a + b;
}

void exibir_resultado(int valor) {
    printf("Resultado: %d\n", valor);
}

2.3. Importância do protótipo para o compilador

Sem protótipos, o compilador assume tipos padrão para funções não declaradas (comportamento obsoleto em C99). Protótipos permitem:

  • Verificação de tipos: o compilador valida se os argumentos correspondem aos parâmetros
  • Forward declaration: funções podem ser usadas antes de sua definição
  • Conversão automática: tipos incompatíveis geram warnings ou erros

3. Definição de Funções

3.1. Sintaxe completa da definição

A definição inclui o cabeçalho (mesmo do protótipo) e o bloco de código:

int calcular_fatorial(int n) {
    int resultado = 1;
    for (int i = 1; i <= n; i++) {
        resultado *= i;
    }
    return resultado;
}

3.2. Parâmetros formais vs. argumentos reais

  • Parâmetros formais: variáveis declaradas na definição da função (ex: int n)
  • Argumentos reais: valores passados na chamada da função (ex: calcular_fatorial(5))

Em C, a passagem é sempre por valor: os argumentos são copiados para os parâmetros formais.

3.3. A cláusula return

  • Funções com tipo de retorno diferente de void devem usar return para devolver um valor
  • Funções void podem usar return; para sair prematuramente, sem valor
  • O tipo do valor retornado deve ser compatível com o tipo declarado
int obter_maior(int a, int b) {
    if (a > b) return a;
    return b;
}

void processar_dados(int valor) {
    if (valor < 0) return;  // saída antecipada
    printf("Processando: %d\n", valor);
}

4. Escopo e Ciclo de Vida em Funções

4.1. Variáveis locais

Variáveis declaradas dentro de uma função têm escopo local e duração automática:

void exemplo() {
    int contador = 0;  // criada na entrada, destruída na saída
    contador++;
    printf("%d\n", contador);
}

4.2. Parâmetros como variáveis locais especiais

Parâmetros formais comportam-se como variáveis locais inicializadas com os argumentos recebidos:

void alterar_valor(int x) {
    x = 100;  // altera apenas a cópia local
}

int main() {
    int num = 10;
    alterar_valor(num);
    printf("%d\n", num);  // ainda imprime 10
    return 0;
}

4.3. Variáveis estáticas dentro de funções

A palavra-chave static mantém o valor entre chamadas, mas o escopo permanece local:

void contador_chamadas() {
    static int vezes = 0;
    vezes++;
    printf("Chamada número %d\n", vezes);
}

5. Passagem de Parâmetros

5.1. Passagem por valor (cópia)

Comportamento padrão em C: os argumentos são copiados para os parâmetros:

void trocar(int a, int b) {
    int temp = a;
    a = b;
    b = temp;  // troca apenas as cópias
}

5.2. Simulação de passagem por referência usando ponteiros

Para modificar variáveis externas, passamos seus endereços:

void trocar(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main() {
    int x = 5, y = 10;
    trocar(&x, &y);  // agora x=10, y=5
    return 0;
}

5.3. Passagem de arrays como parâmetros

Arrays decaem para ponteiros quando passados como argumentos:

void imprimir_array(int vetor[], int tamanho) {
    for (int i = 0; i < tamanho; i++) {
        printf("%d ", vetor[i]);
    }
}

// Equivalente: void imprimir_array(int *vetor, int tamanho)

6. Funções com Número Variável de Argumentos

6.1. Uso de stdarg.h

A biblioteca stdarg.h fornece macros para processar argumentos variáveis:

#include <stdarg.h>

double media(int quantidade, ...) {
    va_list args;
    double soma = 0.0;

    va_start(args, quantidade);
    for (int i = 0; i < quantidade; i++) {
        soma += va_arg(args, double);
    }
    va_end(args);

    return soma / quantidade;
}

6.2. Exemplo prático

#include <stdio.h>
#include <stdarg.h>

int somar_varios(int num_args, ...) {
    va_list args;
    int soma = 0;

    va_start(args, num_args);
    for (int i = 0; i < num_args; i++) {
        soma += va_arg(args, int);
    }
    va_end(args);

    return soma;
}

int main() {
    printf("Soma: %d\n", somar_varios(4, 10, 20, 30, 40));  // 100
    return 0;
}

6.3. Cuidados e limitações

  • Não há verificação de tipos em tempo de compilação
  • O programador é responsável por informar quantos argumentos foram passados
  • Tipos promovidos (ex: float vira double) podem causar confusão

7. Recursão em Funções

7.1. Conceito de função recursiva

Uma função recursiva chama a si mesma para resolver subproblemas menores:

int fatorial(int n) {
    if (n <= 1) return 1;        // caso base
    return n * fatorial(n - 1);  // chamada recursiva
}

7.2. Condição de parada e pilha de chamadas

Toda recursão precisa de um caso base para evitar loop infinito. Cada chamada recursiva empilha um novo contexto na pilha de execução.

7.3. Exemplo: Fibonacci recursivo vs. iterativo

Versão recursiva (menos eficiente):

int fibonacci_rec(int n) {
    if (n <= 1) return n;
    return fibonacci_rec(n - 1) + fibonacci_rec(n - 2);
}

Versão iterativa (mais eficiente):

int fibonacci_iter(int n) {
    if (n <= 1) return n;
    int a = 0, b = 1, temp;
    for (int i = 2; i <= n; i++) {
        temp = a + b;
        a = b;
        b = temp;
    }
    return b;
}

8. Boas Práticas e Erros Comuns

8.1. Sempre declarar protótipos antes do uso

Isso evita warnings de "declaração implícita" e garante verificação de tipos:

// Correto
int dobrar(int x);

int main() {
    printf("%d\n", dobrar(5));
    return 0;
}

int dobrar(int x) { return x * 2; }

8.2. Cuidado com variáveis globais e efeitos colaterais

Variáveis globais podem ser alteradas por qualquer função, dificultando depuração. Prefira passar dados como parâmetros.

8.3. Diferença entre declaração e definição múltipla

A regra de definição única (ODR) em C permite múltiplas declarações (protótipos), mas apenas uma definição:

// Múltiplas declarações (permitido)
int somar(int a, int b);
int somar(int a, int b);
int somar(int, int);

// Uma única definição
int somar(int a, int b) {
    return a + b;
}

Referências