Warum ist eine * Deklaration * von Daten und Funktionen in C-Sprache erforderlich, wenn die Definition am Ende des Quellcodes steht?

15

Betrachten Sie den folgenden "C" Code:

#include<stdio.h>
main()
{   
  printf("func:%d",Func_i());   
}

Func_i()
{
  int i=3;
  return i;
}

Func_i()wird am Ende des Quellcodes definiert und es wird keine Deklaration bereitgestellt, bevor sie in verwendet wird main(). Genau zu dem Zeitpunkt , wenn der Compiler sieht Func_i()in main(), kommt es aus dem heraus main()und findet heraus Func_i(). Der Compiler findet irgendwie den von zurückgegebenen Wert Func_i()und gibt ihn an printf(). Ich weiß auch, dass der Compiler den Rückgabetyp von nicht finden kann Func_i(). Es wird standardmäßig nimmt (Vermutungen?) , Um den Rückgabetyp von Func_i()sein int. Das heißt, wenn der Code float Func_i()den folgenden Fehler hätte, würde der Compiler ihn ausgeben: Konflikttypen fürFunc_i() .

Aus der obigen Diskussion sehen wir, dass:

  1. Der Compiler kann den von zurückgegebenen Wert finden Func_i().

    • Wenn der Compiler den Wert zurückgeführt, indem finden können , Func_i()indem kommen aus der main()den Quellcode und die Suche nach unten, dann warum kann es nicht die Art von Func_i () finden, die sich explizit erwähnt.
  2. Der Compiler muss wissen, dass Func_i()es sich um den Typ float handelt. Aus diesem Grund tritt der Fehler von Typen auf, bei denen Konflikte auftreten.

  • Wenn der Compiler weiß, dass Func_ies sich um den Typ float handelt, warum wird dann immer noch davon ausgegangen Func_i(), dass es sich um den Typ int handelt, und es wird der Fehler in Konflikt stehender Typen ausgegeben? Warum macht es nicht gewaltsam, Func_i()vom Typ float zu sein?

Ich habe den gleichen Zweifel mit der Variablendeklaration . Betrachten Sie den folgenden "C" Code:

#include<stdio.h>
main()
{
  /* [extern int Data_i;]--omitted the declaration */
  printf("func:%d and Var:%d",Func_i(),Data_i);
}

 Func_i()
{
  int i=3;
  return i;
}
int Data_i=4;

Der Compiler gibt den Fehler aus: 'Data_i' nicht deklariert (erstmalige Verwendung in dieser Funktion).

  • Wenn der Compiler sieht Func_i(), geht er zum Quellcode, um den von Func_ () zurückgegebenen Wert zu finden. Warum kann der Compiler nicht dasselbe für die Variable Data_i tun?

Bearbeiten:

Ich kenne die Details der inneren Arbeitsweise von Compiler, Assembler, Prozessor usw. nicht. Die Grundidee meiner Frage ist, dass ich den Rückgabewert der Funktion nach der Verwendung endlich im Quellcode wiedergebe (schreibe) Von dieser Funktion aus kann der Computer mit der Sprache "C" diesen Wert finden, ohne dass ein Fehler auftritt. Warum kann der Computer den Typ nicht auf ähnliche Weise finden? Warum kann der Typ von Data_i nicht gefunden werden, da der Rückgabewert von Func_i () gefunden wurde? Selbst wenn ich die extern data-type identifier;Anweisung verwende, sage ich nicht, dass der Wert von diesem Bezeichner (Funktion / Variable) zurückgegeben werden soll. Wenn der Computer diesen Wert findet, warum findet er dann den Typ nicht? Warum brauchen wir überhaupt die Forward-Deklaration?

Vielen Dank.

