Warum sind C-Zeichen-Literale Ints anstelle von Zeichen?

103

In C ++ , sizeof('a') == sizeof(char) == 1. Dies ist intuitiv sinnvoll, da 'a'es sich um ein Zeichenliteral handelt und sizeof(char) == 1wie im Standard definiert.

In C jedoch sizeof('a') == sizeof(int). Das heißt, es scheint, dass C-Zeichenliterale tatsächlich ganze Zahlen sind. Weiß jemand warum? Ich kann viele Erwähnungen dieser C-Eigenart finden, aber keine Erklärung dafür, warum sie existiert.

Joseph Garvin
quelle
sizeof würde nur die Größe eines Bytes zurückgeben, nicht wahr? Sind ein Char und ein Int nicht gleich groß?
Josh Smeaton
1
Dies ist wahrscheinlich vom Compiler (und der Architektur) abhängig. Möchten Sie sagen, was Sie verwenden? Der Standard (zumindest bis '89) war sehr locker.
dmckee --- Ex-Moderator Kätzchen
2
Nein. Ein Zeichen ist immer 1 Byte groß, also immer sizeof ('a') == 1 (in c ++), während ein int theoretisch sizeof 1 sein kann, aber das würde ein Byte mit mindestens 16 Bit erfordern, was sehr unwahrscheinlich ist: ) so sizeof ('a')! = sizeof (int) ist in C ++ in den meisten Implementierungen sehr wahrscheinlich
Johannes Schaub - litb
2
... während es in C. immer falsch ist
Johannes Schaub - litb
22
'a' ist ein int in C - Periode. C war zuerst da - C hat die Regeln gemacht. C ++ hat die Regeln geändert. Sie können argumentieren, dass die C ++ - Regeln sinnvoller sind, aber eine Änderung der C-Regeln würde mehr Schaden als Nutzen anrichten, so dass das C-Standardkomitee dies mit Bedacht nicht berührt hat.
Jonathan Leffler

Antworten:

36

Diskussion zum gleichen Thema

"Genauer gesagt die integralen Beförderungen. In K & R C war es praktisch (?) Unmöglich, einen Zeichenwert zu verwenden, ohne dass er zuerst zu int befördert wurde. Wenn Sie also zuerst die Zeichenkonstante int festlegen, wurde dieser Schritt eliminiert. Es gab und gibt mehrere Zeichen Konstanten wie 'abcd' oder wie viele auch immer in ein int passen. "

Malx
quelle
Konstanten mit mehreren Zeichen sind selbst zwischen Compilern auf einem einzelnen Computer nicht portierbar (obwohl GCC plattformübergreifend selbstkonsistent zu sein scheint). Siehe: stackoverflow.com/questions/328215
Jonathan Leffler
8
Ich möchte darauf hinweisen, dass a) dieses Zitat nicht zugeordnet ist; In dem Zitat heißt es lediglich: "Würden Sie dieser Meinung nicht zustimmen, die in einem früheren Thread veröffentlicht wurde, in dem das betreffende Problem erörtert wurde?" ... und b) Es ist lächerlich , weil eine charVariable kein int ist, also ist es ein Sonderfall, ein Zeichen konstant zu machen. Und es ist einfach, einen Charakterwert zu verwenden, ohne ihn zu fördern : c1 = c2;. OTOH c1 = 'x'ist eine Abwärtsumwandlung . Am wichtigsten ist sizeof(char) != sizeof('x'), was ein ernsthafter Sprachpfusch ist. Multibyte-Zeichenkonstanten: Sie sind der Grund, aber veraltet.
Jim Balter
27

Die ursprüngliche Frage lautet "Warum?"

Der Grund dafür ist, dass sich die Definition eines Literalzeichens weiterentwickelt und geändert hat, während versucht wurde, abwärtskompatibel mit vorhandenem Code zu bleiben.

