Reforço de Algoritmos e Programação - Instituto de Informática (UFRGS) - Prof. Dennis Giovani Balreira



Aula 8 - Funções, Escopo, Parâmetros e Retorno

Iniciamos aqui a Área II da disciplina. Nesta aula veremos funções: como dividir um programa em partes menores e reaproveitáveis, como elas recebem dados (parâmetros) e devolvem resultados (retorno), e como cada variável tem um escopo — um alcance limitado dentro do programa.


1. Motivação: por que usar funções?

Todos os programas que escrevemos até agora tinham toda a sua lógica dentro de uma única função: a main(). Isso funciona bem para programas pequenos, mas não escala. Imagine um programa "de verdade", com centenas ou milhares de linhas: validar entradas, calcular estatísticas, gerar relatórios, atualizar um cadastro... Tudo dentro de uma única main() gigante se torna:

  • Difícil de ler: para entender uma única tarefa, é preciso "garimpar" o trecho de código certo em meio a centenas de linhas de outras tarefas misturadas.
  • Difícil de testar: não há como testar isoladamente "só o cálculo da média", por exemplo, sem rodar o programa inteiro.
  • Difícil de reaproveitar: se a mesma lógica (por exemplo, calcular a média de um vetor) for necessária em três pontos diferentes do programa, ela normalmente acaba sendo copiada e colada três vezes — e se um dia precisar ser corrigida, é preciso lembrar de corrigir em todos os lugares.

A subprogramação resolve esse problema: divide um programa complexo em partes menores, independentes e reaproveitáveis, chamadas funções. A ideia é parecida com o conceito de função matemática que vocês já conhecem: uma função f(x) = x² recebe uma entrada (x), realiza um processamento bem definido, e produz uma saída (), sem que quem usa f precise saber como esse cálculo é feito por dentro. Uma função em C funciona da mesma forma: recebe entradas (parâmetros), processa algo, e (opcionalmente) devolve uma saída (retorno) — só que, além de calcular valores, uma função em C também pode executar ações (como imprimir algo na tela).

Diretriz de Projeto.
  • Princípio da Responsabilidade Única: cada função deve executar apenas uma tarefa específica. Se uma função está fazendo muitas coisas diferentes (ler dados, calcular valores e imprimir o resultado, tudo junto), ela provavelmente deveria ser dividida em funções menores.

Dois grupos de funções

Em C, as funções se dividem em dois grupos:

  • Pré-definidas (ou de biblioteca): funções já prontas, implementadas pelo compilador ou por bibliotecas padrão do sistema, que só precisamos #includear para usar. Já usamos várias delas nas aulas anteriores: printf e scanf (<stdio.h>), strlen, strcpy e strcmp (<string.h>), sqrt e fabs (<math.h>), rand e srand (<stdlib.h>). Alguém já resolveu esses problemas para nós, então não precisamos reescrever essa lógica do zero.
  • Desenvolvidas pelo programador: funções que nós mesmos criamos, para resolver as necessidades específicas do nosso programa — é isso que aprenderemos a fazer nesta aula.

2. Protótipo, definição e chamada

Usar uma função criada por nós mesmos envolve três etapas essenciais: o protótipo, a definição e a chamada. Vejamos cada uma delas, com sua sintaxe genérica e um exemplo concreto.

a) Protótipo (ou assinatura)

O protótipo declara ao compilador que a função existe, informando seu tipo de retorno, seu nome e os tipos dos parâmetros que ela recebe — sem, no entanto, mostrar como ela funciona por dentro. Ele geralmente fica no início do arquivo, antes da main().

Sintaxe genérica:

tipo nome(tipo1 param1, tipo2 param2, ..., tipoN paramN);

Exemplo — protótipo de uma função que soma dois inteiros:

int somar(int a, int b);
Por que o protótipo é necessário? O compilador lê o arquivo de cima para baixo. Se main() chamar somar antes de o compilador "ver" a definição completa de somar (por exemplo, se ela estiver escrita mais abaixo no arquivo), ele ainda assim precisa saber que essa função existe e qual é sua assinatura — é exatamente isso que o protótipo garante, com apenas uma linha.

b) Definição (ou declaração)

A definição é a implementação de fato: o corpo da função, com as instruções que ela executa para cumprir sua tarefa.

Sintaxe genérica:

tipo nome(tipo1 param1, tipo2 param2, ..., tipoN paramN) {
    ...
    return expressao; // opcional; obrigatório se o tipo de retorno não for void
}

