Implizite Regeln für die Typwerbung


72

Dieser Beitrag soll als FAQ zur impliziten Ganzzahl-Heraufstufung in C verwendet werden, insbesondere zur impliziten Heraufstufung, die durch die üblichen arithmetischen Konvertierungen und / oder die Ganzzahl-Heraufstufungen verursacht wird.

Beispiel 1)
Warum ergibt dies eine seltsame, große Ganzzahl und nicht 255?

Beispiel 2)
Warum ergibt dies "-1 ist größer als 0"?

Beispiel 3)
Warum wird der Typ im obigen Beispiel shortgeändert , um das Problem zu beheben?

(Diese Beispiele waren für einen 32- oder 64-Bit-Computer mit 16-Bit-Kurzschluss gedacht.)


3
Ich schlage vor, die Annahmen für die Beispiele zu dokumentieren, z. B. geht Beispiel 3 davon aus, dass dies shortenger ist als int(oder mit anderen Worten, es wird angenommen, dass intalle Werte von dargestellt werden können unsigned short).
Ian Abbott

Warten Sie eine Sekunde, das OP ist derselbe Typ, der die Frage beantwortet hat? Es heißt, Lundin habe gefragt, die beste Antwort sei auch
Lundins

3
@savram Ja, es ist beabsichtigt, einen FAQ-Eintrag zu schreiben. Das Teilen von Wissen auf diese Weise ist für SO in Ordnung. Wenn Sie das nächste Mal eine Frage stellen, aktivieren Sie das Kontrollkästchen "Beantworten Sie Ihre eigene Frage". Aber natürlich wird die Frage immer noch wie jede andere Frage behandelt und andere können auch Antworten posten. (Und Sie verdienen keine Wiederholung, wenn Sie Ihre eigene Antwort akzeptieren)
Lundin

1
Keine der bisherigen Antworten erwähnt die Tatsache, printf("%u\n", x - y);dass undefiniertes Verhalten verursacht wird
MM

1
Schönes Beispiel ist ~((u8)(1 << 7))die Liste.
0andriy

Antworten:


100

C wurde entwickelt, um die Ganzzahltypen der in Ausdrücken verwendeten Operanden implizit und stillschweigend zu ändern. Es gibt mehrere Fälle, in denen die Sprache den Compiler zwingt, entweder die Operanden in einen größeren Typ zu ändern oder ihre Signatur zu ändern.

Das Grundprinzip dahinter besteht darin, versehentliche Überläufe während der Arithmetik zu verhindern, aber auch zu ermöglichen, dass Operanden mit unterschiedlicher Vorzeichen in demselben Ausdruck nebeneinander existieren.

Leider verursachen die Regeln für die implizite Typwerbung viel mehr Schaden als Nutzen, bis zu dem Punkt, an dem sie möglicherweise einer der größten Fehler in der C-Sprache sind. Diese Regeln sind dem durchschnittlichen C-Programmierer oft nicht einmal bekannt und verursachen daher alle möglichen sehr subtilen Fehler.

In der Regel sehen Sie Szenarien, in denen der Programmierer sagt, "nur auf x umwandeln und es funktioniert" - aber er weiß nicht warum. Oder solche Fehler manifestieren sich als seltenes, intermittierendes Phänomen, das aus scheinbar einfachem und direktem Code hervorgeht. Die implizite Heraufstufung ist besonders problematisch bei Code-Manipulationen, da die meisten bitweisen Operatoren in C ein schlecht definiertes Verhalten aufweisen, wenn ein signierter Operand angegeben wird.


Ganzzahlige Typen und Konvertierungsrang

Die Integer - Typen in C sind char, short, int, long, long longund enum.
_Bool/ boolwird auch als ganzzahliger Typ behandelt, wenn es um Typwerbung geht.

Alle Ganzzahlen haben einen bestimmten Conversion-Rang . C11 6.3.1.1, Schwerpunkt auf den wichtigsten Teilen:

Jeder Integer-Typ hat einen Integer-Konvertierungsrang, der wie folgt definiert ist:
- Keine zwei vorzeichenbehafteten Integer-Typen dürfen denselben Rang haben, auch wenn sie dieselbe Darstellung haben.
- Der Rang eines vorzeichenbehafteten Ganzzahltyps muss größer sein als der Rang eines vorzeichenbehafteten Ganzzahltyps mit geringerer Genauigkeit.
- Der Rang von long long intist größer als der Rang von long int, der größer sein soll als der Rang von int, der größer sein soll als der Rang von short int, der größer sein soll als der Rang von signed char.
- Der Rang eines vorzeichenlosen Integer-Typs entspricht gegebenenfalls dem Rang des entsprechenden vorzeichenbehafteten Integer-Typs.

