¿Cómo funciona el código C que imprime de 1 a 1000 sin bucles o sentencias condicionales?

148

He encontrado Ccódigo que imprime de 1 a 1000 sin bucles o condicionales : pero no entiendo cómo funciona. ¿Alguien puede leer el código y explicar cada línea?

#include <stdio.h>
#include <stdlib.h>

void main(int j) {
  printf("%d\n", j);
  (&main + (&exit - &main)*(j/1000))(j+1);
}
ob_dev
fuente
1
¿Estás compilando como C o como C ++? ¿Qué errores ves? No se puede llamar mainen C ++.
ninjalj
@ninjalj He creado un proyecto C ++ y copié / pegué el código, el error es: ilegal, el operando izquierdo tiene el tipo 'void (__cdecl *) (int)' y la expresión debe ser un puntero a un tipo de objeto completo
ob_dev
1
@ninjalj Este código funciona en ideone.org pero no en visual studio ideone.com/MtJ1M
ob_dev
@oussama Similar, pero un poco más difícil de entender: ideone.com/2ItXm De nada . :)
Mark
2
he eliminado todos los caracteres '&' de estas líneas (& main + (& exit - & main) * (j / 1000)) (j + 1); Y este código todavía funciona.
ob_dev

Respuestas:

264

Nunca escribas código como ese.


Para j<1000, j/1000es cero (división entera). Entonces:

(&main + (&exit - &main)*(j/1000))(j+1);

es equivalente a:

(&main + (&exit - &main)*0)(j+1);

Cual es:

(&main)(j+1);

Que llama maincon j+1.

Si j == 1000, entonces las mismas líneas salen como:

(&main + (&exit - &main)*1)(j+1);

Que se reduce a

(&exit)(j+1);

Que es exit(j+1)y sale del programa.


(&exit)(j+1)y exit(j+1)son esencialmente lo mismo, citando C99 §6.3.2.1 / 4:

Un designador de función es una expresión que tiene tipo de función. Excepto cuando es el operando del operador sizeof o el operador unario y , un designador de función con el tipo " tipo de retorno de función " se convierte en una expresión que tiene el tipo " puntero al tipo de retorno de función ".

exites un designador de funciones. Incluso sin la &dirección unaria del operador, se trata como un puntero para funcionar. (Lo &justo lo hace explícito).

Y las llamadas a funciones se describen en §6.5.2.2 / 1 y siguientes:

La expresión que denota la función llamada tendrá un puntero de tipo para funcionar devolviendo vacío o devolviendo un tipo de objeto que no sea un tipo de matriz.

Por lo tanto, exit(j+1)funciona debido a la conversión automática del tipo de función a un tipo de puntero a función, y (&exit)(j+1)funciona también con una conversión explícita a un tipo de puntero a función.

Dicho esto, el código anterior no es conforme ( maintoma dos argumentos o ninguno), y &exit - &maincreo que no está definido de acuerdo con §6.5.6 / 9:

Cuando se restan dos punteros, ambos apuntarán a elementos del mismo objeto de matriz , o uno más allá del último elemento del objeto de matriz; ...

La adición (&main + ...)sería válida en sí misma y podría usarse, si la cantidad agregada fuera cero, ya que §6.5.6 / 7 dice:

Para los fines de estos operadores, un puntero a un objeto que no es un elemento de una matriz se comporta igual que un puntero al primer elemento de una matriz de longitud uno con el tipo de objeto como su tipo de elemento.

Por lo tanto, agregar cero a &mainestaría bien (pero no mucho uso).

Estera
fuente
44
foo(arg)y (&foo)(arg)son equivalentes, llaman foo con argumento arg. newty.de/fpt/fpt.html es una página interesante sobre punteros de función.
Mat
1
@ Krishnabhadra: en el primer caso, fooes un puntero, &fooes la dirección de ese puntero. En el segundo caso, fooes una matriz, y &fooes equivalente a foo.
Mat
8
Innecesariamente complejo, al menos para C99:((void(*[])()){main, exit})[j / 1000](j + 1);
Por Johansson
1
&foono es lo mismo que foocuando se trata de una matriz. &fooes un puntero a la matriz, fooes un puntero al primer elemento. Sin embargo, tienen el mismo valor. Para funciones, funy &funson ambos punteros a la función.
Por Johansson
1
Para su información, si mira la respuesta relevante a la otra pregunta citada anteriormente , verá que hay una variación que de hecho es compatible con C99. Miedo, pero cierto.
Daniel Pryden
41

Utiliza la recursividad, la aritmética del puntero y explota el comportamiento de redondeo de la división de enteros.

El j/1000término se redondea a 0 para todos j < 1000; una vez que jalcanza 1000, se evalúa a 1.

Ahora, si tiene a + (b - a) * n, donde nes 0 o 1, terminará con aif n == 0y bif n == 1. Usando &main(la dirección de main()) y &exitpara ay b, el término (&main + (&exit - &main) * (j/1000))vuelve &maincuando jestá por debajo de 1000, de lo &exitcontrario. El puntero de función resultante se alimenta luego del argumento j+1.

Toda esta construcción da como resultado un comportamiento recursivo: mientras jestá por debajo de 1000, se mainllama recursivamente; cuando jalcanza 1000, llama en su exitlugar, haciendo que el programa salga con el código de salida 1001 (que está un poco sucio, pero funciona).

tdammers
fuente
1
Buena respuesta, pero una duda ... ¿Cómo sale la salida principal con el código de salida 1001? Main no devuelve nada ... ¿Algún valor de retorno predeterminado?
Krishnabhadra
2
Cuando j alcanza 1000, main ya no se repite en sí mismo; en su lugar, llama a la función libc exit, que toma el código de salida como argumento y, bueno, sale del proceso actual. En ese punto, j es 1000, entonces j + 1 es igual a 1001, que se convierte en el código de salida.
tdammers