C não é tão difícil: void (* (* f []) ()) ()


188

Acabei de ver uma foto hoje e acho que apreciaria explicações. Então, aqui está a imagem:

algum código c

Achei isso confuso e me perguntei se esses códigos são práticos. Pesquisei a imagem no Google e encontrei outra imagem nesta entrada do reddit, e aqui está essa imagem:

alguma explicação interessante

Então essa "leitura em espiral" é algo válido? É assim que os compiladores C analisam?
Seria ótimo se houvesse explicações mais simples para esse código estranho.
Além de tudo, esses tipos de códigos podem ser úteis? Em caso afirmativo, onde e quando?

uma pergunta sobre "regra espiral", mas não estou apenas perguntando sobre como ela é aplicada ou como as expressões são lidas com essa regra. Estou questionando o uso de tais expressões e a validade da regra espiral também. Com relação a isso, algumas respostas legais já foram publicadas.


9
Como a explicação poderia ser mais simples? Abrange todos os aspectos da definição de fw / algumas palavras para cada ponto-chave.
Scott Hunter

29
Talvez C seja difícil? Na verdade, isso declara fcomo uma matriz de ponteiros para funções que poderiam aceitar qualquer argumento ... se fosse void (*(*f[])(void))(void);, então sim, seriam funções que não
precisam de

18
Na prática, não codifique esse código obscuro. Utilize typedef para assinaturas
Basile Starynkevitch

4
qualquer declaração envolvendo ponteiros de função pode ser difícil. Isso não significa que C ou C ++ normal sejam difíceis da mesma maneira. Outras linguagens resolvem isso de maneiras diferentes, incluindo a ausência de ponteiros de função, o que pode ser uma omissão significativa em alguns casos
Kate Gregory

20
Se você apertar os olhos, parece LISP.
user2023861

Respostas:


117

Existe uma regra chamada " Regra no sentido horário / espiral" para ajudar a encontrar o significado de uma declaração complexa.

Do c-faq :

Existem três etapas simples a seguir:

  1. Começando com o elemento desconhecido, mova-se no sentido espiral / horário; ao encontrar os seguintes elementos, substitua-os pelas frases em inglês correspondentes:

    [X]ou []
    => Tamanho da matriz X de ... ou Tamanho indefinido da matriz de ...

    (type1, type2)
    => função passando type1 e type2 retornando ...

    *
    => ponteiro (s) para ...

  2. Continue fazendo isso no sentido espiral / horário até que todos os tokens estejam cobertos.

  3. Sempre resolva qualquer coisa entre parênteses primeiro!

Você pode verificar o link acima para exemplos.

Observe também que, para ajudá-lo, também existe um site chamado:

http://www.cdecl.org

Você pode inserir uma declaração C e ela dará o seu significado em inglês. Para

void (*(*f[])())()

produz:

declare f como matriz do ponteiro para retornar função ponteiro para retornar função vazia

EDITAR:

Conforme apontado nos comentários de Random832 , a regra espiral não aborda a matriz de matrizes e levará a um resultado errado (na maioria) dessas declarações. Por exemplo, int **x[1][2];a regra em espiral ignora o fato de que []tem maior precedência *.

Quando na frente da matriz de matrizes, é possível adicionar primeiro parênteses explícitos antes de aplicar a regra em espiral. Por exemplo: int **x[1][2];é o mesmo que int **(x[1][2]);(também válido C) devido à precedência e a regra espiral o lê corretamente como "x é uma matriz 1 da matriz 2 de ponteiro para ponteiro para int", que é a declaração correta em inglês.

Observe que esse problema também foi abordado nesta resposta por James Kanze (apontado por hackers nos comentários).


5
Eu gostaria que o cdecl.org fosse melhor #
Grady Player

8
Não existe uma "regra espiral" ... "int *** foo [] [] []" define uma matriz de matrizes de matrizes de ponteiros para ponteiros para ponteiros. A "espiral" só vem do fato de que essa declaração passou a agrupar as coisas entre parênteses de uma maneira que as fez se alternar. Está tudo à direita, depois à esquerda, dentro de cada conjunto de parênteses.
usar o seguinte comando

