Universidade Federal de Minas Gerais
Instituto de Ciências Exatas
Departamento de Ciência da Computação

Algoritmos e Estruturas de Dados I

Funções


As funções correspondem a abstrações para trechos de programas. Podemos pegar um trecho de programa e dar um nome significativo para ele. No ponto que o programador deseja executar aquele trecho de programa ele invoca ou chama aquele trecho. Veremos abaixo alguns dos conceitos envolvidos. A possibilidade de poder abstrair de como implementar um trecho de programa e poder dar um nome a este trecho é um mecanismo muito importante. Todas as linguagens de alto-nível provêm alguma forma de abstração que segue essa descrição. Esta abstração se apresenta com vários nomes: procedimento (procedure), subrotina (subroutine), função (function), método (method), etc.

Os programas desta disciplina, vistos até este ponto, apresentavam todo seu código fonte dentro de uma unidade da Linguagem C com o nome de main (algo parecido com):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char *argv[]){

    /* codigo fonte */
}

Esta unidade é denominada função. A função main() é uma função especial pois é a função que funciona como ponto de entrada do fluxo de controle nos programas C. Quando solicitamos ao sistema operacional a execução de um programa C, o fluxo de controle será passado ao primeiro comando da função main() (como será visto à frente existem mais aspectos a serem considerados).

Antes de podermos chamar ou invocar uma função é necessário que seja feita a declaração da função. Em algum momento antes da execução de um programa todas as funções declaradas devem também estar definidas A definição de uma função envolve vários conceitos que serão ilustrados de forma incremental. Vemos abaixo uma função bastante simples (não define nenhum parâmetro, não tem variáveis locais e não retorna nenhum valor):
#include <stdio.h>                        //1
#include <stdlib.h>                       //2
imprime(){                                //3
  printf("uma funcao simples!\n");        //4
}                                         //5
int main(int argc, char *argv[])          //6
{ imprime();                              //7
  system("PAUSE");                        //8
  return 0;                               //9
}
                                                               //10

No programa acima definimos uma função imprime(). Esta função não retorna valor. Uma função pode retornar ou não um valor. Quando a função não retorna nenhum valor, como no caso acima, podemos utilizar a palavra chave void no cabeçalho da definição. Uma função pode retornar um valor do tipo int, double, ou um ponteiro ou não retornar nenhum valor e neste caso podemos utilizar a palavra chave void. A ordem de definição das funções não é importante em C. Podemos reescrever o programa acima definindo a função imprime() depois da função main().

Em tempo de execução, ao ser chamada ou invocada a função imprime(), o fluxo de controle desvia para o primeiro comando da função. Quando é executado o último comando de uma função, o fluxo de controle volta para o ponto onde a função foi chamada. O sistema de execução prepara uma estrutura denominada registro de ativação ou quadro de ativação (activation frame) para controlar as atividades de chamada, execução e retorno de uma função.

Na compilação do programa acima, o compilador gera código para uma chamada de função correspondente à linha 7 e gera código para a função correspondente às linhas 3, 4 e 5. Além disso gera código para que seja construído, de forma adequada, um registro de ativação, quando a função imprime() for invocado, ou seja, o código correspondente a linha 7, entre outras coisas, salva no registro de ativação o endereço de retorno. O endereço de retorno é o endereço para onde desvia o fluxo de controle  quando a função termina (no caso linha 8).

