Ich bin mir nicht sicher, aber ich denke, die Antwort ist aus ziemlich subtilen Gründen nein. Ich habe vor ein paar Jahren nach Theoretischer Informatik gefragt und keine Antwort erhalten, die über das hinausgeht, was ich hier präsentieren werde.
In den meisten Programmiersprachen können Sie eine Turing-Maschine folgendermaßen simulieren:
- Simulieren des endlichen Automaten mit einem Programm, das eine endliche Speichermenge verwendet;
- Simulieren des Bandes mit zwei verknüpften Listen von Ganzzahlen, die den Inhalt des Bandes vor und nach der aktuellen Position darstellen. Wenn Sie den Zeiger bewegen, wird der Kopf einer der Listen auf die andere Liste übertragen.
Eine konkrete Implementierung, die auf einem Computer ausgeführt wird, verfügt nicht über genügend Speicher, wenn das Band zu lang wird. Eine ideale Implementierung kann jedoch das Turing-Maschinenprogramm zuverlässig ausführen. Dies kann mit Stift und Papier oder durch den Kauf eines Computers mit mehr Speicher und eines Compilers erfolgen, der auf eine Architektur mit mehr Bits pro Wort abzielt, und so weiter, falls das Programm jemals keinen Speicher mehr hat.
Dies funktioniert in C nicht, da es unmöglich ist, eine verknüpfte Liste zu haben, die für immer wachsen kann: Die Anzahl der Knoten ist immer begrenzt.
Um zu erklären, warum, muss ich zuerst erklären, was eine C-Implementierung ist. C ist eigentlich eine Familie von Programmiersprachen. Der ISO-C-Standard (genauer gesagt eine bestimmte Version dieses Standards) definiert (mit dem Grad an Formalität, den Englisch zulässt) die Syntax und Semantik einer Familie von Programmiersprachen. C hat viel undefiniertes und implementierungsdefiniertes Verhalten. Eine „Implementierung“ von C codiert das gesamte implementierungsdefinierte Verhalten (die Liste der zu codierenden Dinge befindet sich in Anhang J für C99). Jede Implementierung von C ist eine eigene Programmiersprache. Beachten Sie, dass die Bedeutung des Wortes „Implementierung“ etwas eigenartig ist: Was es wirklich bedeutet, ist eine Sprachvariante. Es kann mehrere verschiedene Compiler-Programme geben, die dieselbe Sprachvariante implementieren.
In einer gegebenen Implementierung von C hat ein Byte mögliche Werte. Alle Daten können als Array von Bytes dargestellt werden: Ein Typ hat höchstens
mögliche Werte. Diese Anzahl variiert in verschiedenen Implementierungen von C, ist jedoch für eine bestimmte Implementierung von C eine Konstante. 2 CHAR_BIT × sizeof (t)2CHAR_BITt
2CHAR_BIT×sizeof(t)
Insbesondere können Zeiger höchstens Werte . Das heißt, es gibt eine endliche maximale Anzahl adressierbarer Objekte.2CHAR_BIT×sizeof(void*)
Die Werte von CHAR_BIT
und sizeof(void*)
sind beobachtbar. Wenn Ihnen also der Speicher ausgeht, können Sie Ihr Programm nicht einfach mit größeren Werten für diese Parameter fortsetzen. Sie würden das Programm unter einer anderen Programmiersprache ausführen - einer anderen C-Implementierung.
Wenn Programme in einer Sprache nur eine begrenzte Anzahl von Zuständen haben können, ist die Programmiersprache nicht ausdrucksvoller als endliche Automaten. Das Fragment von C, das auf adressierbaren Speicher beschränkt ist, lässt höchstens zu, wobei die Größe des abstrakten Syntaxbaums von ist Programm (das den Zustand des Kontrollflusses darstellt), daher kann dieses Programm von einem endlichen Automaten mit so vielen Zuständen simuliert werden. Wenn C aussagekräftiger ist, muss es durch die Verwendung anderer Merkmale erfolgen. nn×2CHAR_BIT×sizeof(void*)n
C legt keine maximale Rekursionstiefe fest. Eine Implementierung darf ein Maximum haben, aber es darf auch keines geben. Aber wie kommunizieren wir zwischen einem Funktionsaufruf und seinem übergeordneten Element? Argumente sind nicht gut, wenn sie adressierbar sind, da dies indirekt die Rekursionstiefe einschränken würde: Wenn Sie eine Funktion haben, haben int f(int x) { … f(…) …}
alle Vorkommen x
auf aktiven Frames f
eine eigene Adresse, und daher ist die Anzahl der verschachtelten Aufrufe durch die Anzahl begrenzt von möglichen Adressen für x
.
Ein Programm kann einen nicht adressierbaren Speicher in Form von register
Variablen verwenden. "Normale" Implementierungen können nur eine kleine, begrenzte Anzahl von Variablen haben, die keine Adresse haben. Theoretisch kann eine Implementierung jedoch eine unbegrenzte Menge an register
Speicherplatz ermöglichen. In einer solchen Implementierung können Sie eine unbegrenzte Anzahl von rekursiven Aufrufen einer Funktion ausführen, sofern das Argument dies zulässt register
. Da es sich bei den Argumenten register
jedoch um Argumente handelt , können Sie keinen Zeiger auf sie erstellen. Daher müssen Sie deren Daten explizit kopieren: Sie können nur eine begrenzte Datenmenge übergeben, keine Datenstruktur beliebiger Größe, die aus Zeigern besteht.
Mit unbegrenzter Rekursionstiefe und der Einschränkung, dass eine Funktion nur Daten von ihrem direkten Aufrufer ( register
Argumente) abrufen und Daten an ihren direkten Aufrufer (den Funktionsrückgabewert) zurückgeben kann, erhalten Sie die Macht deterministischer Pushdown-Automaten .
Ich kann keinen Weg finden, weiter zu gehen.
(Natürlich könnten Sie das Programm veranlassen, den Bandinhalt extern zu speichern, und zwar über Dateieingabe- / ausgabefunktionen. Dann würden Sie jedoch nicht fragen, ob C vollständig ist, sondern ob C plus ein unendliches Speichersystem vollständig ist . , welche die Antwort ist ein langweilige „ja“ Sie könnten als definieren auch die Speicherung ein Turing Orakel zu sein - Aufruf fopen("oracle", "r+")
, fwrite
der anfängliche Bandinhalt zu ihm und fread
. wieder der endgültigen Bandinhalt)