- Der Rang eines Standard-Integer-Typs muss größer sein als der Rang eines erweiterten Integer-Typs mit derselben Breite.
- Der Rang eines Zeichens entspricht dem Rang eines signierten Zeichens und eines nicht signierten Zeichens.
- Der Rang von _Bool muss niedriger sein als der Rang aller anderen Standard-Integer-Typen.
- Der Rang eines Aufzählungstyps muss dem Rang des kompatiblen Ganzzahltyps entsprechen (siehe 6.7.2.2).

Die Typen von stdint.hsortieren auch hier mit dem gleichen Rang wie der Typ, dem sie auf dem gegebenen System entsprechen. Hat zum Beispiel int32_tden gleichen Rang wie intauf einem 32-Bit-System.

Ferner gibt C11 6.3.1.1 an, welche Typen als kleine ganzzahlige Typen betrachtet werden (kein formaler Begriff):

Folgendes kann in einem Ausdruck verwendet werden, wo immer ein intoder unsigned intverwendet werden kann:

- Ein Objekt oder Ausdruck mit einem ganzzahligen Typ (außer intoder unsigned int), dessen ganzzahliger Konvertierungsrang kleiner oder gleich dem Rang von intund ist unsigned int.

Was dieser etwas kryptische Text in der Praxis bedeutet, ist , dass _Bool, charund short(und auch int8_t, uint8_tusw.) die „kleinen Integer - Typen“ sind. Diese werden auf besondere Weise behandelt und unterliegen einer impliziten Werbung, wie nachstehend erläutert.


Die ganzzahligen Aktionen

Immer wenn ein kleiner ganzzahliger Typ in einem Ausdruck verwendet wird, wird er implizit konvertiert, in intden immer signiert ist. Dies wird als Ganzzahl-Heraufstufung oder Ganzzahl-Heraufstufungsregel bezeichnet .

Formal heißt es in der Regel (C11 6.3.1.1):

Wenn a intalle Werte des ursprünglichen Typs darstellen kann (wie durch die Breite für ein Bitfeld eingeschränkt), wird der Wert in a konvertiert int. Andernfalls wird es in ein konvertiert unsigned int. Diese werden als Integer-Promotions bezeichnet .

Dies bedeutet, dass alle kleinen Ganzzahltypen, unabhängig von der Signatur, implizit in (signiert) konvertiert werden, intwenn sie in den meisten Ausdrücken verwendet werden.

Dieser Text wird oft missverstanden als: "Alle kleinen, vorzeichenbehafteten Ganzzahltypen werden in vorzeichenbehaftete int und alle kleinen vorzeichenlosen Ganzzahltypen in vorzeichenlose int konvertiert." Das ist falsch. Der vorzeichenlose Teil bedeutet hier nur, dass der Operand in konvertiert wird , wenn wir beispielsweise einen unsigned shortOperanden haben und intzufällig dieselbe Größe wie shortauf dem angegebenen System haben . Wie in passiert nichts Besonderes. Falls es sich jedoch um einen kleineren Typ als handelt , wird er immer in (signiert) konvertiert , unabhängig davon , ob der Short signiert oder nicht signiert war !unsigned shortunsigned intshortintint

Die harte Realität, die durch die ganzzahligen Beförderungen verursacht wird, bedeutet, dass fast keine Operation in C mit kleinen Typen wie charoder ausgeführt werden kann short. Operationen werden immer an intoder größeren Typen ausgeführt.

Das mag nach Unsinn klingen, aber zum Glück darf der Compiler den Code optimieren. Zum Beispiel würde ein Ausdruck, der zwei unsigned charOperanden enthält, die Operanden heraufstufen intund die Operation als ausführen int. Der Compiler kann den Ausdruck jedoch so optimieren, dass er erwartungsgemäß tatsächlich als 8-Bit-Operation ausgeführt wird. Hier kommt jedoch das Problem: Der Compiler darf die implizite Änderung der Signatur, die durch die Ganzzahl-Heraufstufung verursacht wird, nicht optimieren. Weil der Compiler nicht erkennen kann, ob der Programmierer absichtlich auf implizite Werbung angewiesen ist oder ob dies unbeabsichtigt ist.

