Wie funktioniert der C-Code, der ohne Schleifen oder bedingte Anweisungen von 1 bis 1000 druckt?


148

Ich habe CCode gefunden , der von 1 bis 1000 ohne Schleifen oder Bedingungen druckt : Aber ich verstehe nicht, wie es funktioniert. Kann jemand den Code durchgehen und jede Zeile erklären?

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

void main(int j) {
  printf("%d\n", j);
  (&main + (&exit - &main)*(j/1000))(j+1);
}

1
Kompilieren Sie als C oder als C ++? Welche Fehler sehen Sie? Sie können mainC ++ nicht aufrufen .
Ninjalj

@ninjalj Ich habe ein C ++ - Projekt erstellt und den Code kopiert / eingefügt. Der Fehler lautet: illegal, linker Operand hat den Typ 'void (__cdecl *) (int)' und Ausdruck muss ein Zeiger auf einen vollständigen Objekttyp sein
ob_dev

1
@ninjalj Dieser Code funktioniert auf ideone.org, aber nicht in Visual Studio ideone.com/MtJ1M
ob_dev

@oussama ähnlich, aber etwas mehr schwer zu verstehen: ideone.com/2ItXm Nichts zu danken. :)
Mark

2
Ich habe alle '&' Zeichen aus dieser Zeile entfernt (& main + (& exit - & main) * (j / 1000)) (j + 1); und dieser Code funktioniert immer noch.
ob_dev

Antworten:


264

Schreiben Sie niemals solchen Code.


Für j<1000, j/1000null (Integer - Division). So:

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

ist äquivalent zu:

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

Welches ist:

(&main)(j+1);

Welches ruft mainmit j+1.

Wenn j == 1000, dann kommen die gleichen Zeilen heraus wie:

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

Was darauf hinausläuft

(&exit)(j+1);

Welches ist exit(j+1)und verlässt das Programm.


(&exit)(j+1)und exit(j+1)sind im Wesentlichen dasselbe - zitiert C99 §6.3.2.1 / 4:

Ein Funktionsbezeichner ist ein Ausdruck mit dem Funktionstyp. Außer wenn es sich um den Operanden des Operators sizeof oder des Operators unary & handelt , wird ein Funktionsbezeichner vom Typ " Funktionsrückgabetyp " in einen Ausdruck mit dem Typ " konvertiert". Zeiger auf den Funktionsrückgabetyp .

exitist ein Funktionsbezeichner. Auch ohne das Unäre& Adresse des Operators wird sie als Zeiger auf die Funktion behandelt. (Das &macht es nur explizit.)

Funktionsaufrufe sind in §6.5.2.2 / 1 und folgenden beschrieben:

Der Ausdruck, der die aufgerufene Funktion bezeichnet, muss einen Typzeiger auf die Funktion haben die void zurückgibt oder einen anderen Objekttyp als einen Array-Typ zurückgibt.

So exit(j+1) aufgrund der automatischen Konvertierung des Funktionstyps in einen Zeiger-zu-Funktionstyp und (&exit)(j+1)funktioniert auch mit einer expliziten Konvertierung in einen Zeiger-zu-Funktionstyp.

Abgesehen davon ist der obige Code nicht konform ( mainnimmt entweder zwei oder gar keine Argumente an) und &exit - &mainist meines Erachtens gemäß §6.5.6 / 9 undefiniert:

Wenn zwei Zeiger subtrahiert werden, zeigen beide auf Elemente desselben Array-Objekts oder einen nach dem letzten Element des Array-Objekts. ...

Die Addition (&main + ...)wäre an sich gültig und könnte verwendet werden, wenn die hinzugefügte Menge Null wäre, da in §6.5.6 / 7 Folgendes steht:

Für die Zwecke dieser Operatoren verhält sich ein Zeiger auf ein Objekt, das kein Element eines Arrays ist, genauso wie ein Zeiger auf das erste Element eines Arrays der Länge eins mit dem Objekttyp als Elementtyp.

Das Hinzufügen von Null zu &mainwäre also in Ordnung (aber nicht sehr nützlich).


4
foo(arg)und (&foo)(arg)sind gleichwertig, sie nennen foo mit argument arg. newty.de/fpt/fpt.html ist eine interessante Seite über Funktionszeiger.
Mat

1
@Krishnabhadra: Im ersten Fall fooist ein Zeiger, &fooist die Adresse dieses Zeigers. Im zweiten Fall foohandelt es sich um ein Array, &foodas foo entspricht.
Mat

8
Unnötig komplex, zumindest für C99:((void(*[])()){main, exit})[j / 1000](j + 1);
Per Johansson

1
&fooist nicht dasselbe wie foobei einem Array. &fooist ein Zeiger auf das Array, fooist ein Zeiger auf das erste Element. Sie haben jedoch den gleichen Wert. Für Funktionen funund &funsind beide Zeiger auf die Funktion.
Per Johansson

1
Zu Ihrer Information: Wenn Sie sich die relevante Antwort auf die andere oben genannte Frage ansehen , werden Sie feststellen, dass es eine Variation gibt, die tatsächlich C99 entspricht. Beängstigend, aber wahr.
Daniel Pryden

41

Es verwendet Rekursion, Zeigerarithmetik und nutzt das Rundungsverhalten der Ganzzahldivision.

Der j/1000Begriff rundet für alle auf 0 ab j < 1000; Einmalj 1000 erreicht ist, wird 1 ausgewertet.

Wenn Sie nun haben a + (b - a) * n, wo nentweder 0 oder 1 ist, erhalten Sie aif n == 0undb if n == 1. Wenn Sie &main(die Adresse von main()) und &exitfür aund verwenden b, wird der Begriff (&main + (&exit - &main) * (j/1000))zurückgegeben, &mainwenn er junter 1000 liegt.&exit . Dem resultierenden Funktionszeiger wird dann das Argument zugeführt j+1.

Dieses ganze Konstrukt führt zu rekursivem Verhalten: Während jes unter 1000 liegt, mainruft es sich selbst rekursiv auf; Wenn j1000 erreicht ist, wird exitstattdessen aufgerufen, wodurch das Programm mit dem Exit-Code 1001 beendet wird (was etwas schmutzig ist, aber funktioniert).


1
Gute Antwort, aber ein Zweifel. Wie Hauptausgang mit Exit-Code 1001? Main gibt nichts zurück. Gibt es einen Standardrückgabewert?
Krishnabhadra

2
Wenn j 1000 erreicht, kehrt main nicht mehr in sich zurück; Stattdessen ruft es die libc-Funktion auf exit, die den Exit-Code als Argument verwendet und den aktuellen Prozess beendet. Zu diesem Zeitpunkt ist j 1000, also ist j + 1 gleich 1001, was zum Exit-Code wird.
tdammers
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.