terça-feira, 2 de março de 2010

Ponteiros para funções – C

Ponteiros são os grandes bichos-papões de quem está querendo aprender C, e ponteiro para função é o über monstro para a maioria dos programadores novatos. Mas, basta um pouco de paciência e um olhar destemido para perceber que não existe nada de tão assustador sobre eles. Assumindo que você conhece ponteiros vou me concentrar no tópico ponteiro para funções. É importante saber distinguir uma declaração de função de um ponteiro para função. Nos exemplos abaixo:

- int * func();
- int (*func)();

O primeiro caso define uma função que retorna um ponteiro para um inteiro, já o segundo caso é um ponteiro para uma função que retorna int. Lembre-se ( ) tem precedência ao * logo o primeiro caso será avaliado como uma função e o segundo como um ponteiro (*func) para uma função int (). Ponteiros para função não são somente um jeito complicado de programar, são extremamente úteis, pois podem substituir longas cadeias de if/else ou um switch gigante, são úteis para criar callbacks, e, de certa forma, sem eles não existiria como implementar mecanismos de ligação dinâmica de funções como DLL's já que você solicita um nome na tabela de símbolos do dll, e a partir de um ponteiro de função você consegue chamar a função cujo código foi carregado na memória possivelmente após a imagem do seu processo ter sido carregado. Um ponteiro para função funciona pois, no final das contas, o código das funções também estão na memória e se o controle da cpu for transferido para esse endereço o código será executado, claro que o endereço tem que ser um endereço aonde seu processo tem privilégios de execução. Para declarar um ponteiro para uma função utilizo mentalmente um jeito bem simples, e que funciona bem na maioria dos casos. Esse método consiste em escrever como se você estivesse declarando a função, por exemplo:

int f(int a);

Então coloque parênteses ao redor do nome da função e um asterisco na frente do nome:

int (*f)(int a);

Note que não é necessário colocar o nome da variável que é recebida como parâmetro, mas talvez seja uma boa para fim de entendimento do código. Ok, agora vamos continuar a brincadeira e pensar grande, quem sabe um ponteiro para uma função que receba um inteiro retorne um ponteiro para um float? Simples, primeiro escreva como se estivesse declarando:

float * f(int i);

Depois, faça a jogada que aprendemos anteriormente:

float * (*f)(int i);

Pronto! Viu como a coisa nem é tão complicada assim! Antes de demonstrar aspectos práticos vamos declarar mais uns ponteiros para funções que pareceriam intragáveis antes. Digamos, um ponteiro para uma função que retorna um array de ponteiros para funções que retornam um ponteiro void e não recebem parâmetros. Somando a isso, a função recebe como parâmetro dois inteiros e um ponteiro para int, hehehe bem até a declaração disso é meio bizarro, mas vamos lá:

void * (**f(int a, int b, int * pi))(void)

Ok, isso merece uma explicação hehehe, f é o nome da função, ela recebe dois inteiros e um ponteiro para inteiro, o retorno dela é um ponteiro para ponteiro **f e esse ponteiro é para um array de ponteiros para função do tipo void * (void), ou seja, que recebe void e retorna um ponteiro void. Como é mais comum retornar o endereço do primeiro item do array, então temos nossa função declarada. Criar um ponteiro, neste caso consiste em:

void * (**(*fp)(int a,int b,int* pi)) (void);

Lindo não?! Removendo o nome dos parâmetros fica ainda mais bonito:

void * (**(*fp)(int,int,int*)) (void);

Mas, acredite, é bem pouco provável que você tenha que criar uma loucura dessas, o propósito do meu exemplo é só para demonstrar que até as funções mais loucas podem ter um ponteiro apontando pra elas sem muito trabalho, pelo menos não é tão absurdo como dizem. Que tal um exemplo disso em funcionamento hehehe:

#include <stdio.h>

void * fvoid1(void)
{
fprintf(stderr,"fvoid1\n");
return NULL;
}

void * fvoid2(void)
{
fprintf(stderr,"fvoid2\n");
return NULL;
}

void * (*farray[]) () = {fvoid1,fvoid2};

void * (**f(int a, int b, int * pi))(void)
{
void *(**fp)() = &farray[0];
return fp;
}

int main()
{
void * (**(*fp)(int,int,int*)) (void);
void * (**a)(void);
fp = f;
a = (*fp)(10,20,NULL);

(*a)();

a++;

(*a)();

return 0;
}

Atribuir o endereço de uma função para um ponteiro de função segue o mesmo padrão do resto das coisas em C, digo, se você criar um ponteiro de função que aponte para uma função cuja definição combine com a definição da função printf, pro exemplo, atribuir o endereço é tão simples quanto:

pf = printf;

