Ponteiros para funções é algo extremamente interessante, eficiente e elegante. Pode ser usado para substituir switch-case, definir em tempo de execução qual função deve ser chamada ou implementar callbacks.
Ponteiros para funções são na realidade ponteiros que apontam para o endereço de uma função. Uma função possui um endereço de memória. Quando o programa é executado todo seu conteúdo é colocado na memória, então uma função é como um variável do tipo inteira, nada mais que um endereço de memória.
Então para demonstrar esse conceito, desenvolvi um código em C ANSI , que tem a função de uma calculadora de 4 operações, onde o usuário informa o primeiro valor, o operador + – * ou /, e o segundo valor, e o programa em tempo de execução escolhe a função a ser chamada.
Este pequeno exemplo de código mostra como é possível substituir o switch-case e chamar funções em tempo de execução.
view plaincopy to clipboardprint?
#include <stdio.h>
#include <stdlib.h>
float adicao(float a, float b) {return a+b;};
float subtracao(float a, float b) {return a-b;};
float multiplicacao(float a, float b) {return a*b;};
float divisao(float a, float b) {return a/b;};
int main()
{
float(*pt2Func[4])(float, float) = {NULL};
float(*Func)(float, float) = NULL;
pt2Func['+'] = &adicao;
pt2Func['-'] = &subtracao;
pt2Func['/'] = &divisao;
pt2Func['*'] = &multiplicacao;
float a;
float b;
char operator;
while(1)
{
printf("Informe a: ");
scanf("%f",&a);
printf("\nInforme operador: ");
scanf(" %c",&operator);
printf("\nInforme b: ");
scanf(" %f",&b);
if(operator != '-' && operator != '+' && operator != '*' && operator != '/' )
{
printf("Operação não implementada");
exit(-1);
}
Func = pt2Func[operator];
printf("\nResultado: %f\n", Func(a,b));
printf("\nDeseja Continuar (y/n):");
scanf(" %c",&operator);
if(operator == 'n' || operator == 'N')
break;
Func = NULL;
}
exit(0);
}
</stdlib.h></stdio.h>
Como funciona
A partir da linha 3 até a linha 7 definimos as funções que irão realizar a adição, multiplicação, divisão e subtração note que todas as funções possuem o mesmo protótipo.
Na linha 11 declaramos um ponteiro array que também possui o mesmo protótipo das funções. Esse é uma detalhe importante. O nosso array de ponteiros tem 4 posições onde iremos informar o valor de cada posição e o endereço da função que é feito nas linhas 14,15,16 e 17. Definimos como posição os símbolos da operações matemáticas então quando o usuário escolher uma soma informa o símbolo + e o programa por sua vez aponta para a função desejada isso acontece na linha 40.
O restante do programa é um loop onde após cada cálculo é solicitado ao usuário se deseja continuar ou não.
Este programa foi compilado com o gcc no linux.
Caso tenha dúvida sobre o funcionamento deixe um comentário.
Para saber mais: O livro C completo e Total é muito bom vale a pena ter esse livro na estante.
Desde que uma string é uma array de elementos tipo char, as operações com ponteiros ocorrem normalmente conforme visto em página anterior. Entretanto, alguns procedimentos específicos são úteis neste caso. Seja o programa a seguir.
#include <stdio.h>
main(){
int i;
char rua[] = "nova";
char *ptr = rua;
for (i=0; i<4; i++)
printf( "%d > %c\n", i, *ptr++ );
}
A saída desse programa seria
0 > n
1 > o
2 > v
3 > a
Strings têm um caractere nulo após o último elemento. Assim, uma versão mais elegante do programa seria obtida com o código seguinte.
#include <stdio.h>
main(){
int i = 0;
char rua[] = "nova";
char *ptr = rua;
while( *ptr )
printf( "%d > %c\n", i++, *ptr++ );
}
A saída seria a mesma do anterior. Entretanto o loop while permite uma simplificação, isto é, o conteúdo apontado (*ptr) pára o loop quando chegar ao caractere nulo do final da string.
Uma outra versão do programa é dada a seguir.
#include <stdio.h>
main(){
int i;
char rua[] = "nova";
char *ptr = rua;
for (i=0; i<4; i++)
printf( "%d > %c\n", i, *(rua+i) );
}
O código é quase idêntico ao primeiro, com a diferença do uso de *(rua+i) no lugar de *ptr++ no último argumento de printf. Isso é possível porque, conforme já visto, o nome de uma array é também um ponteiro para ela e pode ser usado como tal. E também poderia ser usado o clássico rua[i].
Assim, as notações
*(rua+i)
rua[i]
são equivalentes e podem ser usadas sem distinção. Entretanto, muitos programadores preferem a notação de ponteiro, *(rua+i), por ser de execução mais rápida sob certas condições. Mas não é só isso. Uma vez adquirido o hábito do uso de ponteiros, a notação específica dá uma aparência mais homogênea aos códigos.
Quando uma função recebe uma variável como argumento, ela dispões de apenas uma cópia do valor dessa variável. Assim, se a variável é externa à função, o seu valor não pode ser modificado pela função. Mas se o endereço (isto é, um ponteiro) da variável for passado à função, a modificação será possível. O uso de ponteiros também permite que uma função possa retornar mais de um valor. Seja o exemplo do programa a seguir.
#include <stdio.h>
void dobrar (int*, int*);
main(){
int a = 2, b = 4;
dobrar(&a, &b);
printf("a=%d b=%d", a, b);
}
void dobrar(int *ptr0, int *ptr1){
*ptr0 *= 2;
*ptr1 *= 2;
}
A saída deste programa seria:
a=4 b=8
Ou seja, as variáveis a e b, externas à função dobrar, foram modificadas pela pela função e ela retornou mais de um valor.
Quando, no lugar de variáveis comuns, se usam ponteiros como argumentos de funções, o ganho de desempenho pode ser significativo se essas variáveis são objetos grandes como arrays ou estruturas. Isso ocorre porque a função recebe uma cópia do argumento. Desde que um ponteiro contém apenas endereço, é eliminada a operação de copiar todo o objeto.
A noção de ponteiros permite supor que eles podem se referir a qualquer objeto localizado na memória. Tais objetos incluem também as funções e, portanto, é possível criar um ponteiro para uma função. A sintaxe é semelhante, mas há algumas regras próprias que podem ser observadas no programa de exemplo a seguir.
#include <stdio.h>
main(){
int (*ptrf) ();
ptrf = printf;
(*ptrf) ("Teste de ponteiro");
}
A saída do programa seria
Teste de ponteiro
Ou seja, a função printf foi chamada através do ponteiro ptrf.
Deve se observada a sintaxe indicada. Inclusive o tipo de dado na declaração do ponteiro
int (*ptrf) ();
deve ser o mesmo retornado pela função (int) porque printf retorna int.
Ponteiros de funções permitem procedimentos avançados, como a criação de tabelas de funções e a execução de cada uma de acordo com um determinado critério.
Ponteiros para Funções
Em C, podemos declarar um ponteiro para função com a sintaxe
void (*f)();
neste caso, f é um ponteiro para uma função sem parâmetros e que retorna void. f pode apontar para uma função compatível :
f = maximo;
maximo é uma função declarada como
void maximo() {
puts(“Olá ! Eu sou o máximo”);
}
maximo pode ser chamada a partir de f usando-se quaisquer das sintaxes abaixo.
(*f)(); /* chama maximo */
f(); /* chama maximo */
Veja o programa completo.
Podemos definir um tipo em C através de typedef:
typedef
int Number[10];
Agora Number é sinônimo de vetores inteiros:
Number v; // v é um vetor de inteiros de 10 posições
Da mesma forma, podemos definir Func como um ponteiro para funções:
typedef
void (*Func)();
Func f;
f = maximo;
f(); // chama maximo
Podemos definir também um vetor de ponteiros para funções:
Func v[] = {
maximo,
minimo
};
minimo é definida como
void minimo() {
puts(“Oi. Sou o mínimo”);
}
Agora, v é um vetor de ponteiros para funções. Ou melhor, v é um vetor de ponteiros para funções que não têm parâmetros nem retornam nada. Então v[0] é um ponteiro para uma função:
v[0](); // chama maximo
v[1](); // chama minimo
(*v[0])(); // chama maximo
(*v[1])(); // chama minimo
Até agora vimos apenas funções sem parâmetros e sem tipo de retorno. E se tivermos uma função
int addOne(int i) {
return i + 1;
}
?
Vejamos:
void (*f)();
f = addOne; // oops ...
f(); // oops ...
Na atribuição, há um erro de tipos. Estamos atribuindo uma função com um parâmetro e valor de retorno para um ponteiro para uma função sem parâmetros e sem valor de retorno. Temos que usar um cast:
f = (void (*)() ) addOne;
O tipo “void (*)()” lê-se “ponteiro para uma função sem parâmetro retornando void”. Na chamada de f, há outro erro. Estamos chamando a função sem passar o parâmetro. f aponta para addOne que espera um parâmetro. Temos que converter f para o tipo de addOne antes de chamar esta função:
n = ((int (*)(int) ) f)(1);
O tipo “int (*)(int)” é um ponteiro para uma função que tem um inteiro como parâmetro e retorna valor inteiro. O código completo deste exemplo está aqui.
Podemos colocar funções de vários tipos em um único vetor:
Func v[] = {
maximo,
addOne,
minimo
};
...
v[0](); // chama maximo
n = ((int (*)(int) ) v[1])(5); // chama addOne passando 5 como parâmetro
v[2](); // chama minimo
Estudando detalhadamente o exemplo acima, descobrimos que há um erro de tipos na inicialização de v. Cada elemento de v deve ser do tipo Func, função sem parâmetros retornando void. Mas addOne possui um parâmetro e retorna int. Então devemos usar um cast:
Func v[] = {
maximo,
(int (*)(int) ) addOne,
minimo
};
Ponteiros são uma abstração da capacidade de endereçamento fornecidas pelas arquiteturas modernas. Em termos simples, um endereço de memória, ou índice numérico, é definido para cada unidade de memória no sistema, no qual a unidade é tipicamente um byte ou uma word, o que em termos práticos transforma toda a memória em um grande vetor.[1] Logo, a partir de um endereço, é possível obter do sistema o valor armazenado na unidade de memória de tal endereço. O ponteiro é um tipo de dado que armazena um endereço.
Na maioria das arquiteturas, um ponteiro é grande o suficiente para indexar todas as unidades de memória presentes no sistema. Isso torna possível a um programa tentar acessar um endereço que corresponde a uma área inválida ou desautorizada da memória, o que é chamado de falha de segmentação. Por outro lado, alguns sistemas possuem mais unidades de memória que endereços. Nesse caso, é utilizado um esquema mais complexo para acessar diferentes regiões da memória, como o de segmentação ou paginação.
Para fornecer uma interface consistente, algumas arquiteturas fornecem E/S mapeada em memória, o que permite que enquanto alguns endereços são referenciados como áreas de memória, outros são referenciados como registradores de dispositivos do computador, como equipamentos periféricos.
Ponteiros são diretamente suportados sem restrições em C, C++, D e Pascal, entre outras linguagens. São utilizados para construir referências, elemento fundamental da maioria das estruturas de dados, especialmente aquelas não alocadas em um bloco contínuo de memória, como listas encadeadas, árvores ou grafos.
Ao lidar com arranjos, uma operação crítica é o cálculo do endereço para o elemento desejado no arranjo, o que é feito através da manipulação de ponteiros. De fato, em algumas linguagens (como C), os conceitos de "arranjo" e "ponteiro" são intercambiáveis. Em outras estruturas de dados, como listas encadeadas, ponteiros são usados como referências para intercalar cada elemento da estrutura com seus vizinhos (seja anterior ou próximo).
Ponteiros também são utilizados para simular a passagem de parâmetros por referência em linguagens que não oferecem essa construção (como o C). Isso é útil se desejamos que uma modificação em um valor feito pela função chamada seja visível pela função que a chamou, ou também para que uma função possa retornar múltiplos valores.
Linguagens como C, C++ e D permitem que ponteiros possam ser utilizados para apontar para funções, de forma que possam ser invocados como uma função qualquer. Essa abordagem é essencial para a implementação de modelos de re-chamada (callback), muito utilizados atualmente em bibliotecas de rotinas para manipulação de interfaces gráficas. Tais ponteiros devem ser tipados de acordo com o tipo de retorno da função o qual apontam. Ponteiros para função se assemelham a functores, ainda que o conceito função objeto seja mais abrangente.
Abaixo é mostrado o exemplo da declaração de uma lista encadeada em C, o que não seria possível sem o uso de ponteiros:
#define LISTA_VAZIA NULL /* a lista encadeada vazia é representada por NULL */
struct link {
void *info; /* conteúdo do nó da lista */
struct link *prox; /* endereço do próximo nó da lista; LISTA_VAZIA se este é o último nó */
};
Note que essa definição recursiva de ponteiros é a mesma que a definida em Haskell:
data Link a = Nil {- lista vazia -}
| Cons a (Link a) {- a cons nó de um valor de tipo a e outro nó -}
A definição com referências, por outro lado, possui checagem de tipo, sem confusão de símbolos. Por essa razão, estruturas de dados em C geralmente são utilizadas com o auxílio de funções de encapsulamento, que são cuidadosamente verificadas contra erros. A mesma definição de lista encadeada pode ser codificada em Fortran utilizando ponteiros, como segue:
type real_list_t
real :: sample_data(100)
type (real_list_t), pointer :: next => null ()
end type
type (real_list_t), target :: my_real_list
type (real_list_t), pointer :: real_list_temp
real_list_temp => my_real_list
do
read (1,iostat=ioerr) real_list_temp%sample_data
if (ioerr /= 0) exit
allocate (real_list_temp%next)
real_list_temp => real_list_temp%next
end do
Vetores em C são somente ponteiros para áreas consecutivas da memória. Logo:
#include <stdio.h>
int main() {
int arranjo[5] = { 2, 4, 3, 1, 5 };
printf("%p\n", arranjo); /* imprime o endereço do arranjo */
printf("%d\n", arranjo[0]); /* imprime o primeiro elemento do arranjo, 2 */
printf("%d\n", *arranjo); /* imprime o primeiro inteiro do endereço apontado pelo arranjo, que é o primeiro elemento, 2 */
printf("%d\n", arranjo[3]); /* imprime o quarto elemento do arranjo, 1 */
printf("%p\n", arranjo+3); /* imprime o terceiro endereço após o início do arranjo */
printf("%d\n", *(arranjo+3)); /* imprime o valor no tercero endereço após o início do arranjo, 1 */
return 0;
}
Tal operação é chamada aritmética de ponteiros, e é usada em índices de ponteiros. O uso dessa técnica em C e C++ é discutido posteriormente neste mesmo artigo.
Ponteiros podem ser usados para passar variáveis por referência, permitindo que seus valores modificados tenham efeito no escopo anterior do programa, como exemplificado no código C abaixo:
#include <stdio.h>
void alter(int *n) {
*n = 120;
}
int main() {
int x = 24;
int *endereco= &x; /* o operador '&' (leia-se "referênca") retorna o endereço de uma variável */
printf("%d\n", x); /* mostra x */
printf("%p\n", endereco); /* mostra o endereço de x */
alter(&x); /* passa o endereço de x como referência, para alteração */
printf("%d\n", x); /* mostra o novo valor de x */
printf("%p %p\n", endereco, &x); /* note que o endereço de x não foi alterado */
return 0;
}
Ponteiros podem ser usados para apontar para funções, permitindo, por exemplo, a passagem de funções como parâmetro de outras funções. O código em C abaixo demonstra tal funcionalidade:
#include <stdio.h>
int soma = 0; /* armazena a soma */
int produto = 1; /* armazena o produto */
void fsoma(int valor)
{
soma += valor;
}
void fproduto(int valor)
{
produto *= valor;
}
void mapeamento_funcao_lista(lista *L, void (*funcaoptr)(int))
{
lista_no *no;
no = L->inicio;
while (no != NULL)
{
funcaoptr(no->valor); /* invoca o ponteiro de função */
no = no->proximo;
}
}
int main()
{
lista *L;
/* ... preenche a lista com valores ... */
mapeamento_funcao_lista(L, fsoma); /* calcula o somatório dos elementos da lista */
mapeamento_funcao_lista(L, fproduto); /* calcula o produtório dos elementos da lista */
printf("Somatorio: %d\nProdutorio %d\n", soma, produto); /* imprime na tela os resultados */
return 0; /* retorno bem sucedido */
}
Note que, no exemplo acima, a rotina para a execução de código para cada elemento da lista (técnica conhecida como mapeamento) é implementada somente uma vez. O mapeamento é novamente exemplificado abaixo em uma rotina para determinar se elementos em uma lista são pares ou ímpares, codificado em ML:
fun map(F, nil) = nil
| map(F, x::xs) = F(x)::map(F, xs);
map(fn x => x mod 2, [1,2,3,4,5,6,7,8,9]);
A função map() recebe como entrada um valor e uma lista. Neste caso, o valor passado é uma função (x mod 2, uma implementação utilizando cálculo lambda para retornar se dado valor é par). Tal função é mapeada recursivamente, sendo executada em cada elemento da lista.
Os ponteiros são necessários para a alocação dinâmica de memória, para seqüências de dados alocados e para a passagem ou o retorno através referência. Neste último é importante citar a relevância no desempenho, pois é muito mais rápido alocar um ponteiro para um objeto de memória já existente do que alocar o objeto inteiro novamente. Além do mais, alocando o objeto novamente não podemos alterar o original.
Em várias linguagens, ponteiros possuem a restrição adicional de apontar para objetos de um tipo específico de dado. Por exemplo, um ponteiro pode ser declarado para apontar para um inteiro. A linguagem tentará prevenir o programador de apontar para objetos que não são inteiros, ou derivados de ponteiros, como números de ponto flutuante, eliminando alguns tipos básicos de erro cometidos por programadores.
Apesar disso, poucas linguagens definem tipagem restrita de ponteiros, pois programadores freqüentemente se encontram em situações nas quais desejam tratar um objeto de um tipo como se tivesse outro. Nesses casos, é possível converter o tipo de um ponteiro. Algumas conversões são sempre seguras, enquanto outras são perigosas, possivelmente resultando em comportamento incorreto do sistema. Apesar de geralmente ser impossível determinar em tempo de compilação se tais conversões são seguras, algumas linguagens armazenam informações sobre tipagem em tempo de execução (como o processo RTTI do C++), que podem ser usadas para confirmar se tais conversões perigosas são válidas, em tempo de execução. Outras linguagens simplesmente aceitam uma aproximação conservadora de conversões seguras, ou apenas não aceitam conversões.
Como ponteiros permitem ao programa acessar objetos que não são explicitamente declarados previamente, permitem uma variedade de erros de programação. Apesar disso, o poder fornecido por eles é tão grande que existem tarefas computacionais que são difíceis de ser implementadas sem sua utilização. Para ajudar nesse aspecto, várias linguagens criaram objetos que possuem algumas das funcionalidades úteis de ponteiros, ainda que evitando alguns tipos de erro.
Um grande problema com ponteiros é que enquanto são manipulados como números, podem apontar para endereços não utilizados, ou para dados que estão sendo usados para outros propósitos. Várias linguagens, incluindo a maioria das linguagens funcionais e linguagens recentes, como C++ e Java, trocaram ponteiros por um tipo mais ameno de referência. Tipicamente chamada de "referência", pode ser usada somente para referenciar objetos sem ser manipulada como número, prevenindo os tipos de erros citados anteriormente. Índices de vetores são lidados como um caso especial. As primeiras versões de Fortran e Basic omitiam completamente o conceito de ponteiros.
Ver artigo principal: ponteiro selvagem
Um ponteiro selvagem (também chamado de apontador pendente) não possui endereço associado. Qualquer tentativa em usá-lo causa comportamento indefinido, ou porque seu valor não é um endereço válido ou porque sua utilização pode danificar partes diferentes do sistema.
Em sistemas com alocação explícita de memória, é possível tornar um ponteiro inválido ao desalocar a região de memória apontada por ele. Esse tipo de ponteiro é perigoso e sutil, pois um região desalocada de memória pode conter a mesma informação que possuía antes de ser desalocada, mas também pode ser realocada e sobreescrita com informação fora do escopo antigo. Linguagens com gerenciamento automático de memória previnem esse tipo de erro, eliminando a possibilidade de ponteiros inválidos e de vazamentos de memória.
Algumas linguagens, como C++, suportam ponteiros inteligentes (smart pointers), que utilizam uma forma simples de contagem de referências para ajudar no rastreamento de alocação de memória dinâmica, além de atuar como referência.
Ver artigo principal: Null (programação)
Um ponteiro nulo possui um valor reservado, geralmente zero, indicando que ele não se refere a um objeto. São usados freqüentemente, particularmente em C e C++, para representar condições especiais como a falta de um sucessor no último elemento de uma lista ligada, mantendo uma estrutura consistente para os nós da lista. Esse uso de ponteiros nulos pode ser comparado ao uso de valores nulos em bancos de dados relacionais e aos valors Nothing e Maybe em mónadas da programação funcional. Em C, ponteiros de tipos diferentes possuem seus próprios valores nulos, isto é, um ponteiro nulo do tipo char e diferente de um ponteiro nulo do tipo int.
Como se referem ao nada, uma tentativa de utilização causa um erro em tempo de execução que geralmente aborta o programa imediatamente (no caso do C com uma falha de segmentação, já que o endereço literalmente aponta para uma região fora da área de alocação do programa). Em Java, o acesso a uma referência nula lança a exceção Java.lang.NullPointerException. Ela pode ser verificada, ainda que a prática comum é tentar se assegurar que tais exceções nunca ocorram.
Um ponteiro nulo não pode ser confundido com um ponteiro não inicializado: ele possui um valor fixo, enquanto um ponteiro não inicializado pode possuir qualquer valor. Uma comparação entre dois ponteiros nulos distintos sempre retorna verdadeiro.
O seguinte exemplo demonstra um ponteiro selvagem:
int main(void)
{
char *p1 = (char *) malloc(sizeof(char)); // aloca memória e inicializa o ponteiro
printf("p1 aponta para: %p\n", p1); // aponta para algum lugar da memória heap
printf("Valor de *p1: %c\n", *p1); // valor (indefinido) de algum lugar na memória heap
char *p2; // ponteiro selvagem
printf("Endereco de p2: %p\n", p2); // valor indefinido, pode não ser um endereço válido
// se você for sortudo, isso irá causar uma exceção de endereçamento
printf("Valor de *p2: %c\n", *p2); // valor aleatório em endereço aleatório
return 0;
}
O seguinte exemplo demonstra um ponteiro inválido por mudança de escopo:
#include <stdio.h>
#include <stdlib.h>
int maIdeia(int **p) // p é um ponteiro para um ponteiro de inteiro
{
int x = 1; // aloca um inteiro na pilha
**p = x; // define o valor de x para o inteiro que p aponta
*p = &x; // faz o ponteiro que p aponta apontar para x
return x; // após retornar x estará fora de escopo e indefinido
}
int main(void)
{
int y = 0;
int *p1 = &y; // ponteiro inicializado para y
int *p2 = NULL; // um bom hábito a ser utilizado
printf("Endereco dep1: %p\n", p1); // imprime o endereço de y
printf("Valor de *p1: %d\n", *p1); // imprime o valor de y
y = maIdeia(&p1); // muda y e muda p1
// p1 agora aponta para onde x estava
// O lugar onde x estada será sobreescrito,
// por exemplo, na próxima interupção, ou na
// próxima sub-rotima, como abaixo...
// algum outro código que utiliza a pilha
p2 = (int *)malloc(5*sizeof(int));
// isso não irá abortar, mas o valor impresso é imprevisível
printf("Valor de *p1: %p\n", *p1); // imprime o valor onde x estava
return 0;
}
Um problema muito comum é usar um ponteiro para a memória heap depois que a memória foi desalocada, como no próximo exemplo:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int *p1 = (int *)malloc(sizeof(int)); // inicializa o ponteiro para a memória heap
int *p2 = p1; // faz uma cópia
*p1 = 0; // inicializa o valor
printf("Endereco de p2: %p\n", p2); // aponta para a heap
printf("Valor de *p2: %d\n", *p2); // deve imprimir 0
free(p1); // desaloca a memória
.... // algum outro código, possivelmente utilizando a heap
// p2 ainda aponta para o local original da alocação, mas é impossível saber o que está lá
printf("Valor de *p2: %d\n", *p2); // uso inválido de p2
return 0;
}
Outra maneira de utilizar incorretamente ponteiros é acessar fora da estrutura de dados a qual eles apontam, como no exemplo a seguir:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int y = 5; // cria a variável
int *p1 = &y; // inicializa o ponteiro para y
printf("Endereco de p1: %p\n", p1); // endereço de y
printf("Valor de *p1: %d\n", *p1); // valor de y
p1 = p1 + y; // aritmética válida de ponteiros
printf("Valor de *p1: %p\n", *p1); // p1 não aponta mais para y
return 0;
}
Se um ponteiro é usado para escrever além do final de uma região local, a pilha de execução pode ser destruída. No caso abaixo, o problema provavelmente se manifestará quando o programa principal retornar:
#include <stdio.h>
#include <stdlib.h>
// copia a origem para o destino, sem checagem de tamanho
void strcopy(char *d, char *s)
{
while (*d++ = *s++) // copia até que '\0' é encontrado
;
}
int main(void)
{
char y[3]; // cria uma região local
char *p1 = (char *)malloc(10*sizeof(char)); // outra região local na memória heap
p1[9] = '\0'; // insere o terminador na maior região
strcopy(y, p1); // sobreescreve a região local
free(p1);
return 0; // agora coisas ruins acontecem
}
novembro 10, 2007 — _andre
Antes de começarmos, crie um arquivo .h com o seguinte conteúdo (chameio de c-funcpointer.h):
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
float plus( float a, float b)
{
return (a + b);
}
float minus( float a, float b)
{
return (a - b);
}
float times( float a, float b)
{
return (a * b);
}
float division( float a, float b)
{
return (a / b);
}
Eu sei que essa não é a forma mais correta de se usar um cabeçalho, mas como vamos utilizar esse código algumas vezes, e ainda não entrei em bibliotecas estáticas (.a), faremos assim. Esse cabeçalho define quatro funções simples, as quatro operações matemáticas mais simples, aceitando dois floats como argumento e retornando outro float.
Nesse primeiro exemplo, veremos o básico: declaração de ponteiros de funções e como usá-los diretamente.
Quando queremos um ponteiro de um determinado tipo de variável, basta acrescentar um *.
float var;
float *ptr = NULL;
Então para termos um ponteiro de funções, basta adicionar um *, certo? Bom, nem tanto.
float *func( float, float );
Define uma função que retorna um ponteiro para um float. Para definirmos um ponteiro, devemos fazer assim:
float (*func) ( float, float );
Podemos também fazer um ponteiro para uma função que retorna um ponteiro, assim:
float* (*func) ( float, float );
Agora vejamos como se usa isso. Esse ponteiro recém-declarado funciona exatamente como um ponteiro qualquer, basta atribuir-lhe o endereço de uma função:
float (*fptr) ( float, float );
fptr = +
fptr = minus; /* também funciona, mas não é modo correto */
E agora é só usá-lo como uma função normal:
var = (*fptr) ( 2, 3); /* ou */
var = fptr( 2, 3 );
Um exemplo funcional (não se esqueça do cabeçalho que foi mostrado no início do artigo):
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <stdio.h>
#include <stdlib.h>
#include "c-funcpointer.h"
/* E assim que se define o ponteiro para uma função */
float (*operation)( float, float ) = NULL;
int main( int argc, char *argv[] )
{
float store;
operation = +
/* Podemos usar a forma curta tambem, como veremos */
store = (*operation) ( 2, 3);
printf( "Plus: 2 + 3 = %g\n", store );
/* O jeito mais curto */
operation = −
store = operation( 7, 3);
printf( "Minus: 7 - 3 = %g\n", store );
operation = ×
store = operation( 4, 2);
printf( "Times: 4 * 2 = %g\n", store );
operation = &division;
store = operation( 6, 2);
printf( "Division: 6 / 2 = %g\n", store );
return 0;
}
Como foi visto no Capítulo 1, um programa é um conjunto de instruções armazenado na memória, assim como seus dados. Por este motivo, é possível referenciar o endereço de uma função. Em C, o endereço de uma função é acessível ao programador através de uma variável do tipo ponteiro para função.
Ponteiros para funções podem ser passados como argumentos para outras funções, e a função apontada pode ser invocada a partir de seu ponteiro. Um exemplo prático desta capacidade é seu uso em uma rotina de ordenação de elementos de um arranjo. Se o arranjo é de inteiros, então uma função de comparação de inteiros deverá ser suportada, tal como
/* * compara dois inteiros, retornando: * 0 se os dois elementos forem iguais * um inteiro negativo se o primeiro elemento for menor * um inteiro positivo se o primeiro elemento for maior */ int comp_int(int *e1, int *e2) { return(*e1 - *e2); }
O problema surge quando se deseja usar o mesmo algoritmo de ordenação para ordenar outros arranjos de tipos que não sejam inteiros. Por exemplo, se os elementos a comparar forem strings, então a rotina de comparação acima não mais serviria, apesar de todo o restante do algoritmo de ordenação ainda ser basicamente o mesmo.
A solução é passar qual função deve ser usada para a comparação como um dos argumentos para a rotina de ordenação genérica. Esta abordagem é adotada por rotinas usualmente supridas juntamente com o compilador C, tal como qsort para ordenação de arranjos e bsearch para a realização de busca binária em arranjos ordenados.
A forma de declarar uma variável do tipo ponteiro para função é ilustrada no seguinte exemplo, com uma referência à função comp_int definida acima:
main() { /* prototipo de comp_int: */ int comp_int(int *, int *); /* ponteiro para uma funcao retornando inteiro */ int (*apcmp)(); int a, b; apcmp = comp_int; /* inicializa ponteiro */ ... (*apcmp)(a, b); /* invoca funcao */ }
Algumas observações relativas a este exemplo são importantes. A primeira refere-se à declaração do ponteiro. A declaração de um ponteiro para a função deve incluir os parênteses em torno do nome da variável ponteiro. Uma definição na forma int *apcmp(); seria interpretada como o protótipo de uma função retornando um ponteiro para um inteiro, o que não é o desejado neste caso.
A segunda observação refere-se à forma utilizada para definir o valor do ponteiro no comando de atribuição. Como o protótipo de comp_int já havia sido definido, então o compilador sabe que este identificador refere-se a uma função. Quando o identificador comp_int é encontrado novamente, desta vez sem parênteses, ele é identificado como o endereço desta função, podendo assim ser atribuído a um ponteiro para uma função com o mesmo tipo de retorno. Repare a semelhança com referências a nomes de arranjos.
Finalmente, a invocação da função através de seu ponteiro: a forma usando o operador de dereferência (*apcmp) indica o conteúdo do ponteiro apcmp, que é a função (neste caso, comp_int). Assim, a última linha no exemplo é apenas uma invocação para a rotina apontada por apcmp, e o que vem a seguir de (*apcmp) são simplesmente os argumentos para a função. O padrão ANSI também permite que a forma equivalente,
apcmp(a, b); seja utilizada. Muitos programadores preferem a forma apresentada no exemplo original para tornar claro que um ponteiro para função está sendo usado, embora internamente não haja diferenças entre a ativação de uma função por seu nome ou através de um ponteiro.
Ponteiros para funções tornam-se interessantes quando o programador não pode determinar qual função deve ser executada em uma dada situação a não ser durante a execução do programa. Em tais casos, o trecho do programa referenciando esta ``função variável'' pode ser escrito em termos de ativação de uma função através de ponteiros para funções, os quais são corretamente inicializados em tempo de execução.