Na execução do programa acima o fluxo de controle está inicialmente com o Runtime C  e é criado um registro de ativação para invocar a função main(). Após isso é feito o desvio para o primeiro comando da função main(), linha 7. O código correspondente à linha 7 é uma invocação da função imprime(). É criado um registro de ativação com conteúdo adequado e é feito o desvio do fluxo de execução para o primeiro comando da função imprime(), isto é, linha 4. Após a execução da linha 4 o fluxo de controle executa o código que consulta o registro de ativação e retorna o fluxo de controle para a linha 8. A linha 9 corresponde a um código que retorna o fluxo de controle para o Runtime C e termina a execução do programa. A seqüência de execução (simplificada) do programa acima é: O Runtime C invoca a função main(). O primeiro comando da função main(), linha 7, cria um registro de ativação que, dentre outras informações, guarda o endereço de retorno. O código relativo à invocação da função imprime() desvia o fluxo de controle para o código correspondente à linha 4. A linha 4 corresponde a uma invocação da função printf(). Neste ponto é criado um registro de ativação (o terceiro!!) para a execução da função printf(). O controle é desviado para o código da função printf(). Quando termina a execução da printf() o controle retorna para o código correspondente à linha 5, destruindo o registro de ativação criado para a invocação da printf(). O código relativo à linha 5 consiste em recuperar o endereço de retorno contido no registro de ativação correspondente à invocação da função imprime(). O endereço de retorno corresponde ao código da linha 8 e o código correspondente a linha 9

  1. Fluxo de execução no Runtime C
  2. invocação de main - criação do registro de ativação da função main();
  3. execução do código correspondente à linha 7;
  4. invocação de imprime - criação do registro de ativação da função imprime();
  5. execução do código correspondente à linha 4;
  6. invocação de printf() - criação do registro de ativação da função printf();
  7. execução do código da função printf();
  8. retorno da função printf() para o código correspondente à linha 5 - destruição do registro de ativação de printf();
  9. execução do código correspondente à linha 5: retorno da função imprime() para o código correspondente à linha 8 - destruição do registro de ativação da função imprime();
  10. execução do código correspondente à linha 9: retorno da função main() para o Runtime C - destruição do registro de ativação da função main().

Uma função pode retornar um valor (este nome “função” sugere que deveria retornar sempre!). Para isso devemos colocar no cabeçalho de definição da função, o tipo do valor que ela irá retornar. Neste caso a execuçao (não é o texto!) da função usualmente irá terminar quando for executado um comando da forma:
return <expressão>;
A expressão especificada no comando de retornar (return) deve ser de tipo compatível na definição do cabeçalho da função. Quando o fluxo de controle atinge um return é obtido o endereço de retorno do registro de ativação corrente, este registro de ativação corrente é destruido e o fluxo de controle retorna para o comando seguinte à invocação onde é tornado disponível o valor retornado. Quando o tipo da função é void a função deve usar somente o comando return sem a expressão.

Uma forma um pouco mais completa de escrever o programa acima é:

#include <stdio.h>
#include <stdlib.h>
void imprime(){
  printf("uma funcao simples!\n");
  return;
}
int main(int argc, char *argv[])
{ imprime();
  system("PAUSE");
  return 0;
}


Quando a função (i)não tem parâmetros (é definida com abre parênteses seguido de fecha parênteses), (ii)não retorna um valor (é definida como void) e não tem variáveis locais (não parecem declarações da forma
int x; ou char c;), o registro de ativação serve basicamente para guardar o endereço de retorno do fluxo de controle. A seguir vamos introduzir novos elementos na definição de funções. Observe a nova função imprime do programa abaixo:

#include <stdio.h>
#include <stdlib.h>
void imprime(char * m){
  printf(“%s”, m);
  return;
}
int main(int argc, char *argv[])
{ imprime("uma funcao simples!\n");
  system("PAUSE");
  return 0;
}

 

A nova definição de imprime() possui agora um parâmetro do tipo ponteiro para caractere. Esta função, para ser invocada adequadamente deve prover um parâmetro. A seqüência do fluxo de execução é similar ao programa anterior, a principal diferença é que o código correspondente à invocação da função imprime(), antes de fazer a invocação irá atribuir o endereço da cadeia de caracteres "uma funcao simples!\n" para o parâmetro m. Os parâmetros são elementos similares a variáveis e ficam disponíveis no registro de ativação de uma função. O programador de uma função pode usar os parâmetros da mesma forma que podem usar uma variável declarada localmente à função. No caso acima o valor do parâmetro é passado para a invocação da função printf().

