Betrachten wir diesen C-Code:
#include <stdio.h>
main()
{
int x=5;
printf("x is ");
printf("%d",5);
}
Als wir das geschrieben haben, haben int x=5;
wir dem Computer gesagt, dass x
es sich um eine Ganzzahl handelt. Der Computer muss sich daran erinnern, dass x
es sich um eine Ganzzahl handelt. Wenn wir jedoch den Wert von x
in ausgeben, müssen printf()
wir dem Computer erneut mitteilen, dass x
es sich um eine Ganzzahl handelt. Warum ist das so?
Warum vergisst der Computer, dass x
es sich um eine Ganzzahl handelt?
c
io
type-safety
user106313
quelle
quelle
printf(char*, ...)
und es wird nur ein Zeiger auf eine Sammlung von Datenprintf("x is %x in hex, and %d in decimal and %o as octal",x,x,x);
?Antworten:
Hier spielen zwei Probleme eine Rolle:
Problem Nr. 1: C ist eine statisch typisierte Sprache. Alle Typinformationen werden zur Kompilierungszeit ermittelt. Mit keinem Objekt im Speicher werden Typinformationen gespeichert, sodass Typ und Größe zur Laufzeit 1 bestimmt werden können . Wenn Sie den Speicher an einer bestimmten Adresse untersuchen, während das Programm ausgeführt wird, sehen Sie nur einen Schlamm von Bytes. Es gibt nichts zu sagen, ob diese bestimmte Adresse tatsächlich ein Objekt enthält, welchen Typ oder welche Größe dieses Objekt hat oder wie diese Bytes zu interpretieren sind (als Ganzzahl- oder Gleitkommatyp oder als Zeichenfolge in einer Zeichenfolge usw.). ). Alle diese Informationen werden beim Kompilieren des Codes in den Maschinencode eingebrannt, basierend auf den im Quellcode angegebenen Typinformationen. Zum Beispiel die Funktionsdefinition
Weist den Compiler an, den entsprechenden Maschinencode zu generieren, der
x
als Ganzzahl,y
als Gleitkommawert undz
als Zeiger auf behandelt werden sollchar
. Beachten Sie, dass Fehlanpassungen in der Anzahl oder Art der Argumente zwischen einem Funktionsaufruf und einer Funktionsdefinition nur erkannt werden, wenn der Code kompiliert wird 2 ; Nur während der Kompilierungsphase werden einem Objekt Typinformationen zugeordnet.Problem Nr. 2:
printf
ist eine variable Funktion; Es werden ein fester Parameter vom Typconst char * restrict
(die Formatzeichenfolge) sowie null oder mehr zusätzliche Parameter verwendet, deren Anzahl und Typ zum Zeitpunkt der Kompilierung nicht bekannt sind :Die
printf
Funktion kann die Anzahl und Art der zusätzlichen Argumente nicht anhand der übergebenen Argumente selbst ermitteln. Es muss sich auf die Formatzeichenfolge verlassen, um zu bestimmen, wie der Byte-Schlamm auf dem Stapel (oder in den Registern) interpretiert werden soll. Noch besser ist , weil es eine variadische Funktion ist, Argumente mit bestimmten Typen werden gefördert , um eine begrenzte Anzahl von Standardtypen (zBshort
wird gefördertint
,float
wird gefördertdouble
, etc.).Auch hier sind den zusätzlichen Argumenten selbst keine Informationen zugeordnet, die
printf
Hinweise auf deren Interpretation oder Formatierung geben könnten. Daher sind die Konvertierungsspezifizierer in der Formatzeichenfolge erforderlich.Beachten Sie, dass
printf
Konvertierungsspezifizierer nicht nur die Anzahl und den Typ zusätzlicher Argumente angeben, sondern auch angeben,printf
wie die Ausgabe formatiert werden soll (Feldbreiten, Genauigkeit, Auffüllung, Ausrichtung, Basis (Dezimal, Oktal oder Hex für Ganzzahltypen) usw.).Bearbeiten
Um ausführliche Diskussionen in den Kommentaren zu vermeiden (und weil die Chat-Seite für mein Arbeitssystem gesperrt ist - ja, ich bin ein böser Junge), werde ich hier die letzten beiden Fragen beantworten.
Während der Übersetzung hält der Compiler eine Tabelle (oft genannt Symboltabelle ) , dass speichert Informationen über den Namen eines Objekts, Typ, Lagerdauer, Umfang, etc. Sie erklärt
b
undc
wiefloat
, so jederzeit der Compiler sieht einen Ausdruck mitb
oderc
darin, Es wird der Maschinencode generiert, um einen Gleitkommawert zu verarbeiten.Ich habe Ihren Code oben genommen und ein vollständiges Programm darum gewickelt:
Ich habe die Optionen
-g
und-Wa,-aldh
mit gcc verwendet, um eine Liste des generierten Maschinencodes zu erstellen, der mit dem C-Quellcode 3 verschachtelt ist :So lesen Sie die Baugruppenliste:
Eine Sache, die hier zu beachten ist. Im generierten Assemblycode gibt es keine Symbole für
b
oderc
; Sie sind nur in der Quellcodeliste vorhanden. Bei dermain
Ausführung zur Laufzeit wird durch Anpassen des Stapelzeigers Platz fürb
undc
(zusammen mit einigen anderen Dingen) vom Stapel zugewiesen:Der Code bezieht sich auf diese Objekte durch ihren Versatz vom Rahmenzeiger 4 ,
b
wobei -8 Bytes von der im Rahmenzeiger gespeicherten Adresse undc
-4 Bytes von dieser wie folgt sind:Da Sie deklariert
b
undc
als Gleitkommazahlen angegeben haben, hat der Compiler Maschinencode generiert, um Gleitkommawerte spezifisch zu verarbeiten. diemovsd
,mulsd
,cvtss2sd
Anweisungen sind spezifisch für Gleitkommaoperationen, und die Register%xmm0
und%xmm1
werden verwendet , mit doppelter Genauigkeit zum Speichern von Gleitkommazahlen.Wenn ich den Quellcode ändern , so dass
b
undc
ganze Zahlen anstelle von Schwimmern, erzeugt der Compiler verschiedenen Maschinencode:Kompilieren mit
gcc -o c2 -g -std=c99 -pedantic -Wall -Werror -Wa,-aldh=c2.lst c2.c
ergibt:Hier ist dieselbe Operation, jedoch mit
b
undc
als Ganzzahlen deklariert:Dies habe ich vorhin gemeint, als ich sagte, dass Typinformationen in den Maschinencode "eingebrannt" wurden. Wenn das Programm ausgeführt wird, wird der Typ nicht überprüft
b
oderc
ermittelt. Es weiß bereits, welcher Typ auf dem generierten Maschinencode basieren soll .Es funktioniert nicht, weil Sie den Compiler anlügen. Sie sagen, dass dies a
b
istfloat
, sodass Maschinencode für die Verarbeitung von Gleitkommawerten generiert wird. Wenn Sie es initialisieren, wird das der Konstante entsprechende Bitmuster'H'
als Gleitkommawert und nicht als Zeichenwert interpretiert.Sie belügen den Compiler erneut, wenn Sie für das Argument den
%c
Konvertierungsspezifizierer verwenden, der einen Wert vom Typ erwartet . Aus diesem Grund wird der Inhalt von nicht richtig interpretiert , und Sie erhalten die Müllausgabe 5 . Auch hier kann die Anzahl oder Art der zusätzlichen Argumente nicht anhand der Argumente selbst ermittelt werden. Alles, was es sieht, ist eine Adresse auf dem Stapel (oder eine Reihe von Registern). Die Formatzeichenfolge muss angeben, welche zusätzlichen Argumente übergeben wurden und welche Typen sie haben.char
b
printf
b
printf
1. Die einzige Ausnahme sind Arrays mit variabler Länge. Da ihre Größe erst zur Laufzeit festgelegt wird, gibt es keine Möglichkeit,
sizeof
eine VLA zur Kompilierungszeit auszuwerten .2. Ab C89 jedenfalls. Zuvor konnte der Compiler nur Fehlanpassungen im Funktionsrückgabetyp abfangen. Es konnten keine Fehlanpassungen in den Funktionsparameterlisten festgestellt werden.
3. Dieser Code wird auf einem 64-Bit-SuSE Linux Enterprise 10-System mit gcc 4.1.2 generiert. Wenn Sie sich in einer anderen Implementierung befinden (Compiler / Betriebssystem / Chip-Architektur), sind die genauen Maschinenanweisungen unterschiedlich, aber der allgemeine Punkt bleibt bestehen. Der Compiler generiert verschiedene Anweisungen zum Behandeln von Floats vs. Ints vs. Strings usw.
4. Wenn Sie eine Funktion in einem laufenden Programm aufrufen, einen Stack-Framewird erstellt, um die Funktionsargumente, lokalen Variablen und die Adresse der Anweisung nach dem Funktionsaufruf zu speichern. Ein spezielles Register, das als Rahmenzeiger bezeichnet wird, wird verwendet, um den aktuellen Rahmen zu verfolgen.
5. Nehmen Sie beispielsweise ein Big-Endian-System an, bei dem das höherwertige Byte das adressierte Byte ist. Das Bitmuster für
H
wird gespeichert werdenb
als0x00000048
. Da der%c
Konvertierungsspezifizierer jedoch angibt, dass das Argument a sein sollchar
, wird nur das erste Byte gelesen, und esprintf
wird versucht, das der Codierung entsprechende Zeichen zu schreiben0x00
.quelle
putchar
Funktion besagt, dass 1 Argument vom Typ erwartet wirdint
. Wenn der Compiler den Maschinencode generiert, geht dieser Maschinencode davon aus, dass er immer dieses einzelne ganzzahlige Argument empfängt. Es ist nicht erforderlich, den Typ zur Laufzeit anzugeben.printf
formatiert die gesamte Ausgabe als Text (ASCII oder anderweitig); Der Konvertierungsspezifizierer gibt an, wie die Ausgabe formatiert werden soll.printf( "%d\n", 65 );
schreibt die Zeichenfolge'6'
und'5'
in die Standardausgabe, da der%d
Konvertierungsspezifizierer anweist, das entsprechende Argument als Dezimalzahl zu formatieren.printf( "%c\n", 65 );
schreibt das Zeichen'A'
in die Standardausgabe, da es%c
anweistprintf
, das Argument als Zeichen aus dem Ausführungszeichensatz zu formatieren.<<
und>>
E / A ableiten ), aber es würde der Sprache eine gewisse Komplexität hinzufügen. Trägheit ist manchmal schwer zu überwinden.Denn in dem Moment
printf
, in dem der Compiler aufgerufen wird und seine Arbeit erledigt, ist er nicht mehr da, um ihm mitzuteilen, was zu tun ist.Die Funktion erhält keine Informationen außer den Parametern, und die vararg-Parameter haben keinen Typ. Sie
printf
hätte also keine Ahnung, wie sie gedruckt werden sollen, wenn sie keine expliziten Anweisungen über die Formatzeichenfolge erhalten. Der Compiler könnte (normalerweise) ableiten, welcher Typ jedes Argument ist, aber Sie müssten immer noch eine Formatzeichenfolge schreiben, um anzugeben, wo jedes Argument relativ zum konstanten Text gedruckt werden soll. Vergleiche"$%d"
und"%d$"
; Sie machen verschiedene Dinge und der Compiler kann nicht erraten, was Sie wollen. Da Sie ohnehin manuell ein Format - String zu komponieren haben Argument angeben Positionen , dann ist es offensichtlich , dass die Wahl die Aufgabe , unter Angabe der Argumentation abzuladen Typen als auch für den Anwender.Die Alternative wäre, dass der Compiler die Formatzeichenfolge nach Positionen durchsucht, dann die Typen ableitet, die Formatzeichenfolge neu schreibt, um die Typinformationen hinzuzufügen, und stattdessen die geänderte Zeichenfolge in Ihre Binärdatei kompiliert. Dies würde jedoch nur für Zeichenfolgen im Literalformat funktionieren . C erlaubt auch dynamisch zugewiesene Formatzeichenfolgen, und es würde immer Fälle geben, in denen der Compiler nicht genau rekonstruieren kann, wie die Formatzeichenfolge zur Laufzeit aussehen wird. (Manchmal möchten Sie auch etwas als einen anderen, verwandten Typ drucken und so eine Verengung effektiv durchführen. Dies kann auch kein Compiler vorhersagen.)
quelle
printf()
wird, ist ein Zeiger auf die Formatzeichenfolge und ein Zeiger auf den Puffer, in dem sich die Argumente befinden. Nicht einmal die Länge dieses Puffers wird übergeben! Dies ist einer der Gründe, warum C so viel schneller sein kann als andere Sprachen. Was Sie vorschlagen, sind Größenordnungen komplexer.cout
in C ++ verwendeten Vorlagenmechanismen .printf()
ist eine so genannte variadische Funktion , die eine variable Anzahl von Argumenten akzeptiert.Variadische Funktionen in C verwenden einen speziellen Prototyp, um dem Compiler mitzuteilen, dass die Liste der Argumente eine unbekannte Länge hat:
Standard C bietet eine Reihe von Funktionen
stdarg.h
, mit denen die Argumente einzeln abgerufen und in einen bestimmten Typ umgewandelt werden können. Dies bedeutet, dass verschiedene Funktionen den Typ jedes Arguments selbst bestimmen müssen.printf()
trifft diese Entscheidung basierend auf dem Format der Zeichenfolge.Dies ist eine grobe Vereinfachung der
printf()
tatsächlichen Funktionsweise, aber der Prozess läuft folgendermaßen ab :Der gleiche Vorgang findet für alle Typen statt
printf()
, die konvertiert werden können. Sie können ein Beispiel dafür im Quellcode für die OpenBSD-Implementierung von sehenvfprintf()
, der Funktion, die zugrunde liegtprintf()
.Einige C-Compiler sind intelligent genug, um Aufrufe zu erkennen
printf()
, die Formatzeichenfolge auszuwerten, wenn sie eine Konstante ist, und zu überprüfen, ob die Typen der übrigen Argumente mit den angegebenen Konvertierungen kompatibel sind. Dieses Verhalten ist nicht erforderlich, weshalb der Standard weiterhin die Angabe des Typs als Teil der Formatzeichenfolge erfordert. Bevor diese Art von Überprüfungen durchgeführt wurde, führten Fehlanpassungen zwischen der Formatzeichenfolge und der Argumentliste einfach zu einer falschen Ausgabe.In C ++
<<
handelt es sich um einen Operator, dercout
beispielsweisecout << foo << bar
einen Infix-Ausdruck verwendet, der zur Kompilierungszeit auf Richtigkeit überprüft und in Code umgewandelt werden kann, der die rechten Ausdrücke in etwas umwandelt, mit dem ercout
umgehen kann.quelle
Die Designer von C wollten den Compiler so einfach wie möglich gestalten. Es wäre zwar möglich gewesen, E / A in ähnlicher Weise wie in anderen Sprachen zu handhaben, und es wäre erforderlich, dass der Compiler die E / A-Routine automatisch mit Informationen über die Arten der übergebenen Parameter versorgt, und dies könnte in vielen Fällen der Fall sein Dies ermöglicht effizienteren Code als mit
printf
(*) möglich, da die Definition von Dingen auf diese Weise den Compiler komplizierter gemacht hätte.In den frühesten Tagen von C wusste Code, der eine Funktion aufrief, weder, noch kümmerte es ihn, welche Argumente er erwartete. Jedes Argument würde je nach Typ eine bestimmte Anzahl von Wörtern auf den Stapel verschieben, und die Funktionen würden erwarten, dass unterschiedliche Parameter im obersten, zweitletzten usw. Stapelschlitz unterhalb der Rücksprungadresse gefunden werden. Wenn eine
printf
Methode herausfinden konnte, wo sich ihre Argumente auf dem Stapel befinden, konnte der Compiler sie nicht anders behandeln als jede andere Methode.In der Praxis wird das von C vorgesehene Muster der Parameterübergabe nur noch sehr selten verwendet, außer wenn verschiedene Funktionen wie aufgerufen werden
printf
und wennprintf
spezielle Konventionen für die Parameterübergabe verwendet wurden [z. B. wenn der erste Parameter vom Compiler generiert wirdconst char*
und automatisch generiert wird Informationen über die zu übergebenden Typen], hätten Compiler besseren Code dafür generieren können (unter anderem ohne Ganzzahl- und Gleitkomma-Heraufstufungen).] Leider sehe ich keine Wahrscheinlichkeit, dass ein Compiler Funktionen hinzufügt Compiler melden Variablentypen an aufgerufenen Code.Ich finde es merkwürdig, dass Nullzeiger aufgrund ihrer Nützlichkeit als "Milliarden-Dollar-Fehler" angesehen werden und dass sie im Allgemeinen nur in Sprachen, die keine Nullzeigerarithmetik und Zugriffe einfangen, ein sehr schlechtes Verhalten verursachen. Ich würde den Schaden, der durch
printf
und nullterminierte Zeichenfolgen verursacht wird, als viel schlimmer betrachten.quelle
Stellen Sie sich vor, Sie übergeben Variablen an eine andere von Ihnen definierte Funktion. Normalerweise teilen Sie der anderen Funktion mit, welche Art von Daten erwartet / empfangen werden soll. Genauso mit
printf()
. Es ist bereits in derstdio.h
Bibliothek definiert und erfordert, dass Sie angeben, welche Daten es empfängt, damit es im richtigen Format ausgegeben werden kann (wie in Ihrem Fallint
).quelle