C no es tan difícil: nulo (* (* f []) ()) ()


188

Acabo de ver una foto hoy y creo que agradecería las explicaciones. Así que aquí está la imagen:

algún código c

Esto me pareció confuso y me pregunté si tales códigos son prácticos. Busqué en Google la imagen y encontré otra imagen en esta entrada de reddit, y aquí está esa imagen:

alguna explicación interesante

¿Entonces esta "lectura en espiral" es algo válido? ¿Es así como analizan los compiladores de C?
Sería genial si hubiera explicaciones más simples para este código extraño.
Además de todo, ¿pueden ser útiles este tipo de códigos? Si es así, ¿dónde y cuándo?

Hay una pregunta sobre la "regla espiral", pero no solo estoy preguntando cómo se aplica o cómo se leen las expresiones con esa regla. También cuestiono el uso de tales expresiones y la validez de la regla espiral. Con respecto a esto, algunas buenas respuestas ya están publicadas.


9
¿Cómo podría la explicación ser más simple? Cubre todos los aspectos de la definición de fw / un par de palabras para cada punto clave.
Scott Hunter

30
Tal vez C es difícil? Que en realidad se declara fcomo una matriz de punteros a funciones que podría tomar cualquier argumento .. si fuera void (*(*f[])(void))(void);, entonces sí, sería funciones que no tienen argumentos ...
txtechhelp

18
En la práctica, no codifique un código tan oscuro. Use typedef para firmas
Basile Starynkevitch

44
Cualquier declaración que implique punteros de función puede ser difícil. Eso no significa que C o C ++ normal sea difícil de la misma manera. Otros idiomas resuelven esto de diferentes maneras, incluyendo no tener punteros de función, lo que puede ser una omisión significativa en algunos casos
Kate Gregory

20
Si entrecierra los ojos, se ve como LISP.
user2023861

Respuestas:


117

Hay una regla llamada "Regla en sentido horario / espiral" para ayudar a encontrar el significado de una declaración compleja.

De c-faq :

Hay tres pasos simples a seguir:

  1. Comenzando con el elemento desconocido, muévase en una espiral / sentido horario; al encontrar los siguientes elementos, reemplácelos con las declaraciones en inglés correspondientes:

    [X]o []
    => Tamaño de matriz X de ... o Tamaño de matriz indefinido de ...

    (type1, type2)
    => función pasando tipo1 y tipo2 regresando ...

    *
    => puntero (s) a ...

  2. Sigue haciendo esto en espiral / sentido horario hasta que se hayan cubierto todas las fichas.

  3. ¡Siempre resuelva cualquier cosa entre paréntesis primero!

Puede consultar el enlace de arriba para ver ejemplos.

También tenga en cuenta que para ayudarlo también hay un sitio web llamado:

http://www.cdecl.org

Puede ingresar una declaración C y le dará su significado en inglés. por

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

produce:

declara f como matriz de puntero para funcionar regresando puntero a función regresando vacío

EDITAR:

Como se señaló en los comentarios de Random832 , la regla espiral no aborda la matriz de matrices y dará lugar a un resultado incorrecto en (la mayoría de) esas declaraciones. Por ejemplo, para int **x[1][2];la regla espiral ignora el hecho de que []tiene mayor prioridad sobre *.

Cuando está frente a una matriz de matrices, primero se pueden agregar paréntesis explícitos antes de aplicar la regla espiral. Por ejemplo: int **x[1][2];es lo mismo que int **(x[1][2]);(también válido C) debido a la precedencia y la regla espiral lo lee correctamente como "x es una matriz 1 de la matriz 2 de puntero a puntero a int", que es la declaración correcta en inglés.

Tenga en cuenta que este problema también ha sido cubierto en esta respuesta por James Kanze (señalado por los hacks en los comentarios).


55
Ojalá cdecl.org fuera mejor
Grady Player

8
No existe una "regla espiral" ... "int *** foo [] [] []" define un conjunto de conjuntos de conjuntos de punteros a punteros a punteros. La "espiral" solo viene del hecho de que esta declaración sucedió para agrupar cosas entre paréntesis de una manera que hizo que se alternaran. Está todo a la derecha, luego a la izquierda, dentro de cada conjunto de paréntesis.
Random832