user106313
quelle
7
Der Compiler "findet" den von Func_i zurückgegebenen Wert nicht. Dies erfolgt zur Ausführungszeit.
James McLeod
26
Ich habe nicht abgelehnt, aber die Frage basiert auf einigen schwerwiegenden Missverständnissen in Bezug auf die Funktionsweise von Compilern, und Ihre Antworten in Kommentaren legen nahe, dass Sie noch einige konzeptionelle Hürden zu überwinden haben.
James McLeod
4
Beachten Sie, dass der erste Beispielcode in den letzten fünfzehn Jahren kein gültiger, standardkonformer Code war. C99 hat das Fehlen eines Rückgabetyps in den Funktionsdefinitionen und die implizite Deklaration Func_iungültig gemacht. Es gab nie eine Regel, um undefinierte Variablen implizit zu deklarieren, daher war das zweite Fragment immer fehlerhaft. (Ja, Compiler akzeptieren das erste Beispiel immer noch, da es unter C89 / C90 gültig war, wenn es schlampig war.)
Jonathan Leffler
19
@ user31782: Fazit zur Frage: Warum benötigt / tut Sprache X Y? Denn das ist die Wahl, die die Designer getroffen haben. Sie scheinen zu argumentieren, dass die Designer einer der erfolgreichsten Sprachen überhaupt vor Jahrzehnten andere Entscheidungen treffen sollten, anstatt zu versuchen, diese Entscheidungen in dem Kontext zu verstehen, in dem sie getroffen wurden. Die Antwort auf Ihre Frage: Warum brauchen wir eine Voraberklärung? wurde angegeben: Weil C einen One-Pass-Compiler verwendet. Die einfachste Antwort auf die meisten Ihrer Anschlussfragen lautet: Weil es dann kein One-Pass-Compiler wäre.
Mr.Mindor
4
@ user31782 Sie möchten unbedingt das Buch des Drachen lesen , um zu verstehen, wie Compiler und Prozessoren tatsächlich funktionieren. Es ist einfach unmöglich, das gesamte erforderliche Wissen in einer einzigen SO-Antwort zusammenzufassen (oder sogar in 100 Antworten). Tolles Buch für alle, die sich für Compiler interessieren.
Voo

Antworten:

26

Weil C eine statisch typisierte , schwach typisierte , kompilierte Sprache mit einem Durchgang ist .

  1. Single-Pass bedeutet, dass der Compiler nicht nach vorne schaut, um die Definition einer Funktion oder Variablen zu sehen. Da der Compiler nicht in die Zukunft blickt, muss die Deklaration einer Funktion vor der Verwendung der Funktion erfolgen, da der Compiler sonst nicht weiß, um welche Typensignatur es sich handelt. Die Definition der Funktion kann jedoch später in derselben Datei oder sogar in einer anderen Datei insgesamt erfolgen. Siehe Punkt 4.

    Die einzige Ausnahme ist das historische Artefakt, bei dem angenommen wird, dass nicht deklarierte Funktionen und Variablen vom Typ "int" sind. Die moderne Praxis besteht darin, implizite Typisierung zu vermeiden, indem Funktionen und Variablen immer explizit deklariert werden.

  2. Statisch typisiert bedeutet, dass alle Typinformationen zur Kompilierzeit berechnet werden. Diese Informationen werden dann verwendet, um Maschinencode zu generieren, der zur Laufzeit ausgeführt wird. In C gibt es kein Konzept für die Typisierung zur Laufzeit. Einmal ein Int, immer ein Int, einmal ein Float, immer ein Float. Diese Tatsache wird jedoch durch den nächsten Punkt etwas verdeckt.

  3. Schwach geschrieben bedeutet, dass der C-Compiler automatisch Code für die Konvertierung zwischen numerischen Typen generiert, ohne dass der Programmierer die Konvertierungsoperationen explizit angeben muss. Aufgrund der statischen Typisierung wird die gleiche Konvertierung jedes Mal im Programm auf die gleiche Weise ausgeführt. Wenn ein Gleitkommawert an einer bestimmten Stelle im Code in einen Int-Wert konvertiert wird, wird ein Gleitkommawert an dieser Stelle im Code immer in einen Int-Wert konvertiert. Dies kann zur Laufzeit nicht geändert werden. Der Wert selbst kann sich natürlich von einer Ausführung des Programms zur nächsten ändern, und bedingte Anweisungen können ändern, welche Codeabschnitte in welcher Reihenfolge ausgeführt werden, aber ein bestimmter einzelner Codeabschnitt ohne Funktionsaufrufe oder Bedingungen führt immer die exakte aus gleiche Operationen, wenn es ausgeführt wird.

  4. Kompiliert bedeutet, dass der Prozess der Analyse des lesbaren Quellcodes und der Umwandlung in maschinenlesbare Anweisungen vollständig ausgeführt wird, bevor das Programm ausgeführt wird. Wenn der Compiler eine Funktion kompiliert, weiß er nicht, auf was er weiter unten in einer bestimmten Quelldatei stößt. Sobald die Kompilierung (und das Zusammenstellen, Verknüpfen usw.) abgeschlossen ist, enthält jede Funktion in der fertigen ausführbaren Datei numerische Zeiger auf die Funktionen, die sie beim Ausführen aufruft. Deshalb kann main () eine Funktion weiter unten in der Quelldatei aufrufen. Wenn main () tatsächlich ausgeführt wird, enthält es einen Zeiger auf die Adresse von Func_i ().

    Maschinencode ist sehr, sehr spezifisch. Der Code zum Hinzufügen von zwei Ganzzahlen (3 + 2) unterscheidet sich von dem zum Hinzufügen von zwei Gleitkommazahlen (3.0 + 2.0). Beides unterscheidet sich vom Hinzufügen eines Int zu einem Float (3 + 2.0) und so weiter. Der Compiler bestimmt für jeden Punkt in einer Funktion, welche exakte Operation an diesem Punkt ausgeführt werden muss, und generiert Code, der diese exakte Operation ausführt. Danach kann es nicht mehr geändert werden, ohne die Funktion neu zu kompilieren.

