Noções básicas sobre typedefs para ponteiros de função em C

237

Sempre fiquei um pouco perplexo quando leio o código de outras pessoas que tinha typedefs para ponteiros para funções com argumentos. Lembro-me de que demorei um pouco para chegar a essa definição enquanto tentava entender um algoritmo numérico escrito em C há um tempo atrás. Então, você poderia compartilhar suas dicas e pensamentos sobre como escrever bons typedefs para ponteiros para funções (faça e não faça), por que eles são úteis e como entender o trabalho de outras pessoas? Obrigado!

nbro
fonte
1
Você pode fornecer alguns exemplos?
21909 Artelius
2
Você não quis dizer typedefs para ponteiros de função, em vez de macros para ponteiros de função? Eu já vi o primeiro, mas não o último.
dave4420

Respostas:

297

Considere a signal()função do padrão C:

extern void (*signal(int, void(*)(int)))(int);

Perfeitamente obscuramente óbvio - é uma função que leva dois argumentos, um número inteiro e um ponteiro para uma função que leva um número inteiro como argumento e não retorna nada, e ele (signal() ) retorna um ponteiro para uma função que leva um número inteiro como argumento e retorna nada.

Se você escrever:

typedef void (*SignalHandler)(int signum);

então você pode declarar signal()como:

extern  SignalHandler signal(int signum, SignalHandler handler);

Isso significa a mesma coisa, mas geralmente é considerado um pouco mais fácil de ler. É mais claro que a função pega um inte a SignalHandlere retorna a SignalHandler.

Demora um pouco para se acostumar, no entanto. A única coisa que você não pode fazer é escrever uma função de manipulador de sinal usando o SignalHandler typedefna definição de função.

Eu ainda sou da velha escola que prefere chamar um ponteiro de função como:

(*functionpointer)(arg1, arg2, ...);

A sintaxe moderna usa apenas:

functionpointer(arg1, arg2, ...);

Eu posso ver por que isso funciona - eu apenas prefiro saber que eu preciso procurar onde a variável é inicializada e não para uma função chamada functionpointer.


Sam comentou:

Eu já vi essa explicação antes. E então, como é o caso agora, acho que o que não recebi foi a conexão entre as duas declarações:

    extern void (*signal(int, void()(int)))(int);  /*and*/

    typedef void (*SignalHandler)(int signum);
    extern SignalHandler signal(int signum, SignalHandler handler);

Ou, o que quero perguntar é: qual é o conceito subjacente que se pode usar para criar a segunda versão que você tem? Qual é o fundamental que conecta "SignalHandler" e o primeiro typedef? Eu acho que o que precisa ser explicado aqui é o que o typedef está realmente fazendo aqui.

Vamos tentar de novo. O primeiro deles é retirado diretamente do padrão C - redigitei-o e verifiquei se os parênteses estavam corretos (até corrigi-lo - é um biscoito difícil de lembrar).

Antes de tudo, lembre-se de que typedefintroduz um alias para um tipo. Portanto, o alias é SignalHandlere seu tipo é:

um ponteiro para uma função que usa um número inteiro como argumento e não retorna nada.

A parte "não retorna nada" está escrita void; o argumento que é um número inteiro é (eu confio) auto-explicativo. A seguinte notação é simplesmente (ou não) como C soletra o ponteiro para funcionar, recebendo argumentos conforme especificado e retornando o tipo especificado:

type (*function)(argtypes);

Depois de criar o tipo de manipulador de sinal, posso usá-lo para declarar variáveis ​​e assim por diante. Por exemplo:

static void alarm_catcher(int signum)
{
    fprintf(stderr, "%s() called (%d)\n", __func__, signum);
}

static void signal_catcher(int signum)
{
    fprintf(stderr, "%s() called (%d) - exiting\n", __func__, signum);
    exit(1);
}

static struct Handlers
{
    int              signum;
    SignalHandler    handler;
} handler[] =
{
    { SIGALRM,   alarm_catcher  },
    { SIGINT,    signal_catcher },
    { SIGQUIT,   signal_catcher },
};

int main(void)
{
    size_t num_handlers = sizeof(handler) / sizeof(handler[0]);
    size_t i;

    for (i = 0; i < num_handlers; i++)
    {
        SignalHandler old_handler = signal(handler[i].signum, SIG_IGN);
        if (old_handler != SIG_IGN)
            old_handler = signal(handler[i].signum, handler[i].handler);
        assert(old_handler == SIG_IGN);
    }

    ...continue with ordinary processing...

    return(EXIT_SUCCESS);
}

Observe como evitar o uso printf()em um manipulador de sinais?

Então, o que fizemos aqui - além de omitir 4 cabeçalhos padrão que seriam necessários para tornar o código compilado corretamente?