1
@ Random832 Existe uma "regra espiral", que abrange o caso que você acabou de mencionar, isto é, fala sobre como lidar com parênteses / matrizes, etc. Claro que não é uma regra C padrão, mas um bom mnemônico para descobrir como lidar com declarações complicadas. IMHO, é extremamente útil e economiza quando você está com problemas ou quando o cdecl.org não pode analisar a declaração. É claro que não se deve abusar de tais declarações, mas é bom saber como elas são analisadas.
Vsoftco 01/01

5
@vsoftco Mas não está "se movendo no sentido espiral / horário" se você apenas virar quando chegar entre parênteses.
precisa saber é o seguinte

2
ouah, você deve mencionar que a regra espiral não é universal .
haccks

105

A regra "espiral" cai das seguintes regras de precedência:

T *a[]    -- a is an array of pointer to T
T (*a)[]  -- a is a pointer to an array of T
T *f()    -- f is a function returning a pointer to T
T (*f)()  -- f is a pointer to a function returning T

O subscrito []() operadores de e chamada de função têm maior precedência que unário *, portanto, *f()são analisados ​​como *(f())e *a[]são analisados ​​como *(a[]).

Portanto, se você deseja um ponteiro para uma matriz ou um ponteiro para uma função, precisa agrupar explicitamente o *com o identificador, como em (*a)[]ou (*f)().

Então você percebe isso ae fpode ser expressões mais complicadas do que apenas identificadores; in T (*a)[N], apoderia ser um identificador simples ou uma chamada de função como (*f())[N]( a->f() ), ou poderia ser uma matriz como (*p[M])[N], ( a-> p[M]), ou poderia ser uma matriz de ponteiros para funções como (*(*p[M])())[N]( a-> (*p[M])()), etc.

Seria bom se o operador de indireção *fosse postfix em vez de unário, o que tornaria as declarações um pouco mais fáceis de ler da esquerda para a direita ( void f[]*()*();definitivamente flui melhor que void (*(*f[])())()), mas não é.

Quando você se deparar com uma declaração cabeluda como essa, comece encontrando o identificador mais esquerda e aplique as regras de precedência acima, aplicando-as recursivamente a qualquer parâmetro de função:

         f              -- f
         f[]            -- is an array
        *f[]            -- of pointers  ([] has higher precedence than *)
       (*f[])()         -- to functions
      *(*f[])()         -- returning pointers
     (*(*f[])())()      -- to functions
void (*(*f[])())();     -- returning void

o signal função na biblioteca padrão é provavelmente o tipo de amostra para esse tipo de insanidade:

       signal                                       -- signal
       signal(                          )           -- is a function with parameters
       signal(    sig,                  )           --    sig
       signal(int sig,                  )           --    which is an int and
       signal(int sig,        func      )           --    func
       signal(int sig,       *func      )           --    which is a pointer
       signal(int sig,      (*func)(int))           --    to a function taking an int                                           
       signal(int sig, void (*func)(int))           --    returning void
      *signal(int sig, void (*func)(int))           -- returning a pointer
     (*signal(int sig, void (*func)(int)))(int)     -- to a function taking an int
void (*signal(int sig, void (*func)(int)))(int);    -- and returning void

Neste ponto, a maioria das pessoas diz "use typedefs", o que certamente é uma opção:

typedef void outerfunc(void);
typedef outerfunc *innerfunc(void);

innerfunc *f[N];

Mas...

Como você usaria f em uma expressão? Você sabe que é uma matriz de ponteiros, mas como você o utiliza para executar a função correta? Você precisa revisar os typedefs e descobrir a sintaxe correta. Por outro lado, a versão "nua" é bastante óbvia, mas mostra exatamente como usar f uma expressão (ou seja (*(*f[i])())();, supondo que nenhuma função use argumentos).


7
Obrigado por dar o exemplo de 'sinal', mostrando que esse tipo de coisa aparece na natureza.
Justsalt