As variáveis definidas dentro de uma função, da mesma forma que os parâmetros, têm seu espaço definido dentro do registro de ativação e portanto todas as variáveis definidas dentro de uma função (juntamente com os parâmetros) serão destruídas quando for destruido o registro de ativação correspondente. As variáveis locais e os parâmetros só podem ser utilizados na função onde foram definidos. Este tipo de variável local às funções são conhecidas como variáveis “automáticas” (“automatic variables”). Observe portanto que as variáveis locais ou automáticas têm um ciclo de vida amarrado ao ciclo de vida dos registros de ativação da função correspondente. A linguagem C prove um mecanismo de definição de variáveis externas às funções. Estas variáveis podem ser utilizadas por todas as funções. A idéia é que existe um região de memória comum a todas as funções. As variáveis definidas fora de uma função têm um ciclo de vida amarrado ao ciclo de vida do programa, ou seja, enquanto o programa estiver em execução elas estão disponíveis. Mais à frente mostraremos como definir variáveis “externas” às funções.

A definição das variáveis locais de uma função podem ser modificadas pela palavra reservada static. Conforme já discutido as variáveis locais são criadas e destruídas juntamente com os registros de ativação. Mas se quisermos preservar as variáveis entre invocações das chamadas devemos defini-las com o modificador static. Compare o funcionamento das duas funções abaixo:

#include <stdio.h>
#include <stdlib.h>

int f1(){ int i=0; return ++i;}
int f2(){ static int i=0; return ++i;}

int main(int argc, char *argv[])
{
    int i;
  for(i=0; i<5; i++) printf("%d\n",f1());
  for(i=0; i<5; i++) printf("%d\n",f2());
  system("PAUSE");     
  return 0;
}

No caso da função f1 a  cada chamada é criada a variável i e inicializada com o valor 0 e o valor retornado é sempre 1. No caso da função f2 a definição da variável local i é modificada por static e a cada chamada a função retorna um valor diferente.

A palavra reservada static pode também modificar a definição de uma função. Considere o trecho de programa abaixo:

static int f1(){<corpo>}

int f2(){<corpo>}

A definição da função f2 está disponível para qualquer outro arquivo que for ligado-editado com o arquivo que contém o trecho acima. Mas a definição da função f2 está disponível apenas no arquivo do trecho. Uma outra maneira de dizer isso é que a linguagem C exporta os nomes de função de maneira “default”, se quisermos impedir a exportação de uma função temos que modificá-la com static. As variáveis globais modificadas por static também só ficam disponíveis naquele arquivo, e não podem ser utilizadas em outros arquivos.

A função abaixo mostra um exemplo de passagem de parâmetros um pouco mais geral.  Considere a definição:
void f(int p1, double p2, int *p3, int p4, char *p5){
  printf("int %d\n",p1);
  printf("double %f\n",p2);
  printf("arranjo de inteiros:\n");
  int i;

  for(i=0; i<p4; i++) printf(“%d “,p3[i]);
  printf(“\n”);
  printf(“%s\n”,p5);
}

Considere a invocação:
int a[]={1,2,3};
f(123, 12.34,a,3, "ola!");

Antes do controle desviar para o primeiro comando da função f, será feita a avaliação das expressões correspondentes a cada parâmetro.[É usual que o termo “argumento” seja usado para uma expressão na invocação e que o termo “parâmetro” seja usado para o nome correspondente na definição, outros termos são parâmetro real/parâmetro formal, ou ainda argumento real/argumento formal.] Após ser feita a avaliação das expressões (dos argumentos) será feita a atribuição dos valores resultantes para cada parâmetro. Os parâmetros ficarão disponíveis no registro de ativação que é construído para a invocação.

A linguagem C tem dois estilos de função: estilo-antigo e estilo-novo. As funções do estilo novo devem ter o tipo dos parâmetros explícitos e isto faz parte do tipo da função. As funções do estilo antigo não precisam explicitar o tipo dos parâmetros. Os dois trechos de programa abaixo ilustram os dois estilos: estilo original, estilo ANSI C:

double f(a, b){ int a; double b; <resto do corpo de f>} //original

double f(int a, double b){<corpo de f>} //ANSI C

 