Aus diesem Grund schlägt Beispiel 1 in der Frage fehl. Beide vorzeichenlosen Zeichenoperanden werden zum Typ heraufgestuft int, die Operation wird für den Typ ausgeführt intund das Ergebnis von x - yist vom Typ int. Das heißt, wir bekommen -1stattdessen 255was erwartet worden sein könnte. Der Compiler generiert möglicherweise Maschinencode, der den Code mit 8-Bit-Anweisungen anstelle von ausführt int, optimiert jedoch möglicherweise nicht die Änderung der Vorzeichen. Dies bedeutet, dass wir am Ende ein negatives Ergebnis haben, das wiederum zu einer seltsamen Zahl führt, wenn printf("%ues aufgerufen wird. Beispiel 1 könnte behoben werden, indem das Ergebnis der Operation auf Typ zurückgesetzt wird unsigned char.

Mit Ausnahme einiger Sonderfälle wie ++und sizeofOperatoren gelten die ganzzahligen Heraufstufungen für fast alle Operationen in C, unabhängig davon, ob unäre, binäre (oder ternäre) Operatoren verwendet werden.


Die üblichen arithmetischen Umrechnungen

Immer wenn eine binäre Operation (eine Operation mit 2 Operanden) in C ausgeführt wird, müssen beide Operanden des Operators vom gleichen Typ sein. Wenn die Operanden unterschiedlichen Typs sind, erzwingt C daher eine implizite Konvertierung eines Operanden in den Typ des anderen Operanden. Die Regeln dafür werden als die üblichen artihmetischen Konvertierungen bezeichnet (manchmal informell als "Balancing" bezeichnet). Diese sind in C11 6.3.18 angegeben:

(Stellen Sie sich diese Regel als lange, verschachtelte if-else ifAnweisung vor und sie ist möglicherweise leichter zu lesen :))

6.3.1.8 Übliche arithmetische Umrechnungen

Viele Operatoren, die Operanden vom arithmetischen Typ erwarten, verursachen Konvertierungen und ergeben auf ähnliche Weise Ergebnistypen. Der Zweck besteht darin, einen gemeinsamen reellen Typ für die Operanden und das Ergebnis zu bestimmen. Für die angegebenen Operanden wird jeder Operand ohne Änderung der Typdomäne in einen Typ konvertiert, dessen entsprechender Realtyp der gemeinsame Realtyp ist. Sofern nicht ausdrücklich anders angegeben, ist der gemeinsame Realtyp auch der entsprechende Realtyp des Ergebnisses, dessen Typdomäne die Typdomäne der Operanden ist, wenn sie gleich sind, und ansonsten komplex. Dieses Muster heißt übliche arithmetische Umrechnung bezeichnet :

  • Erstens, wenn der entsprechende reale Typ eines der Operanden ist long double , wird der andere Operand ohne Änderung der Typdomäne in einen Typ konvertiert, dessen entsprechender realer Typ ist long double.
  • Andernfalls, wenn der entsprechende reale Typ eines der Operanden ist double , der andere Operand ohne Änderung der Typdomäne in einen Typ konvertiert, dessen entsprechender realer Typ ist double.
  • Andernfalls, wenn der entsprechende reale Typ eines der Operanden ist float , der andere Operand ohne Änderung der Typdomäne in einen Typ konvertiert, dessen entsprechender realer Typ float ist.
  • Andernfalls werden die ganzzahligen Heraufstufungen für beide Operanden ausgeführt. Dann werden die folgenden Regeln auf die heraufgestuften Operanden angewendet:

    • Wenn beide Operanden denselben Typ haben, ist keine weitere Konvertierung erforderlich.
    • Andernfalls wird der Operand mit dem Typ des Konvertierungsrangs mit geringerer Ganzzahl in den Typ des Operanden mit höherem Rang konvertiert, wenn beide Operanden Ganzzahltypen mit Vorzeichen oder beide Ganzzahltypen mit Vorzeichen haben.
    • Wenn andernfalls der Operand mit vorzeichenlosem Integer-Typ einen Rang hat, der größer oder gleich dem Rang des Typs des anderen Operanden ist, wird der Operand mit vorzeichenbehaftetem Integer-Typ in den Typ des Operanden mit vorzeichenlosem Integer-Typ konvertiert.
    • Wenn andernfalls der Typ des Operanden mit vorzeichenbehaftetem Integer-Typ alle Werte des Typs des Operanden mit vorzeichenlosem Integer-Typ darstellen kann, wird der Operand mit vorzeichenlosem Integer-Typ in den Typ des Operanden mit vorzeichenbehaftetem Integer-Typ konvertiert.
    • Andernfalls werden beide Operanden in den vorzeichenlosen Integer-Typ konvertiert, der dem Typ des Operanden mit vorzeichenbehaftetem Integer-Typ entspricht.