Esse é um ótimo exemplo.
Casey #

Gostei da sua fárvore de desaceleração, explicando a precedência ... por alguma razão eu sempre obter um chute para fora de ASCII-art, especialmente quando se trata de explicar as coisas :)
txtechhelp

1
supondo que nenhuma das funções receba argumentos : você deve usar as voidfunções entre parênteses, caso contrário, ela pode receber quaisquer argumentos.
haccks

1
@haccks: para a declaração, sim; Eu estava falando sobre a chamada de função.
John Bode

57

Em C, a declaração reflete o uso - é assim que é definida no padrão. A declaração:

void (*(*f[])())()

É uma afirmação de que a expressão (*(*f[i])())()produz um resultado do tipo void. Que significa:

  • f deve ser uma matriz, pois você pode indexá-la:

    f[i]
  • Os elementos de fdevem ser ponteiros, pois você pode desreferenciá-los:

    *f[i]
  • Esses ponteiros devem ser ponteiros para funções sem argumentos, pois você pode chamá-los:

    (*f[i])()
  • Os resultados dessas funções também devem ser indicadores, pois você pode desreferenciá-los:

    *(*f[i])()
  • Esses ponteiros também devem ser ponteiros para funções sem argumentos, pois você pode chamá-los:

    (*(*f[i])())()
  • Esses ponteiros de função devem retornar void

A "regra espiral" é apenas um mnemônico que fornece uma maneira diferente de entender a mesma coisa.


3
Ótima maneira de ver isso que eu nunca vi antes. 1
tbodt

4
Agradável. Visto desta maneira, é realmente simples . Na verdade, é mais fácil do que algo parecido vector< function<function<void()>()>* > f, especialmente se você adicionar o std::s. (Mas bem, o exemplo é inventado ... mesmo f :: [IORef (IO (IO ()))]parece estranho.)
leftaroundabout

1
@TimoDenk: A declaração a[x]indica que a expressão a[i]é válida quando i >= 0 && i < x. Visto que, a[]deixa o tamanho não especificado e, portanto, é idêntico a *a: indica que a expressão a[i](ou equivalente *(a + i)) é válida para algum intervalo de i.
Jon Purdy

4
Esta é de longe a maneira mais fácil de pensar sobre tipos C, obrigado por este
Alex Ozer

4
Eu amo isto! Muito mais fácil de raciocinar do que espirais tolas. (*f[])()é um tipo que você pode indexar, desreferenciar e depois chamar, por isso é uma matriz de indicadores para funções.
Lynn

32

Então essa "leitura em espiral" é algo válido?

A aplicação de regra em espiral ou o uso do cdecl nem sempre são válidos. Ambos falham em alguns casos. A regra espiral funciona para muitos casos, mas não é universal .

Para decifrar declarações complexas, lembre-se destas duas regras simples:

  • Leia sempre as declarações de dentro para fora : Comece entre parênteses mais internos , se houver. Localize o identificador que está sendo declarado e comece a decifrar a declaração a partir daí.

  • Quando há uma opção, sempre favorece []e ()substitui* : Se *precede o identificador e o []segue, o identificador representa uma matriz, não um ponteiro. Da mesma forma, se *precede o identificador e o ()segue, o identificador representa uma função, não um ponteiro. (Parênteses sempre podem ser usados ​​para substituir a prioridade normal de []e para ()cima *.)

Esta regra realmente envolve ziguezague de um lado do identificador para o outro.

Agora decifrando uma declaração simples

int *a[10];

Aplicando regra:

int *a[10];      "a is"  
     ^  

int *a[10];      "a is an array"  
      ^^^^ 

int *a[10];      "a is an array of pointers"
    ^

int *a[10];      "a is an array of pointers to `int`".  
^^^      

Vamos decifrar a declaração complexa como

void ( *(*f[]) () ) ();  

aplicando as regras acima:

void ( *(*f[]) () ) ();        "f is"  
          ^  