As duas primeiras funções são funções que usam um único número inteiro e não retornam nada. Um deles, na verdade, não retorna, graças ao exit(1);mas o outro retorna após a impressão de uma mensagem. Esteja ciente de que o padrão C não permite que você faça muito dentro de um manipulador de sinal; O POSIX é um pouco mais generoso no que é permitido, mas oficialmente não sanciona as chamadas fprintf(). Também imprimo o número do sinal recebido. Na alarm_handler()função, o valor sempre será SIGALRMcomo esse é o único sinal para o qual ele é manipulador, mas signal_handler()pode obter SIGINTou SIGQUITcomo o número do sinal, porque a mesma função é usada para ambos.

Em seguida, crio uma matriz de estruturas, em que cada elemento identifica um número de sinal e o manipulador a ser instalado para esse sinal. Eu escolhi me preocupar com três sinais; Eu costumava me preocupar SIGHUP, SIGPIPEe SIGTERMtambém e se eles são definidos ( #ifdefcompilação condicional), mas isso apenas complica as coisas. Eu provavelmente também usaria POSIX em sigaction()vez designal() , mas isso é outra questão; vamos ficar com o que começamos.

A main()função itera sobre a lista de manipuladores a serem instalados. Para cada manipulador, ele primeiro chama signal()para descobrir se o processo está atualmente ignorando o sinal e, enquanto isso, é instalado SIG_IGNcomo manipulador, o que garante que o sinal permaneça ignorado. Se o sinal não estava sendo ignorado anteriormente, ele chama signal()novamente, desta vez para instalar o manipulador de sinal preferido. (O outro valor é presumivelmente SIG_DFL, o manipulador de sinal padrão para o sinal.) Como a primeira chamada para 'signal ()' define o manipulador SIG_IGNe signal()retorna o manipulador de erro anterior, o valor de oldapós a ifinstrução deve ser SIG_IGN- daí a afirmação. (Bem, poderia serSIG_ERR se algo desse dramaticamente errado - mas eu aprenderia sobre isso com a afirmação.)

O programa então faz suas coisas e sai normalmente.

Observe que o nome de uma função pode ser considerado como um ponteiro para uma função do tipo apropriado. Quando você não aplica os parênteses da chamada de função - como nos inicializadores, por exemplo - o nome da função se torna um ponteiro de função. É também por isso que é razoável invocar funções por meio da pointertofunction(arg1, arg2)notação; quando você vê alarm_handler(1), pode considerar que alarm_handleré um ponteiro para a função e, portanto, alarm_handler(1)é uma invocação de uma função por meio de um ponteiro de função.

Então, até agora, mostrei que uma SignalHandlervariável é relativamente simples de usar, desde que você tenha o tipo certo de valor para atribuir a ela - que é o que as duas funções do manipulador de sinal fornecem.

Agora voltamos à pergunta - como as duas declarações se signal()relacionam.

Vamos revisar a segunda declaração:

 extern SignalHandler signal(int signum, SignalHandler handler);

Se mudarmos o nome da função e o tipo como este:

 extern double function(int num1, double num2);

você não teria problemas em interpretar isso como uma função que recebe um inte um doublecomo argumentos e retorna um doublevalor (você faria? talvez seja melhor você não confessar se isso for problemático - mas talvez você deva ser cauteloso ao fazer perguntas tão difíceis como este se for um problema).

Agora, em vez de ser um double, osignal() função assume a SignalHandlercomo segundo argumento e retorna um como resultado.

A mecânica pela qual isso também pode ser tratado como:

extern void (*signal(int signum, void(*handler)(int signum)))(int signum);

é complicado de explicar - então provavelmente vou estragar tudo. Dessa vez, dei os nomes dos parâmetros - embora os nomes não sejam críticos.

Em geral, em C, o mecanismo de declaração é tal que se você escrever:

type var;

então, quando você escreve var, representa um valor do dado type. Por exemplo:

int     i;            // i is an int
int    *ip;           // *ip is an int, so ip is a pointer to an integer
int     abs(int val); // abs(-1) is an int, so abs is a (pointer to a)
                      // function returning an int and taking an int argument

No padrão, typedefé tratada como uma classe de armazenamento na gramática, um pouco como statice externsão classes de armazenamento.

typedef void (*SignalHandler)(int signum);

significa que quando você vê uma variável do tipo SignalHandler(digamos alarm_handler) chamada como:

(*alarm_handler)(-1);

o resultado tem type void- não há resultado. E (*alarm_handler)(-1);é uma invocação de alarm_handler()com argumento -1.

Então, se declaramos:

extern SignalHandler alt_signal(void);

significa que:

(*alt_signal)();

representa um valor nulo. E portanto:

extern void (*alt_signal(void))(int signum);

é equivalente. Agora, signal()é mais complexo porque não apenas retorna a SignalHandler, mas também aceita os SignalHandlerargumentos int e a as:

extern void (*signal(int signum, SignalHandler handler))(int signum);

extern void (*signal(int signum, void (*handler)(int signum)))(int signum);

Se isso ainda o confunde, não tenho certeza de como ajudar - ele ainda é misterioso em alguns níveis, mas me acostumei a como funciona e, portanto, posso dizer que, se você continuar por mais 25 anos ou assim, se tornará uma segunda natureza para você (e talvez até um pouco mais rápido se você for esperto).

Jonathan Leffler
fonte
3
Eu já vi essa explicação antes. E então, como é o caso agora, acho que o que não recebi foi a conexão entre as duas instruções: extern void ( sinal (int, void ( ) (int))) (int); / * e * / typedef void (* SignalHandler) (int signum); sinal externo SignalHandler (int signum, manipulador SignalHandler); Ou, o que eu quero perguntar é: qual é o conceito subjacente que se pode usar para criar a segunda versão que você tem? Qual é o fundamental que conecta "SignalHandler" e o primeiro typedef? Eu acho que o que precisa ser explicado aqui é o que o typedef está realmente fazendo aqui. Thx
6
Ótima resposta, estou feliz por ter voltado a esta discussão. Acho que não entendo tudo, mas um dia vou entender. É por isso que eu gosto de SO. Obrigado.
Toto
2
Apenas para escolher um nit: não é seguro chamar printf () e amigos dentro de um manipulador de sinal; printf () não é reentrante (basicamente porque ele pode chamar malloc (), que não é de reentrada)
wildplasser
4
Os extern void (*signal(int, void(*)(int)))(int);meios para os quais a signal(int, void(*)(int))função retornará um ponteiro de função void f(int). Quando você deseja especificar um ponteiro de função como o valor de retorno , a sintaxe fica complicada. Você deve colocar o tipo de valor de retorno à esquerda e a lista de argumentos à direita , enquanto este é o meio que você está definindo. E, nesse caso, a signal()própria função usa um ponteiro de função como parâmetro, o que complica ainda mais as coisas. A boa notícia é que, se você pode ler este, a Força já está com você. :).
smwikipedia
1
O que há de antiquado em usar &na frente de um nome de função? É totalmente desnecessário; inútil, até. E definitivamente não é "velha escola". Old school usa um nome de função simples e simples.
Jonathan Leffler
80

