📚 Aula: Funções e Recursividade na Linguagem C
🎯 Objetivo: Compreender o uso de funções em C, a passagem de parâmetros, o escopo de variáveis e o conceito de recursividade. O aluno aprenderá a modularizar o código, tornando-o mais eficiente e reutilizável.
📌 1. Subprogramas: Funções em C
Na programação estruturada, dividir um programa em subprogramas (funções) melhora a organização e a reutilização do código.
🔹 O que são Funções?
Uma função é um bloco de código que executa uma tarefa específica. Em C, toda execução começa na função main()
, mas podemos criar funções personalizadas para modularizar o código.
📌 Estrutura de uma Função em C
tipo_retorno nome_funcao(tipo param1, tipo param2, ...) {
// Corpo da função
return valor;
}
📌 Exemplo 1: Criando e Chamando uma Função
#include <stdio.h>
// Definição da função
void saudacao() {
printf("Olá! Seja bem-vindo ao curso de C!\n");
}
int main() {
saudacao(); // Chamada da função
return 0;
}
📌 Saída esperada:
Olá! Seja bem-vindo ao curso de C!
✅ Vantagem: O código fica modular e organizado.
📌 2. Tipos de Funções em C
- Sem retorno e sem parâmetros
void mensagem() { printf("Isso é uma função sem parâmetros!\n"); }
- Com retorno e sem parâmetros
int soma() { return 5 + 10; }
- Sem retorno e com parâmetros
void exibir_numero(int num) { printf("O número é: %d\n", num); }
- Com retorno e com parâmetros
int quadrado(int x) { return x * x; }
🔗 Leitura Adicional:
📌 Passagem de Parâmetros por Valor em C
A passagem de parâmetros é um conceito fundamental na programação em C, pois permite que funções recebam valores e realizem operações sem alterar diretamente os dados originais.
Uma das formas de passagem de parâmetros é por valor, onde o argumento passado para a função é copiado para uma nova variável local dentro da função. Isso significa que qualquer alteração feita na variável dentro da função não afeta a variável original no programa principal.
📌 O que é Passagem por Valor?
A passagem por valor ocorre quando um argumento é passado para uma função e, dentro dela, uma cópia desse valor é criada. A função então manipula essa cópia, mas não altera a variável original fora dela.
🔹 Características da Passagem por Valor
✔️ A função trabalha com uma cópia do valor da variável original.
✔️ A variável original não é modificada fora da função.
✔️ Segurança: evita que a função altere acidentalmente valores importantes.
✔️ O uso excessivo pode gerar maior consumo de memória se os valores forem muito grandes.
📌 Exemplo 1: Demonstração Simples de Passagem por Valor
O código abaixo demonstra a passagem por valor, onde a função dobrar()
recebe um número, dobra seu valor e exibe o resultado. Porém, a variável original x
não é alterada.
#include <stdio.h>
void dobrar(int num) {
num = num * 2; // A variável original NÃO será alterada
printf("Dentro da função: %d\n", num);
}
int main() {
int x = 10;
dobrar(x); // Passagem de valor (cópia do valor de x é enviada)
printf("Fora da função: %d\n", x);
return 0;
}
📌 Saída esperada:
Dentro da função: 20
Fora da função: 10
✅ Conclusão: A variável x
não é modificada porque a função dobrar()
recebeu apenas uma cópia do seu valor.
📌 Exemplo 2: Função com Retorno
Se quisermos dobrar o valor da variável original, precisamos retornar o novo valor e armazená-lo novamente na variável original.
#include <stdio.h>
int dobrar(int num) {
return num * 2; // Retorna o novo valor sem modificar o original
}
int main() {
int x = 10;
x = dobrar(x); // Agora x recebe o novo valor
printf("Novo valor de x: %d\n", x);
return 0;
}
📌 Saída esperada:
Novo valor de x: 20
✅ Conclusão: Agora, x
foi atualizado porque armazenamos o valor retornado pela função.
📌 Exemplo 3: Passagem por Valor com Vários Argumentos
Podemos passar múltiplos argumentos por valor para uma função.
#include <stdio.h>
void soma(int a, int b) {
int resultado = a + b;
printf("Soma dentro da função: %d\n", resultado);
}
int main() {
int num1 = 5, num2 = 7;
soma(num1, num2); // Passagem por valor
printf("Num1: %d, Num2: %d\n", num1, num2);
return 0;
}
📌 Saída esperada:
Soma dentro da função: 12
Num1: 5, Num2: 7
✅ Conclusão: num1
e num2
não foram modificados, pois foram passados por valor.
📌 Exemplo 4: Passagem por Valor com Estruturas Condicionais
Abaixo, um exemplo onde um número é verificado dentro da função para saber se é positivo ou negativo.
#include <stdio.h>
void verificarNumero(int num) {
if (num >= 0) {
printf("O número %d é positivo.\n", num);
} else {
printf("O número %d é negativo.\n", num);
}
}
int main() {
int x = -10;
verificarNumero(x); // Passagem por valor
printf("Valor de x após a função: %d\n", x);
return 0;
}
📌 Saída esperada:
O número -10 é negativo.
Valor de x após a função: -10
✅ Conclusão: A função apenas analisa o valor de x
, mas não o modifica.
📌 Benefícios e Limitações da Passagem por Valor
✔️ Vantagens:
- Segurança → A variável original não pode ser alterada.
- Facilidade de leitura → O código é mais fácil de entender.
- Proteção contra efeitos colaterais → O valor original fica intacto.
❌ Desvantagens:
- Ineficiente para estruturas grandes → Se passarmos estruturas grandes (como vetores ou matrizes), fazer cópias pode consumir muita memória.
- Necessidade de retorno para modificar valores → Para modificar um valor original, precisamos usar
return
, como no exemplo 2.
📌 Quando Usar a Passagem por Valor?
🔹 Quando a função não precisa modificar a variável original.
🔹 Quando trabalhamos com tipos primitivos pequenos (int, float, char).
🔹 Quando queremos evitar efeitos colaterais em outras partes do programa.
📌 Quando NÃO Usar?
🔸 Quando a função precisa modificar a variável original.
🔸 Quando trabalhamos com grandes quantidades de dados (vetores, structs, etc.).
📌 Conclusão
- A passagem por valor cria uma cópia da variável, garantindo que a original não seja alterada.
- Para modificar valores, usamos
return
e armazenamos o novo resultado na variável original. - Esse método é seguro e eficiente para variáveis pequenas, mas pode ser ineficiente para grandes estruturas de dados.
- O programador deve avaliar quando é mais vantajoso usar passagem por referência em vez de passagem por valor.
🔗 Materiais Complementares:
📌 Passagem de Parâmetros por Referência em C (Uso de Ponteiros)
A passagem de parâmetros por referência é uma técnica fundamental na linguagem C que permite que uma função modifique diretamente o valor da variável original. Para isso, utilizamos ponteiros, que armazenam o endereço de memória das variáveis, permitindo manipular seus valores diretamente dentro da função chamada.
📌 O que é a Passagem por Referência?
Diferente da passagem por valor, onde a função recebe uma cópia do valor original, na passagem por referência, a função recebe um ponteiro para a variável original, podendo modificar diretamente seu conteúdo.
🔹 Características da Passagem por Referência
✔️ A função recebe um ponteiro, que contém o endereço da variável original.
✔️ O valor da variável pode ser alterado dentro da função.
✔️ Eficiente para grandes estruturas de dados (vetores, structs).
✔️ Usado para simular múltiplos retornos em funções.
📌 Exemplo 1: Modificando um Valor com Passagem por Referência
O código abaixo demonstra como a função dobrar()
altera diretamente o valor de x
por meio de um ponteiro.
#include <stdio.h>
// Função que modifica a variável original
void dobrar(int *num) {
*num = (*num) * 2; // Modifica o valor original
}
int main() {
int x = 10;
dobrar(&x); // Passando o endereço de x
printf("Fora da função: %d\n", x);
return 0;
}
📌 Saída esperada:
Fora da função: 20
✅ Conclusão: Como x
foi passado por referência, sua modificação dentro da função afetou a variável original.
📌 Exemplo 2: Troca de Valores Usando Ponteiros
A passagem por referência é frequentemente utilizada para trocar os valores de duas variáveis sem precisar retornar múltiplos valores.
#include <stdio.h>
// Função que troca os valores de duas variáveis
void trocar(int *a, int *b) {
int temp = *a; // Armazena o valor de a
*a = *b; // Atribui b a a
*b = temp; // Atribui temp (valor original de a) a b
}
int main() {
int x = 5, y = 10;
printf("Antes da troca: x = %d, y = %d\n", x, y);
trocar(&x, &y);
printf("Depois da troca: x = %d, y = %d\n", x, y);
return 0;
}
📌 Saída esperada:
Antes da troca: x = 5, y = 10
Depois da troca: x = 10, y = 5
✅ Conclusão: A função trocar()
recebe os endereços de x
e y
, modificando seus valores diretamente.
📌 Exemplo 3: Alterando Múltiplos Valores com Ponteiros
A passagem por referência permite que uma função altere mais de uma variável ao mesmo tempo.
#include <stdio.h>
// Função que modifica dois valores ao mesmo tempo
void modificarValores(int *a, int *b) {
*a += 10;
*b *= 2;
}
int main() {
int num1 = 5, num2 = 8;
printf("Antes: num1 = %d, num2 = %d\n", num1, num2);
modificarValores(&num1, &num2);
printf("Depois: num1 = %d, num2 = %d\n", num1, num2);
return 0;
}
📌 Saída esperada:
Antes: num1 = 5, num2 = 8
Depois: num1 = 15, num2 = 16
✅ Conclusão: Como os parâmetros foram passados por referência, a função modificou os valores das variáveis originais.
📌 Exemplo 4: Uso de Ponteiros para Retornar Múltiplos Valores
Diferente de linguagens como Python ou JavaScript, em C uma função não pode retornar múltiplos valores diretamente. No entanto, podemos contornar essa limitação com ponteiros.
#include <stdio.h>
// Função que calcula a soma e o produto de dois números
void calcular(int a, int b, int *soma, int *produto) {
*soma = a + b;
*produto = a * b;
}
int main() {
int x = 4, y = 3;
int resultadoSoma, resultadoProduto;
calcular(x, y, &resultadoSoma, &resultadoProduto);
printf("Soma: %d\n", resultadoSoma);
printf("Produto: %d\n", resultadoProduto);
return 0;
}
📌 Saída esperada:
Soma: 7
Produto: 12
✅ Conclusão: O uso de ponteiros permitiu que a função calcular()
retornasse dois valores simultaneamente.
📌 Diferenças entre Passagem por Valor e Passagem por Referência
Característica | Passagem por Valor | Passagem por Referência |
Tipo de dado passado | Cópia do valor | Endereço da variável |
Modifica o original? | ❌ Não | ✅ Sim |
Segurança | ✅ Sim (evita mudanças inesperadas) | ❌ Pode alterar valores sem controle |
Eficiência | ❌ Baixa para grandes estruturas | ✅ Alta para grandes estruturas |
Exemplo comum | soma(int a, int b) |
trocar(int *a, int *b) |
🔗 Leitura Adicional:
📌 Vantagens e Desvantagens da Passagem por Referência
✔️ Vantagens:
✅ Permite modificar diretamente a variável original, sem precisar retornar valores.
✅ Eficiente para grandes estruturas de dados (vetores, structs).
✅ Facilita retorno de múltiplos valores sem precisar usar arrays ou structs.
❌ Desvantagens:
❌ Menos seguro, pois permite modificar variáveis inesperadamente.
❌ Menos intuitivo para iniciantes, devido ao uso de ponteiros (*
e &
).
❌ Pode levar a erros de acesso inválido se não tratado corretamente.
📌 Quando Usar a Passagem por Referência?
✔️ Quando precisamos modificar valores dentro de uma função.
✔️ Para evitar cópias desnecessárias de grandes estruturas.
✔️ Quando precisamos retornar múltiplos valores de uma função.
📌 Quando NÃO Usar?
❌ Quando a função não precisa modificar a variável original.
❌ Para tipos primitivos pequenos, onde a cópia tem baixo impacto.
📌 Conclusão
- A passagem por referência permite que uma função modifique diretamente a variável original.
- Usa ponteiros (
*
e&
) para acessar e alterar valores na memória. - É eficiente, mas requer cuidado para evitar acessos inválidos.
- Ideal para grandes estruturas de dados e múltiplos retornos.
🚀 Agora você domina a passagem de parâmetros por referência em C! 🎯
📌 Escopo de Variáveis na Linguagem C
O escopo de uma variável define onde ela pode ser acessada e utilizada dentro do código. Compreender o escopo é essencial para evitar conflitos entre variáveis, otimizar a memória e garantir que o código funcione corretamente.
Na linguagem C, o escopo das variáveis pode ser local, global ou estático, e a sua correta utilização é um dos fatores mais importantes para a organização e eficiência do programa.
📌 O que é o Escopo de uma Variável?
O escopo de uma variável determina onde e por quanto tempo ela existe e pode ser acessada dentro do código.
✔️ Uma variável declarada dentro de uma função só pode ser usada dentro dela.
✔️ Uma variável declarada fora de todas as funções pode ser acessada de qualquer parte do código.
✔️ Algumas variáveis podem manter seu valor entre chamadas de funções, dependendo de como são declaradas.
📌 Tipos de Escopo em C
A linguagem C possui diferentes níveis de escopo, que determinam o comportamento das variáveis. Os principais tipos são:
1️⃣ Escopo Local (Variáveis Locais)
📌 Definição:
- A variável é declarada dentro de uma função ou bloco
{}
e só pode ser acessada nesse contexto. - Criada quando a função é chamada e destruída quando a função termina.
- Evita interferências entre funções diferentes.
📌 Exemplo de Variável Local:
#include <stdio.h>
void funcao() {
int num = 10; // Apenas acessível dentro desta função
printf("Número dentro da função: %d\n", num);
}
int main() {
funcao();
// printf("%d", num); // ERRO! 'num' não existe aqui.
return 0;
}
📌 Saída esperada:
Número dentro da função: 10
✅ Conclusão: A variável num
existe apenas dentro da funcao()
. Se tentarmos acessá-la no main()
, o compilador retornará um erro.
2️⃣ Escopo Global (Variáveis Globais)
📌 Definição:
- A variável é declarada fora de qualquer função.
- Pode ser acessada por todas as funções do programa.
- Criada no início da execução e destruída no final do programa.
📌 Exemplo de Variável Global:
#include <stdio.h>
int contador = 0; // Variável global
void incrementar() {
contador++;
printf("Contador dentro da função: %d\n", contador);
}
int main() {
incrementar();
incrementar();
printf("Contador no main: %d\n", contador);
return 0;
}
📌 Saída esperada:
Contador dentro da função: 1
Contador dentro da função: 2
Contador no main: 2
✅ Conclusão: A variável contador
é compartilhada entre todas as funções.
🚨 Atenção!
🔴 O uso excessivo de variáveis globais pode tornar o código difícil de depurar.
🔴 Funções diferentes podem modificar a mesma variável global, causando efeitos colaterais inesperados.
3️⃣ Escopo de Bloco (Variáveis dentro de {}
)
📌 Definição:
- Qualquer variável declarada dentro de um bloco
{}
tem escopo restrito a esse bloco. - Funciona dentro de
if
,for
,while
ou{}
isolados.
📌 Exemplo de Escopo de Bloco:
#include <stdio.h>
int main() {
int x = 100;
if (x > 50) {
int y = 10; // Escopo restrito ao bloco 'if'
printf("Dentro do bloco: y = %d\n", y);
}
// printf("Fora do bloco: y = %d\n", y); // ERRO! 'y' não existe aqui.
return 0;
}
📌 Saída esperada:
Dentro do bloco: y = 10
✅ Conclusão: A variável y
só existe dentro do if
, não podendo ser acessada depois que o bloco termina.
4️⃣ Escopo Estático (Variáveis static
)
📌 Definição:
- Uma variável local
static
mantém seu valor entre diferentes chamadas da função. - Uma variável global
static
só pode ser acessada no arquivo onde foi definida.
📌 Exemplo de Variável static
Local:
#include <stdio.h>
void contador() {
static int num = 0; // Variável é inicializada apenas UMA vez
num++;
printf("Valor de num: %d\n", num);
}
int main() {
contador();
contador();
contador();
return 0;
}
📌 Saída esperada:
Valor de num: 1
Valor de num: 2
Valor de num: 3
✅ Conclusão: A variável num
mantém seu valor entre chamadas da função.
📌 Comparação entre os Tipos de Escopo
Tipo de Escopo | Declaração | Duração | Onde pode ser acessado? | Uso recomendado |
Local | Dentro de uma função | Apenas enquanto a função executa | Apenas dentro da função | Quando a variável não precisa ser compartilhada entre funções |
Global | Fora de todas as funções | Durante toda a execução do programa | Qualquer parte do código | Quando múltiplas funções precisam acessar o mesmo valor |
Bloco | Dentro de {} isolado |
Enquanto o bloco estiver ativo | Apenas dentro do bloco {} |
Para variáveis temporárias |
Estático | static dentro de uma função |
Mantém o valor entre chamadas | Apenas dentro da função | Quando o valor deve ser preservado |
🔗 Leitura Adicional:
📌 Conclusão
- O escopo de uma variável define onde ela pode ser acessada e manipulada.
- Variáveis locais são mais seguras e evitam interferências no código.
- Variáveis globais devem ser usadas com cautela para evitar efeitos colaterais.
- Blocos
{}
criam variáveis temporárias que desaparecem quando o bloco termina. - Variáveis
static
mantêm seus valores entre chamadas de função.
📌 Recursividade na Linguagem C
A recursividade é um conceito fundamental na programação, especialmente em C, que permite que uma função chame a si mesma para resolver um problema de forma mais simplificada e elegante. Esse conceito é amplamente utilizado em algoritmos matemáticos, processamento de estruturas de dados, backtracking, divisão e conquista, entre outras aplicações.
📌 O que é Recursividade?
Uma função recursiva é uma função que chama a si mesma dentro de sua definição. Esse processo continua até que uma condição de parada seja atingida, garantindo que a recursão não continue indefinidamente.
🔹 Estrutura de uma Função Recursiva
Uma função recursiva sempre segue duas partes principais:
✔️ Caso Base → Define a condição de parada, evitando loops infinitos.
✔️ Chamada Recursiva → A função se chama novamente, resolvendo uma versão menor do problema.
📌 Exemplo de Estrutura Genérica:
tipo funcao_recursiva(parâmetro) {
if (condição_de_parada)
return resultado;
return funcao_recursiva(parâmetro_modificado);
}
📌 Exemplo 1: Cálculo de Fatorial Recursivo
O fatorial de um número n
(n!
) é definido como:
n! = n × (n - 1) × (n - 2) × ... × 1
A versão recursiva dessa operação é escrita como:
n! = n × (n - 1)!
Com condição de parada quando n == 0
:
0! = 1
📌 Código:
#include <stdio.h>
int fatorial(int n) {
if (n == 0) return 1; // Condição de parada
return n * fatorial(n - 1);
}
int main() {
int num = 5;
printf("Fatorial de %d é %d\n", num, fatorial(num));
return 0;
}
📌 Saída esperada:
Fatorial de 5 é 120
🔹 Como a Recursão Funciona?
O código acima gera a seguinte sequência de chamadas recursivas:
fatorial(5) → 5 × fatorial(4)
fatorial(4) → 4 × fatorial(3)
fatorial(3) → 3 × fatorial(2)
fatorial(2) → 2 × fatorial(1)
fatorial(1) → 1 × fatorial(0)
fatorial(0) → 1 (caso base)
Quando o caso base é atingido (n == 0
), o valor 1 é retornado e os valores são multiplicados na pilha de chamadas.
✅ Conclusão: O cálculo de fatorial é um exemplo clássico de recursão, pois a definição matemática já é recursiva.
📌 Exemplo 2: Sequência de Fibonacci Recursiva
A sequência de Fibonacci é definida como:
F(0) = 0
F(1) = 1
F(n) = F(n-1) + F(n-2), para n ≥ 2
Ou seja, cada número da sequência é a soma dos dois anteriores.
📌 Código:
#include <stdio.h>
int fibonacci(int n) {
if (n == 0) return 0;
if (n == 1) return 1;
return fibonacci(n-1) + fibonacci(n-2);
}
int main() {
int i;
for (i = 0; i < 10; i++) {
printf("%d ", fibonacci(i));
}
return 0;
}
📌 Saída esperada:
0 1 1 2 3 5 8 13 21 34
🔹 Como a Recursão Funciona?
Para fibonacci(5)
, as chamadas ocorrem da seguinte forma:
fibonacci(5) → fibonacci(4) + fibonacci(3)
fibonacci(4) → fibonacci(3) + fibonacci(2)
fibonacci(3) → fibonacci(2) + fibonacci(1)
fibonacci(2) → fibonacci(1) + fibonacci(0)
Quando n == 1
ou n == 0
, a função retorna diretamente um valor sem mais chamadas recursivas.
✅ Conclusão: A sequência de Fibonacci pode ser implementada de maneira simples com recursão, mas a versão ingênua é ineficiente, pois repete cálculos desnecessários.
📌 Solução para otimizar? Podemos usar memorização (cache) ou programação dinâmica.
📌 Exemplo 3: Potenciação Recursiva
Outro exemplo clássico de recursão é o cálculo de potência (a^b
).
📌 Código:
#include <stdio.h>
int potencia(int base, int expoente) {
if (expoente == 0) return 1; // Caso base: qualquer número elevado a 0 é 1
return base * potencia(base, expoente - 1);
}
int main() {
int base = 2, expoente = 5;
printf("%d^%d = %d\n", base, expoente, potencia(base, expoente));
return 0;
}
📌 Saída esperada:
2^5 = 32
✅ Conclusão: A função potencia()
se chama repetidamente até atingir expoente == 0
, onde retorna 1.
📌 Exemplo 4: Busca Binária Recursiva
A busca binária é um método eficiente para procurar um elemento em um vetor ordenado, dividindo repetidamente o problema pela metade.
📌 Código:
#include <stdio.h>
int buscaBinaria(int arr[], int inicio, int fim, int chave) {
if (inicio > fim) return -1; // Elemento não encontrado
int meio = (inicio + fim) / 2;
if (arr[meio] == chave) return meio; // Encontrou o elemento
if (arr[meio] > chave) return buscaBinaria(arr, inicio, meio - 1, chave);
return buscaBinaria(arr, meio + 1, fim, chave);
}
int main() {
int arr[] = {1, 3, 5, 7, 9, 11, 15};
int n = sizeof(arr) / sizeof(arr[0]);
int chave = 7;
int resultado = buscaBinaria(arr, 0, n - 1, chave);
if (resultado != -1)
printf("Elemento encontrado na posição %d\n", resultado);
else
printf("Elemento não encontrado\n");
return 0;
}
📌 Saída esperada:
Elemento encontrado na posição 3
✅ Conclusão: A busca binária recursiva é um exemplo de divisão e conquista, reduzindo o problema a metades menores a cada passo.
📌 Vantagens e Desvantagens da Recursividade
✔️ Vantagens:
✅ Código mais legível → Algoritmos recursivos são mais intuitivos.
✅ Natural para certos problemas → Como Fibonacci e fatorial.
✅ Útil em algoritmos de divisão e conquista → Busca binária, ordenação rápida (quicksort), etc.
❌ Desvantagens:
❌ Uso excessivo de memória → Cada chamada recursiva adiciona um novo quadro à pilha de execução.
❌ Possibilidade de stack overflow → Se a recursão não tiver uma condição de parada válida.
❌ Menos eficiente que loops em alguns casos → Como Fibonacci sem memorização.
📌 Conclusão
- A recursividade permite resolver problemas complexos de forma simples.
- Sempre deve haver um caso base para evitar chamadas infinitas.
- Problemas como fatorial, Fibonacci e busca binária são exemplos clássicos de recursão.
- Para otimização, técnicas como memorização e programação dinâmica podem ser aplicadas.
🔗 Materiais Complementares:
📢 Conclusão
- Funções modularizam o código, tornando-o mais reutilizável.
- Parâmetros podem ser passados por valor ou por referência.
- Escopo de variáveis define onde uma variável pode ser usada.
- Recursividade é poderosa, mas deve ser usada com cautela.
📖 Referências Bibliográficas
-
- Livro-base: “Algoritmos e Programação: Teoria e Prática” – Rodrigo César, Marco Medina e Cristina Fertig.
- Livro Complementar: “The C Programming Language” – Brian W. Kernighan, Dennis M. Ritchie.
- Documentação Oficial C – Functions
- Documentação Oficial – Escopo de Variáveis em C
Fim da aula 03