You are on page 1of 13

4.

Funes
W. Celes e J. L. Rangel

4.1. Definio de funes


As funes dividem grandes tarefas de computao em tarefas menores. Os programas em C geralmente consistem de vrias pequenas funes em vez de poucas de maior tamanho. A criao de funes evita a repetio de cdigo, de modo que um procedimento que repetido deve ser transformado numa funo que, ento, ser chamada diversas vezes. Um programa bem estruturado deve ser pensado em termos de funes, e estas, por sua vez, podem (e devem, se possvel) esconder do corpo principal do programa detalhes ou particularidades de implementao. Em C, tudo feito atravs de funes. Os exemplos anteriores utilizam as funes da biblioteca padro para realizar entrada e sada. Neste captulo, discutiremos a codificao de nossas prprias funes. A forma geral para definir uma funo :
tipo_retornado { corpo da funo } nome_da_funo (lista de parmetros...)

Para ilustrar a criao de funes, consideraremos o clculo do fatorial de um nmero. Podemos escrever uma funo que, dado um determinado nmero inteiro no negativo n, imprime o valor de seu fatorial. Um programa que utiliza esta funo seria:
/* programa que le um numero e imprime seu fatorial */ #include <stdio.h> void fat (int n); /* Funo principal */ int main (void) { int n; scanf("%d", &n); fat(n); return 0; } /* Funo para imprimir o valor do fatorial */ void fat ( int n ) { int i; int f = 1; for (i = 1; i <= n; i++) f *= i; printf("Fatorial = %d\n", f); }

Estruturas de Dados PUC-Rio

4-1

Notamos, neste exemplo, que a funo fat recebe como parmetro o nmero cujo fatorial deve ser impresso. Os parmetros de uma funo devem ser listados, com seus respectivos tipos, entre os parnteses que seguem o nome da funo. Quando uma funo no tem parmetros, colocamos a palavra reservada void entre os parnteses. Devemos notar que main tambm uma funo; sua nica particularidade consiste em ser a funo automaticamente executada aps o programa ser carregado. Como as funes main que temos apresentado no recebem parmetros, temos usado a palavra void na lista de parmetros. Alm de receber parmetros, uma funo pode ter um valor de retorno associado. No exemplo do clculo do fatorial, a funo fat no tem nenhum valor de retorno, portanto colocamos a palavra void antes do nome da funo, indicando a ausncia de um valor de retorno.
void fat (int n) { . . . }

A funo main obrigatoriamente deve ter um valor inteiro como retorno. Esse valor pode ser usado pelo sistema operacional para testar a execuo do programa. A conveno geralmente utilizada faz com que a funo main retorne zero no caso da execuo ser bem sucedida ou diferente de zero no caso de problemas durante a execuo. Por fim, salientamos que C exige que se coloque o prottipo da funo antes desta ser chamada. O prottipo de uma funo consiste na repetio da linha de sua definio seguida do caractere (;). Temos ento:
void fat (int n); int main (void) { . . . } void fat (int n) { . . . } /* obs: nao existe ; na definio */ /* obs: existe ; no prottipo */

A rigor, no prottipo no h necessidade de indicarmos os nomes dos parmetros, apenas os seus tipos, portanto seria vlido escrever: void fat (int);. Porm, geralmente mantemos os nomes dos parmetros, pois servem como documentao do significado de cada parmetro, desde que utilizemos nomes coerentes. O prottipo da funo necessrio para que o compilador verifique os tipos dos parmetros na chamada da funo. Por exemplo, se tentssemos chamar a funo com fat(4.5); o compilador provavelmente indicaria o erro, pois estaramos passando um valor real enquanto a funo espera um valor inteiro. devido a esta necessidade que se exige a incluso do arquivo stdio.h para a utilizao das funes de entrada e sada da biblioteca padro. Neste arquivo, encontram-se, entre outras coisas, os prottipos das funes printf e scanf.
Estruturas de Dados PUC-Rio 4-2

Uma funo pode ter um valor de retorno associado. Para ilustrar a discusso, vamos reescrever o cdigo acima, fazendo com que a funo fat retorne o valor do fatorial. A funo main fica ento responsvel pela impresso do valor.
/* programa que le um numero e imprime seu fatorial (verso 2) */ #include <stdio.h> int fat (int n); int main (void) { int n, r; scanf("%d", &n); r = fat(n); printf("Fatorial = %d\n", r); return 0; } /* funcao para calcular o valor do fatorial */ int fat (int n) { int i; int f = 1; for (i = 1; i <= n; i++) f *= i; return f; }