Ou você pode utilizar a forma mais “canônica” e utilizar o operador & para recuperar o endereço da função. Vale lembrar que alguns compiladores vão reclamar da forma direta, então já que o jeito certo é o canônico, utilize o jeito certo! Como em:

pf = &printf;

Para chamar, você tem duas opções, você pode utilizar a sintaxe:

pf(“Alo mundo”);

Desta forma o compilador vai desreferenciar o ponteiro para você, ou você pode fazer na mão:

(*pf)(“Alo mundo”);

Não tenho nenhum argumento contra ou a favor a utilização de um método ou do outro, no máximo eu diria que desreferenciar na mão deixa um pouco mais claro que você está utilizando um ponteiro e não chamando uma função chamada pf.
Agora vamos tratar os assuntos mais comuns relativos ao uso de ponteiros para função. Evitar uma cadeia de if/else gigante ou um switch e funções que são passadas para funções*, de certo modo, para implementar uma técnica conhecida como callback. Pretendo matar dois coelhos com uma cajadada só, e explicar ambos no mesmo exemplo. O problema com as cadeias de if/else ou switch, geralmente, surge quando é necessário fazer a chamada para uma função apropriada para algum caso especifico quando a situação não pode ser determinada em tempo de compilação, ou seja, não há como saber antes de executar qual função deve ser chamada. Vou utilizar o caso mais clássico que é o das quatro operações básicas (+/-/*/div), o código utilizando um switch seria parecido com:



#include <stdio.h>

float Plus (float a, float b) { return a+b; }
float Minus (float a, float b) { return a-b; }
float Multiply (float a, float b) { return a*b; }
float Divide (float a, float b) { return a/b; }

void Solve(float oper1, float oper2, char opCode)
{
float result=0.0f;

switch(opCode)
{
case '+': result = Plus (oper1, oper2);break;
case '-': result = Minus (oper1, oper2);break;
case '*': result = Multiply(oper1, oper2);break;
case '/': result = Divide (oper1, oper2);break;
}

printf("Resultado: %f %c %f = %f\n",oper1,opCode,oper2,result);
}

int main()
{
Solve(2,3,'+');
Solve(7,3,'-');
Solve(2,7,'*');
Solve(25,5,'/');

getchar();

return 0;
}


Utilizando uma função que utiliza ponteiros de função como parâmetro, ou seja, recebe uma função como parâmetro, ficaria parecido com:


#include <stdio.h>

float Plus (float a, float b) { return a+b; }
float Minus (float a, float b) { return a-b; }
float Multiply (float a, float b) { return a*b; }
float Divide (float a, float b) { return a/b; }

float Solve(float oper1, float oper2, float (*f)(float,float))
{
return ((*f)(oper1,oper2));
}

int main()
{
printf("Resultado: 2 + 3 = %f\n",Solve(2,3,Plus));
printf("Resultado: 7 - 3 = %f\n",Solve(7,3,Minus));
printf("Resultado: 2 * 7 = %f\n",Solve(2,7,Multiply));
printf("Resultado: 25/5 = %f\n",Solve(25,5,Divide));

getchar();

return 0;
}


E obvio que este caso é um caso trivial e não existe necessidade real de utilizar um ponteiro para função. A coisa muda de figura em casos aonde algum trabalho extra é pré-requisito para o funcionamento das operações, imagine receber os operandos via RPC ou que seja necessário converter a entrada do usuário em uma GUI para valores float a fim de obter os operandos, ou qualquer coisa do tipo. O código para executar esta tarefa não teria que ser replicado em todos os casos do switch bastaria codificar uma vez e baseado na necessidade chamar passando o ponteiro para a função correta. Essa técnica de passar uma função para outra é chamada de callback, e a função sendo chamada geralmente recebe o nome de callback function.
Normalmente callbacks são utilizadas quando é necessário que duas camadas diferentes de um sistema se comuniquem como, por exemplo, você passa uma função para o SO de forma que, quando um evento qualquer ocorrer o sistema chame essa função pra você.
Note que fiz um apontamento * quando falei sobre passar uma função para outra, no caso de C/C++ não é isso que ocorre de fato. Em linguagens imperativas como é o caso de C/C++, normalmente não permitem tal operação o que estamos fazendo é meramente passar o endereço de onde a função está na memória. Funções que recebem outra função como parâmetro, e de fato implementam funções de alta ordem gerais só são possíveis em linguagens funcionais e não vou tratar disso neste tutorial e possivelmente não neste blog heheheh. Então é isso, da próxima vez que pensar em ponteiros para função ao invés de medo, sorria, pois é um recurso muito poderoso da linguagem e que não é tão complicado quanto dizem por ai!

That's all folks!