Jak działa kod C, który drukuje od 1 do 1000 bez pętli lub instrukcji warunkowych?

148

Znalazłem Ckod, który wyświetla od 1 do 1000 bez pętli i warunków warunkowych : ale nie rozumiem, jak to działa. Czy ktoś może przejść przez kod i wyjaśnić każdą linię?

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

void main(int j) {
  printf("%d\n", j);
  (&main + (&exit - &main)*(j/1000))(j+1);
}
ob_dev
źródło
1
Czy kompilujesz jako C czy C ++? Jakie widzisz błędy? Nie możesz wywoływać mainw C ++.
ninjalj
@ninjalj Utworzyłem projekt w C ++ i skopiowałem / przeszedłem kod, błąd to: niedozwolony, lewy operand ma typ 'void (__cdecl *) (int)', a wyrażenie musi być wskaźnikiem do pełnego typu obiektu
ob_dev
1
@ninjalj Ten kod działa na ideone.org, ale nie w Visual Studio ideone.com/MtJ1M
ob_dev
@oussama podobne, ale nieco bardziej trudne do zrozumienia: ideone.com/2ItXm jesteś mile widziany. :)
Mark
2
usunąłem wszystkie znaki „&” z tej linii (& main + (& exit - & main) * (j / 1000)) (j + 1); i ten kod nadal działa.
ob_dev

Odpowiedzi:

264

Nigdy nie pisz takiego kodu.


Dla j<1000, j/1000wynosi zero (dzielenie liczb całkowitych). Więc:

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

jest równa:

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

Który jest:

(&main)(j+1);

Który nazywa mainsię j+1.

Jeśli j == 1000, to te same linie wychodzą jako:

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

Co sprowadza się do

(&exit)(j+1);

Który jest exit(j+1)i opuszcza program.


(&exit)(j+1)i exit(j+1)zasadniczo są tym samym - cytując C99 §6.3.2.1 / 4:

Desygnator funkcji to wyrażenie, które ma typ funkcji. Z wyjątkiem sytuacji, gdy jest to operand operatora sizeof lub jednoargumentowy & operator , desygnator funkcji z typem „ funkcja zwracająca typ ” jest konwertowany na wyrażenie, które ma typ „ wskaźnik do funkcji zwracającej typ ”.

exitjest wyznacznikiem funkcji. Nawet bez jednoargumentowego &operatora address-of jest traktowany jako wskaźnik do funkcji. (Po &prostu to wyjaśnia).

Wywołania funkcji opisano w §6.5.2.2 / 1 i poniżej:

Wyrażenie, które oznacza wywoływaną funkcję, powinno mieć wskaźnik typu do funkcji zwracającej void lub zwracającej typ obiektu inny niż typ tablicowy.

exit(j+1)Działa więc z powodu automatycznej konwersji typu funkcji na typ wskaźnika do funkcji i (&exit)(j+1)działa również z jawną konwersją na typ wskaźnika do funkcji.

To powiedziawszy, powyższy kod nie jest zgodny ( mainprzyjmuje dwa argumenty lub wcale) i &exit - &mainjest, jak sądzę, niezdefiniowany zgodnie z §6.5.6 / 9:

Gdy odejmowane są dwa wskaźniki, oba wskazują elementy tego samego obiektu tablicy lub jeden za ostatnim elementem obiektu tablicy; ...

Dodatek (&main + ...)byłby ważny sam w sobie i mógłby zostać użyty, gdyby dodana ilość wynosiła zero, ponieważ §6.5.6 / 7 mówi:

Na potrzeby tych operatorów wskaźnik do obiektu, który nie jest elementem tablicy, zachowuje się tak samo, jak wskaźnik do pierwszego elementu tablicy o długości jeden z typem obiektu jako typem elementu.

Więc dodanie zera do &mainbyłoby w porządku (ale nie jest zbyt przydatne).

Mata
źródło
4
foo(arg)i (&foo)(arg)są równoważne, wywołują foo z argumentem arg. newty.de/fpt/fpt.html to interesująca strona o wskaźnikach funkcji.
Mat
1
@Krishnabhadra: w pierwszym przypadku foojest wskaźnikiem, &foojest adresem tego wskaźnika. W drugim przypadku foojest tablicą i &foojest równoważne z foo.
Mat
8
Niepotrzebnie skomplikowane, przynajmniej dla C99:((void(*[])()){main, exit})[j / 1000](j + 1);
Per Johansson
1
&foonie jest tym samym, co w fooprzypadku tablicy. &foojest wskaźnikiem do tablicy, foojest wskaźnikiem do pierwszego elementu. Mają jednak tę samą wartość. Dla funkcji funi &funoba są wskaźnikami do funkcji.
Per Johansson
1
FYI, jeśli spojrzysz na odpowiednią odpowiedź na inne pytanie cytowane powyżej , zobaczysz, że istnieje odmiana, która jest w rzeczywistości zgodna z C99. Straszne, ale prawdziwe.
Daniel Pryden
41

Wykorzystuje rekurencję, arytmetykę wskaźników i wykorzystuje zaokrąglanie podczas dzielenia liczb całkowitych.

Te j/1000krótkoterminowe rundy do 0 dla wszystkich j < 1000; po josiągnięciu 1000 zwraca 1.

Teraz, jeśli masz a + (b - a) * n, gdzie njest 0 lub 1, otrzymasz ajeśli n == 0i bjeśli n == 1. Używając &main(adres main()) oraz &exitfor ai b, termin (&main + (&exit - &main) * (j/1000))zwraca, &maingdy jjest poniżej 1000, w &exitprzeciwnym razie. Wynikowy wskaźnik funkcji jest następnie podawany jako argument j+1.

Cała ta konstrukcja skutkuje rekurencyjnym zachowaniem: gdy jjest poniżej 1000, mainwywołuje się rekurencyjnie; gdy josiągnie 1000, wywołuje exitzamiast tego, kończąc program z kodem zakończenia 1001 (co jest trochę brudne, ale działa).

tdammers
źródło
1
Dobra odpowiedź, ale jedna wątpliwość… Jak główne wyjście z kodem wyjścia 1001? Main nic nie zwraca .. Jakakolwiek domyślna wartość zwracana?
Krishnabhadra
2
Kiedy j osiąga 1000, main już nie powraca do siebie; zamiast tego wywołuje funkcję libc exit, która przyjmuje kod zakończenia jako argument i, cóż, kończy bieżący proces. W tym momencie j wynosi 1000, więc j + 1 równa się 1001, co staje się kodem zakończenia.
tdammers