4.2. Pilha de execuo


Apresentada a forma bsica para a definio de funes, discutiremos agora, em detalhe, como funciona a comunicao entre a funo que chama e a funo que chamada. J mencionamos na introduo deste curso que as funes so independentes entre si. As variveis locais definidas dentro do corpo de uma funo (e isto inclui os parmetros das funes) no existem fora da funo. Cada vez que a funo executada, as variveis locais so criadas, e, quando a execuo da funo termina, estas variveis deixam de existir. A transferncia de dados entre funes feita atravs dos parmetros e do valor de retorno da funo chamada. Conforme mencionado, uma funo pode retornar um valor para a funo que a chamou e isto feito atravs do comando return. Quando uma funo tem um valor de retorno, a chamada da funo uma expresso cujo valor resultante o valor retornado pela funo. Por isso, foi vlido escrevermos na funo main acima a expresso r = fat(n); que chama a funo fat armazenando seu valor de retorno na varivel r. A comunicao atravs dos parmetros requer uma anlise mais detalhada. Para ilustrar a discusso, vamos considerar o exemplo abaixo, no qual a implementao da funo fat foi ligeiramente alterada:

Estruturas de Dados PUC-Rio

4-3

/* programa que le um numero e imprime seu fatorial (verso 3) */ #include <stdio.h> int fat (int n); int main (void) { int n = 5; int r; r = fat ( n ); printf("Fatorial de %d = %d \n", n, r); return 0; } int fat (int n) { int f = 1.0; while (n != 0) { f *= n; n--; } return f; }

Neste exemplo, podemos verificar que, no final da funo fat, o parmetro n tem valor igual a zero (esta a condio de encerramento do lao while). No entanto, a sada do programa ser:
Fatorial de 5 = 120

pois o valor da varivel n no mudou no programa principal. Isto porque a linguagem C trabalha com o conceito de passagem por valor. Na chamada de uma funo, o valor passado atribudo ao parmetro da funo chamada. Cada parmetro funciona como uma varivel local inicializada com o valor passado na chamada. Assim, a varivel n (parmetro da funo fat) local e no representa a varivel n da funo main (o fato de as duas variveis terem o mesmo nome indiferente; poderamos chamar o parmetro de v, por exemplo). Alterar o valor de n dentro de fat no afeta o valor da varivel n de main. A execuo do programa funciona com o modelo de pilha. De forma simplificada, o modelo de pilha funciona da seguinte maneira: cada varivel local de uma funo colocada na pilha de execuo. Quando se faz uma chamada a uma funo, os parmetros so copiados para a pilha e so tratados como se fossem variveis locais da funo chamada. Quando a funo termina, a parte da pilha correspondente quela funo liberada, e por isso no podemos acessar as variveis locais de fora da funo em que elas foram definidas. Para exemplificar, vamos considerar um esquema representativo da memria do computador. Salientamos que este esquema apenas uma maneira didtica de explicar o que ocorre na memria do computador. Suponhamos que as variveis so armazenadas na memria como ilustrado abaixo. Os nmeros direita representam endereos (posies)
Estruturas de Dados PUC-Rio 4-4

fictcios de memria e os nomes esquerda indicam os nomes das variveis. A figura abaixo ilustra este esquema representativo da memria que adotaremos.

c b a

'x' 43.5 7

112 - varivel c no endereo 112 com valor igual a 'x' 108 - varivel b no endereo 108 com valor igual a 43.5 104 - varivel a no endereo 104 com valor igual a 7

Figura 4.1: Esquema representativo da memria.

Podemos, ento, analisar passo a passo a evoluo do programa mostrado acima, ilustrando o funcionamento da pilha de execuo.
1 - Incio do programa: pilha vazia 2 - Declarao das variveis: n, r 3 - Chamada da funo: cpia do parmetro

r main > main > n

fat > main >

n r n

5 5

4 - Declarao da varivel local: f

5 - Final do lao

6 - Retorno da funo: desempilha

f n fat > r n main >

1.0 5 5

f n fat > r n main >

120.0 0 5

main >

r n

120.0 5

Figura 4.2: Execuo do programa passo a passo.

Isto ilustra por que o valor da varivel passada nunca ser alterado dentro da funo. A seguir, discutiremos uma forma para podermos alterar valores por passagem de parmetros, o que ser realizado passando o endereo de memria onde a varivel est armazenada. Vale salientar que existe outra forma de fazermos comunicao entre funes, que consiste no uso de variveis globais. Se uma determinada varivel global visvel em duas funes, ambas as funes podem acessar e/ou alterar o valor desta varivel diretamente. No
Estruturas de Dados PUC-Rio 4-5