1
@ Random832 Existe una "regla espiral", y cubre el caso que acaba de mencionar, es decir, habla sobre cómo tratar con paréntesis / matrices, etc. Por supuesto, no es una regla estándar C, sino una buena mnemotecnia para descubrir cómo tratar con declaraciones complicadas En mi humilde opinión, es extremadamente útil y le ahorra cuando tiene problemas o cuando cdecl.org no puede analizar la declaración. Por supuesto, uno no debe abusar de tales declaraciones, pero es bueno saber cómo se analizan.
vsoftco

55
@vsoftco Pero no es "moverse en espiral / en sentido horario" si solo te das la vuelta cuando alcanzas los paréntesis.
Random832

2
ouah, deberías mencionar que la regla espiral no es universal .
piratea el

105

La regla de "espiral" se cae de las siguientes reglas de precedencia:

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

Los operadores de []llamadas de subíndice y función ()tienen mayor prioridad que los unarios *, por lo que *f()se analiza como *(f())y *a[]se analiza como *(a[]).

Entonces, si desea un puntero a una matriz o un puntero a una función, entonces necesita agrupar explícitamente el *con el identificador, como en (*a)[]o (*f)().

Entonces te das cuenta de eso ay fpuedes ser expresiones más complicadas que solo identificadores; en T (*a)[N], apodría ser un identificador simple, o podría ser una función llamada como (*f())[N]( a-> f()), o podría ser una matriz como (*p[M])[N], ( a-> p[M]), o podría ser una matriz de punteros a funciones como (*(*p[M])())[N]( a-> (*p[M])()), etc.

Sería bueno si el operador de indirección *fuera postfix en lugar de unario, lo que facilitaría la lectura de las declaraciones de izquierda a derecha ( void f[]*()*();definitivamente fluye mejor que void (*(*f[])())()), pero no lo es.

Cuando encuentre una declaración peluda como esa, comience por encontrar el identificador más a la izquierda y aplique las reglas de precedencia anteriores, aplicándolas recursivamente a cualquier parámetro de función:

         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

La signalfunción en la biblioteca estándar es probablemente el espécimen tipo para este tipo de locura:

       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

En este punto, la mayoría de la gente dice "use typedefs", que sin duda es una opción:

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

innerfunc *f[N];

Pero...

¿Cómo usarías f en una expresión? Sabes que es un conjunto de punteros, pero ¿cómo lo usas para ejecutar la función correcta? Tienes que revisar los typedefs y descifrar la sintaxis correcta. Por el contrario, la versión "desnuda" es bastante curiosa, pero le dice exactamente cómo usarla f en una expresión (es decir (*(*f[i])())();, suponiendo que ninguna función tome argumentos).


77
Gracias por dar el ejemplo de 'señal', que muestra que este tipo de cosas aparecen en la naturaleza.
Justsalt

Ese es un gran ejemplo.
Casey

Me gustó tu fárbol de desaceleración, explicando la precedencia ... por alguna razón siempre me gusta el arte ASCII, especialmente cuando se trata de explicar cosas :)
txtechhelp

1
suponiendo que ninguna función tome argumentos : entonces debe usar voidparéntesis de funciones, de lo contrario puede tomar cualquier argumento.
piratea el

1
@haccks: para la declaración, sí; Estaba hablando de la llamada a la función.
John Bode

57

En C, la declaración refleja el uso, así es como se define en el estándar. La declaracion:

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

Es una afirmación de que la expresión (*(*f[i])())()produce un resultado de tipo void. Lo que significa:

  • f debe ser una matriz, ya que puede indexarla:

    f[i]
  • Los elementos de fdeben ser punteros, ya que puede desreferenciarlos:

    *f[i]
  • Esos punteros deben ser punteros a funciones que no toman argumentos, ya que puede llamarlos:

    (*f[i])()
  • Los resultados de esas funciones también deben ser punteros, ya que puede desreferenciarlos:

    *(*f[i])()
  • Esos punteros también deben ser punteros a funciones que no toman argumentos, ya que puede llamarlos:

    (*(*f[i])())()
  • Esos punteros de función deben regresar void

La "regla espiral" es simplemente un mnemónico que proporciona una forma diferente de entender lo mismo.


3
Gran forma de verlo que nunca he visto antes. +1
tbodt

44
Agradable. Visto de esta manera, es realmente simple . En realidad, es bastante más fácil que algo así vector< function<function<void()>()>* > f, especialmente si agrega el std::s. (Pero bueno, el ejemplo es artificial ... incluso se f :: [IORef (IO (IO ()))]ve raro.)
Leftaroundabout