void ( *(*f[]) () ) ();        "f is an array"  
           ^^ 

void ( *(*f[]) () ) ();        "f is an array of pointers" 
         ^    

void ( *(*f[]) () ) ();        "f is an array of pointers to function"   
               ^^     

void ( *(*f[]) () ) ();        "f is an array of pointers to function returning pointer"
       ^   

void ( *(*f[]) () ) ();        "f is an array of pointers to function returning pointer to function" 
                    ^^    

void ( *(*f[]) () ) ();        "f is an array of pointers to function returning pointer to function returning `void`"  
^^^^

Aqui está um GIF demonstrando como você vai (clique na imagem para aumentar a exibição):

insira a descrição da imagem aqui


As regras mencionadas aqui são retiradas do livro C Programming A Modern Approach, de KN KING .


É exatamente como a abordagem do padrão, ou seja, "a declaração reflete o uso". Gostaria de perguntar algo mais neste momento: você sugere o livro de KN King? Estou vendo muitas resenhas legais sobre o livro.
Motun

1
Sim. Eu sugiro esse livro. Comecei a programar a partir desse livro. Bons textos e problemas lá.
haccks

Você pode fornecer um exemplo de cdecl que falha ao entender uma declaração? Eu pensei que o cdecl usasse as mesmas regras de análise dos compiladores e, até onde sei, sempre funciona.
Fabio diz Restabelecer Monica

@FabioTurati; Uma função não pode retornar matrizes ou funções. char (x())[5]deve resultar em erro de sintaxe, mas, cdecl o analisa como: declare xcomo função retornando matriz 5 dechar .
haccks

12

É apenas uma "espiral" porque, nesta declaração, existe apenas um operador de cada lado dentro de cada nível de parênteses. Afirmar que você continua "em espiral" geralmente sugere que você alterne entre matrizes e ponteiros na declaração int ***foo[][][]quando, na realidade, todos os níveis da matriz vêm antes de qualquer um dos níveis do ponteiro.


Bem, na "abordagem em espiral", você ir tão longe direito como você pode, então, tanto à esquerda como você pode, etc. Mas é muitas vezes explicado erroneamente ...
Lynn

7

Duvido que construções como essa possam ter algum uso na vida real. Eu até detesto eles como perguntas da entrevista para os desenvolvedores regulares (provavelmente aceitável para escritores de compiladores). typedefs deve ser usado.


3
No entanto, é importante saber como analisá-lo, mesmo que apenas saiba como analisar o typedef!
Inetknght

1
@inetknght, a maneira como você faz isso com typedefs é simplificá-los o suficiente para que nenhuma análise seja necessária.
Sergeya

2
As pessoas que fazem esse tipo de pergunta durante as entrevistas fazem isso apenas para acariciar seus egos.
Casey #

1
@JohnBode, e você faria um favor a si mesmo digitando o valor de retorno da função.
Sergeya

1
@ JohnBode, acho que é uma questão de escolha pessoal que não vale a pena debater. Eu vejo sua preferência, ainda tenho a minha.
Sergeya

7

Como um trivial aleatório, você pode achar divertido saber que existe uma palavra real em inglês para descrever como as declarações C são lidas: Boustrophedonically , ou seja, alternando da direita para a esquerda e da esquerda para a direita.

Referência: Van der Linden, 1994 - Página 76


1
Essa palavra não indica dentro como aninhado por parens ou em uma única linha. Ele descreve um padrão de "cobra", com uma linha LTR seguida por uma linha RTL.
Potatoswatter

5

Com relação à utilidade disso, ao trabalhar com o shellcode, você vê muito essa construção:

int (*ret)() = (int(*)())code;
ret();

Embora não seja tão sintaticamente complicado, esse padrão específico surge muito.

Exemplo mais completo neste questão SO.

Portanto, embora a utilidade na extensão da imagem original seja questionável (eu sugiro que qualquer código de produção seja drasticamente simplificado), existem algumas construções sintáticas que surgem bastante.


5

A declaração

void (*(*f[])())()

