Como o código C que imprime de 1 a 1000 sem loops ou instruções condicionais funciona?

148

Encontrei um Ccódigo que imprime de 1 a 1000 sem loops ou condicionais : mas não entendo como funciona. Alguém pode ler o código e explicar cada linha?

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

void main(int j) {
  printf("%d\n", j);
  (&main + (&exit - &main)*(j/1000))(j+1);
}
ob_dev
fonte
1
Você está compilando como C ou C ++? Que erros você vê? Você não pode chamar mainem C ++.
Ninjalj 29/10
@ninjalj Criei um projeto C ++ e copiei / passei o código, o erro é: ilegal, o operando esquerdo tem o tipo 'void (__cdecl *) (int)' e a expressão deve ser um ponteiro para um tipo de objeto completo
ob_dev
1
@ninjalj Esse código está funcionando em ideone.org, mas não no visual studio ideone.com/MtJ1M
ob_dev
@oussama Semelhante, mas um pouco mais difícil de entender: ideone.com/2ItXm De nada. :)
Marque
2
removi todos os caracteres '&' dessas linhas (& main + (& exit - & main) * (j / 1000)) (j + 1); e esse código ainda funciona.
ob_dev 30/10

Respostas:

264

Nunca escreva código assim.


Para j<1000, j/1000é zero (divisão inteira). Assim:

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

é equivalente a:

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

Qual é:

(&main)(j+1);

O que chama maincom j+1.

Se j == 1000, as mesmas linhas serão exibidas como:

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

O que se resume a

(&exit)(j+1);

Qual é exit(j+1)e sai do programa.


(&exit)(j+1)e exit(j+1)são essencialmente a mesma coisa - citando C99 §6.3.2.1 / 4:

Um designador de função é uma expressão que possui um tipo de função. Exceto quando é o operando do operador sizeof ou o operador unary & , um designador de função com o tipo " tipo de retorno de função " é convertido em uma expressão que possui o tipo " ponteiro para o tipo de retorno de função ".

exité um designador de função. Mesmo sem o operador de &endereço unário , ele é tratado como um ponteiro para funcionar. (O &just torna explícito.)

E as chamadas de função são descritas em §6.5.2.2 / 1 e seguintes:

A expressão que denota a função chamada deve ter ponteiro de tipo para retornar nulo ou retornar um tipo de objeto diferente de um tipo de matriz.

Assim, exit(j+1)funciona devido à conversão automática do tipo de função em um tipo de ponteiro para função e também (&exit)(j+1)funciona com uma conversão explícita em um tipo de ponteiro para função.

Dito isto, o código acima não está em conformidade ( mainaceita dois argumentos ou nenhum) e &exit - &main, acredito, é indefinido de acordo com §6.5.6 / 9:

Quando dois ponteiros são subtraídos, ambos apontam para elementos do mesmo objeto de matriz ou um após o último elemento do objeto de matriz; ...

A adição (&main + ...)seria válida por si só e poderia ser utilizada se a quantidade adicionada fosse zero, uma vez que o §6.5.6 / 7 diz:

Para os fins desses operadores, um ponteiro para um objeto que não é um elemento de uma matriz se comporta da mesma forma que um ponteiro para o primeiro elemento de uma matriz de comprimento um com o tipo de objeto como seu tipo de elemento.

Portanto, adicionar zero a &mainseria aceitável (mas não muito útil).

Esteira
fonte
4
foo(arg)e (&foo)(arg)são equivalentes, eles chamam foo com o argumento arg. newty.de/fpt/fpt.html é uma página interessante sobre ponteiros de função.
29511 Mat
1
@ Krishnabhadra: no primeiro caso, fooé um ponteiro, &fooé o endereço desse ponteiro. No segundo caso, fooé uma matriz e &fooé equivalente a foo.
Mat
8
Desnecessariamente complexo, pelo menos para C99:((void(*[])()){main, exit})[j / 1000](j + 1);
Por Johansson
1
&foonão é o mesmo que fooquando se trata de uma matriz. &fooé um ponteiro para a matriz, fooé um ponteiro para o primeiro elemento. Eles têm o mesmo valor, no entanto. Para funções fune &funsão ambos ponteiros para a função.
Per Johansson
1
Para sua informação, se você olhar a resposta relevante para a outra pergunta citada acima , verá que há uma variação que é de fato o C99. Assustador, mas é verdade.
Daniel Pryden 30/10/11
41

Ele usa recursão, aritmética de ponteiro e explora o comportamento de arredondamento da divisão inteira.

O j/1000termo é arredondado para 0 para todos j < 1000; quando jatingir 1000, ele avalia como 1.

Agora, se você tem a + (b - a) * n, onde né 0 ou 1, você termina com ase n == 0e bse n == 1. Usando &main(o endereço de main()) e &exitpara ae b, o termo (&main + (&exit - &main) * (j/1000))retornará &mainquando jestiver abaixo de 1000, &exitcaso contrário. O ponteiro de função resultante é então alimentado com o argumento j+1.

Toda essa construção resulta em comportamento recursivo: enquanto jestiver abaixo de 1000, mainchama a si mesmo recursivamente; quando jchega a 1000, ele chama exit, fazendo com que o programa saia com o código de saída 1001 (que é meio sujo, mas funciona).

tdammers
fonte
1
Boa resposta, mas uma dúvida .. Como saída principal com o código de saída 1001? Principal não está retornando nada .. Qualquer valor de retorno padrão?
Krishnabhadra 29/10
2
Quando j atinge 1000, o main não se recupera mais; em vez disso, chama a função libc exit, que aceita o código de saída como argumento e, assim, sai do processo atual. Nesse ponto, j é 1000, então j + 1 é igual a 1001, que se torna o código de saída.
tdammers