Um ponteiro de função é como qualquer outro ponteiro, mas aponta para o endereço de uma função em vez do endereço de dados (na pilha ou pilha). Como qualquer ponteiro, ele precisa ser digitado corretamente. As funções são definidas pelo valor de retorno e pelos tipos de parâmetros que eles aceitam. Portanto, para descrever completamente uma função, você deve incluir seu valor de retorno e o tipo de cada parâmetro é aceito. Quando você digita essa definição, atribui-lhe um 'nome amigável', o que facilita a criação e referência de ponteiros usando essa definição.

Por exemplo, suponha que você tenha uma função:

float doMultiplication (float num1, float num2 ) {
    return num1 * num2; }

então o seguinte typedef:

typedef float(*pt2Func)(float, float);

pode ser usado para apontar para esta doMulitplicationfunção. É simplesmente definir um ponteiro para uma função que retorna um float e aceita dois parâmetros, cada um do tipo float. Esta definição tem o nome amigável pt2Func. Observe quept2Func pode apontar para QUALQUER função que retorne um float e receba 2 floats.

Portanto, você pode criar um ponteiro que aponte para a função doMultiplication da seguinte maneira:

pt2Func *myFnPtr = &doMultiplication;

e você pode chamar a função usando este ponteiro da seguinte maneira:

float result = (*myFnPtr)(2.0, 5.1);

Isso faz uma boa leitura: http://www.newty.de/fpt/index.html