Exemplo — definição da função somar:

int somar(int a, int b) {
    return a + b;
}

c) Chamada

A chamada é o momento em que a função é de fato invocada e executada, a partir de outro ponto do programa (por exemplo, dentro da main(), ou dentro de outra função).

Sintaxe genérica:

variavel = nome(argumento1, argumento2, ..., argumentoN);

Exemplo — chamando somar dentro da main():

int resultado = somar(x, y);

Juntando as três etapas em um único programa completo:

#include <stdio.h>

// (a) Protótipo
int somar(int a, int b);

int main() {
    int x = 5, y = 3;

    // (c) Chamada
    int resultado = somar(x, y);

    printf("Soma: %d\n", resultado);
    return 0;
}

// (b) Definição
int somar(int a, int b) {
    return a + b;
}

3. Parâmetros, argumentos e retorno

Termo Significado
Parâmetros As variáveis declaradas na assinatura da função — recebem os valores enviados na chamada.
Argumentos Os valores (ou variáveis) realmente fornecidos no momento da chamada da função.
Retorno O valor devolvido pela função para quem a chamou, através de return. Ao executar return, a função é encerrada imediatamente.

Uma função pode ou não ter parâmetros, e pode ou não ter retorno — são quatro combinações possíveis:

#include <stdio.h>

// Protótipos
void cabecalho(); // 1. Sem retorno (void) e sem parâmetros
float obterPi(); // 2. Com retorno (float) e sem parâmetros
void imprimirRepetido(char texto[], int vezes); // 3. Sem retorno e com parâmetros
int quadrado(int num); // 4. Com retorno (int) e com parâmetros

int main() {
    cabecalho();

    printf("Valor de PI: %f\n", obterPi());

    imprimirRepetido("Ola!", 3);

    int val = 5;
    printf("O quadrado de %d e %d\n", val, quadrado(val));

    return 0;
}

// Implementações
void cabecalho() {
    printf("====================\n");
    printf("   SISTEMA DE TESTE\n");
    printf("====================\n");
}

float obterPi() {
    return 3.14159;
}

void imprimirRepetido(char texto[], int vezes) {
    int i;
    for (i = 0; i < vezes; i++) {
        printf("%s\n", texto);
    }
}

int quadrado(int num) {
    return num * num;
}

4. Escopo de variáveis

O escopo de uma variável define onde ela pode ser vista e usada, e por quanto tempo ela existe. Vimos, na Aula 1, que cada variável ocupa um espaço na memória — o escopo determina justamente quando esse espaço é reservado e liberado.

Tipo Onde é declarada Quem enxerga Tempo de vida
Variável local Dentro do corpo de uma função (inclusive dentro da main) ou de um bloco. Apenas a função onde foi declarada. Termina quando a função retorna.
Variável global Fora de qualquer função, no topo do arquivo. Qualquer função do programa. Toda a execução do programa.
#include <stdio.h>

int contador = 0; // variável global: visível em todo o arquivo

// Protótipo
void incrementarContador();

int main() {
    int x = 10; // variável local: só existe dentro da main()

    incrementarContador();
    incrementarContador();

    printf("%d\n", contador); // imprime 2
    return 0;
}

// Implementação
void incrementarContador() {
    contador++; // acessa a global diretamente, sem precisar recebê-la como parâmetro
}
Regra de Ouro (variáveis locais). Variáveis locais devem ser amplamente utilizadas: elas garantem o encapsulamento dos dados de uma função (o que acontece "dentro" dela fica isolado do resto do programa) e evitam efeitos colaterais em outras partes do código — ninguém mais precisa se preocupar com o que uma função faz com suas próprias variáveis internas.
Regra de Ouro (variáveis globais): evite-as. Variáveis globais não devem ser usadas nos programas desta disciplina. Como qualquer função pode alterá-las livremente, sem que isso apareça na assinatura da função, elas dificultam o rastreamento de erros, impedem a modularização e comprometem a segurança e a legibilidade do código — um bug causado por uma variável global pode estar escondido em qualquer lugar do programa. Prefira sempre variáveis locais, e passe dados entre funções através de parâmetros e retorno.

5. Passagem de parâmetros por valor