No estilo novo de funções (ANSI C) o número e o tipo dos parâmetros na definição devem sempre ser compatíveis com o número e o tipo das expressões na invocação. Os parâmetros na definição da função são conhecidos como parâmetros formais, os valores ou expressões dos parâmetros na invocação da função são conhecidos como parâmetros reais. A invocação de uma função faz com que os valores dos parâmetros reais sejam atribuídos ao parâmetros formais. Cada invocação de uma função dá origem a um registro de ativação contendo os parâmetros formais inicializados com os valores obtidos na avaliação das expressões correspondentes aos parâmetros reais. Se fizermos uma atribuição a um parâmetro formal esta atribuição, no caso da linguagem C, não afeta o parâmetro real correspondente:
#include <stdio.h>
#include <stdlib.h>
void f(int pf){
     printf("f recebeu valor:%d\n" ,pf);
     pf=5;
     printf("f atribuiu %d ao parametro\n", pf);
}

int main(int argc, char *argv[])
{
    int pr=3;
    printf("parametro real antes da chamada:%d\n",pr);
    f(pr);
    printf("parametro real apos chamada:%d\n",pr);
 
  system("PAUSE");     
  return 0;
}
 

O programa acima imprime:
parametro real antes da chamada:3
f recebeu valor:3
f atribuiu 5 ao parametro
parametro real apos chamada:3

O modelo de passagem de parâmetro da linguagem C é denominado passagem de parâmetro por valor. Existem outros mecanismos de amarramento (binding) de parâmetros em outras linguagens. Os parâmetros e as variáveis declaradas dentro de uma função só estão disponíveis no escopo desta função. O registro de ativação contém os parâmetros e irá conter todas as variáveis locais que forem definidas, quando o registro de ativação é destruído as variáveis locais e parâmetros deixam de existir. Se quisermos que uma função f1 altere uma variável em f2 a chamada de f1 em f2 terá que passar o endereço da variável (ponteiro!) e a definição de f1 tem que levar isto em conta:

#include <stdio.h>
#include <stdlib.h>
void f(int *pi){
     printf("f recebeu valor:%d\n" ,*pi);
     *pi=5;
     printf("f atribuiu %d ao parametro\n", *pi);
}

int main(int argc, char *argv[])
{
    int pr=3;
    printf("parametro real antes da chamada:%d\n",pr);
    f(&pr);
    printf("parametro real apos chamada:%d\n",pr);
 
  system("PAUSE");     
  return 0;
}

Uma função pode retornar um valor. Para isso devemos colocar no cabeçalho de definição, ao invés da palavra chave void, o tipo do valor que a função irá retornar. Neste caso a execuçao (não é o texto!) da função usualmente irá terminar quando da execução de um comando da forma:
return <expressão>;
A expressão especificada no comando de retornar (return) deve ser de tipo compatível na definição do cabeçalho da função. A função abaixo recebe como parâmetro um caractere hexadecimal e retorna um valor inteiro correspondente ao caractere:
int charHex2int(char c){
  switch(c){
    case '0': case '1': case '2': case '3': case '4':
    case '5': case '6': case '7': case '8': case '9':
      return c-'0';
    case 'A': case 'B': case 'C': case 'D': case 'E': case 'F':
      return c-'A'+10;
    case 'a': case 'b': case 'c': case 'd': case 'e': case 'f':
      return c-'a'+10;
    default:  return -1;
  }
}
Observe que as expressões nos comandos de retorno são de tipo int. No local onde existe a <chamada da função>, após a execução da função, passa a ficar disponível o valor retornado.

Observe as funções f1 e f2 abaixo:

void f1(){return;}

void f2(){}

No caso de f1 o retorno é explícito; no caso de f2 o retorno ocorre pelo que é denominado “drop off”( o fluxo de controle saiu do bloco!), podemos considerar que o compilador coloca uma instrução de return ao final do bloco da função f2.

Uma função pode ter variáveis locais. As variáveis declaradas dentro de uma função são de acesso exclusivo daquela função. Na linguagem C não podemos definir uma função dentro de outra função (não existem definições aninhadas de funções). A definição de uma função define um espaço de nomes. Uma função só pode acessar dados de outra função se esta outra função passar um ponteiro para seus dados.

Além dos (i)dados locais, (ii) dos dados de outra função manipulados através de ponteiros uma função pode acessar variáveis globais. A estrutura de um programa C é formada não só pelo conjunto das funções mas também por um conjunto de variáveis definidas externamente às funções.

considere um arquivo arq.c contendo o seguinte trecho de programa C:

int vg=3;