Wenn Sie all diese Konzepte zusammenfassen, kann main () nicht weiter unten nachsehen, um den Typ von Func_i () zu bestimmen. Dies liegt daran, dass die Typanalyse zu Beginn des Kompilierungsprozesses stattfindet. Zu diesem Zeitpunkt wurde nur der Teil der Quelldatei bis zur Definition von main () gelesen und analysiert, und die Definition von Func_i () ist dem Compiler noch nicht bekannt.

Der Grund, warum main () "sehen" kann, wo Func_i () aufzurufen ist, ist, dass der Aufruf zur Laufzeit erfolgt, nachdem die Kompilierung bereits alle Namen und Typen aller Bezeichner aufgelöst hat, hat Assembly bereits alle konvertiert Die Verknüpfung von Funktionen mit dem Maschinencode hat bereits die korrekte Adresse jeder Funktion an jeder Stelle eingefügt, an der sie aufgerufen wird.

Ich habe natürlich die meisten blutigen Details ausgelassen. Der eigentliche Prozess ist sehr viel komplizierter. Ich hoffe, ich habe genug Überblick gegeben, um Ihre Fragen zu beantworten.

Bitte denken Sie außerdem daran, dass das, was ich oben geschrieben habe, speziell für C gilt.

In anderen Sprachen führt der Compiler möglicherweise mehrere Durchläufe durch den Quellcode durch, sodass der Compiler die Definition von Func_i () abrufen kann, ohne dass diese deklariert wird.

In anderen Sprachen können Funktionen und / oder Variablen dynamisch typisiert werden, sodass eine einzelne Variable zu unterschiedlichen Zeiten enthalten oder eine einzelne Funktion übergeben oder zurückgegeben werden kann, eine Ganzzahl, ein Gleitkomma, eine Zeichenfolge, ein Array oder ein Objekt.

In anderen Sprachen ist die Typisierung möglicherweise stärker, sodass die Konvertierung von Gleitkomma in Ganzzahl explizit angegeben werden muss. In noch anderen Sprachen kann die Eingabe schwächer sein, sodass die Konvertierung von der Zeichenfolge "3.0" in die Gleitkommazahl 3.0 in die Ganzzahl 3 automatisch ausgeführt wird.

In anderen Sprachen kann Code zeilenweise interpretiert oder zu Byte-Code kompiliert und dann interpretiert oder just-in-time kompiliert oder einer Vielzahl anderer Ausführungsschemata unterzogen werden.

Clement Cherlin
quelle
1
Vielen Dank für eine umfassende Antwort. Deine und Nikies Antwort ist das, was ich wissen wollte. ZB Func_()+1: hier bei der Kompilierung der Compiler haben die Art der wissen , Func_i()um den entsprechenden Maschinencode zu erzeugen. Vielleicht ist es entweder nicht möglich, dass die Assembly Func_()+1den Typ zur Laufzeit aufruft, oder es ist möglich, aber dies verlangsamt das Programm zur Laufzeit. Ich denke, es ist genug für mich.
user106313
1
Wichtiges Detail der implizit deklarierten Funktionen von C: Es wird angenommen, dass sie vom Typ int func(...)... sind, dh, dass sie eine Liste variabler Argumente enthalten. Das bedeutet, wenn Sie eine Funktion als definieren, int putc(char)aber vergessen, sie zu deklarieren, wird sie stattdessen als aufgerufen int putc(int)(da char, das über eine Liste variabler Argumente übergeben wird, in befördert wird int). Während also das Beispiel des OP funktioniert hat, weil seine Signatur mit der impliziten Deklaration übereinstimmt, ist verständlich, warum von diesem Verhalten abgeraten wurde (und entsprechende Warnungen hinzugefügt wurden).
uliwitness
37