entanto, conforme j mencionamos, o uso de variveis globais em um programa deve ser feito com critrio, pois podemos criar cdigos com uma alto grau de interdependncia entre as funes, o que dificulta a manuteno e o reuso do cdigo.

4.3. Ponteiro de variveis


A linguagem C permite o armazenamento e a manipulao de valores de endereos de memria. Para cada tipo existente, h um tipo ponteiro que pode armazenar endereos de memria onde existem valores do tipo correspondente armazenados. Por exemplo, quando escrevemos:
int a;

declaramos uma varivel com nome a que pode armazenar valores inteiros. Automaticamente, reserva-se um espao na memria suficiente para armazenar valores inteiros (geralmente 4 bytes). Da mesma forma que declaramos variveis para armazenar inteiros, podemos declarar variveis que, em vez de servirem para armazenar valores inteiros, servem para armazenar valores de endereos de memria onde h variveis inteiras armazenadas. C no reserva uma palavra especial para a declarao de ponteiros; usamos a mesma palavra do tipo com os nomes das variveis precedidas pelo caractere *. Assim, podemos escrever:
int *p;

Neste caso, declaramos uma varivel com nome p que pode armazenar endereos de memria onde existe um inteiro armazenado. Para atribuir e acessar endereos de memria, a linguagem oferece dois operadores unrios ainda no discutidos. O operador unrio & (endereo de), aplicado a variveis, resulta no endereo da posio da memria reservada para a varivel. O operador unrio * (contedo de), aplicado a variveis do tipo ponteiro, acessa o contedo do endereo de memria armazenado pela varivel ponteiro. Para exemplificar, vamos ilustrar esquematicamente, atravs de um exemplo simples, o que ocorre na pilha de execuo. Consideremos o trecho de cdigo mostrado na figura abaixo.

/*varivel inteiro */

int a;

/*varivel ponteiro p/ inteiro */

int *p;

p a

112 108 104

Figura 4.3: Efeito de declaraes de variveis na pilha de execuo.

Estruturas de Dados PUC-Rio

4-6

Aps as declaraes, ambas as variveis, a e p, armazenam valores "lixo", pois no foram inicializadas. Podemos fazer atribuies como exemplificado nos fragmentos de cdigo da figura a seguir:

/* a recebe o valor 5 */

a = 5;

p a

112 108 104

/* p recebe o endereo de a (diz-se p aponta para a) */

p = &a;
/* contedo de p recebe o valor 6 */

p a

104 5

112 108 104

*p = 6;

p a

104 6

112 108 104

Figura 4.4: Efeito de atribuio de variveis na pilha de execuo.

Com as atribuies ilustradas na figura, a varivel a recebe, indiretamente, o valor 6. Acessar a equivalente a acessar *p, pois p armazena o endereo de a. Dizemos que p aponta para a, da o nome ponteiro. Em vez de criarmos valores fictcios para os endereos de memria no nosso esquema ilustrativo da memria, podemos desenhar setas graficamente, sinalizando que um ponteiro aponta para uma determinada varivel.

p a 6

Figura 4.5: Representao grfica do valor de um ponteiro.

A possibilidade de manipular ponteiros de variveis uma das maiores potencialidades de C. Por outro lado, o uso indevido desta manipulao o maior causador de programas que "voam", isto , no s no funcionam como, pior ainda, podem gerar efeitos colaterais no previstos.

Estruturas de Dados PUC-Rio

4-7

A seguir, apresentamos outros exemplos de uso de ponteiros. O cdigo abaixo:


int main ( void ) { int a; int *p; p = &a; *p = 2; printf(" %d ", a); return; }

imprime o valor 2. Agora, no exemplo abaixo:


int main ( void ) { int a, b, *p; a = 2; *p = 3; b = a + (*p); printf(" %d ", b); return 0; }

cometemos um ERRO tpico de manipulao de ponteiros. O pior que esse programa, embora incorreto, s vezes pode funcionar. O erro est em usar a memria apontada por p para armazenar o valor 3. Ora, a varivel p no tinha sido inicializada e, portanto, tinha armazenado um valor (no caso, endereo) "lixo". Assim, a atribuio *p = 3; armazena 3 num espao de memria desconhecido, que tanto pode ser um espao de memria no utilizado, e a o programa aparentemente funciona bem, quanto um espao que armazena outras informaes fundamentais por exemplo, o espao de memria utilizado por outras variveis ou outros aplicativos. Neste caso, o erro pode ter efeitos colaterais indesejados. Portanto, s podemos preencher o contedo de um ponteiro se este tiver sido devidamente inicializado, isto , ele deve apontar para um espao de memria onde j se prev o armazenamento de valores do tipo em questo. De maneira anloga, podemos declarar ponteiros de outros tipos:
float *m; char *s;