In den dunklen Tagen des frühen C gab es überhaupt keine Typen. Als ich das Programmieren in C zum ersten Mal lernte, wurden Typen eingeführt, aber Funktionen hatten keine Prototypen, um dem Aufrufer die Argumenttypen mitzuteilen. Stattdessen wurde standardisiert, dass alles, was als Parameter übergeben wird, entweder die Größe eines Int hat (dies schließt alle Zeiger ein) oder ein Double.

Dies bedeutete, dass beim Schreiben der Funktion alle Parameter, die nicht doppelt waren, als Ints auf dem Stapel gespeichert wurden, unabhängig davon, wie Sie sie deklariert haben, und der Compiler Code in die Funktion einfügte, um dies für Sie zu erledigen.

Dies machte die Dinge etwas inkonsistent. Als K & R ihr berühmtes Buch schrieb, setzten sie die Regel ein, dass ein Zeichenliteral in jedem Ausdruck immer zu einem int heraufgestuft wird, nicht nur zu einem Funktionsparameter.

Als das ANSI-Komitee C zum ersten Mal standardisierte, änderten sie diese Regel so, dass ein Zeichenliteral einfach ein int war, da dies ein einfacherer Weg war, dasselbe zu erreichen.

Bei der Entwicklung von C ++ mussten alle Funktionen über vollständige Prototypen verfügen (dies ist in C immer noch nicht erforderlich, obwohl es allgemein als bewährte Methode akzeptiert wird). Aus diesem Grund wurde entschieden, dass ein Zeichenliteral in einem Zeichen gespeichert werden kann. Dies hat in C ++ den Vorteil, dass eine Funktion mit einem char-Parameter und eine Funktion mit einem int-Parameter unterschiedliche Signaturen haben. Dieser Vorteil ist bei C nicht der Fall.

Deshalb sind sie unterschiedlich. Evolution...

John Vincent
quelle
2
+1 von mir für die tatsächliche Antwort "Warum?". Aber ich bin mit der letzten Aussage nicht einverstanden - "Der Vorteil davon in C ++ ist, dass eine Funktion mit einem char-Parameter und eine Funktion mit einem int-Parameter unterschiedliche Signaturen haben" - in C ++ ist es immer noch möglich, dass 2 Funktionen Parameter von haben gleiche Größe und verschiedene Signaturen, zB void f(unsigned char)Vs void f(signed char).
Peter K
3
@PeterK John hätte es besser ausdrücken können, aber was er sagt, ist im Wesentlichen korrekt. Die Motivation für die Änderung in C ++ war, wenn Sie schreiben f('a'), dass Sie wahrscheinlich möchten, dass die Überlastungsauflösung f(char)für diesen Aufruf gewählt wird und nicht f(int). Die relativen Größen von intund charsind nicht relevant, wie Sie sagen.
zwol
21

Ich kenne die spezifischen Gründe nicht, warum ein Zeichenliteral in C vom Typ int ist. Aber in C ++ gibt es einen guten Grund, diesen Weg nicht zu gehen. Bedenken Sie:

void print(int);
void print(char);

print('a');

Sie würden erwarten, dass der Aufruf zum Drucken die zweite Version mit einem Zeichen auswählt. Ein Charakter-Literal als Int zu haben, würde dies unmöglich machen. Beachten Sie, dass in C ++ - Literalen mit mehr als einem Zeichen immer noch der Typ int vorhanden ist, obwohl ihr Wert durch die Implementierung definiert ist. Also, 'ab'hat Typ int, während 'a'hat Typ char.

Johannes Schaub - litb
quelle
Ja, laut "Design and Evolution of C ++" waren überladene Eingabe- / Ausgaberoutinen der Hauptgrund, warum C ++ die Regeln geändert hat.
Max Lybbert
5
Max, ja, ich habe geschummelt. Ich habe in der Norm im Kompatibilitätsbereich gesucht :)
Johannes Schaub - Litb
18

Mit gcc auf meinem MacBook versuche ich:

