Seja Bem-Vindo. Este site tem recursos de leitura de texto, basta marcar o texto e clicar no ícone do alto-falante   Click to listen highlighted text! Seja Bem-Vindo. Este site tem recursos de leitura de texto, basta marcar o texto e clicar no ícone do alto-falante

Linguagem de Programação II C – AULA 03

📚 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

  1. Sem retorno e sem parâmetros
    void mensagem() {
        printf("Isso é uma função sem parâmetros!\n");
    }
    
  2. Com retorno e sem parâmetros
    int soma() {
        return 5 + 10;
    }
    
  3. Sem retorno e com parâmetros
    void exibir_numero(int num) {
        printf("O número é: %d\n", num);
    }
    
  4. 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:

  1. Segurança → A variável original não pode ser alterada.
  2. Facilidade de leitura → O código é mais fácil de entender.
  3. Proteção contra efeitos colaterais → O valor original fica intacto.

❌ Desvantagens:

  1. Ineficiente para estruturas grandes → Se passarmos estruturas grandes (como vetores ou matrizes), fazer cópias pode consumir muita memória.
  2. 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

Fim da aula 03

Click to listen highlighted text!