Passando ponteiros para funes Os ponteiros oferecem meios de alterarmos valores de variveis acessando-as indiretamente. J discutimos que as funes no podem alterar diretamente valores de variveis da funo que fez a chamada. No entanto, se passarmos para uma funo os valores dos endereos de memria onde suas variveis esto armazenadas, a funo pode alterar, indiretamente, os valores das variveis da funo que a chamou.

Estruturas de Dados PUC-Rio

4-8

Vamos analisar o uso desta estratgia atravs de um exemplo. Consideremos uma funo projetada para trocar os valores entre duas variveis. O cdigo abaixo:
/* funcao troca (versao ERRADA) */ #include <stdio.h> void troca (int x, int y ) { int temp; temp = x; x = y; y = temp; } int main ( void ) { int a = 5, b = 7; troca(a, b); printf("%d %d \n", a, b); return 0; }

no funciona como esperado (sero impressos 5 e 7), pois os valores de a e b da funo main no so alterados. Alterados so os valores de x e y dentro da funo troca, mas eles no representam as variveis da funo main, apenas so inicializados com os valores de a e b. A alternativa fazer com que a funo receba os endereos das variveis e, assim, alterar seus valores indiretamente. Reescrevendo:
/* funcao troca (versao CORRETA) */ #include <stdio.h> void troca (int *px, int *py ) { int temp; temp = *px; *px = *py; *py = temp; } int main ( void ) { int a = 5, b = 7; troca(&a, &b); /* passamos os endereos das variveis */ printf("%d %d \n", a, b); return 0; }

A Figura 4.6 ilustra a execuo deste programa mostrando o uso da memria. Assim, conseguimos o efeito desejado. Agora fica explicado por que passamos o endereo das variveis para a funo scanf, pois, caso contrrio, a funo no conseguiria devolver os valores lidos.

Estruturas de Dados PUC-Rio

4-9

1 -Declarao das variveis: a, b

2 - Chamada da funo: passa endereos


120

main

b a >

7 5

112 108 104

troca main

py px >b a >

108 104 7 5

116 112 108 104

3 - Declarao da varivel local: temp


temp py troca main px > b a > 108 104 7 5 120 116 112 108 104

4 - temp recebe contedo de px


temp py px troca >b a main > 5 108 104 7 5 120 116 112 108 104

5 -Contedo de px recebe contedo de py


temp py px troca >b main > a 5 108 104 7 7 120 116 112 108 104

6 -Contedo de py recebe temp


temp py px troca >b main > a 5 108 104 5 7 120 116 112 108 104

Figura 4.6: Passo a passo da funo que troca dois valores.

4.4. Recursividade
As funes podem ser chamadas recursivamente, isto , dentro do corpo de uma funo podemos chamar novamente a prpria funo. Se uma funo A chama a prpria funo A, dizemos que ocorre uma recurso direta. Se uma funo A chama uma funo B que, por sua vez, chama A, temos uma recurso indireta. Diversas implementaes ficam muito mais fceis usando recursividade. Por outro lado, implementaes no recursivas tendem a ser mais eficientes. Para cada chamada de uma funo, recursiva ou no, os parmetros e as variveis locais so empilhados na pilha de execuo. Assim, mesmo quando uma funo chamada recursivamente, cria-se um ambiente local para cada chamada. As variveis locais de chamadas recursivas so independentes entre si, como se estivssemos chamando funes diferentes.
Estruturas de Dados PUC-Rio 4-10

As implementaes recursivas devem ser pensadas considerando-se a definio recursiva do problema que desejamos resolver. Por exemplo, o valor do fatorial de um nmero pode ser definido de forma recursiva:

1, se n = 0 n!= n (n 1)!, se n > 0 Considerando a definio acima, fica muito simples pensar na implementao recursiva de uma funo que calcula e retorna o fatorial de um nmero.
/* Funo recursiva para calculo do fatorial */ int fat (int n) { if (n==0) return 1; else return n*fat(n-1); }

4.5. Variveis estticas dentro de funes**