#include <stdio.h>
#define test(A) do{printf(#A":\t%i\n",sizeof(A));}while(0)
int main(void){
  test('a');
  test("a");
  test("");
  test(char);
  test(short);
  test(int);
  test(long);
  test((char)0x0);
  test((short)0x0);
  test((int)0x0);
  test((long)0x0);
  return 0;
};

was beim Ausführen gibt:

'a':    4
"a":    2
"":     1
char:   1
short:  2
int:    4
long:   4
(char)0x0:      1
(short)0x0:     2
(int)0x0:       4
(long)0x0:      4

was darauf hindeutet, dass ein Zeichen 8 Bit hat, wie Sie vermuten, aber ein Zeichenliteral ist ein int.

dmckee --- Ex-Moderator Kätzchen
quelle
7
+1 für interessant zu sein. Die Leute denken oft, dass sizeof ("a") und sizeof ("") Zeichen sind und 4 (oder 8) geben sollten. Tatsächlich sind sie zu diesem Zeitpunkt char [] (sizeof (char [11]) ergibt 11). Eine Falle für Neulinge.
Paxdiablo
3
Ein Zeichenliteral wird nicht zu einem int heraufgestuft, es ist bereits ein int. Es findet keinerlei Werbung statt, wenn das Objekt ein Operand der Größe des Operators ist. Wenn dies der Fall wäre, würde dies den Zweck von sizeof zunichte machen.
Chris Young
@ Chris Young: Ja. Prüfen. Vielen Dank.
dmckee --- Ex-Moderator Kätzchen
8

Als C geschrieben wurde, hatte die MACRO-11-Assemblersprache des PDP-11:

MOV #'A, R0      // 8-bit character encoding for 'A' into 16 bit register

Diese Art von Dingen ist in der Assemblersprache weit verbreitet - die niedrigen 8 Bits enthalten den Zeichencode, andere Bits werden auf 0 gelöscht. PDP-11 hatte sogar:

MOV #"AB, R0     // 16-bit character encoding for 'A' (low byte) and 'B'

Dies bot eine bequeme Möglichkeit, zwei Zeichen in die niedrigen und hohen Bytes des 16-Bit-Registers zu laden. Sie können diese dann an eine andere Stelle schreiben und einige Textdaten oder den Bildschirmspeicher aktualisieren.

Die Idee, Zeichen zur Registergröße zu befördern, ist also ganz normal und wünschenswert. Angenommen, Sie müssen 'A' nicht als Teil des fest codierten Opcodes in ein Register eintragen, sondern von einem Ort im Hauptspeicher, der Folgendes enthält:

address: value
20: 'X'
21: 'A'
22: 'A'
23: 'X'
24: 0
25: 'A'
26: 'A'
27: 0
28: 'A'

Wenn Sie nur ein 'A' aus diesem Hauptspeicher in ein Register einlesen möchten, welches würden Sie lesen?

  • Einige CPUs unterstützen möglicherweise nur das direkte Einlesen eines 16-Bit-Werts in ein 16-Bit-Register, was bedeuten würde, dass beim Lesen bei 20 oder 22 die Bits von 'X' gelöscht werden müssen, und dies hängt von der Endigkeit der CPU ab müsste in das Byte niedriger Ordnung verschoben werden.

  • Einige CPUs erfordern möglicherweise einen speicherausgerichteten Lesevorgang. Dies bedeutet, dass die niedrigste betroffene Adresse ein Vielfaches der Datengröße sein muss: Möglicherweise können Sie von den Adressen 24 und 25 lesen, nicht jedoch von den Adressen 27 und 28.

Ein Compiler, der Code generiert, um ein 'A' in das Register zu bringen, kann es daher vorziehen, ein wenig zusätzlichen Speicher zu verschwenden und den Wert als 0 'A' oder 'A' 0 zu codieren - abhängig von der Endianität und auch sicherzustellen, dass er richtig ausgerichtet ist ( dh nicht an einer ungeraden Speicheradresse).