Eine Konstruktionsbeschränkung der C-Sprache bestand darin, dass sie von einem Single-Pass-Compiler kompiliert werden sollte, wodurch sie für Systeme mit sehr eingeschränktem Speicher geeignet ist. Daher kennt der Compiler zu jedem Zeitpunkt nur die zuvor genannten Dinge. Der Compiler kann in der Quelle nicht vorwärts springen, um eine Funktionsdeklaration zu finden, und dann zurückgehen, um einen Aufruf dieser Funktion zu kompilieren. Daher sollten alle Symbole deklariert werden, bevor sie verwendet werden. Sie können eine Funktion wie vorab deklarieren

int Func_i();

am Anfang oder in einer Header-Datei, um den Compiler zu helfen.

In Ihren Beispielen verwenden Sie zwei zweifelhafte Merkmale der C-Sprache, die vermieden werden sollten:

  1. Wenn eine Funktion verwendet wird, bevor sie ordnungsgemäß deklariert wurde, wird dies als "implizite Deklaration" verwendet. Der Compiler verwendet den unmittelbaren Kontext, um die Funktionssignatur herauszufinden. Der Compiler durchsucht den Rest des Codes nicht, um herauszufinden, was die echte Deklaration ist.

  2. Wenn etwas ohne einen Typ deklariert wird, wird der Typ als gegeben angenommen int. Dies ist zB bei statischen Variablen oder Funktionsrückgabetypen der Fall.

Also printf("func:%d",Func_i()), wir eine implizite Erklärung haben int Func_i(). Wenn der Compiler die Funktionsdefinition erreicht Func_i() { ... }, ist dies mit dem Typ kompatibel. Aber wenn Sie float Func_i() { ... }an dieser Stelle geschrieben haben, haben Sie die Implizität deklariert int Func_i()und die explizit deklariert float Func_i(). Da die beiden Deklarationen nicht übereinstimmen, gibt der Compiler einen Fehler aus.

Einige Missverständnisse beseitigen

  • Der Compiler findet den von zurückgegebenen Wert nicht Func_i. Das Fehlen eines expliziten Typs bedeutet, dass der Rückgabetyp intstandardmäßig ist. Auch wenn Sie dies tun:

    Func_i() {
        float f = 42.3;
        return f;
    }

    dann ist der Typ int Func_i()und der Rückgabewert wird stillschweigend abgeschnitten!

  • Der Compiler lernt schließlich den realen Typ von kennen Func_i, kennt jedoch den realen Typ während der impliziten Deklaration nicht. Erst wenn es später die echte Deklaration erreicht, kann es herausfinden, ob der implizit deklarierte Typ korrekt war. Zu diesem Zeitpunkt ist die Assembly für den Funktionsaufruf möglicherweise bereits geschrieben und kann im C-Kompilierungsmodell nicht geändert werden.

