Ultimamente, tive alguma experiência com ponteiros de função em C.
Continuando com a tradição de responder às suas próprias perguntas, decidi fazer um pequeno resumo do básico, para aqueles que precisam mergulhar rapidamente no assunto.
c
function-pointers
Yuval Adam
fonte
fonte
Respostas:
Ponteiros de função em C
Vamos começar com uma função básica para a qual apontaremos :
Primeiro, vamos definir um ponteiro para uma função que recebe 2 se
int
retorna umint
:Agora podemos apontar com segurança para nossa função:
Agora que temos um ponteiro para a função, vamos usá-lo:
Passar o ponteiro para outra função é basicamente o mesmo:
Também podemos usar ponteiros de função em valores de retorno (tente acompanhar, fica confuso):
Mas é muito melhor usar um
typedef
:fonte
pshufb
, ela é lenta, portanto a implementação anterior ainda é mais rápida. x264 / x265 usam isso extensivamente e são de código aberto.Ponteiros de função em C podem ser usados para executar programação orientada a objetos em C.
Por exemplo, as seguintes linhas são escritas em C:
Sim, a
->
falta de umnew
operador é um sacrifício total, mas com certeza parece implicar que estamos definindo o texto de algumaString
classe"hello"
.Ao usar os ponteiros de função, é possível emular os métodos em C .
Como isso é realizado?
A
String
classe é na verdade umastruct
com vários ponteiros de função que funcionam como uma maneira de simular métodos. A seguir, uma declaração parcial daString
classe:Como pode ser visto, os métodos da
String
classe são, na verdade, indicadores de função para a função declarada. Ao preparar a instância deString
, anewString
função é chamada para configurar os ponteiros de função para suas respectivas funções:Por exemplo, a
getString
função que é chamada invocando oget
método é definida da seguinte maneira:Uma coisa que pode ser notada é que não existe o conceito de uma instância de um objeto e que existem métodos que realmente fazem parte de um objeto; portanto, um "auto-objeto" deve ser passado em cada chamada. (E
internal
é apenas um ocultostruct
que foi omitido da listagem de códigos anteriormente - é uma maneira de ocultar informações, mas isso não é relevante para os ponteiros de função.)Portanto, ao invés de ser capaz de fazer
s1->set("hello");
, é preciso passar o objeto para executar a açãos1->set(s1, "hello")
.Com essa explicação menor ter que passar uma referência a si mesmo fora do caminho, vamos passar para a próxima parte, que é herança em C .
Digamos que queremos criar uma subclasse de
String
, digamos, umImmutableString
. A fim de tornar o imutável string, oset
método não será acessível, mantendo o acesso aget
elength
, e forçar o "construtor" para aceitar umchar*
:Basicamente, para todas as subclasses, os métodos disponíveis são mais uma vez indicadores de função. Desta vez, a declaração para o
set
método não está presente, portanto, não pode ser chamada em aImmutableString
.Quanto à implementação do
ImmutableString
, o único código relevante é a função "construtor", anewImmutableString
:Ao instanciar o
ImmutableString
, os ponteiros de função para os métodosget
elength
realmente se referem ao métodoString.get
eString.length
, passando pelabase
variável que é umString
objeto armazenado internamente .O uso de um ponteiro de função pode obter a herança de um método de uma superclasse.
Podemos ainda continuar a polimorfismo em C .
Se, por exemplo, queremos mudar o comportamento do
length
método para retornar0
o tempo todo naImmutableString
classe por algum motivo, tudo o que precisa ser feito é:length
método de substituição .length
método de substituição .A adição de um
length
método de substituição emImmutableString
pode ser executada adicionando umlengthOverrideMethod
:Em seguida, o ponteiro de função para o
length
método no construtor é conectado aolengthOverrideMethod
:Agora, em vez de ter um comportamento idêntico para o
length
método naImmutableString
classe que aString
classe, agora olength
método fará referência ao comportamento definido nalengthOverrideMethod
função.Devo acrescentar um aviso de que ainda estou aprendendo a escrever com um estilo de programação orientado a objetos em C; portanto, provavelmente há pontos que não expliquei bem ou que podem estar errados em termos da melhor maneira de implementar o OOP em C. Mas meu objetivo era tentar ilustrar um dos muitos usos de ponteiros de função.
Para obter mais informações sobre como executar a programação orientada a objetos em C, consulte as seguintes perguntas:
fonte
ClassName_methodName
convenções de nomenclatura de funções. Somente então você obtém os mesmos custos de tempo de execução e armazenamento que em C ++ e Pascal.O guia para ser demitido: Como abusar de ponteiros de função no GCC em máquinas x86, compilando seu código manualmente:
Esses literais de cadeia de caracteres são bytes de código de máquina x86 de 32 bits.
0xC3
é umaret
instrução x86 .Você normalmente não escreveria isso à mão, escreveria em linguagem assembly e, em seguida, usaria um assembler
nasm
para montá-lo em um binário plano que você despeja em um literal de string C.Retorna o valor atual no registro EAX
Escreva uma função de swap
Escreva um contador de loop for para 1000, chamando alguma função cada vez
Você pode até escrever uma função recursiva que conta até 100
Observe que os compiladores colocam literais de seqüência de caracteres na
.rodata
seção (ou.rdata
no Windows), que é vinculada como parte do segmento de texto (junto com o código para funções).O segmento de texto tem permissão de leitura + Exec, então lançando strings literais de ponteiros de função funciona sem a necessidade
mprotect()
ouVirtualProtect()
chamadas de sistema como você precisaria para memória alocada dinamicamente. (Ougcc -z execstack
vincula o programa à pilha + segmento de dados + heap executável, como um hack rápido).Para desmontá-los, você pode compilar isso para colocar um rótulo nos bytes e usar um desmontador.
Compilando
gcc -c -m32 foo.c
e desmontandoobjdump -D -rwC -Mintel
, podemos obter a montagem e descobrir que esse código viola a ABI derrotando o EBX (um registro preservado de chamada) e geralmente é ineficiente.Esse código de máquina (provavelmente) funcionará no código de 32 bits no Windows, Linux, OS X e assim por diante: as convenções de chamada padrão em todos esses sistemas operacionais passam argumentos na pilha, em vez de serem mais eficientes nos registros. Mas o EBX é preservado em todas as convenções de chamada normais, portanto, usá-lo como um registro de rascunho sem salvá-lo / restaurá-lo pode facilmente causar uma falha no chamador.
fonte
Um dos meus usos favoritos para ponteiros de função é como iteradores baratos e fáceis -
fonte
int (*cb)(void *arg, ...)
. O valor de retorno do iterador também me permite parar mais cedo (se diferente de zero).Os ponteiros de função tornam-se fáceis de declarar depois que você tiver os declaradores básicos:
ID
: ID é um*D
: ponteiro D paraD(<parameters>)
: D função de tomada de<
parâmetros>
retornarEnquanto D é outro declarador criado usando essas mesmas regras. No final, em algum lugar, termina com
ID
(veja um exemplo abaixo), que é o nome da entidade declarada. Vamos tentar criar uma função que leva um ponteiro para uma função que não aceita nada e retorna int, e retorna um ponteiro para uma função que aceita um char e retorna int. Com defs tipo é assimComo você vê, é muito fácil construí-lo usando typedefs. Sem typedefs, também não é difícil com as regras do declarador acima, aplicadas de forma consistente. Como você vê, perdi a parte que o ponteiro aponta e a coisa que a função retorna. Isso é o que aparece no lado esquerdo da declaração e não é interessante: será adicionado no final se alguém já tiver construído o declarador. Vamos fazer isso. Construindo-o de forma consistente, primeiro prolixo - mostrando a estrutura usando
[
e]
:Como você vê, pode-se descrever um tipo completamente anexando declaradores um após o outro. A construção pode ser feita de duas maneiras. Uma é de baixo para cima, começando com a coisa mais certa (folhas) e trabalhando até o identificador. A outra maneira é de cima para baixo, começando no identificador, indo até as folhas. Eu vou mostrar para os dois lados.
Debaixo para cima
A construção começa com a coisa à direita: a coisa retornada, que é a função que leva char. Para manter os declaradores distintos, vou numerá-los:
Inserido o parâmetro char diretamente, pois é trivial. Adicionando um ponteiro ao declarador substituindo
D1
por*D2
. Note que temos que colocar parênteses*D2
. Isso pode ser conhecido consultando a precedência do*-operator
operador de chamada de função e()
. Sem nossos parênteses, o compilador o leria*(D2(char p))
. Mas isso não seria mais um substituto simples do D1*D2
, é claro. Parênteses sempre são permitidos em torno dos declaradores. Portanto, você não cometerá nada de errado se adicionar muitos deles, na verdade.O tipo de retorno está completo! Agora, vamos substituir
D2
pelo declarator função de tomada função<parameters>
retornar , o que éD3(<parameters>)
que estamos agora.Observe que nenhum parênteses é necessário, pois queremos
D3
ser um declarador de função e não um declarador de ponteiro neste momento. Ótimo, a única coisa que resta são os parâmetros para isso. O parâmetro é feito exatamente da mesma maneira que fizemos o tipo de retorno, apenas comchar
substituído porvoid
. Então eu vou copiá-lo:Eu substituí
D2
porID1
, já que terminamos com esse parâmetro (já é um ponteiro para uma função - não é necessário outro declarador).ID1
será o nome do parâmetro. Agora, como eu disse acima, no final, adicionamos o tipo que todos os declaradores modificam - o que aparece à esquerda de todas as declarações. Para funções, esse se torna o tipo de retorno. Para ponteiros, o tipo apontado etc ... É interessante quando escrito o tipo, ele aparecerá na ordem oposta, à direita :) De qualquer forma, substituindo-o, produz a declaração completa. Ambos os tempos, éint
claro.Chamei o identificador da função
ID0
nesse exemplo.Careca
Isso começa no identificador à esquerda na descrição do tipo, envolvendo esse declarador enquanto percorremos o caminho à direita. Comece com a função tendo
<
parâmetros>
retornandoA próxima coisa na descrição (depois de "retornar") foi apontar para . Vamos incorporar:
Em seguida, o próximo passo
<
>
foi executar os parâmetros retornando . O parâmetro é um caractere simples, então o colocamos imediatamente novamente, pois é realmente trivial.Observe os parênteses que adicionamos, já que queremos novamente que os
*
vínculos primeiro e depois o(char)
. Caso contrário, ele iria ler a função de tomar<
parâmetros de>
função retornando ... . Não, funções que retornam funções nem são permitidas.Agora só precisamos colocar
<
parâmetros>
. Vou mostrar uma versão curta da deriveração, pois acho que você já tem a ideia de como fazê-lo.Basta colocar
int
diante dos declarantes, como fizemos de baixo para cima, e terminamosA coisa legal
É de baixo para cima ou de cima para baixo? Estou acostumado a subir de cima para baixo, mas algumas pessoas podem ficar mais confortáveis com a descida. É uma questão de gosto, eu acho. Aliás, se você aplicar todos os operadores nessa declaração, você receberá um int:
Essa é uma boa propriedade de declarações em C: a declaração afirma que, se esses operadores forem usados em uma expressão usando o identificador, ele produzirá o tipo à esquerda. É assim também para matrizes.
Espero que você tenha gostado deste pequeno tutorial! Agora podemos vincular isso quando as pessoas se perguntam sobre a estranha sintaxe de declaração de funções. Eu tentei colocar o mínimo de C internos possível. Sinta-se livre para editar / consertar as coisas nele.
fonte
Outro bom uso para ponteiros de função:
alternando entre versões sem problemas
Eles são muito úteis para usar quando você deseja funções diferentes em momentos diferentes ou fases diferentes de desenvolvimento. Por exemplo, estou desenvolvendo um aplicativo em um computador host que possui um console, mas a versão final do software será colocada em um Avnet ZedBoard (que possui portas para monitores e consoles, mas eles não são necessários / desejados para o último lançamento). Portanto, durante o desenvolvimento, usarei
printf
para exibir mensagens de status e de erro, mas quando terminar, não quero nada impresso. Aqui está o que eu fiz:version.h
Em
version.c
vou definir os 2 protótipos de função presentes emversion.h
version.c
Observe como o ponteiro da função é prototipado
version.h
comovoid (* zprintf)(const char *, ...);
Quando é referenciado no aplicativo, ele começa a ser executado onde quer que esteja apontando, o que ainda não foi definido.
Em
version.c
, observe naboard_init()
função em quezprintf
é atribuída uma função exclusiva (cuja assinatura da função corresponde), dependendo da versão definida emversion.h
zprintf = &printf;
O zprintf chama printf para fins de depuraçãoou
zprintf = &noprint;
O zprintf simplesmente retorna e não executa código desnecessárioA execução do código ficará assim:
mainProg.c
O código acima será usado
printf
se estiver no modo de depuração ou não fará nada se estiver no modo de liberação. Isso é muito mais fácil do que passar pelo projeto inteiro e comentar ou excluir o código. Tudo o que eu preciso fazer é alterar a versãoversion.h
e o código fará o resto!fonte
O ponteiro de função é geralmente definido por
typedef
e usado como parâmetro e valor de retorno.As respostas acima já explicaram bastante, apenas dou um exemplo completo:
fonte
Um dos grandes usos para ponteiros de função em C é chamar uma função selecionada em tempo de execução. Por exemplo, a biblioteca de tempo de execução C possui duas rotinas
qsort
ebsearch
, que levam um ponteiro para uma função chamada para comparar dois itens que estão sendo classificados; isso permite que você classifique ou pesquise, respectivamente, qualquer coisa, com base nos critérios que deseja usar.Um exemplo muito básico, se houver uma função chamada
print(int x, int y)
que, por sua vez, pode exigir a chamada de uma função (add()
ousub()
que são do mesmo tipo), o que faremos, adicionaremos um argumento de ponteiro deprint()
função à função, como mostrado abaixo :A saída é:
fonte
A função Partir do zero possui um endereço de memória de onde eles começam a executar. Na linguagem Assembly, eles são chamados como (chamada "endereço de memória da função"). Agora volte para C Se a função tiver um endereço de memória, eles poderão ser manipulados por ponteiros em C. Então, pelas regras de C
1.Primeiro você precisa declarar um ponteiro para funcionar 2.Passar o endereço da função desejada
**** Nota-> as funções devem ser do mesmo tipo ****
Este programa simples ilustrará todas as coisas.
Depois disso, veja Como a máquina os entende.Glimpse das instruções da máquina do programa acima na arquitetura de 32 bits.
A área de marca vermelha está mostrando como o endereço está sendo trocado e armazenado no eax. Então é uma instrução de chamada no eax. eax contém o endereço desejado da função.
fonte
Um ponteiro de função é uma variável que contém o endereço de uma função. Como é uma variável de ponteiro, embora com algumas propriedades restritas, você pode usá-lo como faria com qualquer outra variável de ponteiro nas estruturas de dados.
A única exceção em que consigo pensar é tratar o ponteiro da função como apontando para algo diferente de um único valor. Fazer a aritmética do ponteiro aumentando ou diminuindo um ponteiro de função ou adicionando / subtraindo um deslocamento a um ponteiro de função não é realmente útil, pois um ponteiro de função aponta apenas para uma única coisa, o ponto de entrada de uma função.
O tamanho de uma variável de ponteiro de função, o número de bytes ocupados pela variável, pode variar dependendo da arquitetura subjacente, por exemplo, x32 ou x64 ou o que for.
A declaração para uma variável de ponteiro de função precisa especificar o mesmo tipo de informação que uma declaração de função para que o compilador C faça os tipos de verificação que normalmente faz. Se você não especificar uma lista de parâmetros na declaração / definição do ponteiro de função, o compilador C não poderá verificar o uso de parâmetros. Há casos em que essa falta de verificação pode ser útil, mas lembre-se de que uma rede de segurança foi removida.
Alguns exemplos:
As duas primeiras declarações são um pouco semelhantes:
func
é uma função que recebe umint
e umchar *
e retorna umint
pFunc
é um ponteiro de função ao qual é atribuído o endereço de uma função que recebe umint
e umchar *
e retorna umint
Portanto, do acima exposto, poderíamos ter uma linha de origem na qual o endereço da função
func()
é atribuído à variável de ponteiro da funçãopFunc
como empFunc = func;
.Observe a sintaxe usada com uma declaração / definição de ponteiro de função na qual parênteses são usados para superar as regras de precedência do operador natural.
Vários exemplos de uso diferentes
Alguns exemplos de uso de um ponteiro de função:
Você pode usar listas de parâmetros de comprimento variável na definição de um ponteiro de função.
Ou você não pode especificar uma lista de parâmetros. Isso pode ser útil, mas elimina a oportunidade do compilador C de executar verificações na lista de argumentos fornecida.
Moldes de estilo C
Você pode usar projeções de estilo C com ponteiros de função. No entanto, esteja ciente de que um compilador C pode ser negligente quanto a verificações ou fornecer avisos em vez de erros.
Comparar ponteiro de função com igualdade
Você pode verificar se um ponteiro de função é igual a um endereço de função específico usando uma
if
instrução, embora não tenha certeza de quão útil isso seria. Outros operadores de comparação parecem ter ainda menos utilidade.Uma matriz de ponteiros de função
E se você quiser ter uma matriz de ponteiros de função, cada um dos elementos cuja lista de argumentos possui diferenças, poderá definir um ponteiro de função com a lista de argumentos não especificada (não o
void
que significa que não há argumentos, mas apenas não especificado), algo como o seguinte, embora você pode ver avisos do compilador C. Isso também funciona para um parâmetro de ponteiro de função para uma função:Estilo C
namespace
Usando Globalstruct
com Ponteiros de FunçãoVocê pode usar a
static
palavra-chave para especificar uma função cujo nome é escopo do arquivo e atribuí-la a uma variável global como uma maneira de fornecer algo semelhante ànamespace
funcionalidade do C ++.Em um arquivo de cabeçalho, defina uma estrutura que será nosso espaço para nome, juntamente com uma variável global que a utilize.
Em seguida, no arquivo de origem C:
Isso seria usado especificando o nome completo da variável de estrutura global e o nome do membro para acessar a função. O
const
modificador é usado no global para que não possa ser alterado por acidente.Áreas de aplicação de ponteiros de função
Um componente de biblioteca DLL pode fazer algo semelhante à
namespace
abordagem de estilo C , na qual uma interface de biblioteca específica é solicitada a partir de um método de fábrica em uma interface de biblioteca que suporta a criação destruct
ponteiros de função contendo essa interface. Essa interface de biblioteca carrega a versão DLL solicitada, cria uma estrutura com os ponteiros de função necessários e, em seguida, retorna a estrutura ao chamador solicitante para uso.e isso pode ser usado como em:
A mesma abordagem pode ser usada para definir uma camada de hardware abstrata para código que usa um modelo específico do hardware subjacente. Os ponteiros de função são preenchidos com funções específicas de hardware por uma fábrica para fornecer a funcionalidade específica de hardware que implementa as funções especificadas no modelo de hardware abstrato. Isso pode ser usado para fornecer uma camada de hardware abstrata usada pelo software que chama uma função de fábrica para obter a interface específica da função de hardware e, em seguida, usa os ponteiros de função fornecidos para executar ações para o hardware subjacente sem a necessidade de conhecer detalhes de implementação sobre o destino específico .
Ponteiros de função para criar delegados, manipuladores e retornos de chamada
Você pode usar ponteiros de função como uma maneira de delegar alguma tarefa ou funcionalidade. O exemplo clássico em C é o ponteiro da função de delegação de comparação usado com as funções da biblioteca C padrão
qsort()
ebsearch()
para fornecer a ordem de intercalação para classificar uma lista de itens ou executar uma pesquisa binária em uma lista classificada de itens. O delegado da função de comparação especifica o algoritmo de intercalação usado na classificação ou na pesquisa binária.Outro uso é semelhante à aplicação de um algoritmo a um contêiner da Biblioteca de Modelos Padrão do C ++.
Outro exemplo é o código fonte da GUI, no qual um manipulador para um evento específico é registrado, fornecendo um ponteiro de função que é realmente chamado quando o evento acontece. A estrutura do Microsoft MFC com seus mapas de mensagens usa algo semelhante para lidar com mensagens do Windows entregues em uma janela ou thread.
Funções assíncronas que requerem um retorno de chamada são semelhantes a um manipulador de eventos. O usuário da função assíncrona chama a função assíncrona para iniciar alguma ação e fornece um ponteiro de função que a função assíncrona chamará quando a ação for concluída. Nesse caso, o evento é a função assíncrona que completa sua tarefa.
fonte
Como os ponteiros de função geralmente são retornos de chamada digitados, você pode dar uma olhada nos retornos de chamada seguros do tipo . O mesmo se aplica aos pontos de entrada, etc, de funções que não são retornos de chamada.
C é bastante inconstante e perdoa ao mesmo tempo :)
fonte