Em C, tudo o que é passado como argumento para uma função é sempre passado por valor: o valor do argumento é copiado para o parâmetro da função. Qualquer alteração feita no parâmetro, dentro da função, afeta apenas essa cópia local — a variável original, na função que fez a chamada, permanece inalterada. Isso vale mesmo para tipos mais complexos, como struct (que veremos mais adiante): eles também são copiados por completo. Mais para frente, quando estudarmos ponteiros, vamos ver que um endereço de memória também é apenas um valor como outro qualquer — e, por isso, também é copiado quando passado para uma função. A diferença é que, nesse caso, o que se copia é o endereço, e não o dado apontado por ele — o que dá a impressão de estarmos passando algo "por referência", ainda que, por baixo dos panos, continue sendo uma cópia.

#include <stdio.h>

// Protótipo
void incrementar(int n);

int main() {
    int num = 10;

    incrementar(num); // passagem por valor: copia o valor de "num"

    printf("Na main: %d\n", num); // imprime 10 (num não mudou!)
    return 0;
}

// Implementação
void incrementar(int n) {
    n = n + 1; // altera apenas a cópia local do parâmetro "n"
    printf("Dentro da funcao: %d\n", n);
}
Um problema para resolver na próxima aula. E se quisermos que uma função altere de verdade a variável original da main, e não apenas uma cópia? A passagem por valor, sozinha, não permite isso. Esse problema será resolvido na Aula 9, quando estudarmos ponteiros e a passagem por referência.
Boas práticas: organização do arquivo-fonte. A partir de agora, todo programa em C desta disciplina deve seguir sempre a mesma ordem de organização:
  1. #include
  2. #define
  3. variáveis globais (evitar)
  4. protótipos
  5. main()
  6. implementação das funções
Seguir sempre essa mesma estrutura torna qualquer programa em C mais fácil de ler — inclusive um programa escrito por outra pessoa — porque você sempre sabe onde procurar cada coisa.

Resumo

  • Função: bloco de código reaproveitável, responsável por uma única tarefa (Princípio da Responsabilidade Única).
  • Protótipo (assinatura) × definição (implementação) × chamada (execução em algum ponto do programa).
  • Parâmetros recebem os argumentos passados na chamada; retorno (via return) devolve um valor e encerra a função imediatamente.
  • Uma função pode ter (ou não) parâmetros, e pode ter (ou não) retorno — quatro combinações possíveis.
  • Variáveis locais: vivem só dentro da função onde foram declaradas. Variáveis globais: visíveis em todo o programa, mas não devem ser usadas.
  • Passagem por valor: o parâmetro recebe uma cópia do argumento; alterá-lo dentro da função não afeta a variável original.

Exercícios

Nível fácil

  1. Escreva uma função void saudacao(), sem parâmetros e sem retorno, que imprima "Bem-vindo ao programa!". Chame-a a partir da main().
  2. Escreva uma função int maior(int a, int b) que retorne o maior entre dois inteiros. Chame-a a partir da main(), lendo dois valores do usuário e imprimindo o resultado.
  3. Escreva uma função float paraFahrenheit(float celsius) que converta uma temperatura de Celsius para Fahrenheit (fórmula: F = C * 9.0 / 5.0 + 32), e um programa principal que leia uma temperatura em Celsius e imprima o resultado da conversão.

Nível médio

  1. Escreva uma função int ehPrimo(int n) que retorne 1 se n for primo, ou 0 caso contrário (reaproveite a lógica vista na Aula 6). Escreva um main() que leia vários números até o usuário digitar 0, informando para cada um se é primo, usando a função.
  2. Escreva uma função void imprimirTabuada(int n), sem retorno, que imprima a tabuada de multiplicação de 1 a 10 do número n recebido como parâmetro.
  3. Explique, sem executar o código, qual será a saída do programa abaixo, e por quê:
    #include <stdio.h>
    
    void dobrar(int x);
    
    int main() {
        int valor = 7;
        dobrar(valor);
        printf("%d\n", valor);
        return 0;
    }
    
    void dobrar(int x) {
        x = x * 2;
    }
    

Nível difícil

  1. Escreva uma função int fatorial(int n) que calcule o fatorial de n de forma iterativa (com um laço, sem recursão — a recursão será vista na Aula 12). Utilize-a para escrever um programa que leia vários valores de n (até o usuário digitar um valor negativo) e imprima o fatorial de cada um.
  2. O programa abaixo tenta trocar os valores de duas variáveis usando uma função, mas não funciona como o programador esperava. Explique, com base no que vimos sobre escopo e passagem por valor, por que a troca não ocorre, e o que aconteceria se você imprimisse a e b logo após a chamada de trocar, dentro da main().
    #include <stdio.h>
    
    void trocar(int x, int y);
    
    int main() {
        int a = 3, b = 8;
        trocar(a, b);
        printf("a = %d, b = %d\n", a, b);
        return 0;
    }
    
    void trocar(int x, int y) {
        int temp = x;
        x = y;
        y = temp;
    }
    