se este arquivo for compilado e editado-ligado a este main.c:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]){
  extern int vg;
  int vl=5;
  printf("%d %d\n",vg, vl);
  system("PAUSE");     
  return 0;
}

O resultado é um programa onde a função main além de ter acesso à variável “local” vl tem acesso a uma variável global vg. Observe o uso da palavra reservada extern como modificador da declaração de vg. A variável vg é definida no arquivo arq.c e declarada na função main do arquivo main.c conforme pode ser visto acima. Observe que a semântica da linguagem C é a disponibilização ou exportação por “default”. Se quisermos limitar o escopo de uma variável global ao arquivo onde ela foi definida temos que utilizar o modificador static.

A linguagem C permite a declaração de uma função em um local ou arquivo e a definição da função em outro local ou arquivo. Considere o trecho de programa abaixo:

int f1();
int f2(){ <corpo de f2>}

A função f1() foi declarada mas não foi definida, a função f2() foi definida (e considerada declarada!). A declaração pode ser no estilo original ou no estilo ANSI C:

int f(); //original

int f(int p1, int p2); ANSI C

Funções recursivas

Uma função pode invocar a si mesma! Quando uma função invoca a si mesma em sua definição dizemos que ela é uma função recursiva. O programa abaixo mostra uma função para calcular o fatorial de um número em uma versão recursiva e outra não recursiva:
#include <stdio.h>
#include <stdlib.h>

int fatNaoRecursivo(int n){
    if(n<2) return 1;
    int f=1;
    int i;
    for(i=2; i<=n; i++) f*=i;
    return f;
}
int fatRecursivo(int n){
    if(n<2) return 1;
    return fatRecursivo(n-1)*n;
}

int main(int argc, char *argv[]){
    printf("%d\n",fatNaoRecursivo(5));
    printf("%d\n",fatRecursivo(5));
  system("PAUSE");     
  return 0;
}
 

No programa acima vemos que a versão não recursiva que implementa o cálculo do fatorial utiliza um comando for para iterar as multiplicações necessárias. Para podermos entender a versão recursiva da funçao temos que lembrar que existe um registro de ativação para cada invocação da função. A tabela abaixo mostra algumas caracteristicas da correlação entre registros de ativação e o parâmetro n:
 

n

No. de registros
de ativação

valor
retornado

0

1

1

1

1

1

2

2

2

3

3

6

Como regra geral é sempre preferível utilizar a versão não recursiva pois as versões recursivas são, em geral, muito pouco eficientes. Considere a série de Fibonacci: 0,1, 1, 2, 3, 5, 8, 13, 21, 34, ... Esta série inicia-se com o elemento de ordem 0 igual a 0 e o elemento de ordem 1 igual a 1, o elemento de ordem n é igual a soma dos elementos de ordem n-1 e ordem n-2. O programa abaixo mostra uma solução recursiva para calcular o termo de ordem i:
#include <stdio.h>
#include <stdlib.h>
int fib(int n){
  if(n<=0) return 0;
  if(n==1) return 1;
  return fib(n-1)+fib(n-2);
}