amon
quelle
3
@ user31782: Die Reihenfolge des Codes spielt bei der Kompilierung eine Rolle, jedoch nicht zur Laufzeit. Der Compiler ist nicht im Bild, wenn das Programm ausgeführt wird. Zur Laufzeit wurde die Funktion zusammengestellt und verknüpft, ihre Adresse wurde aufgelöst und in den Adressplatzhalter des Aufrufs eingefügt. (Es ist etwas komplexer, aber das ist die Grundidee.) Der Prozessor kann vorwärts oder rückwärts verzweigen.
Blrfl
20
@ user31782: Der Compiler nicht der Fall ist , den Wert drucken. Ihr Compiler führt das Programm nicht aus !!
Leichtigkeit Rennen mit Monica
1
@LightnessRacesinOrbit Das weiß ich. Ich schrieb fälschlicherweise Compiler in meinem Kommentar oben , weil ich den Namen vergessen Prozessor .
user106313
3
@Carcigenicate C wurde stark von der B-Sprache beeinflusst, die nur einen einzigen Typ hatte: einen ganzzahligen numerischen Typ mit Wortbreite, der auch für Zeiger verwendet werden konnte. C hat dieses Verhalten ursprünglich kopiert, ist jedoch seit dem C99-Standard vollständig verboten. Unitmacht einen schönen Standardtyp aus typentheoretischer Sicht, scheitert aber an der praktischen Anwendbarkeit der nahe an den Metallsystemen liegenden Programmierung, für die B und dann C entworfen wurden.
amon
2
@ user31782: Der Compiler muss den Typ der Variablen kennen, um die richtige Assembly für den Prozessor zu generieren. Wenn der Compiler das Implizite findet Func_i(), generiert und speichert er sofort den Code, damit der Prozessor zu einer anderen Position springen, eine Ganzzahl empfangen und dann fortfahren kann. Wenn der Compiler die Func_iDefinition später findet , stellt er sicher, dass die Signaturen übereinstimmen. Wenn dies der Fall ist, platziert er die Assembly für Func_i()diese Adresse und weist sie an, eine Ganzzahl zurückzugeben. Wenn Sie das Programm ausführen, folgt der Prozessor diesen Anweisungen mit dem Wert 3.
Mooing Duck
10

Erstens sind Ihre Programme für den C90-Standard gültig, jedoch nicht für die folgenden. implizites int (das Deklarieren einer Funktion ohne Angabe des Rückgabetyps) und implizites Deklarieren von Funktionen (das Verwenden einer Funktion ohne Angabe) sind nicht mehr gültig.

Zweitens funktioniert das nicht so, wie Sie denken.

  1. Der Ergebnistyp ist in C90 optional und gibt kein intErgebnis an. Das gilt auch für die Variablendeklaration (aber Sie müssen eine Speicherklasse angeben, staticoder extern).

  2. Wenn der Compiler das Func_iohne vorherige Deklaration aufruft, geht er davon aus, dass es eine Deklaration gibt

    extern int Func_i();

    Im Code wird nicht weiter untersucht, wie effektiv Func_idie Deklaration ist. Wenn dies Func_inicht deklariert oder definiert wurde, ändert der Compiler sein Verhalten beim Kompilieren nicht main. Die implizite Deklaration gilt nur für Funktion, für Variable gibt es keine.

    Beachten Sie, dass die leere Parameterliste in der Deklaration nicht bedeutet, dass die Funktion keine Parameter akzeptiert (Sie müssen dies angeben (void)). Dies bedeutet, dass der Compiler die Typen der Parameter nicht überprüfen muss und dies auch tun wird implizite Konvertierungen, die auf Argumente angewendet werden, die an verschiedene Funktionen übergeben werden.

Ein Programmierer
quelle
Wenn der Compiler den von Func_i () zurückgegebenen Wert finden kann, indem er aus main () kommt und den Quellcode durchsucht, warum kann er dann den explizit erwähnten Typ von Func_i () nicht finden?
user106313
1
@ user31782 Wenn es keine vorherige Deklaration von Func_i gab, verhält sich Func_i bei der Verwendung in einem Aufrufausdruck so, als ob es eine gab extern int Func_i(). Es sieht nirgendwo aus.
AProgrammer
1
@ user31782, der Compiler springt nirgendwo hin. Es wird Code ausgegeben, um diese Funktion aufzurufen. Der zurückgegebene Wert wird zur Laufzeit ermittelt. Nun, im Fall einer so einfachen Funktion, die in der gleichen Kompilierungseinheit vorhanden ist, kann die Optimierungsphase die Funktion einschließen, aber das sollten Sie nicht berücksichtigen, wenn Sie die Regeln der Sprache berücksichtigen, es ist eine Optimierung.
AProgrammer
10
@ user31782, Sie haben ernsthafte Missverständnisse über die Funktionsweise von Programmen. So ernst, dass ich nicht denke, dass p.se ein guter Ort ist, um sie zu korrigieren (vielleicht der Chat, aber ich werde nicht versuchen, es zu tun).
AProgrammer
1
@ user31782: Wenn Sie ein kleines Snippet schreiben und es mit kompilieren -S(sofern Sie es verwenden gcc), können Sie den vom Compiler generierten Assembly-Code anzeigen. Dann können Sie eine Vorstellung davon bekommen, wie Rückgabewerte zur Laufzeit behandelt werden (normalerweise unter Verwendung eines Prozessorregisters oder etwas Platz auf dem Programmstapel).
Giorgio
7