Sugestões de Respostas dos Exercícios

Exercício 1
#include <stdio.h>

void saudacao();

int main() {
    saudacao();
    return 0;
}

void saudacao() {
    printf("Bem-vindo ao programa!\n");
}

Exercício 2
#include <stdio.h>

int maior(int a, int b);

int main() {
    int x, y;

    printf("Digite dois numeros: ");
    scanf("%d %d", &x, &y);

    printf("O maior e %d\n", maior(x, y));

    return 0;
}

int maior(int a, int b) {
    if (a > b) {
        return a;
    }
    return b;
}

Exercício 3
#include <stdio.h>

float paraFahrenheit(float celsius);

int main() {
    float temperatura;

    printf("Digite a temperatura em Celsius: ");
    scanf("%f", &temperatura);

    printf("Em Fahrenheit: %.2f\n", paraFahrenheit(temperatura));

    return 0;
}

float paraFahrenheit(float celsius) {
    return celsius * 9.0 / 5.0 + 32;
}

Exercício 4
#include <stdio.h>

int ehPrimo(int n);

int main() {
    int n;

    printf("Digite um numero (0 para sair): ");
    scanf("%d", &n);

    while (n != 0) {
        if (ehPrimo(n)) {
            printf("%d e primo\n", n);
        } else {
            printf("%d nao e primo\n", n);
        }

        printf("Digite um numero (0 para sair): ");
        scanf("%d", &n);
    }

    return 0;
}

int ehPrimo(int n) {
    int i;

    if (n < 2) {
        return 0;
    }

    for (i = 2; i < n; i++) {
        if (n % i == 0) {
            return 0;
        }
    }

    return 1;
}

Repare como a função ehPrimo retorna diretamente 1 ou 0 assim que a resposta é conhecida (usando return para encerrar a função imediatamente), em vez de precisar de uma variável flag como fizemos na Aula 6 — o próprio return já cumpre esse papel aqui.


Exercício 5
#include <stdio.h>

void imprimirTabuada(int n);

int main() {
    int numero;

    printf("Digite um numero: ");
    scanf("%d", &numero);

    imprimirTabuada(numero);

    return 0;
}

void imprimirTabuada(int n) {
    int i;
    for (i = 1; i <= 10; i++) {
        printf("%d x %d = %d\n", n, i, n * i);
    }
}

Exercício 6

A saída será 7, e não 14. Isso acontece porque x, dentro de dobrar, é um parâmetro — ele recebe apenas uma cópia do valor de valor (passagem por valor). A instrução x = x * 2; altera somente essa cópia local, que deixa de existir assim que a função dobrar termina; a variável valor, na main(), nunca é tocada, e continua valendo 7 quando o printf é executado.


Exercício 7
#include <stdio.h>

int fatorial(int n);

int main() {
    int n;

    printf("Digite um numero (negativo para sair): ");
    scanf("%d", &n);

    while (n >= 0) {
        printf("%d! = %d\n", n, fatorial(n));

        printf("Digite um numero (negativo para sair): ");
        scanf("%d", &n);
    }

    return 0;
}

int fatorial(int n) {
    int resultado = 1;
    int i;

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

    return resultado;
}

Cada chamada de fatorial(n) é independente: a variável local resultado é recriada do zero a cada chamada, então não há risco de um cálculo "vazar" para o próximo, mesmo chamando a função várias vezes dentro do while.


Exercício 8

A troca não ocorre porque x e y, dentro de trocar, são parâmetros que recebem apenas cópias dos valores de a e b (passagem por valor). A função troca corretamente essas cópias locais entre si (usando temp), mas isso não tem nenhum efeito sobre as variáveis originais a e b, que vivem em um escopo diferente (o da main()) e deixam de existir assim que a chamada retorna.

Se imprimíssemos a e b logo após a chamada de trocar(a, b), ainda dentro da main(), o resultado seria exatamente o mesmo do printf já presente no código: a = 3, b = 8 — ou seja, sem nenhuma troca, como se a função nunca tivesse sido chamada (do ponto de vista de a e b). Para conseguir alterar de fato as variáveis da main() a partir de uma função, será necessário usar ponteiros e passagem por referência, assunto da Aula 9.