psychotik
fonte
psychotik, obrigado! Isso foi útil. O link para a página da web de ponteiros de função é realmente útil. Lendo agora.
... No entanto, esse link newty.de não aparecer para falar sobre typedefs em tudo :( Assim, mesmo que essa ligação é grande, mas as respostas neste tópico sobre typedefs são de valor inestimável!
11
Você pode querer fazer em pt2Func myFnPtr = &doMultiplication;vez de pt2Func *myFnPtr = &doMultiplication;como myFnPtrjá é um ponteiro.
Tamilselvan
1
declarando pt2Func * myFnPtr = & doMultiplication; em vez de pt2Func myFnPtr = & doMultiplication; lança um aviso.
AlphaGoku
2
@Tamilselvan está correto. myFunPtrjá é um ponteiro de função de modo utilizaçãopt2Func myFnPtr = &doMultiplication;
Dustin Biser
35

Uma maneira muito fácil de entender o typedef do ponteiro de função:

int add(int a, int b)
{
    return (a+b);
}

typedef int (*add_integer)(int, int); //declaration of function pointer

int main()
{
    add_integer addition = add; //typedef assigns a new variable i.e. "addition" to original function "add"
    int c = addition(11, 11);   //calling function via new variable
    printf("%d",c);
    return 0;
}
user2786027
fonte
32

cdeclé uma ótima ferramenta para decifrar sintaxe estranha como declarações de ponteiros de função. Você pode usá-lo para gerá-los também.

Quanto a dicas para facilitar a análise de declarações complicadas para manutenção futura (por você ou por outras pessoas), recomendo criar typedefpequenos pedaços e usar esses pequenos pedaços como blocos de construção para expressões maiores e mais complicadas. Por exemplo:

typedef int (*FUNC_TYPE_1)(void);
typedef double (*FUNC_TYPE_2)(void);
typedef FUNC_TYPE_1 (*FUNC_TYPE_3)(FUNC_TYPE_2);

ao invés de:

typedef int (*(*FUNC_TYPE_3)(double (*)(void)))(void);

cdecl pode ajudá-lo com essas coisas:

cdecl> explain int (*FUNC_TYPE_1)(void)
declare FUNC_TYPE_1 as pointer to function (void) returning int
cdecl> explain double (*FUNC_TYPE_2)(void)
declare FUNC_TYPE_2 as pointer to function (void) returning double
cdecl> declare FUNC_TYPE_3 as pointer to function (pointer to function (void) returning double) returning pointer to function (void) returning int
int (*(*FUNC_TYPE_3)(double (*)(void )))(void )

E é (de fato) exatamente como eu gere essa bagunça louca acima.

Carl Norum
fonte
2
Olá Carl, esse foi um exemplo e explicação muito perspicaz. Além disso, obrigado por mostrar o uso do cdecl. Muito apreciado.
Existe cdecl para windows?
Jack
@ Jack, tenho certeza que você pode construí-lo, sim.
Carl Norum
2
Há também o cdecl.org, que fornece o mesmo tipo de recurso, mas online. Útil para nós desenvolvedores de Windows.
Zaknotzach
12
int add(int a, int b)
{
  return (a+b);
}
int minus(int a, int b)
{
  return (a-b);
}

typedef int (*math_func)(int, int); //declaration of function pointer

int main()
{
  math_func addition = add;  //typedef assigns a new variable i.e. "addition" to original function "add"
  math_func substract = minus; //typedef assigns a new variable i.e. "substract" to original function "minus"

  int c = addition(11, 11);   //calling function via new variable
  printf("%d\n",c);
  c = substract(11, 5);   //calling function via new variable
  printf("%d",c);
  return 0;
}

Saída disso é:

22

6

Observe que o mesmo definidor math_func foi usado para declarar a função.

A mesma abordagem do typedef pode ser usada para estrutura externa. (Usando sturuct em outro arquivo.)

Harshal Doshi Jain
fonte
5

Use typedefs para definir tipos mais complicados, como ponteiros de função

Vou dar o exemplo de definir uma máquina de estado em C

    typedef  int (*action_handler_t)(void *ctx, void *data);

agora definimos um tipo chamado action_handler que pega dois ponteiros e retorna um int

defina sua máquina de estado

    typedef struct
    {
      state_t curr_state;   /* Enum for the Current state */
      event_t event;  /* Enum for the event */
      state_t next_state;   /* Enum for the next state */
      action_handler_t event_handler; /* Function-pointer to the action */

     }state_element;

O ponteiro de função para a ação parece um tipo simples e o typedef serve principalmente para esse propósito.

Todos os meus manipuladores de eventos agora devem aderir ao tipo definido por action_handler

    int handle_event_a(void *fsm_ctx, void *in_msg );

    int handle_event_b(void *fsm_ctx, void *in_msg );

Referências:

Programação C especializada por Linden

vaaz
fonte
4

Este é o exemplo mais simples de ponteiros de função e matrizes de ponteiros de função que escrevi como exercício.

    typedef double (*pf)(double x);  /*this defines a type pf */

    double f1(double x) { return(x+x);}
    double f2(double x) { return(x*x);}

    pf pa[] = {f1, f2};


    main()
    {
        pf p;

        p = pa[0];
        printf("%f\n", p(3.0));
        p = pa[1];
        printf("%f\n", p(3.0));
    }
Bing Bang
fonte