1
@TimoDenk: la declaración a[x]indica que la expresión a[i]es válida cuando i >= 0 && i < x. Mientras que, a[]deja el tamaño sin especificar, y por lo tanto es idéntico a *a: indica que la expresión a[i](o equivalente *(a + i)) es válida para algún rango de i.
Jon Purdy

44
Esta es, con mucho, la forma más fácil de pensar sobre los tipos C, gracias por esto
Alex Ozer el

44
¡Me encanta esto! Mucho más fácil de razonar que las espirales tontas. (*f[])()es un tipo que puede indexar, luego desreferenciar, luego llamar, por lo que es una matriz de punteros a funciones.
Lynn

32

¿Entonces esta "lectura en espiral" es algo válido?

La aplicación de la regla espiral o el uso de cdecl no son válidos siempre. Ambos fallan en algunos casos. La regla espiral funciona para muchos casos, pero no es universal .

Para descifrar declaraciones complejas recuerde estas dos reglas simples:

  • Siempre lea las declaraciones de adentro hacia afuera : comience desde el paréntesis más interno, si lo hay. Localice el identificador que se declara y comience a descifrar la declaración desde allí.

  • Cuando hay una opción, siempre favorece []y ()supera* : si *precede al identificador y lo []sigue, el identificador representa una matriz, no un puntero. Del mismo modo, si *precede al identificador y lo ()sigue, el identificador representa una función, no un puntero. (Los paréntesis siempre se pueden usar para anular la prioridad normal de una []y ()otra vez *).

Esta regla en realidad implica zigzaguear de un lado del identificador al otro.

Ahora descifrando una declaración simple

int *a[10];

Aplicando regla:

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`".  
^^^      

Descifremos la declaración compleja como

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

aplicando las reglas anteriores:

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`"  
^^^^

Aquí hay un GIF que muestra cómo te va (haz clic en la imagen para ampliarla):

ingrese la descripción de la imagen aquí


Las reglas mencionadas aquí están tomadas del libro C Programming A Modern Approach de KN KING .


Esto es exactamente como el enfoque del estándar, es decir, "el uso refleja la declaración". Sin embargo, me gustaría preguntar algo más en este momento: ¿Sugieres el libro de KN King? Estoy viendo muchas buenas críticas sobre el libro.
Motun

1
Si. Sugiero ese libro. Empecé a programar desde ese libro. Buenos textos y problemas allí.
piratea el

¿Puede proporcionar un ejemplo de cdecl que no comprende una declaración? Pensé que cdecl usaba las mismas reglas de análisis que los compiladores, y por lo que puedo decir, siempre funciona.
Fabio dice Restablecer a Mónica el

@FabioTurati; Una función no puede devolver matrices o funciones. char (x())[5]debería dar como resultado un error de sintaxis, pero cdecl lo analiza como: declarar xcomo función que devuelve la matriz 5 dechar .
Hackea el

12

Es solo una "espiral" porque en esta declaración solo hay un operador en cada lado dentro de cada nivel de paréntesis. Afirmar que procede "en espiral" generalmente le sugerirá que alterne entre matrices y punteros en la declaración int ***foo[][][]cuando en realidad todos los niveles de la matriz van antes que cualquiera de los niveles de puntero.


Bueno, en el "enfoque en espiral", vas tan a la derecha como puedes, luego tan a la izquierda como puedes, etc. Pero a menudo se explica erróneamente ...
Lynn

7

Dudo que construcciones como esta puedan tener algún uso en la vida real. Incluso los detesto como preguntas de entrevista para los desarrolladores habituales (probablemente aceptables para los escritores de compiladores). typedefs debería usarse en su lugar.


3
Sin embargo, es importante saber cómo analizarlo, ¡aunque solo sea para analizar el typedef!
inetknght

1
@inetknght, la forma de hacerlo con typedefs es tenerlos lo suficientemente simples para que no se requiera un análisis.
SergeyA

2
Las personas que hacen este tipo de preguntas durante las entrevistas solo lo hacen para acariciar sus Egos.
Casey

1
@JohnBode, y te harías un favor escribiendo el valor de retorno de la función.
SergeyA

1
@ JohnBode, me parece una cuestión de elección personal que no vale la pena debatir. Veo tu preferencia, todavía tengo la mía.
SergeyA

