Wenn ein C-Programm ausgeführt wird, werden die Daten auf dem Heap oder dem Stack gespeichert. Die Werte werden in RAM-Adressen gespeichert. Aber was ist mit den Typindikatoren (z. B. int
oder char
)? Werden sie auch gespeichert?
Betrachten Sie den folgenden Code:
char a = 'A';
int x = 4;
Ich habe gelesen, dass A und 4 hier in RAM-Adressen gespeichert sind. Aber was ist mit a
und x
? Am verwirrendsten ist, woher die Ausführung weiß, dass a
es sich um ein Zeichen und x
ein Int handelt. Ich meine, ist das int
und char
irgendwo im RAM erwähnt?
Angenommen, ein Wert wird irgendwo im RAM als 10011001 gespeichert. Wenn ich das Programm bin, das den Code ausführt, woher weiß ich dann, ob es sich bei dieser 10011001 um eine char
oder eine handelt int
?
Was ich nicht verstehe, ist, wie der Computer weiß, wenn er den Wert einer Variablen von einer Adresse wie 10001 liest, ob es sich um eine int
oder handelt char
. Stellen Sie sich vor, ich klicke auf ein Programm namens anyprog.exe
. Der Code wird sofort ausgeführt. Enthält diese ausführbare Datei Informationen darüber, ob die gespeicherten Variablen vom Typ sind int
oder char
?
x
das ein Zeichen ist, aber es ist der Zeichen-Druckcode, der ausgeführt wird, weil der Compiler dies ausgewählt hat.Antworten:
Um die Frage zu beantworten, die Sie in mehreren Kommentaren gepostet haben (die Sie meines Erachtens in Ihrem Beitrag bearbeiten sollten):
Fügen wir also Code hinzu. Angenommen, Sie schreiben:
Und nehmen wir an, dass es im RAM gespeichert wird:
Der erste Teil ist die Adresse, der zweite Teil ist der Wert. Wenn Ihr Programm (das als Maschinencode ausgeführt wird) ausgeführt wird,
0x00010004
wird nur der Wert angezeigt0x000000004
. Der Typ dieser Daten ist nicht bekannt, und es ist nicht bekannt, wie sie verwendet werden sollen.Wie findet Ihr Programm das Richtige heraus? Betrachten Sie diesen Code:
Wir haben hier ein Lesen und ein Schreiben. Wenn Ihr Programm
x
aus dem Speicher liest , wird es0x00000004
dort gefunden. Und Ihr Programm kann es ergänzen0x00000005
. Und der Grund, warum Ihr Programm "weiß", dass dies eine gültige Operation ist, liegt darin, dass der Compiler durch Typensicherheit sicherstellt, dass die Operation gültig ist. Ihr Compiler hat bereits überprüft, ob Sie4
und5
zusammen hinzufügen können . Wenn Ihr Binärcode (die Exe) ausgeführt wird, muss er diese Überprüfung nicht durchführen. Es führt jeden Schritt einfach blind aus, vorausgesetzt, alles ist in Ordnung (schlechte Dinge passieren, wenn sie tatsächlich sind, nicht in Ordnung).So kann man es sich auch vorstellen. Ich gebe Ihnen diese Informationen:
Gleiches Format wie zuvor - Adresse links, Wert rechts. Welcher Typ ist der Wert? Zu diesem Zeitpunkt kennen Sie genau so viele Informationen zu diesem Wert wie Ihr Computer, wenn er Code ausführt. Wenn Sie 12743 zu diesem Wert hinzufügen sollten, könnten Sie es tun. Sie haben keine Ahnung, welche Auswirkungen diese Operation auf das gesamte System haben wird, aber das Hinzufügen von zwei Zahlen ist etwas, in dem Sie wirklich gut sind, also können Sie es tun. Macht das den Wert an
int
? Nicht unbedingt - Sie sehen nur zwei 32-Bit-Werte und den Additionsoperator.Vielleicht liegt ein Teil der Verwirrung dann darin, die Daten wieder herauszuholen. Wenn wir haben:
Woher weiß der Computer, dass er
a
in der Konsole angezeigt wird? Nun, dazu gibt es viele Schritte. Das erste ist, zuA
s Speicherort im Speicher zu gehen und ihn zu lesen:Der
a
hexadezimale Wert für in ASCII ist 0x61, so dass der obige Wert möglicherweise im Speicher angezeigt wird. Unser Maschinencode kennt jetzt also den ganzzahligen Wert. Woher weiß es, dass der ganzzahlige Wert in ein Zeichen umgewandelt werden muss, um ihn anzuzeigen? Einfach ausgedrückt, der Compiler hat dafür gesorgt, dass alle erforderlichen Schritte für diesen Übergang ausgeführt wurden. Ihr Computer selbst (oder das Programm / die Exe-Datei) hat jedoch keine Ahnung, um welche Art von Daten es sich handelt. Das 32-Bit - Wert könnte alles sein -int
, diechar
Hälfte eindouble
, einen Zeiger, einen Teil einer Anordnung, die Teil einesstring
, einen Teil eines Befehls usw.Hier ist eine kurze Interaktion, die Ihr Programm (exe) möglicherweise mit dem Computer / Betriebssystem hat.
Programm: Ich möchte anfangen. Ich brauche 20 MB Speicher.
Betriebssystem: Findet 20 MB freien Speicher, der nicht verwendet wird, und übergibt sie
(Die wichtige Anmerkung ist , dass diese zurückkehren konnten alle 20 kostenlosen MB Speicher, sie haben nicht einmal zusammenhängend sein müssen. Zu diesem Zeitpunkt kann das Programm nun im Speicher arbeiten hat , ohne auf die OS im Gespräch)
Programm: Ich gehe davon aus, dass der erste Punkt im Speicher eine 32-Bit-Ganzzahlvariable ist
x
.(Der Compiler stellt sicher, dass Zugriffe auf andere Variablen diese Stelle im Speicher niemals berühren. Es gibt nichts auf dem System, was besagt, dass das erste Byte eine Variable ist
x
, oder dass diese Variablex
eine Ganzzahl ist. Eine Analogie: Sie haben eine Tasche. Sie sagen dies den Leuten Sie werden nur gelbe Kugeln in diese Tasche legen. Wenn jemand später etwas aus der Tasche zieht, dann wäre es schockierend, wenn er etwas Blaues oder einen Würfel herausziehen würde - etwas ist schrecklich schief gelaufen. Gleiches gilt für Computer: Ihre Das Programm geht jetzt davon aus, dass der erste Speicherplatz die Variable x und eine Ganzzahl ist. Wenn jemals etwas anderes über dieses Byte des Speichers geschrieben wurde oder angenommen wird, dass es sich um etwas anderes handelt, ist etwas Schreckliches passiert nicht passieren)Programm: Ich werde jetzt
2
auf die ersten vier Bytes schreiben , bei denen ich davon ausgehe, dassx
es sich um handelt.Programm: Ich möchte 5 hinzufügen
x
.Liest den Wert von X in ein temporäres Register
Fügt dem temporären Register 5 hinzu
Speichert den Wert des temporären Registers zurück in das erste Byte, das immer noch angenommen wird
x
.Programm: Ich gehe davon aus, dass das nächste verfügbare Byte die Variable char ist
y
.Programm: Ich werde
a
in Variable schreibeny
.Eine Bibliothek wird verwendet, um den Bytewert für zu finden
a
Das Byte wird an die Adresse geschrieben, von der das Programm ausgeht
y
.Programm: Ich möchte den Inhalt von anzeigen
y
Liest den Wert im zweiten Speicherpunkt
Verwendet eine Bibliothek, um aus dem Byte ein Zeichen zu konvertieren
Verwendet Grafikbibliotheken zum Ändern des Konsolenbildschirms (Einstellen der Pixel von Schwarz auf Weiß, Scrollen um eine Zeile usw.)
(Und es geht weiter von hier)
Woran werden Sie wahrscheinlich hängen bleiben
x
? Was passiert, wenn der erste Punkt in der Erinnerung nicht mehr vorhanden ist ? oder ist die zweite nicht mehry
? Was passiert, wenn jemandx
alschar
odery
als Zeiger liest ? Kurz gesagt, schlimme Dinge passieren. Einige dieser Dinge haben ein genau definiertes Verhalten, andere undefiniertes. Undefiniertes Verhalten ist genau das - alles kann passieren, von nichts bis zum Absturz des Programms oder des Betriebssystems. Sogar genau definiertes Verhalten kann böswillig sein. Wenn ichx
zu einem Zeiger auf mein Programm wechseln und Ihr Programm dazu bringen kann, ihn als Zeiger zu verwenden, kann ich Ihr Programm dazu bringen, mein Programm auszuführen - genau das tun Hacker. Der Compiler ist da, um sicherzustellen, dass wir nichtint x
alsstring
und solche Dinge. Der Maschinencode selbst kennt keine Typen und tut nur das, was in den Anweisungen angegeben ist. Es gibt auch eine große Menge an Informationen, die zur Laufzeit entdeckt werden: Welche Bytes an Speicher darf das Programm verwenden? Beginntx
am ersten Byte oder am 12.?Aber Sie können sich vorstellen, wie schrecklich es wäre, Programme wie dieses zu schreiben (und das können Sie auch in der Assemblersprache). Sie beginnen mit der 'Deklaration' Ihrer Variablen - Sie sagen sich, dass Byte 1
x
Byte 2 isty
, und wenn Sie jede Codezeile schreiben, Register laden und speichern, müssen Sie sich (als Mensch) merken, welches istx
und welches Eines isty
, weil das System keine Ahnung hat. Und Sie (als Mensch) müssen sich merken, welche Typenx
und welchey
sind, denn auch hier hat das System keine Ahnung.quelle
Otherwise how can console or text file outputs a character instead of int
Da es eine andere Reihenfolge von Anweisungen für die Ausgabe des Inhalts eines Speicherorts als Ganzzahl oder als alphanumerische Zeichen gibt. Der Compiler kennt die Variablentypen, wählt zur Kompilierungszeit die entsprechende Anweisungsfolge aus und zeichnet sie in der EXE auf.Ich denke, Ihre Hauptfrage scheint zu lauten: "Wenn der Typ zur Kompilierungszeit gelöscht und zur Laufzeit nicht beibehalten wird, wie kann der Computer dann feststellen, ob er Code ausführen soll, der ihn als einen interpretiert,
int
oder ob er Code ausführen soll, der ihn als einen interpretiertchar
? "Und die Antwort ist ... der Computer tut es nicht. Allerdings ist der Compiler nicht wissen, und es wird einfach den richtigen Code in den binären in erster Linie gesetzt hat. Wenn die Variable wie folgt eingegeben würde
char
, würde der Compiler den Code für die Behandlung als a nichtint
in das Programm einfügen, sondern den Code für die Behandlung als achar
.Es gibt Gründe, den Typ zur Laufzeit beizubehalten:
+
Operator), weshalb der Laufzeittyp nicht benötigt wird. Der Laufzeit-Typ unterscheidet sich jedoch ohnehin von dem statischen Typ. In Java können Sie beispielsweise die statischen Typen theoretisch löschen und den Laufzeit-Typ für den Polymorphismus beibehalten. Beachten Sie auch, dass Sie, wenn Sie den Typ-Lookup-Code dezentralisieren und spezialisieren und in das Objekt (oder die Klasse) einfügen, den Laufzeit-Typ nicht unbedingt benötigen, z. B. C ++ vtables.Der einzige Grund, den Typ zur Laufzeit in C beizubehalten, ist das Debuggen. Das Debuggen wird jedoch normalerweise mit der verfügbaren Quelle durchgeführt. Anschließend können Sie den Typ einfach in der Quelldatei nachschlagen.
Typ Löschen ist ganz normal. Dies wirkt sich nicht auf die Typensicherheit aus: Die Typen werden beim Kompilieren überprüft. Wenn der Compiler überzeugt ist, dass das Programm typensicher ist, werden die Typen (aus diesem Grund) nicht mehr benötigt. Dies hat keinen Einfluss auf den statischen Polymorphismus (auch als Überladung bezeichnet): Sobald die Überladungsauflösung abgeschlossen ist und der Compiler die richtige Überladung ausgewählt hat, werden die Typen nicht mehr benötigt. Typen können auch als Leitfaden für die Optimierung dienen. Sobald der Optimierer die Optimierungen basierend auf den Typen ausgewählt hat, werden sie nicht mehr benötigt.
Das Beibehalten von Typen zur Laufzeit ist nur erforderlich, wenn Sie etwas mit den Typen zur Laufzeit tun möchten.
Haskell ist eine der strengsten, strengsten und typsichersten statisch typisierten Sprachen, und Haskell-Compiler löschen normalerweise alle Typen. (Die Ausnahme ist die Übergabe von Methodenwörterbüchern für Typklassen, glaube ich.)
quelle
char
in die kompilierte Binärdatei aus. Es gibt keinen Code für ein ausint
, es gibt keinen Code für ein ausbyte
, es gibt keinen Code für einen Zeiger aus, es gibt einfach nur den Code für ein auschar
. Es werden keine Laufzeitentscheidungen basierend auf dem Typ getroffen. Sie brauchen den Typ nicht. Es ist völlig irrelevant. Alle relevanten Entscheidungen wurden bereits zum Zeitpunkt der Kompilierung getroffen.public class JoergsAwesomeNewType {};
Sehen? Ich habe gerade einen neuen Typ erfunden! Sie müssen eine neue CPU kaufen!Der Computer "weiß" nicht, welche Adressen was sind, sondern was in die Anweisungen Ihres Programms eingearbeitet ist.
Wenn Sie ein C-Programm schreiben, das eine char-Variable schreibt und liest, erstellt der Compiler Assembly-Code, der diese Daten als char schreibt, und es gibt einen anderen Code, der eine Speicheradresse liest und als char interpretiert. Das einzige, was diese beiden Operationen zusammenhält, ist der Ort dieser Speicheradresse.
Wenn es Zeit zum Lesen gibt, heißt es in den Anweisungen nicht "Sehen, welcher Datentyp vorhanden ist", sondern "Laden Sie diesen Speicher als Float". Wenn die Adresse, von der gelesen werden soll, geändert wurde oder etwas diesen Speicher mit etwas anderem als einem Float überschrieben hat, lädt die CPU diesen Speicher ohnehin glücklich als Float, und alle möglichen seltsamen Dinge können als Ergebnis auftreten.
Schlechte Analogiezeit: Stellen Sie sich ein kompliziertes Versandlager vor, in dem das Lager aus Speicher und die Kommissionierung aus der CPU besteht. Ein Teil des Lagerprogramms legt verschiedene Artikel in das Regal. Ein anderes Programm greift nach Artikeln aus dem Lager und legt sie in Kisten. Wenn sie abgezogen werden, werden sie nicht überprüft, sie gehen einfach in den Behälter. Das gesamte Lager funktioniert so, dass alles synchron läuft und die richtigen Artikel immer zur richtigen Zeit am richtigen Ort sind. Ansonsten stürzt alles ab, genau wie in einem tatsächlichen Programm.
quelle
Das tut es nicht. Sobald C zu Maschinencode kompiliert ist, sieht die Maschine nur eine Reihe von Bits. Wie diese Bits interpretiert werden, hängt davon ab, welche Vorgänge an ihnen ausgeführt werden, und nicht von einigen zusätzlichen Metadaten.
Die Typen, die Sie in Ihren Quellcode eingeben, sind nur für den Compiler bestimmt. Es nimmt den von Ihnen angegebenen Datentyp an und versucht nach besten Kräften sicherzustellen, dass diese Daten nur auf sinnvolle Weise verwendet werden. Sobald der Compiler die Logik Ihres Quellcodes so gut wie möglich überprüft hat, konvertiert er ihn in Maschinencode und verwirft die Typdaten, da der Maschinencode keine Möglichkeit hat, dies darzustellen (zumindest auf den meisten Maschinen). .
quelle
int a = 65
und,char b = 'A'
sobald der Code kompiliert wurde.Die meisten Prozessoren bieten unterschiedliche Anweisungen für die Arbeit mit Daten unterschiedlichen Typs, sodass Typinformationen normalerweise in den generierten Maschinencode "eingebrannt" werden. Es müssen keine zusätzlichen Typmetadaten gespeichert werden.
Einige konkrete Beispiele könnten helfen. Der folgende Maschinencode wurde mit gcc 4.1.2 auf einem x86_64-System unter SuSE Linux Enterprise Server (SLES) 10 generiert.
Nehmen Sie den folgenden Quellcode an:
Hier ist das Fleisch des generierten Assembler-Codes, der der obigen Quelle entspricht (unter Verwendung von
gcc -S
), mit von mir hinzugefügten Kommentaren:Es folgen einige zusätzliche Dinge, die
ret
jedoch für die Diskussion nicht relevant sind.%eax
ist ein 32-Bit-Allzweck-Datenregister.%rsp
ist ein 64-Bit-Register, das zum Speichern des Stapelzeigers reserviert ist und die Adresse des zuletzt auf den Stapel geschobenen Objekts enthält.%rbp
ist ein 64-Bit-Register, das zum Speichern des Rahmenzeigers reserviert ist und die Adresse des aktuellen Stapelrahmens enthält . Beim Eingeben einer Funktion wird auf dem Stapel ein Stapelrahmen erstellt, der Platz für die Argumente und lokalen Variablen der Funktion reserviert. Auf Argumente und Variablen wird mit Offsets vom Frame-Zeiger zugegriffen. In diesem Fall ist der Speicher für die Variablex
12 Bytes "unterhalb" der in gespeicherten Adresse%rbp
.Im obigen Code kopieren wir den ganzzahligen Wert von
x
(1, gespeichert bei-12(%rbp)
) in das Register unter%eax
Verwendung desmovl
Befehls, der zum Kopieren von 32-Bit-Wörtern von einer Stelle zu einer anderen verwendet wird. Wir rufen dann aufaddl
, wodurch der ganzzahlige Wert vony
(gespeichert bei-8(%rbp)
) zu dem Wert addiert wird , der bereits in enthalten ist%eax
. Wir speichern dann das Ergebnis auf-4(%rbp)
, das heißtz
.Jetzt wollen wir das ändern, damit wir es mit
double
Werten anstatt mit Werten zu tunint
haben:Das
gcc -S
erneute Laufen gibt uns:Mehrere Unterschiede. Anstelle von
movl
und verwendenaddl
wirmovsd
undaddsd
(Zuweisen und Hinzufügen von Gleitkommazahlen mit doppelter Genauigkeit). Anstatt Zwischenwerte in zu speichern%eax
, verwenden wir%xmm0
.Dies ist, was ich meine, wenn ich sage, dass der Typ in den Maschinencode "eingebrannt" ist. Der Compiler generiert einfach den richtigen Maschinencode für diesen bestimmten Typ.
quelle
Historisch betrachtet C das Gedächtnis als aus einer Anzahl von Gruppen von nummerierten Slots des Typs bestehend
unsigned char
(auch "Byte" genannt, obwohl es nicht immer 8 Bits sein muss). Jeder Code, der irgendetwas verwendet, was im Speicher gespeichert ist, müsste wissen, in welchem Steckplatz oder in welchen Steckplätzen die Informationen gespeichert sind und was mit den Informationen dort zu tun ist [z. B. "interpretiere die vier Bytes ab Adresse 123: 456 als 32-Bit Gleitkommawert "oder" die unteren 16 Bits der zuletzt berechneten Größe ab Adresse 345: 678 in zwei Bytes speichern]. Der Speicher selbst würde weder wissen noch interessieren, was die in den Speicherplätzen gespeicherten Werte "bedeuteten". Wenn Der Code hat versucht, den Speicher mit einem Typ zu schreiben und ihn als einen anderen zu lesen. Die beim Schreiben gespeicherten Bitmuster werden gemäß den Regeln des zweiten Typs interpretiert, mit welchen Konsequenzen auch immer.Wenn der Code beispielsweise
0x12345678
in einem 32-Bit-Format gespeichert werdenunsigned int
soll und dann versucht wird, zwei aufeinanderfolgende 16-Bit-unsigned int
Werte von seiner Adresse und den oben angegebenen zu lesen, liestunsigned int
der Code möglicherweise die Werte , je nachdem, welche Hälfte von wo gespeichert wurde 0x1234 und 0x5678 oder 0x5678 und 0x1234.Der C99-Standard erfordert jedoch nicht mehr, dass sich der Speicher wie eine Reihe nummerierter Slots verhält, die nichts darüber wissen, was ihre Bitmuster darstellen . Ein Compiler darf sich so verhalten, als ob Speichersteckplätze die darin gespeicherten Datentypen kennen und nur Daten
unsigned char
lesen dürfen, die mit einem anderen Typ als dem Typunsigned char
oder dem gleichen Typ wie sie geschrieben wurden mit; Compiler dürfen sich weiterhin so verhalten, als ob Speicher-Slots die Fähigkeit und Neigung hätten, das Verhalten eines Programms willkürlich zu verfälschen, das versucht, auf Speicher in einer Weise zuzugreifen, die diesen Regeln widerspricht.Gegeben:
Einige Implementierungen geben möglicherweise 0x1234 und andere 0x5678 aus. Unter dem C99-Standard ist es jedoch zulässig, dass eine Implementierung "FRINK RULES!" oder irgendetwas anderes tun, unter der Annahme, dass es zulässig wäre, dass die Speicherorte
a
Hardware enthalten, die aufzeichnet, welcher Typ zum Schreiben verwendet wurde, und dass diese Hardware auf einen ungültigen Leseversuch in irgendeiner Weise reagiert, einschließlich durch Verursachen "FRINK REGELN!" ausgegeben werden.Beachten Sie, dass es keine Rolle spielt, ob eine solche Hardware tatsächlich vorhanden ist. Die Tatsache, dass diese Hardware legal vorhanden sein könnte, macht es für Compiler legal, Code zu generieren, der sich so verhält, als würde er auf einem solchen System ausgeführt. Wenn der Compiler bestimmen kann, dass ein bestimmter Speicherort als ein Typ geschrieben und als ein anderer gelesen wird, kann er vorgeben, dass er auf einem System ausgeführt wird, dessen Hardware eine solche Bestimmung vornehmen könnte, und kann mit jedem Grad an Willkür reagieren, den der Compilerautor für angebracht hält .
Der Zweck dieser Regel bestand darin, Compilern, die wussten, dass eine Gruppe von Bytes, die einen Wert eines bestimmten Typs enthielten, zu einem bestimmten Zeitpunkt einen bestimmten Wert enthielt und dass seitdem kein Wert desselben Typs geschrieben wurde, den Rückschluss auf diese Gruppe zu ziehen von Bytes würde immer noch diesen Wert halten. Ein Prozessor hatte beispielsweise eine Gruppe von Bytes in ein Register eingelesen und wollte später die gleichen Informationen wieder verwenden, während sie sich noch im Register befanden. Der Compiler konnte den Inhalt des Registers verwenden, ohne den Wert aus dem Speicher erneut lesen zu müssen. Eine sinnvolle Optimierung. In den ersten zehn Jahren der Regel würde ein Verstoß gegen diese Regel im Allgemeinen bedeuten, dass sich das Schreiben auf den gelesenen Wert auswirken kann, wenn eine Variable mit einem anderen Typ als dem Typ geschrieben wird, der zum Lesen verwendet wird. Ein solches Verhalten kann in einigen Fällen katastrophal sein, in anderen Fällen jedoch harmlos.
Um 2009 haben die Autoren einiger Compiler wie CLANG jedoch festgestellt, dass Compiler in Fällen, in denen Speicher mit einem Typ geschrieben und als anderer gelesen wird, niemals Eingaben erhalten, die sie erhalten könnten, da der Standard Compilern erlaubt, alles zu tun, was sie möchten veranlassen, dass so etwas passiert. Da der Standard angibt, dass der Compiler bei solchen ungültigen Eingaben alles tun darf, was er möchte, sollte Code weggelassen werden, der nur dann Auswirkungen hat, wenn der Standard keine Anforderungen auferlegt (und nach Ansicht einiger Compilerautoren) als irrelevant. Dies ändert das Verhalten von Aliasing-Verstößen dahingehend, dass es wie ein Speicher ist, der bei einer Leseanforderung willkürlich den letzten Wert zurückgibt, der mit demselben Typ wie eine Leseanforderung geschrieben wurde, oder jeden neueren Wert, der mit einem anderen Typ geschrieben wurde.
quelle
int x,y,z;
Ausdruckx*y > z
niemals etwas anderes als 1 oder 0 zurückgibt, oder bei dem Aliasing-Verstöße Auswirkungen haben würden mit der Ausnahme, dass der Compiler willkürlich entweder einen alten oder einen neuen Wert zurückgibt.unsigned char
Werte stammen, die zum Erstellen eines Typs "herkommen". Wenn ein Programm einen Zeiger in einen zerlegen sollunsigned char[]
, zeigen Sie seinen Hex-Inhalt kurz auf dem Bildschirm an, und löschen Sie dann den Zeigerunsigned char[]
, und akzeptieren Sie später einige Hex-Zahlen von der Tastatur, kopieren Sie sie zurück in einen Zeiger und geben Sie den Zeiger dereferenziert auf In dem Fall, in dem die eingegebene Nummer mit der angezeigten Nummer übereinstimmt, wäre das Verhalten eindeutig.In C ist es nicht. Andere Sprachen (z. B. Lisp, Python) haben dynamische Typen, C ist jedoch statisch. Das bedeutet, dass Ihr Programm wissen muss, welcher Datentyp richtig interpretiert werden soll, und zwar als Zeichen, Ganzzahl usw.
Normalerweise erledigt der Compiler das für Sie, und wenn Sie etwas falsch machen, wird beim Kompilieren ein Fehler (oder eine Warnung) angezeigt.
quelle
10001
. Es ist entweder Ihre Aufgabe oder die Aufgabe des Compilers , in Abhängigkeit vom Einzelfall mit solchen Aufgaben manuell Schritt zu halten, während Sie Maschinen- oder Assembler-Code schreiben.Man muss zwischen
compiletime
undruntime
einerseits undcode
unddata
andererseits unterscheiden.Aus maschineller Sicht ist es kein Unterschied zwischen dem, was Sie anrufen
code
oderinstructions
und dem, was Sie anrufendata
. Auf die Zahlen kommt es an. Aber einige Sequenzen - wie wir es nennen würdencode
- tun etwas, was wir für nützlich halten, andere nurcrash
die Maschine.Die Arbeit, die von der CPU ausgeführt wird, ist eine einfache 4-Stufen-Schleife:
instruction
)Dies wird Befehlszyklus genannt .
a
undx
sind Variablen, die Platzhalter für die Adressen sind, in denen das Programm den "Inhalt" der Variablen finden konnte. Wenn also die Variablea
verwendet wird, gibt es effektiv die Adresse des verwendeten Inhaltsa
.Die Ausführung weiß nichts. Nach dem, was in der Einleitung gesagt wurde, holt die CPU nur Daten und interpretiert diese Daten als Anweisungen.
Die printf-Funktion soll "wissen", welche Art von Eingabe Sie eingeben, dh der resultierende Code gibt die richtigen Anweisungen zum Umgang mit einem speziellen Speichersegment. Natürlich ist es möglich, eine Nonsense-Ausgabe zu generieren: Wenn Sie eine Adresse verwenden, in der keine Zeichenfolge zusammen mit "% s" in gespeichert ist,
printf()
wird die Nonsense-Ausgabe nur durch einen zufälligen Speicherort gestoppt, an dem eine 0 (\0
) steht.Gleiches gilt für den Einstiegspunkt eines Programms. Unter dem C64 war es möglich, Ihre Programme in (fast) jeder bekannten Adresse abzulegen. Assembly-Programme wurden mit einer Anweisung gestartet, die
sys
gefolgt von einer Adresse aufgerufen wurde:sys 49152
War ein üblicher Ort, um Ihren Assembler-Code abzulegen. Aber nichts hindert Sie daran, zB grafische Daten zu laden49152
, was zu einem Maschinenabsturz führt, nachdem Sie von diesem Punkt aus "gestartet" haben. In diesem Fall begann der Befehlszyklus mit dem Lesen von "grafischen Daten" und dem Versuch, sie als "Code" zu interpretieren (was natürlich keinen Sinn ergab). die auswirkungen waren erstaunlich;)Wie gesagt: Der "Kontext" - dh die vorherigen und nächsten Anweisungen - helfen dabei, die Daten so zu behandeln, wie wir es wollen. Aus Sicht der Maschine gibt es keinen Unterschied in Bezug auf den Speicherort.
int
undchar
ist nur ein Wortschatz, der Sinn ergibtcompiletime
; Währendruntime
(auf Baugruppenebene) gibt es keinchar
oderint
.Der Computer weiß nichts. Der Programmierer tut es. Der kompilierte Code generiert den Kontext , der erforderlich ist, um aussagekräftige Ergebnisse für den Menschen zu generieren.
Ja und Nein . Die Information, ob es sich um eine
int
oder einechar
handelt, geht verloren. Andererseits bleibt der Kontext (die Anweisungen, die angeben, wie mit Speicherstellen umgegangen wird, an denen Daten gespeichert sind) erhalten. also implizit ja, die "information" ist implizit verfügbar.quelle
Lassen Sie uns diese Diskussion nur auf die C- Sprache beschränken.
Das Programm, auf das Sie sich beziehen, ist in einer höheren Sprache wie C geschrieben. Der Computer versteht nur die Maschinensprache. Höhere Programmiersprachen geben dem Programmierer die Möglichkeit, Logik auf menschlichere Weise auszudrücken, die dann in Maschinencode übersetzt wird, den der Mikroprozessor decodieren und ausführen kann. Lassen Sie uns nun den Code besprechen, den Sie erwähnt haben:
Versuchen wir, jeden Teil zu analysieren:
Int / char-Datentypkennungen werden daher nur vom Compiler und nicht vom Mikroprozessor während der Programmausführung verwendet. Sie werden daher nicht gespeichert.
quelle
Meine Antwort hier ist etwas vereinfacht und bezieht sich nur auf C.
Nein, Typinformationen werden nicht im Programm gespeichert.
int
oderchar
sind keine Typindikatoren für die CPU; nur an den Compiler.Die vom Compiler erstellte Exe enthält Anweisungen zum Bearbeiten von
int
s, wenn die Variable als deklariert wurdeint
. Wenn die Variable als a deklariert wurdechar
, enthält die exe Anweisungen zum Manipulieren von achar
.In C:
Dieses Programm gibt seine Meldung aus, da die
char
undint
die gleichen Werte im RAM haben.Wenn Sie sich nun fragen, wie
printf
die Ausgabe65
für einint
undA
für ein gelingt,char
müssen Sie in der "Formatzeichenfolge" angeben, wieprintf
der Wert behandelt werden soll .(Bedeutet zum Beispiel,
%c
den Wert als achar
und%d
den Wert als ganze Zahl zu behandeln. In beiden Fällen jedoch den gleichen Wert.)quelle
printf
. @OP:int a = 65; printf("%c", a)
wird ausgegeben'A'
. Warum? Weil es dem Prozessor egal ist. Alles, was es sieht, sind Bits. Ihr Programm hat den Prozessor angewiesen, 65 (zufällig den Wert von'A'
in ASCII) zu speicherna
und dann ein Zeichen auszugeben, was gerne der Fall ist. Warum? Weil es egal ist.Auf der untersten Ebene gibt es in der tatsächlichen physischen CPU überhaupt keine Typen (ohne Berücksichtigung der Gleitkommaeinheiten). Nur Muster von Bits. Ein Computer manipuliert Bitmuster sehr, sehr schnell.
Das ist alles, was die CPU jemals tun kann. Es gibt keine int oder char.
Wird ausgeführt als:
Der Befehl iadd löst Hardware aus, die sich so verhält, als wären die Register 1 und 2 Ganzzahlen. Wenn sie keine ganzen Zahlen darstellen, können später alle möglichen Probleme auftreten. Das beste Ergebnis ist normalerweise ein Absturz.
Es ist Aufgabe des Compilers, die richtige Anweisung basierend auf den in source angegebenen Typen auszuwählen, aber im tatsächlichen Maschinencode, der von der CPU ausgeführt wird, gibt es nirgendwo Typen.
Bearbeiten: Beachten Sie, dass der tatsächliche Maschinencode nirgendwo 4, 5 oder eine Ganzzahl erwähnt. Es sind nur zwei Bitmuster und ein Befehl, der zwei Bitmuster annimmt, dass sie Ints sind, und sie addiert.
quelle
Kurze Antwort, der Typ ist in den vom Compiler generierten CPU-Anweisungen kodiert.
Obwohl die Informationen über den Typ oder die Größe der Informationen nicht direkt gespeichert werden, verfolgt der Compiler diese Informationen, wenn er auf Werte in diesen Variablen zugreift, diese ändert und diese speichert.
Dies ist nicht der Fall, aber wenn der Compiler den ihm bekannten Maschinencode erzeugt. Ein
int
undchar
können unterschiedlich groß sein. In einer Architektur, in der ein Zeichen die Größe eines Bytes und ein Int 4 Bytes hat, befindet sich die Variablex
nicht in der Adresse 10001, sondern auch in 10002, 10003 und 10004. Wenn der Code den Wert vonx
in ein CPU-Register laden muss , es verwendet die Anweisung zum Laden von 4 Bytes. Beim Laden eines Zeichens wird der Befehl zum Laden von 1 Byte verwendet.Wie wähle ich welche der beiden Anweisungen aus? Der Compiler entscheidet während der Kompilierung, dies geschieht nicht zur Laufzeit, nachdem die Werte im Speicher überprüft wurden.
Beachten Sie auch, dass die Register unterschiedlich groß sein können. Auf Intel x86-CPUs ist der EAX 32 Bit breit, die Hälfte davon ist AX (16 Bit) und AX ist in AH und AL (beide 8 Bit) aufgeteilt.
Wenn Sie also eine Ganzzahl laden möchten (auf x86-CPUs), verwenden Sie den MOV-Befehl für Ganzzahlen, und zum Laden eines Zeichens verwenden Sie den MOV-Befehl für Zeichen. Sie heißen beide MOV, haben aber unterschiedliche Op-Codes. Eigentlich zwei verschiedene Anweisungen. Der Typ der Variablen ist in der zu verwendenden Anweisung codiert.
Dasselbe passiert mit anderen Operationen. Abhängig von der Größe der Operanden und auch wenn sie signiert oder nicht signiert sind, gibt es viele Anweisungen zum Ausführen der Addition. Siehe https://en.wikipedia.org/wiki/ADD_(x86_instruction), in der verschiedene mögliche Ergänzungen aufgeführt sind.
Erstens wäre ein char 10011001, aber ein int wäre 00000000 00000000 00000000 10011001, weil sie unterschiedliche Größen haben (auf einem Computer mit den gleichen Größen wie oben erwähnt). Aber lassen Sie uns den Fall für
signed char
vs betrachtenunsigned char
.Was an einem Speicherort gespeichert ist, kann beliebig interpretiert werden. Es gehört zu den Aufgaben des C-Compilers, sicherzustellen, dass das, was in einer Variablen gespeichert und gelesen wird, auf konsistente Weise erfolgt. Es ist also nicht so, dass das Programm weiß, was in einem Speicherort gespeichert ist, sondern dass es vorher vereinbart, dass es dort immer die gleichen Dinge liest und schreibt. (Dinge wie Casting-Typen nicht mitgerechnet).
quelle
In typüberprüften Sprachen wie C # wird die Typüberprüfung vom Compiler durchgeführt. Der Code, den benji schrieb:
Würde mich einfach weigern zu kompilieren. Ebenso, wenn Sie versucht haben, einen String und eine Ganzzahl zu multiplizieren (ich wollte add sagen, aber der Operator '+' ist mit einer String-Verkettung überladen und es könnte einfach funktionieren).
Der Compiler würde es einfach ablehnen, Maschinencode aus diesem C # zu generieren, unabhängig davon, wie sehr Ihre Zeichenfolge darauf geküsst wurde.
quelle
Die anderen Antworten stimmen insofern, als im Wesentlichen jedes Endgerät, auf das Sie stoßen, keine Typinformationen speichert. In der Vergangenheit (und in der Gegenwart im Forschungskontext) gab es jedoch mehrere Hardware-Designs, die eine mit Tags versehene Architektur verwendeten - sie speichern sowohl die Daten als auch den Typ (und möglicherweise auch andere Informationen). Dazu gehören vor allem die Lisp-Maschinen .
Ich erinnere mich vage an eine Hardwarearchitektur für objektorientierte Programmierung, die etwas Ähnliches hatte, aber ich kann sie jetzt nicht finden.
quelle