Hierbei ist zu beachten, dass die üblichen arithmetischen Konvertierungen sowohl für Gleitkomma- als auch für Ganzzahlvariablen gelten. Bei Ganzzahlen können wir auch feststellen, dass die Ganzzahl-Promotions innerhalb der üblichen arithmetischen Konvertierungen aufgerufen werden. Und danach, wenn beide Operanden mindestens den Rang habenint , werden die Operatoren auf den gleichen Typ mit der gleichen Vorzeichen ausgeglichen.

Dies ist der Grund, warum a + bin Beispiel 2 ein seltsames Ergebnis erzielt wird. Beide Operanden sind Ganzzahlen und mindestens von Rang int, sodass die Ganzzahl-Promotions nicht gelten. Die Operanden sind nicht vom gleichen Typ - aist unsigned intund bist signed int. Daher wird der Operator bvorübergehend in Typ konvertiertunsigned int . Während dieser Konvertierung verliert es die Vorzeicheninformationen und endet als großer Wert.

Der Grund, warum das Ändern des Typs shortin Beispiel 3 das Problem behebt, liegt darin, dass shortes sich um einen kleinen Ganzzahltyp handelt. Dies bedeutet, dass beide Operanden eine Ganzzahl sind, intdie zu einem signierten Typ heraufgestuft wird. Nach der Ganzzahl-Heraufstufung haben beide Operanden den gleichen Typ ( int), es ist keine weitere Konvertierung erforderlich. Und dann kann die Operation wie erwartet an einem signierten Typ ausgeführt werden.


"Wenn ein kleiner ganzzahliger Typ in einem Ausdruck verwendet wird, wird er implizit in int konvertiert, das immer signiert ist." Könnten Sie auf den genauen Ort in der Norm verweisen, der sagt , dass es sollte geschehen? Das C11 6.3.1.1-Zitat sagt, wie es passiert (wenn es passiert), aber es sagt nicht, dass es passieren muss , z. B. warum sich x - ydie Frage in der Frage wie (unsigned)(int)((int)x - (int)y)anstelle von (unsigned)(int)((Uchar)((Uchar)x - (Uchar)y))goo.gl/nCvJy5 verhält . Wo sagt der Standard, dass wenn xchar ist, dann +xist int(oder nicht signiert)? In c ++ ist es §5.3.1.7 goo.gl/FkEakX
jfs

@jfs Bei binären Operatoren, bei denen die üblichen arithmetischen Konvertierungen gelten, ist der Teil "sollte" als Teil der oben genannten Regeln enthalten. Für andere Operatoren werden sie befördert, wenn der jeweilige Operator (Kapitel 6.5) dies erfordert. Nehmen wir zum Beispiel den unären -Operator 6.5.3.3: "Das Ergebnis des unären Operators ist das Negativ seines (heraufgestuften) Operanden. Die ganzzahligen Heraufstufungen werden für den Operanden ausgeführt, und das Ergebnis hat den heraufgestuften Typ." Ein weiteres Beispiel (Sonderfall) ist das Präfix ++, in dem keine Ganzzahl-Heraufstufung erwähnt wird, und daher wird sein Operand nicht heraufgestuft.
Lundin

@Lundin «der" sollte "Teil ist als Teil dieser Regeln enthalten» Ich sehe es nicht. Könnten Sie ein spezifisches Zitat aus dem Standard angeben, das besagt, dass Operanden desselben kleinen Ganzzahltyps beim Ausführen einer binären Operation wie z x-y. (Mit anderen Worten, es gibt keinen oder (char)-(char)nur einen Operator (int)-(int)- soweit ich weiß, ist das wahr). c11 6.5.6 / 4 verweist auf 6.3.1.8/1, in dem steht, dass "der Zweck darin besteht, den gemeinsamen realen Typ zu bestimmen". Wenn jedoch Operanden bereits vom gleichen Typ sind, ist es nicht explizit, dass eine Konversation durchgeführt werden sollte.
JFS

