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 (x²), 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.
|
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:printfescanf(<stdio.h>),strlen,strcpyestrcmp(<string.h>),sqrtefabs(<math.h>),randesrand(<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:
|
Resumo
|
Exercícios
Nível fácil
-
Escreva uma função
void saudacao(), sem parâmetros e sem retorno, que imprima"Bem-vindo ao programa!". Chame-a a partir damain(). -
Escreva uma função
int maior(int a, int b)que retorne o maior entre dois inteiros. Chame-a a partir damain(), lendo dois valores do usuário e imprimindo o resultado. -
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
-
Escreva uma função
int ehPrimo(int n)que retorne1senfor primo, ou0caso contrário (reaproveite a lógica vista na Aula 6). Escreva ummain()que leia vários números até o usuário digitar0, informando para cada um se é primo, usando a função. -
Escreva uma função
void imprimirTabuada(int n), sem retorno, que imprima a tabuada de multiplicação de1a10do númeronrecebido como parâmetro. -
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
-
Escreva uma função
int fatorial(int n)que calcule o fatorial dende 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 den(até o usuário digitar um valor negativo) e imprima o fatorial de cada um. -
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
aeblogo após a chamada detrocar, dentro damain().#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.