LPE 09 - Modularização
LPE 09 - Modularização
Capítulo 5
Funções
O BJETIVOS DO CAPÍTULO
Ao final deste capítulo você deverá ser capaz de:
• Criar e usar funções e procedimentos em C
• Identificar quando escrever uma função ou um procedimento
• Entender as regras de escopo para variáveis em C
• Entender os diferentes tipos de passagem de parâmetro e quando utilizá-los
• Trabalhar com funções recursivas
Importante
Neste capítulo, vamos descartar a utilização de pseudocódigo. Agora que você já possui
um conhecimento básico sobre a linguagem C e, provavelmente, escreveu alguns programas
nela, consideramos que não haverá mais necessidade de apresentar sintaxes para pseudo-
códigos. Portanto, deste capítulo em diante, o conteúdo apresentadao utilizará somente a
sintaxe da linguagem C.
Funções e procedimentos podem ser compreendidos como trechos reutilizáveis de código. Uma fun-
ção ou procedimento pode, então, ser utilizado várias vezes por um mesmo programa. Isso simplifica
63 / 92
Introdução à Programação
a criação de programas maiores, dividindo-os em unidades menores que trabalham em conjunto. Fun-
ções e procedimentos são bastante semelhantes e, posteriormente, vamos entender as diferenças entre
eles.
Revisão
Lembre-se que durante o curso você já utilizou funções diversas vezes, o que nós sabemos
sobre funções até agora?
• Para executar uma função, utilizamos o nome da função e passamos alguns parâmetros
entre parênteses e separados por vírgula. Exemplo: printf("R$ %1.2f", preco);
• As funções strlen, strcpy, strcat e strchr são utilizadas para manipular strings;
Quando realizamos um processo com um determinado objetivo, é comum identificarmos partes que se
repetem. Quando se trata de processos no mundo físico, muitas vezes criamos máquinas que ajudam
a realizar as partes que se repetem.
Por exemplo, pense no processo de limpar as roupas que usamos para podermos usá-las novamente.
Este processo envolve lavar as roupas com água e sabão, secar as roupas lavadas e, finalmente, passar
as roupas para evitar que fiquem amassadas. Para reduzir a quantidade de trabalho necessária para
realizar esse processo, foi inventada uma máquina de lavar, que realiza a lavagem das roupas. Uma
pessoa que tem uma máquina de lavar pode usá-la repetidamente e, sempre que precisa de roupas
limpas, precisa apenas se encarregar de enxugar e passar as roupas; a lavagem fica por conta da
máquina. Dizemos inclusive que a função da máquina é lavar as roupas.
As funções e os procedimentos funcionam desta forma, capturando partes menores de um algoritmo
que podem ser utilizadas por outros algoritmos, economizando o trabalho de sempre refazer uma
determinada tarefa.
5.1.1 Um exemplo
Vejamos um exemplo. Queremos criar um programa que calcule a média final de um aluno em uma
disciplina com três provas; o programa deve pedir ao usuário que entre com as notas das três provas,
calcular a média aritmética das três notas e apresentar o resultado. O programador que resolveu o
problema decidiu imprimir um separador na tela entre cada entrada de dados e o resultado final, desta
forma, produzindo o seguinte código:
Código fonte
Cálculo da média de um aluno em uma disciplina
1 #include <stdio.h>
2
3 int main () {
4 float nota1, nota2, nota3, media;
64 / 92
Introdução à Programação
9 printf("\n"); // 1x
10 printf("=============================================\n"); // 2x
11 printf("\n"); // 3x
12
16 printf("\n");
17 printf("=============================================\n");
18 printf("\n");
19
23 printf("\n");
24 printf("=============================================\n");
25 printf("\n");
26
30 return 0;
31 }
x, 2x, 3x Código
1 que imprime um separador na tela.
=============================================
=============================================
=============================================
Media: 8.200000
É fácil de notar que o código usado para imprimir um separador se repete três vezes no programa. Po-
demos melhorar esse programa utilizando um procedimento que terá a tarefa de imprimir o separador.
Isso teria o seguinte resultado:
Código fonte
Cálculo da média usando procedimento
65 / 92
Introdução à Programação
1 #include <stdio.h>
2
3 void imprime_separador() { // 1x
4 printf("\n"); // 2x
5 printf("=============================================\n"); // 3x
6 printf("\n"); // 4x
7 }
8
9 int main () {
10 float nota1, nota2, nota3, media;
11
15 imprime_separador(); // 5x
16
20 imprime_separador();
21
25 imprime_separador();
26
30 return 0;
31 }
x
1 Definição do procedimento imprime_separador.
x, 3x, 4x Código
2 no corpo do procedimento.
x
5 Chamada do procedimento imprime_separador.
66 / 92
Introdução à Programação
5.2 Parâmetros
O exemplo usando imprime_separador é o caso mais simples, mas menos interessante do uso
de procedimentos e funções, quando o código a ser reutilizado é sempre o mesmo. Na maioria das
situações de interesse, queremos utilizar uma função ou procedimento em situações com algumas
diferenças. Para tornar um procedimento (ou função) mais flexível, é preciso que informações sejam
passadas para o procedimento. Isso é feito com o uso de parâmetros.
Já vimos muitas vezes o uso de procedimentos com parâmetros. Por exemplo, printf é um proce-
dimento da biblioteca padrão da linguagem C que imprime a string passada como parâmetro. Assim,
printf("ola, mundo!");
14 int main () {
15 float nota1, nota2, nota3, media;
16
20 imprime_separador(nota1); // 2x
21
25 imprime_separador(nota2);
26
30 imprime_separador(nota3);
31
67 / 92
Introdução à Programação
35 imprime_separador(media);
36
37 return 0;
38 }
x
1 Definição do procedimento imprime_separador, com o parâmetro nota, do tipo float.
x
2 Chamada do procedimento imprime_separador, passando o argumento nota1.
Media: 7.73
imprime_separador(nota1);
68 / 92
Introdução à Programação
x
1 Atribuição do valor do argumento nota1 para o parâmetro nota.
x
2 Resto do corpo do procedimento imprime_separador.
Este exemplo é apenas uma ilustração; na prática, o chamado do procedimento não funciona exata-
mente desta forma. Em particular, a variável nota, que designa o parâmetro do procedimento, só
existe enquanto o procedimento executa. Isso será detalhado mais tarde.
O uso de parâmetros nos procedimentos os tornam muito mais flexíveis para uso em diferentes situ-
ações. Mas assim como é útil que o código que chama o procedimento comunique informações para
o procedimento chamado, muitas vezes também é útil que o procedimento comunique algo de volta
para o código que o chamou; neste caso, passamos dos procedimentos para as funções.
Até agora só temos usado procedimentos como ferramenta de modularização do código, mas muitas
vezes é útil chamar um procedimento que retorna alguma informação de volta para o código que o
chamou. Esta é a diferença entre procedimentos e funções: as funções retornam algum valor. Desta
forma, as funções em linguagem C são similares às funções da matemática: uma função como
f : Z → Z, f (x) = x2 + 2
tem um parâmetro x (um inteiro), e retorna um determinado valor que depende do parâmetro passado;
por exemplo,
f (5) = 52 + 2 = 27
É fácil escrever a mesma função em linguagem C:
int f(int x) { // 1x
return x * x + 2; // 2x
}
69 / 92
Introdução à Programação
x
1 Definição da função f. A definição começa com int f(...), significando que o tipo de
retorno da função é int.
x
2 A palavra-chave return determina o valor de retorno da função f, que será o resultado da
expressão x * x + 2.
A função f do exemplo faz o mesmo que a versão matemática: dado o valor do parâmetro x, retorna
um valor que é igual a x ao quadrado, somado a dois. Note que é preciso especificar o tipo do valor
que é retornado pela função e, por isso, toda função começa com o tipo de retorno antes do nome.
Especificar os tipos dos parâmetros e o tipo do valor de retorno também é similar às funções na
matemática, para as quais devemos especificar os conjuntos domínio e contra-domínio.
Neste ponto pode surgir uma pergunta: se é preciso especificar o tipo do valor retornado por uma
função antes do nome da função (por exemplo int f(...)), por que nos procedimentos usa-se a
palavra-chave void?
A verdade é que, embora algumas linguagens de programação façam uma distinção entre procedi-
mentos e funções, na linguagem C existem apenas funções. Como a diferença entre procedimentos e
funções é apenas o fato de retornar ou não um valor, os procedimentos em C são considerados funções
que retornam um valor vazio. É isso que significa o void no início da definição de um procedimento
como imprime_separador, que vimos anteriormente; a rigor, imprime_separador é uma
função, mas retorna um valor vazio, ou seja, nenhum valor. O tipo void na linguagem C é um tipo
especial que denota a ausência de valores.
Como procedimentos em C são funções, também é possível usar return em procedimentos, mas
apenas para terminar sua execução e retornar imediatamente. Isso às vezes é útil para terminar um
procedimento em pontos diferentes do seu final. Também pode-se utilizar return ao final do pro-
cedimento, mas este uso é supérfluo e não é recomendado.
O seguinte exemplo demonstra o uso do return em procedimentos. Continuando no tema relaci-
onado ao cálculo de médias, queremos detectar se uma das notas entradas pelo usuário é uma nota
inválida antes de fazer o cálculo da média. Neste caso, o programa deve apenas imprimir se algum
valor negativo foi entrado pelo usuário. O procedimento possui_negativo será responsável por
imprimir uma mensagem caso um dos valores seja negativo.
Código fonte
Uso de return em um procedimento
1 #include <stdio.h>
2
70 / 92
Introdução à Programação
13
22 int main() {
23 float nota1, nota2, nota3;
24
32 return 0;
33 }
x
1 Uso do return para sair prematuramente do procedimento.
x
2 Este comando de impressão será executado se nenhuma das condições testadas em
possui_negativo for verdade, ou seja, se nenhum dos valores dos parâmetros for nega-
tivo.
O procedimento possui_negativo deve verificar se um dos três números passados como argu-
mentos, mas basta achar um número entre eles para que o resultado possa ser impresso imediatamente
e o procedimento pode retornar; por isso, usamos return assim que o primeiro valor negativo é en-
contrado.
Esse exemplo ainda tem um problema: como pode ser visto nos exemplos de execução, mesmo que
o usuário entre um valor negativo, a média aritmética das três notas ainda é impressa na tela (o
usuário apenas é avisado que um dos valores foi negativo). Isso é uma indicação que seria melhor que
possui_negativo fosse uma função, e que o programa principal verificasse o valor retornado e
tomasse uma decisão. Se fizermos essas alterações ficamos com o seguinte programa:
Código fonte
Reescrevendo o exemplo anterior para usar uma função
71 / 92
Introdução à Programação
1 #include <stdio.h>
2
7 return 0; // 4x
8 }
9
10 int main() {
11 float nota1, nota2, nota3;
12
21 return 0;
22 }
x
1 A função possui_negativo agora retorna um inteiro de valor 1 caso um dos valores dos
parâmetros seja negativo, e 0 caso contrário (todos são positivos).
x
2 Teste para identificar se um ou mais dos parâmetros informados são negativos.
x
3 A função retorna 1 se um dos números passados para a função for negativo.
x
4 Caso nenhum dos números seja negativo, o controle passa para o comando return ao final da
função e o valor 0 é retornado para indicar que nenhum número negativo foi encontrado.
x
5 O programa principal verifica o valor de retorno da função possui_negativo e imprime
informações adequadas na tela para cada caso.
Como pode-se ver nos dois exemplos de execução do programa, a saída agora é mais adequada.
Caso uma das notas informadas seja negativa, o programa não imprime um valor de média, apenas
avisando o usuário do erro na entrada de dados. O código da função possui_negativo também
foi simplificado pelo uso do operador lógico OU.
72 / 92
Introdução à Programação
Nesta seção, veremos mais um exemplo do uso de funções em um programa para calcular as raízes
de uma equação de segundo grau. Neste exemplo, as funções não serão utilizadas várias vezes, mas o
programa principal será mais claro e mais fácil de entender graças à melhor modularização conseguida
com o uso das funções.
Lembremos que um polinômio de segundo grau é uma soma de três termos em potências de uma
variável, por exemplo
P(x) = ax2 + bx + c
onde a, b e c são coeficientes constantes. Uma equação de segundo grau é formada ao igualar um
polinômio de segundo grau a zero, ou seja
ax2 + bx + c = 0
Sabemos que uma equação de segundo grau pode ter até duas raízes reais; cada raiz é um valor
da variável x que satisfaz a equação. Essas raízes podem ser encontradas pela chamada fórmula de
Bhaskara. A fórmula consiste em calcular um valor auxiliar chamado de ∆ (delta), e usar o valor
calculado para identificar quantas raízes reais distintas podem ser encontradas para a equação:
x2 − 5x + 6 = 0
73 / 92
Introdução à Programação
16 int main() {
17 float a, b, c;
18 float delta;
19
32 return 0;
33 }
x
1 Função que calcula o valor do ∆.
x
2 Função que calcula a raiz positiva da equação.
x
3 Função que calcula a raiz negativa da equação.
74 / 92
Introdução à Programação
Podemos ver neste exemplo funções para calcular o valor do ∆ e das duas raízes da equação. O
programa obtém o valor do ∆ e verifica se a equação tem nenhuma, uma ou duas raízes, e imprime o
resultado de acordo com isso. Embora cada função seja usada apenas uma vez, o programa principal
é mais claro e mais fácil de entender porque cada função faz uma parte do processo necessário, ao
invés de ter todo o código junto na função main. Funções são importantes não só para reutilizar
código e diminuir esforço de programação, mas também para melhorar a modularização do programa
e torná-lo mais fácil de ser lido. Em situações práticas, muitas vezes é necessário ler um código que
já foi produzido antes e entendê-lo, seja para consertar defeitos encontrados ou para extender suas
funcionalidades. Tornar um programa mais legível auxilia e reduz o custo relacionado à manutenção
do mesmo.
Entretanto, este último exemplo pode parecer estranho do ponto de vista da modularização, já
que duas de suas funções são quase idênticas. As funções que calculam o valor das raízes,
raiz_positiva e raiz_negativa, mudam apenas em uma operação. Podemos pensar em
como reescrever o programa para usar apenas uma função ao invés de duas funções quase idênticas.
A repetição desnecessária de código pode ser um problema para a manutenção de um programa.
A chave para criar uma só função que calcula os dois valores é criar um novo parâmetro que indica
qual das duas raízes deve ser calculada. Vamos usar um parâmetro chamado sinal que indica, pelo
seu valor, se será usada uma soma ou subtração no cálculo da raiz. Se sinal for 1, será usada uma
soma, e se for -1 será usada uma subtração. O código resultante é mais compacto e evita repetições:
Código fonte
Cálculo de raízes de uma equação de segundo grau
1 #include <stdio.h>
2 #include <math.h>
3
15 int main() {
16 float a, b, c;
17 float delta;
75 / 92
Introdução à Programação
18
31 return 0;
32 }
Quando trabalhamos com programas compostos por várias funções, nos deparamos com questões
relativas à visibilidade das variáveis em diferentes partes do programa. Ou seja, se uma variável é
visível ou acessível em certas partes de um programa.
Um programador iniciante poderia escrever o seguinte programa para calcular a média aritmética de
três notas:
Código fonte
Cálculo da média usando código incorreto
1 #include <stdio.h>
2
3 float calc_media() {
4 return (nota1 + nota2 + nota3) / 3.0; // 1x
5 }
6
7 int main() {
8 float nota1, nota2, nota3;
9
15 return 0;
16 }
76 / 92
Introdução à Programação
x
1 Esta linha contém erros e o programa não será compilado.
O raciocínio do programador é "se as variáveis nota1, nota2 e nota3 existem na função main
e a função calc_media é chamada dentro de main, as variáveis nota1, nota2 e nota3 não
deveriam ser visíveis dentro de calc_media ?"
Acontece que isso não é válido na linguagem C, e qualquer compilador da linguagem vai acusar
erros de compilação neste programa, avisando que as variáveis nota1, nota2 e nota3 não foram
declaradas.
Para entender como funciona a visibilidade das variáveis em um programa na linguagem C, precisa-
mos falar sobre as regras de escopo desta linguagem. O escopo de uma variável é a parte do programa
na qual ela é visível e pode ser acessada.
A linguagem C usa um conjunto de regras de escopo que recebe o nome de escopo estático ou escopo
léxico. Essas regras são bastante simples de entender e aplicar, como veremos a seguir.
Em programas na linguagem C existem dois tipos de escopo (regiões de visibilidade):
• escopo global;
• escopos locais.
Existe apenas um escopo global e, como indicado pelo seu nome, ele contém elementos que são
visíveis em todo o programa. Já os escopos locais são vários e particulares: basicamente, cada função
define um escopo local que corresponde com o corpo da função.
Desta forma, variáveis declaradas no escopo global (ou seja, "fora"de qualquer função) são visíveis
em todo programa, enquanto variáveis declaradas dentro de uma função são visíveis apenas dentro
da mesma função. No exemplo anterior, as variáveis nota1, nota2 e nota3 são visíveis apenas
dentro da função main, e por isso não podem ser acessadas dentro da função calc_media.
Isso pode ser resolvido mudando as variáveis nota1, nota2 e nota3 para o escopo global, ou seja,
tornando-as variáveis globais, como no seguinte programa:
Código fonte
Cálculo de raízes de uma equação de segundo grau
1 #include <stdio.h>
2
5 float calc_media() {
6 return (nota1 + nota2 + nota3) / 3.0; // 2x
7 }
8
9 int main() {
10 printf("Entre as três notas: ");
11 scanf("%f %f %f", ¬a1, ¬a2, ¬a3); // 3x
12
15 return 0;
16 }
77 / 92
Introdução à Programação
x
1 Declaração das variáveis nota1, nota2 e nota3 como variáveis globais. Note que elas estão
declaradas "fora"de qualquer função.
x
2 Código dentro de calc_media que usa as variáveis globais. Neste programa, as variáveis
estão visíveis e não ocorrerá um erro durante a compilação.
x
3 Código dentro de main que usa as variáveis globais. Variáveis globais são visíveis em todo o
programa, incluindo na função principal.
Este programa agora compila corretamente e funciona para o cálculo da média. Mas é importante
observar que esse tipo de prática não é recomendada. Entre as boas práticas da programação está a
sugestão de usar variáveis globais apenas quando absolutamente necessário. Como variáveis globais
podem ser acessadas e ter seu valor alterado por qualquer parte do programa, fica difícil saber que
partes podem influenciar ou serem influenciadas pelas variáveis globais, o que torna todo o programa
mais difícil de entender. Para o exemplo das notas, é melhor e mais de acordo com boas práticas de
programação comunicar as notas para a função calc_media usando parâmtros, como segue:
Código fonte
Cálculo de raízes de uma equação de segundo grau
1 #include <stdio.h>
2
7 int main() {
8 float nota1, nota2, nota3;
9
15 return 0;
16 }
Uma pergunta que pode surgir (especialmente após o exemplo anterior) é “qual o escopo dos parâme-
tros das funções?” A resposta é simples: para questões de visibilidade, o escopo dos parâmetros das
funções é o escopo local da função da qual eles pertencem. Ou seja, os parâmetros de uma função
funcionam exatamente como variáveis locais declaradas dentro da função.
O que acontece se duas variáveis tiverem o mesmo nome em um só programa? A resposta depende
de onde as variáveis em questão são declaradas.
78 / 92
Introdução à Programação
Não podem existir duas variáveis de mesmo nome em um mesmo escopo; um programa que tente
declarar duas variáveis de mesmo nome no mesmo escopo ocasionará um erro quando for compilado.
Assim, não podem existir duas variáveis globais de nome x ou duas variáveis de nome y em uma
mesma função.
Em escopos diferentes a regra muda: variáveis de mesmo nome podem existir em um programa se
forem declarados em escopos distintos. Isso é bastante útil: imagine um programa com 20 ou 30
mil linhas de código (o que hoje em dia é considerado um programa de pequeno a médio porte); um
programa deste tamanho precisa usar um grande número de variáveis, se cada uma delas precisasse
ter um nome diferente de todas as outras, seria muito difícil dar nomes a vários milhares de variáveis.
Imagine que um programa deste tamanho pode ter mais de mil laços for, cada um com uma variável
de controle, e cada uma dessas variáveis teria que ter um nome diferente. Por isso, as regras de
escopo também são úteis para estabelecer espaços locais onde os nomes não entram em conflitos com
os nomes de outros escopos locais.
Quando temos duas variáveis de mesmo nome em diferentes escopos locais, ou seja, duas funções
diferentes, o resultado é simples, já que essas variáveis de mesmo nome nunca seriam visíveis no
mesmo local do programa. Mas e se tivermos duas variáveis de mesmo nome, sendo uma variável
local e uma global? Neste caso, dentro da função que declara a variável com mesmo nome da global,
existirão duas variáveis que poderiam ser visíveis com o mesmo nome. O que acontece nesses casos é
chamado de sombreamento: a variável do escopo local esconde a variável do escopo global. Vamos
ilustrar essa regra com um exemplo:
Código fonte
Exemplo de sombreamento de variáveis globais
1 #include <stdio.h>
2
3 int x = 5; // 1x
4
5 void f() {
6 int x = 60; // 2x
7 int y = x * x; // 3x
8
12 int g() {
13 int y = x * x; // 4x
14
15 return y;
16 }
17
18 int main() {
19 f();
20
23 return 0;
24 }
x
1 Declaração da variável global x.
79 / 92
Introdução à Programação
x
2 Declaração da variável local x na função f.
x
3 Declaração da variável local y na função f.
x
4 Declaração da variável local y na função g.
Vemos no exemplo que existe uma variável global chamada x e uma variável local x na função f.
A função f também tem uma variável local chamada y, e há uma variável local de mesmo nome na
função g. As variáveis chamadas y em f e g não interferem, pois são escopos totalmente diferentes.
Já as variáveis chamadas x interferem, já que uma está no escopo global e outra está no escopo local
da função f. A questão é: o que é impresso pelo programa? Isso depende dos valores de x dentro
da função f e na função g (que usa x para calcular o valor de y, que é retornado). A execução do
programa imprime o seguinte:
Resultado da execução do programa code/cap5/sombra.c
x = 60, y = 3600
g = 25
A primeira linha é o que é impresso na função f. Como existe uma variável local x declarada em f,
dentro da função f a variável x tem o valor 60, como declarado; o valor de y calculado em f é, então,
60 × 60 = 3600. Já na função g não existe uma variável x local, então o valor de x dentro de g é o
valor da variável global x, que é igual a 5; desta forma, y em g tem valor 5 × 5 = 25. Isso explica a
saída do programa como visto acima.
Nota
Uma consequência da regra de sombreamento é que dentro de funções que tenham variáveis
locais que escondem variáveis globais de mesmo nome, é impossível acessar ou utilizar
as variáveis globais escondidas. No exemplo anterior, dentro da função f é impossível ter
acesso à variável global x.
Com o que vimos até agora sobre parâmetros de funções, eles funcionam de maneira simples: o
código que chama uma função especifica expressões para cada um dos argumentos da função. Os
valores de cada expressão são calculados e transmitidos como o valor dos parâmetros declarados na
função.
Entretanto, isso não é suficiente para todos os casos em que podemos querer usar parâmetros. Por
exemplo, digamos que temos uma situação em que é necessário trocar o valor de duas variáveis, e
que isso é necessário várias vezes ao longo do programa. Para evitar repetição, a melhor solução é
escrever uma função que realiza a troca do valor de duas variáveis. O exemplo a seguir mostra o que
acontece quando tentamos fazer isso apenas com o que vimos até agora:
Código fonte
Tentativa de trocar o valor de duas variáveis usando uma função
1 #include <stdio.h>
2
80 / 92
Introdução à Programação
4 int temp = a;
5 a = b;
6 b = temp;
7
11 int main() {
12 int x = 5;
13 int y = 7;
14
A passagem de parâmetros por valor é a situação padrão na linguagem C. Este modo de passagem de
parâmetros comunica apenas valores entre o código chamador e a função chamada.
A passagem por valor funciona da seguinte forma: para uma função f com N parâmetros, uma cha-
mada de f deve conter N expressões como argumentos (se o número de argumentos não corresponder
ao número de parâmetros declarados, o compilador acusará um erro no programa). Então o seguinte
processo de chamada de função acontece:
1. O valor de cada uma das N expressões usadas como argumento é calculado e guardado;
2. N variáveis locais são criadas para a função chamada, uma para cada parâmetro da função, e
usando o nome declarado na função;
Como as variáveis criadas para os parâmetros são locais, elas deixam de existir quando a função
chamada termina, e isso não tem nenhum efeito nas expressões que foram usadas para atribuir valor
aos parâmetros ao início da função. Isso significa que o programa para troca de valor de variáveis
81 / 92
Introdução à Programação
mostrado acima funciona de maneira similar ao seguinte programa (no qual colocamos o código da
função troca_variaveis diretamente na função main):
Código fonte
Troca do valor de duas variáveis usando outra variável temporária
#include <stdio.h>
int main() {
int x = 5;
int y = 7;
// troca_variaveis
int a = x, b = y;
int temp = a;
a = b;
b = temp;
printf("Dentro de troca_variaveis: a = %d, b = %d\n", a, b);
// fim de troca_variaveis
Neste caso, fica claro que as variáveis x e y são usadas apenas para obter o valor inicial das variáveis
a e b, e portanto a mudança de valor das duas últimas não deve afetar x e y.
A passagem de parâmetros por valor é simples e funciona bem na maioria dos casos. Mas em algumas
situações pode ser desejável ter uma forma de afetar variáveis externas à uma determinada função e,
para isso, usa-se a passagem de parâmetros por referência.
A passagem de parâmetros por referência funciona passando para a função chamada referências para
variáveis ao invés de valores de expressões. Isso permite à função chamada afetar as variáveis usadas
como argumento para a função.
Vamos ilustrar como isso funciona demonstrando como criar uma função que troca o valor de duas
variáveis e realmente funciona:
Código fonte
Função para trocar o valor de duas variáveis usando passagem por referência
#include <stdio.h>
82 / 92
Introdução à Programação
int main() {
int x = 5;
int y = 7;
troca_variaveis(&x, &y); // 3x
x
1 Definição do procedimento. Os parâmetros a e b são declarados usando int * ao invés de
simplesmente int. Isso indica passagem por referência.
x
2 Ao usar as variáveis a e b que foram passadas por referência, é necessário usar *a e *b para
acessar ou modificar seu valor.
x
3 Na chamada da função troca_variaveis é preciso passar referências para as variáveis x e
y, isso é conseguido usando &x e &y.
A primeira coisa a notar é que são necessárias algumas mudanças sintáticas para usar parâmetros
por referência. A declaração dos parâmetros no início da função agora define os parâmetros a e b
como tendo tipo int *. Quando esses parâmetros são usados na função troca_variaveis, eles
precisam ser precedidos de asterisco (*a ao invés de a). E para chamar a função, é preciso passar
referências para as variáveis x e y ao invés de passar seu valor, por isso usamos &x e &y na chamada.
De maneira simplificada, a passagem por referência funciona da seguinte forma: ao escrever um
argumento como &x para a função troca_variaveis, não estamos passando o valor de x
para a função, mas sim uma referência para a própria variável x. Isso significa que, dentro de
troca_variáveis, o parâmetro a se torna um nome diferente para a mesma variável x; desta
forma, alterações feitas em a (através da sintaxe *a) são alterações também no valor da variável ori-
ginal x. É por isso que o programa acima funciona, como pode ser visto no resultado da execução:
a função troca_variaveis recebe referências para as variáveis x e y, e por isso pode alterar o
valor destas variáveis diretamente, trocando o valor das duas.
A passagem de parâmetros por referência é usada quando uma função precisa alterar o valor de uma
variável que existe fora da própria função e que não necessariamente é uma variável global. O mesmo
efeito de ter uma função alterando uma variável externa poderia ser atingido usando variáveis globais
ao invés de passagem por referência, mas com grande perda de flexibilidade. Além disso, o uso
desnecessário de variáveis globais não é recomendado, como comentado antes.
Uma outra forma de trocar os valores de duas variáveis, dentro de uma função, poderia ser elabo-
rada utilizando variáveis globais. Por exemplo, poderíamos trocar os valores das variáveis x e y no
exemplo anterior se ambas fossem alteradas para serem variáveis globais:
83 / 92
Introdução à Programação
Código fonte
Função para trocar o valor de duas variáveis globais
#include <stdio.h>
int x = 5; // 1x
int y = 7;
void troca_variaveis() { // 2x
int temp = x;
x = y;
y = temp;
int main() {
printf("Antes da troca: x = %d, y = %d\n", x, y);
troca_variaveis();
printf("Depois da troca: x = %d, y = %d\n", x, y);
}
x
1 x e y agora são variáveis globais.
x
2 troca_variaveis não utiliza parâmetros, já que acessa diretamente as variáveis globais.
O programa funciona, mas note que agora troca_variaveis só altera o valor de duas variáveis
específicas, x e y, enquanto que a versão usando passagem por referência era geral e podia trocar o
valor de quaisquer duas variáveis inteiras. Se um programa precisa trocar os valores de vários pares
de variáveis, em vários locais diferentes, seria preciso criar uma função de troca para cada par, e
fazer todas as variáveis serem globais. Isso acarretaria em muita repetição, e muito uso desnecessário
de variáveis globais que tornariam a manutenção do código muito mais difícil. Neste caso, é muito
mais recomendado usar a passagem por referência para chegar a um código mais geral, mais fácil de
manter e com menos repetição.
Nota
A passagem por referência vem sido usada neste livro há vários capítulos, pois a função
scanf (Seção 2.10.2 [28]) usa esse modo de passagem de parâmetros. Em toda chamada
a scanf passamos referências para as variáveis que vão receber os valores, e não os
valores dessas variáveis. Isso faz sentido já que scanf precisa alterar o valor das variáveis
passadas como parâmetro, e ao mesmo tempo scanf não utiliza o valor original dessas
variáveis para nada.
84 / 92
Introdução à Programação
Nota
Com a passagem por referência e por valor na linguagem C acontece algo semelhante ao
que vimos com os conceitos de procedimento e função: a rigor na linguagem C só existe a
passagem por valor, mas a passagem por referência pode ser obtida pelo uso de ponteiros,
um conceito avançado da linguagem C. Como se trata de um conceito avançado, não vamos
detalhar mais sobre eles aqui neste livro.
Em todos os exemplos que vimos neste capítulo até agora, nós sempre definimos uma função antes
de chamá-la em outra função. Nesses exemplos, a função main sempre aparece no final do arquivo,
já que as chamadas para as outras funções apareciam apenas em main.
Mas em muitos casos pode ser necessário chamar uma função que é definida posteriormente no ar-
quivo, sem precisar mudar as funções de lugar. O mesmo ocorre em programas maiores, quando
usamos vários arquivos de código-fonte para um programa (mas este caso não será detalhado aqui).
Se tentarmos usar uma função antes de sua definição vamos observar um erro de compilação. No
exemplo abaixo, a intenção é definir uma função que imprime a situação de um aluno em uma disci-
plina cuja média é 7.0. Para isso é necessário passar para a função as três notas do aluno na disciplina.
Mas se definirmos a função após main, como abaixo:
Código fonte
Chamando uma função antes de sua definição
#include <stdio.h>
int main() {
float nota1, nota2, nota3;
x
1 A função main chama situacao antes de sua definição.
85 / 92
Introdução à Programação
x
2 A definição de situacao começa após a função main.
Isso acontece porque o compilador não pode identificar se a função foi realmente definida em algum
lugar e, principalmente, se o tipo dos parâmetros e o tipo do retorno da função estão sendo usados
corretamente.
Esse problema pode ser resolvido através do uso de protótipos para declarar uma função antes que
seja definida. Um protótipo de função é uma declaração que especifica as seguintes informações:
Com essas informações, o compilador pode identificar se a função está sendo usada de maneira correta
com relação aos tipos, e evita os erros anteriores.
Para consertar o erro no exemplo anterior basta adicionar uma linha com o protótipo da função:
Código fonte
Usando protótipos de funções
#include <stdio.h>
int main() {
float nota1, nota2, nota3;
86 / 92
Introdução à Programação
x
1 Protótipo da função situacao. Note que o protótipo inclui o tipo de retorno (void), o nome
da função, que a função aceita três parâmetros, e que todos eles possuem tipo float. Não
é preciso especificar o nome dos parâmetros em um protótipo, mas é possível especificar os
nomes, se desejado; incluir os nomes dos parâmetros pode ser útil como uma forma simples de
documentação.
x, 2x A
1 chamada a situacao em main agora pode acontecer sem problema.
Nesse exemplo, seria possível consertar o problema simplesmente movendo a função situacao
completa para antes da função main, mas em programas maiores o uso de protótipos toma grande
importância.
Uma função pode ser chamada a partir de qualquer outra função no mesmo programa. Dessa forma,
podemos pensar em uma função chamando a si mesma. Isso é possível e útil em muitos casos.
Quando uma função f chama a si mesma em algum ponto, dizemos que f é uma função recursiva.
Recursividade se refere a um objeto auto-referente, ou seja, que referencia a si próprio; isso inclui as
funções recursivas mas também outros tipos de objetos.
Um exemplo é o cálculo do fatorial de um número. O fatorial de um número inteiro positivo N (cuja
notação é N!) é definido como o produto de todos os números de 1 até N:
N! = N × (N − 1) × · · · × 2 × 1
N! = N × (N − 1)!
87 / 92
Introdução à Programação
#include <stdio.h>
// prototipo
int fatorial(int); // 1x
int main() {
int n;
return 0;
}
int fatorial(int n) {
if (n == 0 || n == 1)
return 1; // 2x
else
return n * fatorial(n - 1); // 3x
}
x
1 Protótipo da função fatorial.
x
2 Caso-base na função fatorial: para n igual a 0 ou 1, retorna 1.
x
3 Caso recursivo na função fatorial: se n for maior que 1, retorne n multiplicado pelo fatorial
de n - 1.
88 / 92
Introdução à Programação
A falha de segmentação (segmentation fault) mostrada quando se passa um argumento negativo para
a função fatorial é o resultado da recursão infinita que ocorre na função: como em cada etapa
o número é diminuído de um, um número negativo nunca vai chegar a 1 (ou 0) e portanto nunca vai
parar no caso-base. Mesmo a noção matemática do fatorial não está definida para números negativos.
Uma forma de consertar o programa acima seria imprimir um erro quando fatorial fosse chamado
com um argumento negativo, ou retornar um valor qualquer, sem chamar a função recursivamente.
Uma forma de fazer isso seria mudando o teste do caso base:
Código fonte
Cálculo do fatorial com alteração do teste do caso base
#include <stdio.h>
// prototipo
int fatorial(int);
int main() {
int n;
return 0;
}
int fatorial(int n) {
if (n <= 1) // 1x
return 1;
else
return n * fatorial(n - 1);
}
x
1 Caso-base da função fatorial. O teste agora é se o parâmetro é menor ou igual a 1, o que
inclui os números negativos. Neste caso, a função retorna 1 sem fazer chamadas recursivas.
Neste caso, a função sempre retorna 1 para números negativos, o que é um resultado estranho, mas
como o fatorial não está definido para números negativos, isso não chega a ser um grande problema.
89 / 92
Introdução à Programação
5.9 Recapitulando
Neste capítulo, vimos uma importante ferramenta para a criação de programas na linguagem C: fun-
ções. As funções (e os procedimentos, que são tratados na linguagem C como funções) possibilitam
a reutilização de trechos de código em várias partes de um programa, e permitem isolar determi-
nadas componentes do programa em unidades mais autocontidas, melhorando a modularização do
programa. É raro que um programa não-trivial na linguagem C não faça uso de funções.
Vimos exemplos do uso de procedimentos (funções que não retornam valores) com e sem parâmetros.
Vimos também como usar parâmetros para funções e como retornar valores a partir de uma função.
As regras que regem a visibilidade de variáveis locais, globais e parâmetros de funções foram apre-
sentadas, assim como os diferentes modos de passagem de parâmetros e como utilizá-los: passagem
por valor e por referência.
Também vimos como declarar funções usando protótipos, para poder utilizar essas funções antes de
sua definição (ou em arquivos diferentes de onde elas estão definidas). Um exemplo simples de função
recursiva foi mostrado, como forma de introdução ao conceito.
Compreender o conteúdo deste capítulo é extremamente importante para aprender a programar bem
na linguagem C.
1. Escreva uma função que calcula a média final de um aluno que fez prova final em uma disci-
plina. A função deve receber a média parcial do aluno (média das notas nas provas regulares da
disciplina) e a nota obtida na prova final. O cálculo para a média final é
MF = 6×MP+4×PF
10 onde MF é a média final, MP é a média parcial e PF é a nota da prova
final. Escreva um programa que utiliza esta função, pedindo os dados necessários ao usuário e
imprimindo o valor da média final na tela.
2. Às vezes é útil limpar a "tela"antes de imprimir informações adicionais no console. Uma forma
de limpar a tela é imprimir mais de 24 linhas em branco (dependendo do tamanho do console).
Em geral, imprimir 30 linhas em branco deve ser suficiente. Escreva uma função (procedi-
mento) que limpe a tela imprimindo 30 linhas em branco.
3. Escreva uma função que recebe dois parâmetros a e b e troca o valor de a com o valor de b se
o valor de a for maior do que o de b; o objetivo é ter, ao final, o menor dos dois valores em a e
o maior em b. Por exemplo, se a = 5 e b = 3, então os valores das duas variáveis devem ser
trocados, mas se a = 2 e b = 7, então a ordem já está correta e não é necessário trocar os
valores. Utilize passagem de parâmetros por referência para poder afetar o valor das variáveis.
Escreva um programa para testar a função.
90 / 92
Introdução à Programação
int x = 5;
int y = 9;
int f(int x) {
int z = x * x;
return z * y;
}
int main() {
int y = 3;
return 0;
}
91 / 92