7

Como un factor de trivia aleatorio, puede resultarle divertido saber que hay una palabra real en inglés para describir cómo se leen las declaraciones C: Boustrofedónicamente , es decir, alternando de derecha a izquierda con de izquierda a derecha.

Referencia: Van der Linden, 1994 - Página 76


1
Esa palabra no indica dentro como anidado por parens o en una sola línea. Describe un patrón de "serpiente", con una línea LTR seguida de una línea RTL.
Potatoswatter

5

Con respecto a la utilidad de esto, cuando trabajas con shellcode ves mucho esta construcción:

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

Si bien no es tan sintácticamente complicado, este patrón en particular aparece mucho.

Un ejemplo más completo en esta pregunta SO.

Entonces, aunque la utilidad en la medida de la imagen original es cuestionable (sugeriría que cualquier código de producción se simplifique drásticamente), hay algunas construcciones sintácticas que surgen bastante.


5

La declaracion

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

es solo una forma oscura de decir

Function f[]

con

typedef void (*ResultFunction)();

typedef ResultFunction (*Function)();

En la práctica, se necesitarán nombres más descriptivos en lugar de ResultFunction y Function . Si es posible, también especificaría las listas de parámetros como void.


4

Encontré que el método descrito por Bruce Eckel es útil y fácil de seguir:

Definir un puntero de función

Para definir un puntero a una función que no tiene argumentos ni valor de retorno, usted dice:

void (*funcPtr)();

Cuando está buscando una definición compleja como esta, la mejor manera de atacarla es comenzar en el medio y salir. "Comenzar en el medio" significa comenzar con el nombre de la variable, que es funcPtr. "Trabajar para salir" significa buscar a la derecha el elemento más cercano (nada en este caso; el paréntesis derecho lo detiene), luego mirar a la izquierda (un puntero indicado por el asterisco), luego mirar a la derecha (un lista de argumentos vacía que indica una función que no toma argumentos), luego mirando a la izquierda (nula, lo que indica que la función no tiene valor de retorno). Este movimiento derecha-izquierda-derecha funciona con la mayoría de las declaraciones.