int main(int argc, char *argv[]){
  int n=4;
  printf("o valor do termo de ordem %d"
         " da serie de Fibonacci e' %d\n",n,fib(n));
  system("PAUSE");     
  return 0;

A tabela abaixo mostra a correlação entre o valor de n e o número de registros de ativação de fib():
 

n

Número de Registros
de ativação

valor retornado

0

1

0

1

1

1

2

3

1

3

5

2

4

9

3

 

Ponteiros para Funções

A linguagem C permite a definição de variáveis do tipo “ponteiro para função”. A metáfora neste caso é que uma variável deste tipo quando usada de “certa”maneira pode receber o “endereço” [do código?] da função e pode “invocar” a execução do código da função. Observe o programa abaixo:
#include <stdio.h>
#include <stdlib.h>

void f(){ printf("Eu sou a funcao f\n");}
void g(){ printf(“Eu sou a funcao g\n”);}

int main(int argc, char *argv[]){
  void (*pf)();
  pf=f; /*sem acucar sintático do & */
  (*pf)(); /*invoca f utilizando a variavel pf*/
  pf=g;
  (*pf)(); /*invoca g utilizando a variavel pf */
  system("PAUSE");     
  return 0;
}

Os projetistas de C decidiram que não podemos aplicar o operador & sobre o nome da função! ao atribuir o nome da função ao ponteiro estamos atribuindo algum tipo de endereço a ele (inicio do código?). a invocação da função é feita “derreferenciando” o ponteiro (operador de indireção *) aplicando os argumentos (abre e fecha parênteses). No exemplo acima o ponteiro pf primeiramente aponta para a função f e depois passa a apontar para a função g.

Tipos de variáveis na linguagem C

No material sobre variáveis alertamos que existem vários tipos de variáveis. Em particular alertamos que as variáveis podem ser definidas dentro ou fora das funções e que as variáveis podem ser definidas com ou sem a palavra chave static. No material sobre variáveis alertamos também que as variáveis definidas dentro de funções sem o modificador static (variáveis automáticas) têm o conteúdo indefinido (lixo de memória).

A linguagem C permite a definição de variáveis ligadas ao bloco da função ou a outros blocos aninhados a este. No trecho de programa abaixo:
  if(n>0){
    int i;
    for(i=0; i<n; i==) ...

o escopo da variável i é o bloco “verdadeiro” do “se”, e pode haver uma variável i externa a este bloco! Uma variável automática declarada e inicializada em um bloco é inicializada cada vez que o fluxo de controle atinge este bloco. Uma variável estática é inicializada somente na primeira vez que o fluxo de controle chega até o bloco correspondente.

As variáveis definidas com o modificador static (variáveis static não são destruídas e criadas a cada invocação da função onde foram definidas) são garantidas de serem inicializadas com zero. No programa abaixo a impressão de vne corresponde a qualquer valor e ve corresponde a um zero:

  int vne;
  static int ve;
  printf("%d %d\n", ve, vne);

O trecho de programa acima deve ser condenado pois faz uso de conteúdo de variável que não foi inicializado ou atribuido. Na maioria das organizações de desenvolvimento de software e a boa prática de programação recomendam que não se deve confiar na inicialização implícita!

Para variáveis externas (variáveis declaradas com extern) e variáveis estáticas, a inicialização só pode ser feita utilizando expressões constantes. O seguinte trecho de programa não compila:
  int i=2;
  static int j=i;

Por outro lado, no caso de variáveis automáticas podemos utilizar expressões envolvendo valores previamente definidos, até mesmo chamadas de funções:
int f(int x){
  int y=0;
  int z=x-1;

Conforme foi discutido no material de arranjos uma “variável” do tipo arranjo não é exatamente uma variável em termos da possibilidade de podermos manipula-la como um todo. Vamos esclarecer, considere o trecho:
int i=8;
int j;
j=i;

Tentando fazer algo similar a isso com arranjos iremos entender porque uma variável do tipo arranjo não é exatamente uma variável:
int ai[2]={100,200};
int aj[2];
aj=ai;

O trecho de programa acima não é correto porque a Linguagem C não prevê a manipulação de um arranjo como um todo. Por outro lado podemos manipular as estruturas como um todo:
struct s{int a[2]; int j;} sa={{100,200}, 300};
struc s sb;
sb=sa;

Todo o conteúdo de sa (em particular o conteúdo do arranjo!) é copiado de sa para sb.

A semântica de atribuição entre parâmetros formais e parâmetros reais na invocação de funções tem portanto um problema: Como lidar com a passagem de arranjos como parâmetro? (Todos os tipos de variáveis podem ser alvo de atribuição exceto os arranjos!). Os projetistas decidiram manter as restrições de atribuição entre arranjos (não pode haver atribuição de arranjos mesmo no contexto de funções) portanto funções não retornam arranjos (uma função pode retornar um apontador para arranjo). Com relação aos parâmetros os projetistas da linguagem C permitiram o “açucar sintático” de arranjos:
int *f(int a[]){...}

A função f acima aparentemente recebe um “arranjo” como parâmetro, mas a é um ponteiro que irá funcionar como determinado pelos projetistas com relação à equivalência entre indireção e indexação:  <exp1>[<exp2>] equivale a *(<exp1> + <exp2>). Esta sintaxe pode ser enganadora e pode induzir alguém a acreditar que um arranjo é uma variável semelhantemente às variáveis do tipo apontador. Além disso podem acontecer fatos “estranhos”:
int g(int a[]){return a[-1];}
...
     int a[2]={100,200};
    printf("%d\n", g(&a[1]));

O trecho acima imprime o valor 100, utilizando indexação com valor negativo!

No material sobre arranjos ainda não haviamos discutido as funções. Os projetistas da linguagem C não permitem um arranjo onde os elementos são do tipo função (tente imaginar o que seria isso!), mas é permitido um arranjo de ponteiros para funções. A sintaxe sem o uso do “typedef” é bem convoluta, mas com o uso de “typedef” fica razoavelmente legível:
typedef double (*PonteiroParaFuncao)(double);
...
PonteiroParaFuncao apf[10];
apf[0]=&funcao;
printf("%f\n", (*apf[0])(2.1));

Na linguagem C a indireção no uso de apf para invocar as funções é opcional, portanto funciona também:
printf("%f\n", (apf[0])(2.1));

 

Listas de parâmetros com tamanho variável (Variable-length Argument Lists)

A linguagem C oferece mecanismos para definir uma função que pode ser chamada com 1, 2, ou mais argumentos. O cabeçalho da função deve ser definido da forma (observe que deve haver 1(um) ou mais parâmetros com nome e a lista termina com a notação de reticência [três pontos])

<tipo> <nome> ( <parametro com nome 1>, <parametro com nome 2> ...)

O arquivo de cabeçalho padrão <stdarg.h> contem um conjunto de definições de macros para lidar com os argumentos. O tipo va_list pode ser usado para definir uma espécie de “ponteiro de argumentos”. A macro va_start inicializa um “ponteiro de argumentos” para apontar para um primeiro argumento (A linguagem C exige que o primeiro argumento tenha nome, somente os “outros” argumentos podem ser “anônimos”!). =A cada “chamada” va_arg retorna um argumento e incrementa o “apontador de argumentos”. A macro va_end deve ser chamada antes do retorno e faz a faxina que for necessária. O programa abaixo define uma função que retorna a soma de seus argumentos. O primeiro argumento diz quantos argumentos devem ser considerados:

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>

int soma(int na, ...){
  va_list pa;
  int i,k, acum=0;
  va_start(pa, na);
  for(i=0; i<na; i++){
    k=va_arg(pa, int);
    acum+=k;
  }
  va_end(pa);
  return acum;
}

int main(int argc, char *argv[]){
  printf("%d\n",soma(0));
  printf("%d\n",soma(1,10));
  printf("%d\n",soma(2,10,20));
  printf("%d\n",soma(3, 10,20,30));
  system("PAUSE");     
  return 0;
}


Exercícios:

  1. Escreva uma função que recebe um arranjo de inteiros como parâmetro e devolve o índice do maior elemento do arranjo.

int indMax(int *a, int tamanho)

  1. Escreva uma função que recebe um arranjo de valores do tipo double e devolve a soma dos elementos deste arranjo.

double somatorioDouble(double *ad, int tamanho)

  1. Escreva uma função que recebe um arranjo de inteiros e devolve um valor lógico (int) informando se todos os elementos do arranjo são iguais.

int iguais(int *a, int tamanho)

  1. Escreva uma função que recebe dois arranjos de valores do tipo double e devolve o produto interno correspondente a vetores que sejam representados por estes dois arranjos.

double prodInterno(double *a1, double *a2, int tamanho)

  1. Escreva uma função não recursiva que calcula o termo de ordem n de uma série de Fibonacci.
    int fib(int n)

  2. Considerando que as variáveis de uma função são “destruidas” junto com os respectivos registros de ativação, explique o problema da seguinte função:
    int *f(){ int a=10; int *pa=&a; return pa; }

  3. Escreva um programa definindo uma função com número variável de argumentos; o primeiro argumento define o número de argumentos e a função retorna a média aritmética dos argumentos.
  4.  

Exercícios extra:

Reesecreva o programa de multiplicação de matrizes utilizando funções com nomes ilustrativos da operação.

Reescreva os programas relacionados ao jogo da velha e ao Nurikabe utilizando funções.