Sie haben in einem Kommentar geschrieben:

Die Ausführung erfolgt zeilenweise. Die einzige Möglichkeit, den von Func_i () zurückgegebenen Wert zu finden, besteht darin, aus dem Hauptmenü zu springen

Das ist ein Irrtum: Die Ausführung erfolgt nicht zeilenweise. Die Kompilierung erfolgt zeilenweise und die Namensauflösung erfolgt während der Kompilierung. Dabei werden nur Namen aufgelöst, keine Werte zurückgegeben.

Ein hilfreiches konzeptionelles Modell ist: Wenn der Compiler die Zeile liest:

  printf("func:%d",Func_i());

es gibt Code aus, der äquivalent ist zu:

  1. call "function #2" and put the return value on the stack
  2. put the constant string "func:%d" on the stack
  3. call "function #1"

Der Compiler macht auch eine Notiz in einer internen Tabelle, bei der function #2es sich um eine noch nicht deklarierte Funktion mit dem Namen Func_ihandelt, die eine nicht angegebene Anzahl von Argumenten verwendet und ein int (die Standardeinstellung) zurückgibt.

Später, wenn es das analysiert:

 int Func_i() { ...

Der Compiler schlägt Func_iin der oben genannten Tabelle nach und prüft, ob die Parameter und der Rückgabetyp übereinstimmen. Wenn dies nicht der Fall ist, wird es mit einer Fehlermeldung beendet. In diesem Fall wird die aktuelle Adresse zur internen Funktionstabelle hinzugefügt und in die nächste Zeile gewechselt.

Der Compiler hat also nicht "gesucht", Func_ials er die erste Referenz analysiert hat. Es machte einfach eine Notiz in einer Tabelle, die die nächste Zeile weiter analysierte. Am Ende der Datei befinden sich eine Objektdatei und eine Liste der Sprungadressen.

Später nimmt der Linker all dies und ersetzt alle Zeiger auf "Funktion # 2" durch die tatsächliche Sprungadresse, so dass er ungefähr Folgendes ausgibt:

  call 0x0001215 and put the result on the stack
  put constant ... on the stack
  call ...
...
[at offset 0x0001215 in the file, compiled result of Func_i]:
  put 3 on the stack
  return top of the stack

Viel später, wenn die ausführbare Datei ausgeführt wird, ist die Sprungadresse bereits aufgelöst, und der Computer kann einfach zur Adresse 0x1215 springen. Keine Namenssuche erforderlich.

Haftungsausschluss : Wie gesagt, das ist ein konzeptionelles Modell und die reale Welt ist komplizierter. Compiler und Linker führen heute alle möglichen verrückten Optimierungen durch. Sie könnten sogar "rauf und runter springen" Func_i, obwohl ich es bezweifle. Aber die C-Sprachen sind so definiert, dass man so einen supereinfachen Compiler schreiben kann. Es ist also die meiste Zeit ein sehr nützliches Modell.

Nikie
quelle
Vielen Dank für Ihre Antwort. Kann der Compiler den Code nicht ausgeben:1. call "function #2", put the return-type onto the stack and put the return value on the stack?
user106313
1
(Forts.) Außerdem: Was, wenn Sie geschrieben haben printf(..., Func_i()+1);- der Compiler muss den Typ kennen Func_i, damit er entscheiden kann, ob er eine add integeroder eine add floatAnweisung ausgeben soll . In einigen Sonderfällen kann es vorkommen, dass der Compiler ohne die Typinformationen weiterarbeitet, der Compiler jedoch in allen Fällen arbeiten muss.
Nikie
4
@ user31782: Maschinenanweisungen sind in der Regel sehr einfach: Fügen Sie zwei 32-Bit-Ganzzahlregister hinzu. Laden Sie eine Speicheradresse in ein 16-Bit-Ganzzahlregister. Zu einer Adresse springen. Es gibt auch keine Typen : Sie können einen Speicherort, der eine 32-Bit-Gleitkommazahl darstellt, problemlos in ein 32-Bit-Ganzzahlregister laden und damit rechnen. (Es macht nur selten Sinn.) Nein, Sie können so einen Maschinencode nicht direkt ausgeben. Sie könnten einen Compiler schreiben, der all diese Dinge mit Laufzeitprüfungen und zusätzlichen Typdaten auf dem Stack erledigt. Aber es wäre kein C-Compiler.
Nikie
1
@ user31782: Abhängig von IIRC. floatWerte können in einem FPU-Register gespeichert werden - dann gäbe es überhaupt keine Anweisung. Der Compiler verfolgt nur, welcher Wert in welchem ​​Register während des Kompilierens gespeichert wird, und gibt Dinge wie "Addiere Konstante 1 zu FP-Register X" aus. Oder es könnte auf dem Stapel leben, wenn es keine freien Register gibt. Dann gäbe es den Befehl "Stapelzeiger um 4 erhöhen", und der Wert würde als etwas wie "Stapelzeiger - 4" "referenziert". All diese Dinge funktionieren jedoch nur, wenn die Größen aller Variablen (davor und danach) auf dem Stapel zur Kompilierungszeit bekannt sind.
Nikie
1
Aus der ganzen Diskussion bin ich zu folgendem Ergebnis gekommen: Damit der Compiler einen plausiblen Assembly-Code für eine Anweisung einschließlich Func_i()oder / und erstellen kann Data_i, muss er deren Typ bestimmen. In der Assemblersprache ist ein Aufruf des Datentyps nicht möglich. Ich muss die Dinge selbst im Detail studieren, um sicher zu sein.
user106313
5

C und eine Reihe anderer Sprachen, die Deklarationen erfordern, wurden in einer Ära entwickelt, in der Prozessorzeit und Speicher teuer waren. Die Entwicklung von C und Unix Hand in Hand ging seit geraumer Zeit, und dieser hatte nicht den virtuellen Speicher bis 3BSD 1979. Ohne den zusätzlichen Raum zur Arbeit erschien, neigten Compiler zu sein Single-Pass - Angelegenheiten , weil sie es nicht taten erfordern die Fähigkeit, eine Darstellung der gesamten Datei auf einmal im Speicher zu halten.

Single-Pass-Compiler haben wie wir die Unfähigkeit, in die Zukunft zu schauen. Dies bedeutet, dass die einzigen Dinge, die sie mit Sicherheit wissen können, das sind, was ihnen explizit gesagt wurde, bevor die Codezeile kompiliert wird. Es ist für jeden von uns klar, dass dies Func_i()später in der Quelldatei deklariert wird, aber der Compiler, der jeweils nur einen kleinen Teil des Codes bearbeitet, hat keine Ahnung, dass er kommt.

Im frühen C (AT & T, K & R, C89) führte die Verwendung einer Funktion foo()vor der Deklaration zu einer de facto oder impliziten Deklaration von int foo(). Ihr Beispiel funktioniert, wenn Func_i()deklariert wurde, intweil es mit dem übereinstimmt, was der Compiler in Ihrem Namen deklariert hat. Das Ändern in einen anderen Typ führt zu einem Konflikt, da er nicht mehr mit dem übereinstimmt, was der Compiler ohne explizite Deklaration ausgewählt hat. Dieses Verhalten wurde in C99 behoben, wo die Verwendung einer nicht deklarierten Funktion zu einem Fehler wurde.

Was ist mit Rückgabetypen?

Die Aufrufkonvention für Objektcode erfordert in den meisten Umgebungen, dass nur die Adresse der aufgerufenen Funktion bekannt ist, was für Compiler und Linker relativ einfach zu handhaben ist. Die Ausführung springt zum Start der Funktion und kehrt zurück, wenn sie zurückkehrt. Alles andere, insbesondere die Anordnung der Übergabe von Argumenten und ein Rückgabewert, wird vollständig vom Aufrufer und Angerufenen in einer Anordnung bestimmt, die als Aufrufkonvention bezeichnet wird . Solange beide dieselben Konventionen verwenden, kann ein Programm Funktionen in anderen Objektdateien aufrufen, unabhängig davon, ob sie in einer Sprache kompiliert wurden, die diese Konventionen verwendet. (Beim wissenschaftlichen Rechnen stößt man auf viele C-Aufrufe von FORTRAN und umgekehrt, und die Fähigkeit, dies zu tun, beruht auf einer Aufrufkonvention.)

Ein weiteres Merkmal von Anfang C war, dass Prototypen, wie wir sie jetzt kennen, nicht existierten. Sie können den Rückgabetyp einer Funktion deklarieren (z. B. int foo()), aber nicht ihre Argumente ( z. B. int foo(int bar)war keine Option). Dies lag daran, dass das Programm, wie oben ausgeführt, immer an einer Aufrufkonvention festhielt, die durch die Argumente bestimmt werden konnte. Wenn Sie eine Funktion mit dem falschen Argumenttyp aufgerufen haben, war dies eine Garbage-In-, Garbage-Out-Situation.

Da der Objektcode den Begriff "return" hat, jedoch keinen Rückgabetyp, muss der Compiler den Rückgabetyp kennen, um den zurückgegebenen Wert verarbeiten zu können. Wenn Sie Maschinenbefehle ausführen, handelt es sich nur um Bits, und es ist dem Prozessor egal, ob der Speicher, in dem Sie einen Vergleich durchführen möchten, doubletatsächlich einen enthält int. Es macht einfach, was Sie verlangen, und wenn Sie es brechen, besitzen Sie beide Teile.

Betrachten Sie diese Codebits:

double foo();         double foo();
double x;             int x;
x = foo();            x = foo();

Der Code auf der linken Seite kompiliert sich zu einem Aufruf und foo()kopiert das über die Call / Return-Konvention bereitgestellte Ergebnis an den xSpeicherort. Das ist der einfache Fall.

Der Code auf der rechten Seite zeigt eine Typkonvertierung und ist der Grund, warum Compiler den Rückgabetyp einer Funktion kennen müssen. Gleitkommazahlen können nicht in den Speicher geschrieben werden, wo anderer Code eine erwartet, intda keine magische Konvertierung stattfindet. Wenn das Endergebnis eine Ganzzahl sein muss, müssen Anweisungen vorhanden sein, die den Prozessor anleiten, die Konvertierung vor dem Speichern durchzuführen. Ohne den Rückgabetyp von foo()im Voraus zu kennen, hätte der Compiler keine Ahnung, dass Konvertierungscode erforderlich ist.

Multi-Pass-Compiler ermöglichen alle Arten von Dingen, darunter die Möglichkeit, Variablen, Funktionen und Methoden nach ihrer ersten Verwendung zu deklarieren. Dies bedeutet, dass der Compiler, wenn er mit dem Kompilieren des Codes beschäftigt ist, bereits die Zukunft gesehen hat und weiß, was zu tun ist. Java zum Beispiel schreibt Mehrfachdurchläufe vor, da seine Syntax eine Deklaration nach der Verwendung ermöglicht.

Blrfl
quelle
Vielen Dank für Ihre Antwort (+1). Ich kenne die Details der inneren Arbeitsweise von Compiler, Assembler, Prozessor usw. nicht. Der Grundgedanke meiner Frage ist, dass ich den Rückgabewert der Funktion nach der Verwendung endlich im Quellcode wiedergebe (schreibe) Von dieser Funktion aus erlaubt die Sprache dem Computer , diesen Wert zu finden, ohne einen Fehler zu verursachen. Warum kann der Computer den Typ nicht auf ähnliche Weise finden ? Warum kann der Typ von Data_i nicht gefunden werden, da Func_i()der Rückgabewert von gefunden wurde?
user106313
Ich bin immer noch nicht zufrieden. double foo(); int x; x = foo();gibt einfach den fehler aus. Ich weiß, dass wir das nicht können. Meine Frage ist, dass der Prozessor im Funktionsaufruf nur den Rückgabewert findet; warum kann es auch den return-Typ nicht finden?
user106313
1
@ user31782: Sollte es nicht. Es gibt einen Prototyp für foo(), damit der Compiler weiß, was er damit machen soll.
Blrfl
2
@ user31782: Prozessoren kennen keinen Rückgabetyp.
Blrfl
1
@ user31782 Für die Frage zur Kompilierungszeit: Es ist möglich, eine Sprache zu schreiben, in der alle diese Typanalysen zur Kompilierungszeit durchgeführt werden können. C ist keine solche Sprache. Der C-Compiler kann das nicht, weil er nicht dafür ausgelegt ist. Könnte es anders gestaltet worden sein? Sicher, aber es würde viel mehr Rechenleistung und Speicher erfordern, um dies zu tun. Unterm Strich war es nicht. Es wurde in einer Weise entworfen, die die Computer des Tages am besten handhaben konnten.
Mr.Mindor