Para revisar, “comience en el medio” (“funcPtr es un ...”), vaya a la derecha (nada allí, está parado por el paréntesis derecho), vaya a la izquierda y encuentre el '*' (“ ... apunta a un ... "), ve a la derecha y encuentra la lista de argumentos vacía (" ... función que no toma argumentos ... "), ve a la izquierda y encuentra el vacío (" funcPtr es un puntero a una función que no toma argumentos y devuelve nulo ").

Quizás se pregunte por qué * funcPtr requiere paréntesis. Si no los usó, el compilador vería:

void *funcPtr();

Estaría declarando una función (que devuelve un vacío *) en lugar de definir una variable. Puede pensar en el compilador como si estuviera pasando por el mismo proceso que usted cuando se da cuenta de lo que se supone que es una declaración o definición. Necesita esos paréntesis para "chocar contra", así que vuelve a la izquierda y encuentra el '*', en lugar de continuar hacia la derecha y encontrar la lista de argumentos vacía.

Declaraciones y definiciones complicadas

Por otro lado, una vez que descubra cómo funciona la sintaxis de declaración C y C ++, puede crear elementos mucho más complicados. Por ejemplo:

//: 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() {} ///:~ 

Camine a través de cada uno y use la guía derecha-izquierda para resolverlo. El número 1 dice "fp1 es un puntero a una función que toma un argumento entero y devuelve un puntero a una matriz de 10 punteros vacíos".

El número 2 dice "fp2 es un puntero a una función que toma tres argumentos (int, int y float) y devuelve un puntero a una función que toma un argumento entero y devuelve un float".

Si está creando muchas definiciones complicadas, es posible que desee utilizar un typedef. El número 3 muestra cómo un typedef guarda escribiendo la complicada descripción cada vez. Dice "Un fp3 es un puntero a una función que no toma argumentos y devuelve un puntero a una matriz de 10 punteros a funciones que no toman argumentos y devuelven dobles". Luego dice "a es uno de estos tipos de fp3". typedef es generalmente útil para construir descripciones complicadas a partir de simples.

El número 4 es una declaración de función en lugar de una definición de variable. Dice "f4 es una función que devuelve un puntero a una matriz de 10 punteros a funciones que devuelven enteros".

Rara vez, si alguna vez, necesitará declaraciones y definiciones tan complicadas como estas. Sin embargo, si realiza el ejercicio de resolverlos, ni siquiera se sentirá levemente molesto con los ligeramente complicados que puede encontrar en la vida real.

Tomado de: Pensando en C ++ Volumen 1, segunda edición, capítulo 3, sección "Direcciones de funciones" por Bruce Eckel.


4

Recuerde que estas reglas para C declaran
y la precedencia nunca estará en duda:
comience con el sufijo, continúe con el prefijo
y lea ambos conjuntos desde adentro hacia afuera.
- yo, a mediados de los 80

Excepto por modificaciones entre paréntesis, por supuesto. Y tenga en cuenta que la sintaxis para declarar esto refleja exactamente la sintaxis para usar esa variable para obtener una instancia de la clase base.

En serio, esto no es difícil de aprender de un vistazo; solo tienes que estar dispuesto a pasar un tiempo practicando la habilidad. Si va a mantener o adaptar el código C escrito por otras personas, definitivamente vale la pena invertir ese tiempo. También es un truco de fiesta divertido para enloquecer a otros programadores que no lo han aprendido.

Para su propio código: como siempre, el hecho de que algo se pueda escribir como una línea no significa que deba serlo, a menos que sea un patrón extremadamente común que se haya convertido en un idioma estándar (como el bucle de copia de cadena) . Usted y aquellos que lo siguen serán mucho más felices si construye tipos complejos a partir de tipos de letra en capas y desreferencias paso a paso en lugar de confiar en su capacidad para generar y analizar estos "de una sola vez". El rendimiento será igual de bueno, y la legibilidad y la facilidad de mantenimiento del código serán tremendamente mejores.

Podría ser peor, ya sabes. Hubo una declaración legal PL / I que comenzó con algo como:

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

2
La declaración PL / I fue IF IF = THEN THEN THEN = ELSE ELSE ELSE = ENDIF ENDIFy se analiza como if (IF == THEN) then (THEN = ELSE) else (ELSE = ENDIF).
Cole Johnson

Creo que hubo una versión que lo llevó un paso más allá al usar una expresión condicional IF / THEN / ELSE (equivalente a C's? :), que obtuvo el tercer conjunto en la mezcla ... pero han pasado algunas décadas y puede haber tenido dependía de un dialecto particular del idioma. El punto sigue siendo que cualquier lenguaje tiene al menos una forma patológica.
keshlam el

4

Soy el autor original de la regla espiral que escribí hace tantos años (cuando tenía mucho cabello :) y me honró cuando se agregó al cfaq.

Escribí la regla espiral como una forma de facilitar a mis alumnos y colegas leer las declaraciones C "en su cabeza"; es decir, sin tener que usar herramientas de software como cdecl.org, etc. Nunca fue mi intención declarar que la regla espiral sea la forma canónica de analizar las expresiones C. Sin embargo, estoy encantada de ver que la regla ha ayudado literalmente a miles de estudiantes y profesionales de programación de C a lo largo de los años.

Para el registro,

Se ha identificado "correctamente" numerosas veces en muchos sitios, incluso por Linus Torvalds (alguien a quien respeto inmensamente), que hay situaciones en las que mi regla espiral "se rompe". El ser más común:

char *ar[10][10];

Como señalaron otros en este hilo, la regla podría actualizarse para decir que cuando encuentre matrices, simplemente consuma todos los índices como si estuvieran escritos como:

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

Ahora, siguiendo la regla espiral, obtendría:

"ar es una matriz bidimensional de 10x10 de punteros a char"

¡Espero que la regla espiral continúe siendo útil para aprender C!

PD:

Me encanta la imagen "C no es difícil" :)


3
  • vacío (*(*f[]) ()) ()

Resolviendo void>>

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

Resoiving ()>>

  • (* (*f[]) ()) = función que regresa (nula)

Resolviendo *>>

  • (*f[]) () = puntero a (función que regresa (nula))

Resolviendo ()>>

  • (* f[]) = función que regresa (puntero a (función que regresa (nulo)))

Resolviendo *>>

  • f[] = puntero a (función que regresa (puntero a (función que regresa (nulo))))

Resolviendo [ ]>>

  • f = matriz de (puntero a (función de retorno (puntero a (función de retorno (nulo)))))
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.