Ich vermute, dass Cs diese Ebene des CPU-zentrierten Verhaltens einfach übernommen haben, indem sie an Zeichenkonstanten gedacht haben, die Registergrößen des Speichers belegen, was die allgemeine Einschätzung von C als "High-Level-Assembler" bestätigt.

(Siehe 6.3.3 auf Seite 6-25 von http://www.dmv.net/dec/pdf/macro.pdf )

Tony Delroy
quelle
5

Ich erinnere mich, wie ich K & R gelesen und ein Code-Snippet gesehen habe, das jeweils ein Zeichen las, bis es EOF traf. Da alle Zeichen gültige Zeichen für einen Datei- / Eingabestream sind, bedeutet dies, dass EOF kein Zeichenwert sein kann. Der Code hat das gelesene Zeichen in ein int eingefügt, dann auf EOF getestet und dann in ein Zeichen konvertiert, wenn dies nicht der Fall war.

Mir ist klar, dass dies Ihre Frage nicht genau beantwortet, aber es wäre sinnvoll, wenn der Rest der Zeichenliterale sizeof (int) wäre, wenn das EOF-Literal wäre.

int r;
char buffer[1024], *p; // don't use in production - buffer overflow likely
p = buffer;

while ((r = getc(file)) != EOF)
{
  *(p++) = (char) r;
}
Kyle Cronin
quelle
Ich denke nicht, dass 0 ein gültiges Zeichen ist.
Gbjbaanb
3
@gbjbaanb: Sicher ist es. Es ist das Nullzeichen. Denk darüber nach. Denken Sie, dass eine Datei keine Null-Bytes enthalten darf?
P Daddy
1
Lesen Sie Wikipedia - "Der tatsächliche Wert von EOF ist eine systemabhängige negative Zahl, üblicherweise -1, die garantiert nicht mit einem gültigen Zeichencode übereinstimmt."
Malx
2
Wie Malx sagt - EOF ist kein Char-Typ - es ist ein Int-Typ. getchar () und Freunde geben ein int zurück, das sowohl char als auch EOF ohne Konflikte enthalten kann. Dies würde wirklich keine wörtlichen Zeichen erfordern, um den Typ int zu haben.
Michael Burr
2
EOF == -1 kam lange nach Cs Zeichenkonstanten, daher ist dies keine Antwort und nicht einmal relevant.
Jim Balter
5

Ich habe keine Begründung dafür gesehen (C-Zeichen-Literale sind int-Typen), aber hier ist etwas, was Stroustrup dazu zu sagen hatte (aus Design und Evolution 11.2.1 - Feinkornauflösung):

In C, wie der Typ eines Zeichenliteral solchen 'a'ist int. Überraschenderweise verursacht die Angabe von 'a'Typ charin C ++ keine Kompatibilitätsprobleme. Mit Ausnahme des pathologischen Beispiels sizeof('a')liefert jedes Konstrukt, das sowohl in C als auch in C ++ ausgedrückt werden kann, das gleiche Ergebnis.

Zum größten Teil sollte es also keine Probleme verursachen.

Michael Burr
quelle
Interessant! Ein bisschen widerspricht dem, was andere darüber sagten, wie das C-Normungskomitee "weise" beschlossen hat, diese Eigenart nicht aus C. zu entfernen
j_random_hacker
2

Der historische Grund dafür ist, dass C und sein Vorgänger B ursprünglich auf verschiedenen Modellen von DEC PDP-Minicomputern mit verschiedenen Wortgrößen entwickelt wurden, die 8-Bit-ASCII unterstützten, aber nur Arithmetik für Register ausführen konnten. (Nicht der PDP-11, der später kam.) Frühere Versionen von C wurden intals native Wortgröße der Maschine definiert, und jeder Wert, der kleiner als a ist int, musste erweitert intwerden, um an oder von einer Funktion übergeben zu werden oder in einem bitweisen, logischen oder arithmetischen Ausdruck verwendet, weil die zugrunde liegende Hardware so funktionierte.

Das ist auch der Grund, warum die Regeln für die Ganzzahl-Heraufstufung immer noch besagen, dass jeder Datentyp, der kleiner als ein intist, heraufgestuft wird int. C-Implementierungen dürfen aus ähnlichen historischen Gründen auch die Eins-Komplement-Mathematik anstelle der Zwei-Komplement-Mathematik verwenden. Der Grund dafür, dass Oktalzeichen entkommen und Oktalkonstanten im Vergleich zu Hex erstklassige Bürger sind, liegt ebenfalls darin, dass diese frühen DEC-Minicomputer Wortgrößen hatten, die in Drei-Byte-Blöcke, aber nicht in Vier-Byte-Halbbytes unterteilt werden konnten.

Davislor
quelle
... und charwar genau 3 Oktalstellen lang
Antti Haapala
1

Dies ist das richtige Verhalten, das als "integrale Werbung" bezeichnet wird. Es kann auch in anderen Fällen passieren (hauptsächlich binäre Operatoren, wenn ich mich richtig erinnere).

BEARBEITEN: Nur um sicherzugehen, habe ich meine Kopie von Expert C Programming: Deep Secrets überprüft und bestätigt, dass ein Zeichenliteral nicht mit einem Typ int beginnt . Es ist zunächst vom Typ char , aber wenn es in einem verwendet wird , Ausdruck wird gefördert zu einem int . Folgendes wird aus dem Buch zitiert:

Zeichenliterale haben den Typ int und gelangen dorthin, indem sie die Regeln für die Heraufstufung vom Typ char befolgen. Dies wird in K & R 1 auf Seite 39 zu kurz behandelt, wo es heißt:

Jedes Zeichen in einem Ausdruck wird in ein int konvertiert. Beachten Sie, dass alle Gleitkommazahlen in einem Ausdruck in double konvertiert werden. Da ein Funktionsargument ein Ausdruck ist, finden Typkonvertierungen auch statt, wenn Argumente an Funktionen übergeben werden: in Insbesondere werden char und short int, float wird double.

PolyThinker
quelle
Wenn man den anderen Kommentaren glauben will , beginnt der Ausdruck 'a' mit dem Typ int - innerhalb einer sizeof () wird keine Typ-Promotion durchgeführt. Dass 'a' den Typ int hat, ist anscheinend nur eine Eigenart von C.
j_random_hacker
2
Ein Zeichenliteral hat den Typ int. Der ANSI / ISO 99-Standard nennt sie "ganzzahlige Zeichenkonstanten" (um sie von "breiten Zeichenkonstanten" vom Typ wchar_t zu unterscheiden) und sagt speziell: "Eine ganzzahlige Zeichenkonstante hat den Typ int."
Michael Burr
Was ich damit meinte war, dass es nicht mit dem Typ int beginnt , sondern von char in ein int konvertiert wird (Antwort bearbeitet). Natürlich betrifft dies wahrscheinlich niemanden außer Compiler-Autoren, da die Konvertierung immer erfolgt.
PolyThinker
3
Nein! Wenn Sie den ANSI / ISO 99 C-Standard lesen, werden Sie feststellen, dass in C der Ausdruck 'a' mit dem Typ int beginnt . Wenn Sie eine Funktion void f (int) und eine Variable char c, dann f (c) wird integral Förderung durchführen, aber f ( 'a') wird nicht als Typ der 'a' ist bereits int. Komisch aber wahr.
j_random_hacker
2
"Nur um sicher zu gehen" - Sie können sicherer sein, indem Sie die Anweisung lesen: "Zeichenliterale haben den Typ int". "Ich kann nur annehmen, dass dies eine der stillen Änderungen war" - Sie nehmen falsch an. Zeichenliterale in C waren immer vom Typ int.
Jim Balter
0

Ich weiß es nicht, aber ich denke, es war einfacher, es so zu implementieren, und es war nicht wirklich wichtig. Erst in C ++, als der Typ bestimmen konnte, welche Funktion aufgerufen werden würde, musste sie behoben werden.

Roland Rabien
quelle
0

Das wusste ich wirklich nicht. Bevor es Prototypen gab, wurde alles, was schmaler als ein int war, in ein int konvertiert, wenn es als Funktionsargument verwendet wurde. Das kann Teil der Erklärung sein.

Blaisorblade
quelle
1
Eine weitere schlechte "Antwort". Die automatische Konvertierung von charin intwürde es ziemlich unnötig machen , dass Zeichenkonstanten Ints sind. Relevant ist, dass die Sprache Zeichenkonstanten anders behandelt (indem sie ihnen einen anderen Typ gibt) als charVariablen, und was benötigt wird, ist eine Erklärung dieses Unterschieds.
Jim Balter
Vielen Dank für die Erklärung, die Sie unten gegeben haben. Vielleicht möchten Sie Ihre Erklärung ausführlicher in einer Antwort beschreiben, wo sie hingehört, hochgestimmt werden kann und von Besuchern leicht gesehen werden kann. Außerdem habe ich nie gesagt, dass ich hier eine gute Antwort habe. Daher hilft Ihnen Ihr Werturteil nicht weiter.
Blaisorblade
0

Dies ist nur tangential zur Sprachspezifikation, aber in der Hardware hat die CPU normalerweise nur eine Registergröße - sagen wir 32 Bit - und wann immer es tatsächlich auf einem Zeichen funktioniert (durch Addieren, Subtrahieren oder Vergleichen), gibt es eine implizite Konvertierung in int, wenn es in das Register geladen wird. Der Compiler sorgt dafür, dass die Zahl nach jeder Operation richtig maskiert und verschoben wird. Wenn Sie beispielsweise 2 zu (vorzeichenloses Zeichen) 254 hinzufügen, wird sie auf 0 anstatt auf 256 umgebrochen, aber im Silizium ist es wirklich ein Int bis Sie es wieder im Speicher speichern.

Es ist eine Art akademischer Punkt, da die Sprache ohnehin einen 8-Bit-Literaltyp hätte angeben können, aber in diesem Fall spiegelt die Sprachspezifikation genauer wider, was die CPU wirklich tut.

(x86-Wonks stellen möglicherweise fest, dass es z. B. ein natives Add-Op gibt, das die Short-Wide-Register in einem Schritt hinzufügt. Innerhalb des RISC-Kerns bedeutet dies jedoch zwei Schritte: Addieren Sie die Zahlen und erweitern Sie das Vorzeichen wie ein Add / Extsh-Paar der PowerPC)

Crashworks
quelle
1
Noch eine falsche Antwort. Hier geht es darum, warum Zeichenliterale und charVariablen unterschiedliche Typen haben. Automatische Heraufstufungen, die die Hardware widerspiegeln, sind nicht relevant - sie sind tatsächlich nicht relevant, da charVariablen automatisch heraufgestuft werden, sodass Zeichenliterale nicht vom Typ sind char. Der wahre Grund sind Multibyte-Literale, die mittlerweile veraltet sind.
Jim Balter
@ Jim Balter Multibyte-Literale sind überhaupt nicht veraltet. Es gibt Multibyte-Unicode- und UTF-Zeichen.
Crashworks
@Crashworks Wir reden über Multibyte Zeichen Literalen, nicht Multibyte String Literale. Versuchen Sie, aufmerksam zu sein.
Jim Balter
4
Chrashworks hat Charaktere geschrieben . Sie sollten geschrieben haben, dass breite Zeichenliterale (z. B. L'à ') mehr Bytes benötigen, aber nicht als Multibyte-Zeichenliterale bezeichnet werden. Weniger arrogant zu sein würde Ihnen helfen, selbst genauer zu sein.
Blaisorblade
@Blaisorblade Breite Zeichenliterale sind hier nicht relevant - sie haben nichts mit dem zu tun, was ich geschrieben habe. Ich war genau und Sie haben kein Verständnis dafür und Ihr falscher Versuch, mich zu korrigieren, ist arrogant.
Jim Balter