é apenas uma maneira obscura de dizer

Function f[]

com

typedef void (*ResultFunction)();

typedef ResultFunction (*Function)();

Na prática, serão necessários nomes mais descritivos em vez de ResultFunction e Function . Se possível, também especificaria as listas de parâmetros como void.


4

Eu achei o método descrito por Bruce Eckel útil e fácil de seguir:

Definindo um ponteiro de função

Para definir um ponteiro para uma função que não possui argumentos nem valor de retorno, você diz:

void (*funcPtr)();

Quando você está olhando para uma definição complexa como essa, a melhor maneira de atacá-la é começar pelo meio e sair do lugar.“Começando pelo meio” significa começar pelo nome da variável, que é funcPtr. "Trabalhar para sair" significa olhar para a direita pelo item mais próximo (nada neste caso; o parêntese direito o interrompe), depois olhar para a esquerda (um ponteiro indicado pelo asterisco) e depois olhar para a direita (um lista de argumentos vazia indicando uma função que não aceita argumentos) e, em seguida, olhando para a esquerda (vazio, que indica que a função não tem valor de retorno). Esse movimento da direita-esquerda-direita funciona com a maioria das declarações.

Para revisar, "comece no meio" ("funcPtr é um ..."), vá para a direita (nada lá - você é interrompido pelo parêntese direito), vá para a esquerda e encontre o '*' (" ... apontador para um ... "), vá para a direita e encontre a lista de argumentos vazia (" ... função que não aceita argumentos ... "), vá para a esquerda e encontre o vazio (" funcPtr is um ponteiro para uma função que não aceita argumentos e retorna nula ”).

Você pode se perguntar por que * funcPtr requer parênteses. Se você não os usasse, o compilador veria:

void *funcPtr();

Você declararia uma função (que retorna um nulo *) em vez de definir uma variável. Você pode pensar no compilador como passando pelo mesmo processo que ele faz quando descobre o que deveria ser uma declaração ou definição. Ele precisa desses parênteses para "esbarrar" para voltar à esquerda e encontrar o '*', em vez de continuar à direita e encontrar a lista de argumentos vazia.

Declarações e definições complicadas

Como um aparte, depois de descobrir como a sintaxe das declarações C e C ++ funciona, você pode criar itens muito mais complicados. Por exemplo:

//: C03:ComplicatedDefinitions.cpp

/* 1. */     void * (*(*fp1)(int))[10];

/* 2. */     float (*(*fp2)(int,int,float))(int);

/* 3. */     typedef double (*(*(*fp3)())[10])();
             fp3 a;

/* 4. */     int (*(*f4())[10])();


int main() {} ///:~ 

Percorra cada uma delas e use a diretriz direita-esquerda para descobrir. O número 1 diz que "fp1 é um ponteiro para uma função que pega um argumento inteiro e retorna um ponteiro para uma matriz de 10 ponteiros nulos".

O número 2 diz que "fp2 é um ponteiro para uma função que recebe três argumentos (int, int e float) e retorna um ponteiro para uma função que recebe um argumento inteiro e retorna um float".

Se você estiver criando muitas definições complicadas, convém usar um typedef. O número 3 mostra como um typedef salva digitando sempre a descrição complicada. Ele diz "Um fp3 é um ponteiro para uma função que não aceita argumentos e retorna um ponteiro para uma matriz de 10 ponteiros para funções que não aceitam argumentos e retornam dobras". Então diz "a é um desses tipos de fp3". O typedef é geralmente útil para criar descrições complicadas a partir de descrições simples.

O número 4 é uma declaração de função em vez de uma definição de variável. Ele diz que "f4 é uma função que retorna um ponteiro para uma matriz de 10 ponteiros para funções que retornam números inteiros".

Você raramente ou nunca precisará de declarações e definições complicadas como essas. No entanto, se você realizar o exercício de descobri-las, nem ficará levemente perturbado com as levemente complicadas que poderá encontrar na vida real.

Retirado de: Thinking in C ++ Volume 1, segunda edição, capítulo 3, seção "Function Addresses", de Bruce Eckel.


4

Lembre-se destas regras para C declara
E a precedência nunca estará em dúvida:
Comece com o sufixo, continue com o prefixo
E leia os dois conjuntos de dentro para fora.
- eu, meados dos anos 80

Exceto quando modificado por parênteses, é claro. E observe que a sintaxe para declarar isso reflete exatamente a sintaxe para usar essa variável para obter uma instância da classe base.

Sério, não é difícil aprender a fazer de relance; você só precisa gastar algum tempo praticando a habilidade. Se você deseja manter ou adaptar o código C escrito por outras pessoas, definitivamente vale a pena investir esse tempo. Também é um truque divertido para assustar outros programadores que não o aprenderam.

Para seu próprio código: como sempre, o fato de que algo pode ser escrito como uma linha não significa que deve ser, a menos que seja um padrão extremamente comum que se tornou um idioma padrão (como o loop de cópia de cadeia) . Você e aqueles que o seguem serão muito mais felizes se você criar tipos complexos a partir de typedefs em camadas e dereferências passo a passo, em vez de confiar na sua capacidade de gerar e analisar esses "de uma só vez". O desempenho será igualmente bom e a legibilidade e a manutenção do código serão tremendamente melhores.

Poderia ser pior, você sabe. Havia uma declaração legal de PL / I que começava com algo como:

if if if = then then then = else else else = if then ...

2
A instrução PL / I foi IF IF = THEN THEN THEN = ELSE ELSE ELSE = ENDIF ENDIFe é analisada como if (IF == THEN) then (THEN = ELSE) else (ELSE = ENDIF).
Cole Johnson

Eu acho que houve uma versão que deu um passo adiante usando uma expressão condicional IF / THEN / ELSE (equivalente a C's? :), que colocou o terceiro conjunto na mistura ... mas já se passaram algumas décadas e podem ter dependia de um dialeto específico do idioma. Resta dizer que qualquer idioma tem pelo menos uma forma patológica.
precisa saber é o seguinte

4

Por acaso, sou o autor original da regra espiral que escrevi há muitos anos (quando eu tinha muitos cabelos :) e fiquei honrado quando foi adicionado ao cfaq.

Escrevi a regra da espiral como uma maneira de tornar mais fácil para meus alunos e colegas ler as declarações C "na cabeça"; ou seja, sem ter que usar ferramentas de software como cdecl.org etc. Nunca foi minha intenção declarar que a regra espiral é a maneira canônica de analisar expressões em C. Estou, no entanto, feliz em ver que a regra ajudou literalmente milhares de estudantes e profissionais de programação C ao longo dos anos!

Para o registro,

Foi "corretamente" identificado várias vezes em muitos sites, inclusive por Linus Torvalds (alguém a quem eu respeito imensamente), que há situações em que minha regra em espiral "quebra". O ser mais comum:

char *ar[10][10];

Conforme apontado por outras pessoas neste segmento, a regra pode ser atualizada para dizer que, quando você encontrar matrizes, consuma todos os índices como se estivesse escrito como:

char *(ar[10][10]);

Agora, seguindo a regra da espiral, eu obteria:

"ar é uma matriz bidimensional 10x10 de ponteiros para char"

Espero que a regra da espiral continue sendo útil na aprendizagem de C!

PS:

Eu amo a imagem "C não é difícil" :)


3
  • vazio (*(*f[]) ()) ()

Resolução void>>

  • (*(*f[]) ()) () = nulo

Recuperação ()>>

  • (* (*f[]) ()) = função retornando (nula)

Resolução *>>

  • (*f[]) () = ponteiro para (função retornando (vazio))

Resolução ()>>

  • (* f[]) = função retornando (ponteiro para (função retornando (vazio))))

Resolução *>>

  • f[] = ponteiro para (função retornando (ponteiro para (função retornando (vazio))))

Resolução [ ]>>

  • f = matriz de (ponteiro para (função retornando (ponteiro para (função retornando (vazio)))))
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.