Podemos declarar variveis estticas dentro de funes. Neste caso, as variveis no so armazenadas na pilha, mas sim numa rea de memria esttica que existe enquanto o programa est sendo executado. Ao contrrio das variveis locais (ou automticas), que existem apenas enquanto a funo qual elas pertencem estiver sendo executada, as estticas, assim como as globais, continuam existindo mesmo antes ou depois de a funo ser executada. No entanto, uma varivel esttica declarada dentro de uma funo s visvel dentro dessa funo. Uma utilizao importante de variveis estticas dentro de funes quando se necessita recuperar o valor de uma varivel atribuda na ltima vez que a funo foi executada. Para exemplificar a utilizao de variveis estticas declaradas dentro de funes, consideremos uma funo que serve para imprimir nmeros reais. A caracterstica desta funo que ela imprime um nmero por vez, separando-os por espaos em branco e colocando, no mximo, cinco nmeros por linha. Com isto, do primeiro ao quinto nmero so impressos na primeira linha, do sexto ao dcimo na segunda, e assim por diante.
void imprime ( float a ) { static int n = 1; printf(" %f ", a); if ((n % 5) == 0) printf(" \n "); n++; }

Se uma varivel esttica no for explicitamente inicializada na declarao, ela automaticamente inicializada com zero. (As variveis globais tambm so, por default, inicializadas com zero.)
Estruturas de Dados PUC-Rio 4-11

4.6. Pr-processador e macros**


Um cdigo C, antes de ser compilado, passa por um pr-processador. O pr-processador de C reconhece determinadas diretivas e altera o cdigo para, ento, envi-lo ao compilador. Uma das diretivas reconhecidas pelo pr-processador, e j utilizada nos nossos exemplos, #include. Ela seguida por um nome de arquivo e o pr-processador a substitui pelo corpo do arquivo especificado. como se o texto do arquivo includo fizesse parte do cdigo fonte. Uma observao: quando o nome do arquivo a ser includo envolto por aspas ("arquivo"), o pr-processador procura primeiro o arquivo no diretrio atual e, caso no o encontre, o procura nos diretrios de include especificados para compilao. Se o arquivo colocado entre os sinais de menor e maior (<arquivo>), o pr-processador no procura o arquivo no diretrio atual. Outra diretiva de pr-processamento que muito utilizada e que ser agora discutida a diretiva de definio. Por exemplo, uma funo para calcular a rea de um crculo pode ser escrita da seguinte forma:
#define PI 3.14159

float area (float r) { float a = PI * r * r; return a; }

Neste caso, antes da compilao, toda ocorrncia da palavra PI (desde que no envolvida por aspas) ser trocada pelo nmero 3.14159. O uso de diretivas de definio para representarmos constantes simblicas fortemente recomendvel, pois facilita a manuteno e acrescenta clareza ao cdigo. C permite ainda a utilizao da diretiva de definio com parmetros. vlido escrever, por exemplo:
#define MAX(a,b) ((a) > (b) ? (a) : (b))

assim, se aps esta definio existir uma linha de cdigo com o trecho:
v = 4.5; c = MAX ( v, 3.0 );

o compilador ver:
v = 4.5; c = ((v) > (4.5) ? (v) : (4.5));

Estas definies com parmetros recebem o nome de macros. Devemos ter muito cuidado na definio de macros. Mesmo um erro de sintaxe pode ser difcil de ser detectado, pois o
Estruturas de Dados PUC-Rio 4-12

compilador indicar um erro na linha em que se utiliza a macro e no na linha de definio da macro (onde efetivamente encontra-se o erro). Outros efeitos colaterais de macros mal definidas podem ser ainda piores. Por exemplo, no cdigo abaixo:
#include <stdio.h> #define DIF(a,b) a - b

int main (void) { printf(" %d ", 4 * DIF(5,3)); return 0; }

o resultado impresso 17 e no 8, como poderia ser esperado. A razo simples, pois para o compilador (fazendo a substituio da macro) est escrito:
printf(" %d ", 4 * 5 - 3);

e a multiplicao tem precedncia sobre a subtrao. Neste caso, parnteses envolvendo a macro resolveriam o problema. Porm, neste outro exemplo que envolve a macro com parnteses:
#include <stdio.h> #define PROD(a,b) (a * b)

int main (void) { printf(" %d ", PROD(3+4, 2)); return 0; }

o resultado 11 e no 14. A macro corretamente definida seria:


#define PROD(a,b) ((a) * (b))

Conclumos, portanto, que, como regra bsica para a definio de macros, devemos envolver cada parmetro, e a macro como um todo, com parnteses.

Estruturas de Dados PUC-Rio

4-13

You might also like