1
"Beispiel 1 könnte behoben werden, indem ein oder beide Operanden in unsigned int umgewandelt werden." Die vorgeschlagenen Darsteller ergeben nicht 255, wie vom OP erwartet. Die richtige Lösung besteht darin, das Ergebnis der Subtraktion auf das zurückzusetzen (unsigned char), von dem die Operanden (unsigned char) (x-y)ausgegangen sind , wie in : Dies gibt dem OP die erwarteten 255. Die Leute schätzen das Casting auf eine kleinere Größe oft nicht, dies ist jedoch der richtige Weg um eine Kürzung zu erreichen (gefolgt von einer impliziten / automatischen vorzeichenbehafteten oder Null-Erweiterung auf ~ int-Größe).
Erik Eidt

1
@Student Ah, jetzt verstehe ich, die Erwartung der Erklärung entspricht in der Tat nicht der vorgeschlagenen Lösung. Aktualisiert, danke.
Lundin

4

Gemäß dem vorherigen Beitrag möchte ich zu jedem Beispiel weitere Informationen geben.

Beispiel 1)

Da vorzeichenloses Zeichen kleiner als int ist, wenden wir die ganzzahlige Heraufstufung auf sie an. Dann haben wir (int) x- (int) y = (int) (- 1) und vorzeichenloses int (-1) = 4294967295.

Die Ausgabe des obigen Codes: (wie erwartet)

Wie man es repariert?

Ich habe versucht, was im vorherigen Beitrag empfohlen wurde, aber es funktioniert nicht wirklich. Hier ist der Code, der auf dem vorherigen Beitrag basiert:

Ändern Sie einen von ihnen in unsigned int

Da x bereits eine vorzeichenlose Ganzzahl ist, wenden wir die Ganzzahl-Heraufstufung nur auf y an. Dann erhalten wir (unsigned int) x- (int) y. Da sie immer noch nicht den gleichen Typ haben, wenden wir die üblichen arithmetischen Konvertierungen an. Wir erhalten (unsigned int) x- (unsigned int) y = 4294967295.

Die Ausgabe des obigen Codes: (wie erwartet):

In ähnlicher Weise erhält der folgende Code das gleiche Ergebnis:

Ändern Sie beide in unsigned int

Da beide int ohne Vorzeichen sind, ist keine Ganzzahl-Heraufstufung erforderlich. Durch die übliche arithmetische Konvertierung (haben den gleichen Typ), (vorzeichenloses int) x- (vorzeichenloses int) y = 4294967295.

Die Ausgabe des obigen Codes: (wie erwartet):

Eine der möglichen Möglichkeiten, den Code zu korrigieren: (Fügen Sie am Ende eine Typumwandlung hinzu)

Die Ausgabe des obigen Codes:

Beispiel 2)

Da beide Ganzzahlen sind, ist keine Ganzzahl-Heraufstufung erforderlich. Durch die übliche arithmetische Konvertierung erhalten wir (unsigned int) a + (unsigned int) b = 1 + 4294967294 = 4294967295.

Die Ausgabe des obigen Codes: (wie erwartet)

Wie man es repariert?

Die Ausgabe des obigen Codes:

Beispiel 3)

Das letzte Beispiel hat das Problem behoben, da a und b aufgrund der Ganzzahl-Heraufstufung beide in int konvertiert wurden.

Die Ausgabe des obigen Codes:

Wenn ich einige Konzepte durcheinander gebracht habe, lassen Sie es mich bitte wissen. Danke ~


1
Ihr Fix zu Beispiel 2 signed int c = a+b;oben ruft UB auf. Der resultierende Typ von a + b ist ohne Vorzeichen und der berechnete Wert liegt außerhalb des Bereichs einer vorzeichenbehafteten Ganzzahl.
Cheshar

1
@Cheshar außerhalb des Bereichs Zuordnung ist nicht UB
MM

1
Viele der Beispiele in dieser Antwort verursachen UB durch die Verwendung des falschen Formatbezeichners, und es wird auch eine ungerechtfertigte Annahme über die Größe einesint
MM

@ MM Mein schlechtes! Stimmen Sie zu, es sollte " implementierungsdefiniert oder ein implementierungsdefiniertes Signal ausgelöst " sein. Der signierte Überlauf ist jedoch UB. Es ist einfacher, UB / IB aus den Augen zu verlieren.
Cheshar

@Cheshar: Entgegen dem von einigen Compiler-Betreuern verbreiteten Mythos lautet der Begriff des Standards für Aktionen, die von 99,9% der Implementierungen identisch verarbeitet werden sollten, von Implementierungen, bei denen dies unpraktisch wäre, nicht sinnvoll verarbeitet werden müssen, "Undefiniertes Verhalten". Der Begriff IDB wird nur für Aktionen verwendet, die alle Implementierungen sinnvoll verarbeiten sollen.
Supercat
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.