Professional Documents
Culture Documents
Funes
W. Celes e J. L. Rangel
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); }
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-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
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
n r n
5 5
5 - Final do lao
1.0 5 5
120.0 0 5
main >
r n
120.0 5
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.
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;
int *p;
p a
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
p = &a;
/* contedo de p recebe o valor 6 */
p a
104 5
*p = 6;
p a
104 6
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
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.
4-7
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.
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.
4-9
main
b a >
7 5
troca main
py px >b a >
108 104 7 5
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); }
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
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
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)
Conclumos, portanto, que, como regra bsica para a definio de macros, devemos envolver cada parmetro, e a macro como um todo, com parnteses.
4-13