Em geral, os problemas podem ser divididos em subproblemas. A solução de um problema pode ser obtido combinando-se as soluções dos seus subproblemas.
Este tipo de estruturação permite que a solução de um problema seja organizado através de módulos.
No caso de desenvolvimento de programas de computador, o que queremos dizer é que, em geral, é mais fácil construir um programa a partir de pequenos pedaços ou módulos.
Função (ou também procedimento, rotina, etc) é um mecanismo de programação que permite a soluçao de um problema combinando-se as soluções para os seus subproblemas.
Vamos tentar entender o conceito de funções analisando um exemplo:
Problema: Dados dois reais x e y e
inteiros a e b, calcular
.
Quais subproblemas (módulos) semelhantes existem nesse problema ? Três potências: um número real elevado a um número inteiro !
Podemos criar uma função para calcular um número real elevado a um número inteiro positivo. Como usar esta função para resolver o problema ?
O formato de definição de uma função é dado a seguir (note semelhanças com o famoso int main()).
tipo_do_valor_de_retorno nome_da_função( lista_de_parâmetros ) { /* declarações de variáveis */ /* comandos */ return valor_de_retorno ; }
Uma função para calcular o valor de um real elevado a um inteiro poderia ser definido como segue:
double pot(double x, int a) { double res=1.0 ; int i ; for( i=1; i<=a; i++ ) { res = res*a ; } return res ; }
Variáveis definidas dentro de uma função (variáveis locais), assim como os seus parâmetros só podem ser referenciados dentro da função. Isto é, nenhuma outra função pode fazer referência às variáveis locais e aos parâmetros da função.
Uma função não precisa necessariamente retornar um valor. Para definir uma função que não retorna valor, utiliza-se void. A lista de parâmetros de uma função pode ser vazia. Por exemplo, a função abaixo não retorna valor e não possui parâmetros.
void hello() { printf("Hello world\n") ; }
Uma vez definidas, funções podem ser ``chamadas'' de vários lugares de um programa. Qualquer chamada de uma função deve passar argumentos (argumento é o valor do parâmetro) correspondentes aos seus parâmetros. Mais adiante veremos onde definir as funções.
double pot( double, int ) ;
Os tipos entre parênteses indicam que a função pot possui dois parâmetros, de tipo double e int, respectivamente. O double à esquerda de pot indica que o seu valor de retorno é do tipo double. O comando return devolve um valor do tipo double para o programa que chamou a função pot. Esse valor de retorno pode ser dado por uma constante numérica, uma variável, uma função ou uma expressão numérica (que pode envolver contantes numéricas, variáveis e chamadas a funções).
Os protótipos de funções devem ser declarados no programa antes das chamadas. O usual é declará-los antes do int main().
Os protótipos são utilizados pelo compilador para verificar se as chamadas para a função tem tipo correto de valor de retorno, número, tipo e ordem correta de argumentos. Em suma, são utilizadas para validar as chamada das respectivas funções.
Na ausência de protótipos, o compilador ``identifica'' o protótipo na primeira chamada encontrada durante a compilação. Nesse processo, quando os tipos de retorno e dos parâmetros não forem claros, o compilador assume que eles são do tipo int.
z = pot(3.5, 2) ;
2.5 + pot(1.1, 3)*pot(2.0, 4)Neste caso, a situação é análoga a uma expressão do tipo
2.5 + x*yNesta expressão, o número 2.5 é somado ao resultado do produto entre os conteúdos de x e y. Na expressão acima (isto é, em 2.5 + pot(1.1, 3)*pot(2.0, 4)) o número 2.5 é somado ao resultado do produto entre os dois valores de retorno.
if (pot(5.5, 3) > 125) { ... }
O tipo dos argumentos passados numa chamada a uma função deve corresponder aos dos parâmetros especificados na função (a ordem dos argumentos é importante!). Argumentos numéricos podem ser dados por qualquer expressão. Assim, no caso da função pot as seguintes são exemplos de passagem de argumento válidas:
pot(1.0, 2) ; pot(x, 2) ; pot(x, a) ; pot(x-y, a+b) ; pot(pot(x,a), b) ;
Em geral, as variáveis declaradas como parâmetros da função ou declaradas localmente existem na memória apenas enquanto a função estiver em execução. Tão logo a sua execução seja encerrada, o espaço de memória ocupada por essas variáveis é liberado.
01 #include <stdio.h> 02 03 /* prototipo de funções a serem utilizadas */ 04 int pot(double,int) ; 05 06 int main() 07 { 08 double x, y ; 09 int a, b ; 10 11 /* leitura dos dados de entrada */ 12 printf("Digite os valores de x e y: "); 13 scanf("%lf %lf",&x,&y); 14 15 printf("Digite os valores de a e b:"); 16 scanf("%d %d",&a,&b); 17 18 /* calculo e impressao do resultado */ 19 printf("O resultado e' %g\n", pot(x,a)+pot(y,b)+pot(x-y,a+b)) ; 20 21 return 0; 22 } 23 24 /* funcao que calcula um real elevado a um inteiro positivo */ 25 double pot(double x, int a) 26 { 27 double res=1.0 ; 28 int i ; 29 30 for( i=1; i<=a; i++ ) { 31 res = res*a ; 32 } 33 34 return res ; 35 }
Deve-se incluir antes do main o protótipo de todas as funções usadas pelo programa. Todas as funções usadas no programa devem estar definidas em algum lugar (nesse exemplo, estão no mesmo arquivo que contém a definição do main, da linha 25 a linha 35).
Na execução deste programa, a função pot é chamada (e, portanto, executada) três vezes (linha 19 do programa). Os argumentos de uma chamada para outra podem ser diferentes. Nesse exemplo, quando ocorre uma chamada à função pot, a execução do programa (main) é desviada para a parte onde se encontra o código da função pot. Depois que a execução da função pot termina, a execução volta ao ponto do programa (main) imediatamente posterior à chamada da função.
Todos os programas em C contém uma função principal: o main.
Vantagens do uso de funções
Na passagem de parâmetros também ocorre conversão implícita de tipos. Por exemplo, se o parâmetro é do tipo double e um argumento do tipo int é passado, então este é automaticamente convertido para double.
Identificador refere-se a nome de variáveis, funções, contantes simbólicas, ...
O escopo de um identificador é a região do programa na qual ele pode ser referenciado. Por exemplo, parâmetros e variáveis locais de uma função só podem ser referenciados diretamente dentro da própria função. Nenhuma outra função pode fazer referência a eles.
Para entender um pouco o escopo de variáveis, execute o programa abaixo, alterando o valor de x em diferentes lugares do programa.
#include <stdio.h> void a() ; void b() ; int x = 1 ; /* esta variável e' global */ int main() { int x = 5 ; /* esta variavel e' local a funcao main */ printf("Valor de x no bloco externo em main=%d\n", x) ; { /* bloco interno no main */ int x = 7 ; printf("Valor de x no bloco interno do main = %d\n", x) ; } printf("Valor do x no bloco externo em main = %d\n", x) ; printf("Note que o que aconteceu no bloco interno nao afetou o valor de x no bloco externo\n") ; a() ; b() ; a() ; b() ; printf("Valor do x local no main e'%d\n", x) ; return 0 ; } void a() { int x = 25 ; printf("O valor de x quando entrou em a = %d\n", x) ; x++ ; printf("O valor de x antes de sair de a = %d\n", x) ; } void b() { printf("O valor de x (global) quando entrou em b = %d\n", x) ; x = x*10 ; printf("O valor de x (global) antes de sair de b = %d\n", x) ; }
No exemplo acima, existe uma variável global com nome x. Variáveis globais podem ser referenciados em qualquer parte do programa. No entanto, se uma função também possui uma variável local com o mesmo nome x, então dentro desta função a variável global não pode ser referenciada. Em outras palavras, qualquer referência a uma variável dentro de um bloco privilegia primeiramente variáveis locais. Caso uma variável referenciada não seja local, então a mesma é procurada no bloco imediatamente externo e assim por diante.
OBS.: Não se recomenda o uso indiscriminado de variáveis globais.
Um coleção de funções pode ser organizada em uma biblioteca, formando as bibliotecas de funções. As bibliotecas de funções contém várias funções compiladas. Em geral, para cada biblioteca existe um arquivo de cabeçalhos (header file). Por exemplo, o header file da biblioteca de funções matemáticas do C é o arquivo math.h.
Um header file contém, entre outras coisas, definições de constantes simbólicas (os equivalentes aos #define TRUE 1) e os protótipos de funções.
Vejamos como fica a solução de um dos problemas anteriores, que utilizava cálculo de potências, com o uso da função pow
double pow(double x, double y);da biblioteca math.h.
#include <stdio.h> #include <math.h> int main() { double x, y ; int a, b ; /* leitura dos dados de entrada */ printf("Digite os valores de x e y: "); scanf("%lf %lf",&x,&y); printf("Digite os valores de a e b:"); scanf("%d %d",&a,&b); /* calculo e impressao do resultado */ printf("O resultado e' %g\n", pow(x,a)+pow(y,b)+pow(x-y,a+b)) ; return 0; }
Observe que em vez de declarar o protótipo da função pow no início do programa, aqui incluímos o header file math.h.
Observe também que os parâmetros da função pow são dois double (nesse caso ocorre conversão implícita do segundo argumento).
Se você quisesse, você poderia criar uma função que calcula o valor do polinômio dado e chamar esta função no programa principal.
Quais seriam as funções úteis para resolver este problema ?
Para responder essa questão, vamos analisar a lógica de uma solução.
Como calcular ? Basta calcular
.
Falta calcular , onde
é um natural qualquer. Mas isso vocês
já sabem fazer.
Assim, quais seriam as funções para resolver o problema ?
#include <stdio.h> /* declaração de protótipos */ int fatorial(int num); int coef(int m, int n); /* programa principal */ int main() { int n, i ; printf("Digite o valor de n: ") ; scanf("%d", &n) ; for(i=0; i<=n; i++) printf("%d ",coef(n,i)) ; return 0; } /* função que calcula fatorial */ int fatorial(int num) { int fat; fat = 1; while (num > 0) { fat = fat*num; num = num -1; } return fat; } /* função que calcula coeficiente */ int coef(int m, int n) { return fatorial(m)/(fatorial(n)*fatorial(m-n)); }
Em muitas situações é conveniente que uma função altere o valor de variáveis declaradas em uma outra função. Lembrem-se que uma função não pode fazer referência direta a uma variável declarada em uma outra função (entre outras palavras, ela não tem conhecimento das variáveis declaradas em outra função). Portanto ela não pode alterar o valor de tal variável diretamente.
[Para o seu conhecimento] Um mecanismo que possibilita que uma função f1 altere o valor de uma variável x declarada em outra função f2 é a chamada de funções por referência. Nesse caso, a função f1 precisa ter um parâmetro declarado de forma especial. Esta declaração especial indica que o conteúdo da variável passada no lugar desse parâmetro, quando f1 é chamada, poderá ser alterado por f1.
A chamada por referência pode ser contraposta à chamada por valor. Na chamada de função por valor, o valor dos argumentos passados para a função na chamada são copiados para as variáveis dos respectivos parâmetros. Qualquer modificação no conteúdo do parâmetro não altera o valor de nenhuma variável da função que a chamou. Mais adiante vocês poderão ver um exemplo e uma explicação mais pé-no-chão.
Na linguagem C, todas as chamadas de função são por valor. Portanto, para que uma função altere o valor de uma variável declarada em outra função, precisamos entender endereços de variáveis e como alterar o valor em uma certa posição de memória (argh!).
Antes disso, devemos lembrar que variáveis correspondem a uma área da memória do computador, e essa área possui ``endereço'' (na verdade, um número).
Vou introduzir esses conceitos usando um exemplo:
/* Exemplo para ilustrar uso dos operadores & e * */ #include <stdio.h> int main() { int x ; int *px ; /* variavel do tipo apontador (ou ponteiro) */ x = 7 ; /* atribui valor 7 a variavel x */ px = &x ; /* atribui endereco de x a variavel px */ printf("Valor de x = %d\n", x) ; printf("Enderereco de x = %d\n", &x) ; printf("Valor de px = %d\n", px) ; printf("Valor apontado por px, ou seja, valor de *px = %d\n", *px) ; return 0 ; }
O programa acima tem o intuito de ilustrar o uso dos operadores & e *. Inicialmente o valor 7 é atribuído à variável x. Em seguida, o endereço da variável x (isto é, &x) é atribuído à variável px. Veja figura a seguir:
Em seguida, a função printf é executada quatro vezes. Vejamos o que está sendo impresso:
Valor de x = 7
Enderereco de x = 100
Valor de px = 100
Valor apontado por px, ou seja, valor de *px = 7
O que é o tal do valor apontado por px ? Para falar nisso, o valor de px precisa ser um endereço, digamos E. O valor apontado por px (ou seja, *px) é o valor que está no endereço E da memória do computador. Lembre-se que só tem sentido falarmos em valor apontado por px se px é uma variável do tipo apontador. Imaginem o que acontece se E é um endereço inválido...
Às vezes é mais fácil pensarmos em *px como sendo o conteúdo do endereço px, em vez do valor apontado por px.
No programa acima, você saberia dizer o que acontece se uma atribuição do tipo *px = 20; é executada depois de px = &x ; ?
Sim, é isso mesmo. O valor de x passa a ser 20 ( e não mais 7).
VOLTANDO às funções, vamos analisar agora os parâmetros do tipo apontador. O que acontece se o seguinte programa é executado ?
/* Exemplo para ilustrar uso dos operadores & e * na chamada de funcoes */ #include <stdio.h> void testando(int *a) ; int main() { int x ; x = 7 ; /* atribui valor 7 a variavel x */ printf("Valor de x antes da chamada a funcao testando = %d\n", x); testando(&x) ; printf("Valor de x depois da chamada a funcao testando = %d\n", x); return 0 ; } void testando(int *a) { *a = (*a) * (*a) ; }
Vocês sabem responder ? Se sim, muito bem; estamos fazendo progressos ! Se não, pense um pouco mais, não desista.
Só para ter certeza que está tudo bem, o que acontece no próximo programa ? Ele é bem parecido, mas a função chamada é o testando2.
/* Exemplo 2 para ilustrar chamada de funcoes */ #include <stdio.h> void testando2(int a) ; int main() { int x ; x = 7 ; /* atribui valor 7 a variavel x */ printf("Valor de x antes da chamada a funcao testando = %d\n", x); testando2(x) ; printf("Valor de x depois da chamada a funcao testando = %d\n", x); return 0 ; } void testando2(int a) { a = a * a ; }
(não estão relacionados com ponteiros)
Problema: Dados inteiros a e b, verificar se b é permutação dos dígitos de a. Suponha que o dígito 0 não aparece em a e b. Exemplo: 3321315 é permutação de 1532133.
Antes de olhar a solução abaixo, analise bem a questão. Lembre-se: o primeiro passo para resolver a questão é entendê-la. O segundo, é pensar como VOCÊ, um ser humano, resolveria a questão. No terceiro passo você deve estruturar a solução: identifique quais são as entradas e saídas do programa, como organizar de forma lógica a sua solução, quais funções seriam interessantes para organizar melhor a sua solução. Neste passo você pode usar fluxograma, rascunho, enfim, papel e lápis. Voce deveria sentar na frente do computador somente após ter tudo bem estruturado.
#include<stdio.h> #define TRUE 1 #define FALSE 0 int contadigitos(int n, int d); int main() { int a, b, d, permuta ; permuta = TRUE; printf("Entre com os valores de a e b:"); scanf("%d %d",&a,&b); for (d=1; d<=9 && permuta==TRUE; d++) { if (contadigitos(a,d) != contadigitos(b,d)) { permuta = FALSE ; } } if (permuta == TRUE) printf("E permutacao"); else printf("Nao e permutacao"); return 0; } int contadigitos(int n, int d) { int cont; cont = 0; while (n>0) { if (n%10 == d) { cont = cont+1 ; } n = n/10; } return cont; }
Problema: Dado um inteiro positivo , verificar se
pode ser escrito como
, com
e
primos.