Typkonvertierung - nicht signiert in signiertes int / char

73

Ich habe versucht, das folgende Programm auszuführen:

#include <stdio.h>

int main() {
    signed char a = -5;
    unsigned char b = -5;
    int c = -5;
    unsigned int d = -5;

    if (a == b)
        printf("\r\n char is SAME!!!");
    else
        printf("\r\n char is DIFF!!!");

    if (c == d)
        printf("\r\n int is SAME!!!");
    else
        printf("\r\n int is DIFF!!!");

    return 0;
}

Für dieses Programm erhalte ich die Ausgabe:

char ist DIFF !!! int ist gleich !!!

Warum erhalten wir für beide unterschiedliche Ausgänge?
Sollte die Ausgabe wie folgt sein?

char ist gleich !!! int ist gleich !!!

Ein Codepad-Link .

user2522685
quelle
22
Die implizite Ganzzahl-Promotion schlägt erneut zu!
Mysticial
Detaillierte Erklärung hier .
Lundin

Antworten:

82

Dies liegt an den verschiedenen impliziten Typkonvertierungsregeln in C. Es gibt zwei davon, die ein C-Programmierer kennen muss: die üblichen arithmetischen Konvertierungen und die ganzzahligen Heraufstufungen (letztere sind Teil der ersteren).

Im char-Fall haben Sie die Typen (signed char) == (unsigned char). Dies sind beide kleine Ganzzahltypen . Andere solche kleinen ganzzahligen Typen sind boolund short. Die Regeln für die Ganzzahl-Heraufstufung besagen, dass immer dann, wenn ein kleiner Ganzzahl-Typ ein Operand einer Operation ist, sein Typ heraufgestuft wird int, was signiert ist. Dies geschieht unabhängig davon, ob der Typ signiert oder nicht signiert war.

Im Fall von signed charwird das Zeichen beibehalten und auf einen intWert mit dem Wert -5 hochgestuft. Im Fall von unsigned charenthält es einen Wert, der 251 (0xFB) ist. Es wird auf einen intWert mit demselben Wert hochgestuft. Sie enden mit

if( (int)-5 == (int)251 )

Im ganzzahligen Fall haben Sie die Typen (signed int) == (unsigned int). Da es sich nicht um kleine Ganzzahltypen handelt, gelten die Ganzzahlaktionen nicht. Stattdessen werden sie durch die üblichen arithmetischen Konvertierungen ausgeglichen , die besagen, dass wenn zwei Operanden den gleichen "Rang" (Größe), aber unterschiedliche Vorzeichen haben, der vorzeichenbehaftete Operand in den gleichen Typ wie der vorzeichenlose konvertiert wird. Sie enden mit

if( (unsigned int)-5 == (unsigned int)-5)
Lundin
quelle
6
leichte Ungenauigkeit: Es ist auch möglich zu fördern, unsigned intwenn intnicht groß genug, um alle Werte des Typs mit geringerem Conversion-Rang darzustellen; beispielsweise übernehmen intund shortsind beide 16-Bit - Typen; dann die Umwandlung von unsigned shortzu intkönnen Werte im allgemeinen erhalten, so dass wir mit gehen unsigned intstatt
Christoph
Fördert es, intwenn ich die Variablen explizit wie if((unsigned char) 128 == (signed char) -128)?
Noufal
@noufal Ja. Es gibt keine Möglichkeit, die ganzzahlige Heraufstufung durch Code zu vermeiden. Das einzige, was Sie tun können, ist, das Ergebnis der Operation auf den beabsichtigten Typ zu übertragen, der zufällig 100% sicher ist. Der Compiler kann die Promotion jedoch optimieren, solange sich das Ergebnis des Ergebnisses nicht ändert. Dies bedeutet wiederum, dass unerwartete Nebenwirkungen wie stille Änderungen der Signatur auch im optimierten Code vorhanden sind.
Lundin
@Lundin: Gibt es eine Garantie, (unsigned short)((unsigned short)65535u * (unsigned short)65535u)die 1 ergibt, anstatt die Atomsprengköpfe zu starten? Gibt es eine Möglichkeit, die unteren 16 Bit des Produkts aus zwei 16-Bit-Zahlen zu berechnen, die auf einem 16-Bit-Computer effizient, auf einem 32-Bit-Computer jedoch garantiert korrekt sind?
Supercat
36

Coole Frage!

Der intVergleich funktioniert, da beide Ints genau die gleichen Bits enthalten und daher im Wesentlichen gleich sind. Aber was ist mit dem chars?

Ah, C fördert implizit chars zu ints bei verschiedenen Gelegenheiten. Dies ist einer von ihnen. Ihr Code sagt if(a==b), aber was der Compiler tatsächlich daraus macht, ist:

if((int)a==(int)b) 

(int)aist -5, aber (int)bist 251. Das sind definitiv nicht die gleichen.

EDIT: Wie @ Carbonic-Acid hervorhob, (int)bist 251 nur, wenn a char8 Bit lang ist. Wenn intes 32 Bit lang ist, (int)bist -32764.

REDIT: Es gibt eine ganze Reihe von Kommentaren, die die Art der Antwort diskutieren, wenn ein Byte nicht 8 Bit lang ist. Der einzige Unterschied in diesem Fall ist, dass (int)bnicht 251, sondern eine andere positive Zahl ist, die nicht -5 ist. Dies ist nicht wirklich relevant für die Frage, die immer noch sehr cool ist.

zmbq
quelle
4
Stimmt, aber ich möchte OP lieber nicht verwechseln. Ich werde eine kleine Bemerkung hinzufügen.
Zmbq
11
Wo haben Sie in den letzten 40 Jahren ein Byte mit mehr als 8 Bits gesehen?
Zmbq
6
@ user2522685 Weil die C-Sprache dies erfordert und die C-Sprache weder rational, konsistent noch logisch ist.
Lundin
3
@zmbq: DSPs müssen keine 8 Bit pro Byte haben, Unisys ist immer noch im Mainframe-Geschäft, es gibt einige seltsame Forth-Prozessoren (die jedoch nicht mit einem C-Compiler geliefert werden müssen) - wenn Sie genau hinschauen, sind Sie es immer noch kann solche Systeme finden, die heute hergestellt werden
Christoph
3
Beachten Sie auch, dass die Antwort irreführend ist - der intVergleich funktioniert nicht, weil die Variablen dieselben Bits enthalten, sondern weil ihre Werte nach der Konvertierung gleich sind. Die C-Sprache kümmert sich meistens nicht um die Repräsentation - (unsigned)-1 == UINT_MAXgilt auch dann, wenn eine Repräsentation in Vorzeichengröße verwendet wird, bei der die Konvertierung im Gegensatz zum Zweierkomplement kein Noop ist
Christoph
21

Willkommen bei der Ganzzahl-Promotion . Wenn ich von der Website zitieren darf:

Wenn ein int alle Werte des ursprünglichen Typs darstellen kann, wird der Wert in einen int konvertiert. Andernfalls wird es in ein vorzeichenloses int konvertiert. Diese werden als Integer-Promotions bezeichnet. Alle anderen Typen bleiben durch die ganzzahligen Aktionen unverändert.

C kann sehr verwirrend sein, wenn Sie Vergleiche wie diese anstellen. Ich habe kürzlich einige meiner Nicht-C-Programmierfreunde mit folgendem Scherz verwirrt:

#include <stdio.h>
#include <string.h>

int main()
{
    char* string = "One looooooooooong string";

    printf("%d\n", strlen(string));

    if (strlen(string) < -1) printf("This cannot be happening :(");

    return 0;
}

Was tatsächlich druckt This cannot be happening :(und anscheinend zeigt, dass 25 kleiner als -1 ist!

Was jedoch darunter passiert, ist, dass -1 als vorzeichenlose Ganzzahl dargestellt wird, die aufgrund der zugrunde liegenden Bitdarstellung auf einem 32-Bit-System gleich 4294967295 ist. Und natürlich ist 25 kleiner als 4294967295.

Wenn wir jedoch den von zurückgegebenen size_tTyp explizit strlenals vorzeichenbehaftete Ganzzahl umwandeln:

if ((int)(strlen(string)) < -1)

Dann wird es 25 gegen -1 vergleichen und alles wird gut mit der Welt.

Ein guter Compiler sollte Sie vor dem Vergleich zwischen einer vorzeichenlosen und einer vorzeichenbehafteten Ganzzahl warnen, und dennoch ist es so leicht zu übersehen (insbesondere, wenn Sie keine Warnungen aktivieren).

Dies ist besonders verwirrend für Java-Programmierer, da alle primitiven Typen dort signiert sind. Hier ist, was James Gosling (einer der Schöpfer von Java) zu diesem Thema zu sagen hatte :

Gosling: Für mich als Sprachdesigner, den ich heutzutage nicht wirklich als solche bezeichne, bedeutete "einfach" letztendlich, dass ich erwarten konnte, dass J. Random Developer die Spezifikation in seinem Kopf hält. Diese Definition besagt, dass Java zum Beispiel nicht Java ist - und tatsächlich enden viele dieser Sprachen mit vielen Eckfällen, Dinge, die niemand wirklich versteht. Fragen Sie jeden C-Entwickler nach unsigned, und schon bald stellen Sie fest, dass fast kein C-Entwickler wirklich versteht, was mit unsigned vor sich geht, was unsigned-Arithmetik ist. Solche Dinge machten C komplex. Der sprachliche Teil von Java ist meiner Meinung nach ziemlich einfach. Die Bibliotheken müssen Sie nachschlagen.

Nobilis
quelle
1
Goslings Begründung versäumt es zu bemerken, dass nicht signierte ByteTypen in Pascal keinerlei Schwierigkeiten bereiteten. Die einzigen vorzeichenlosen Typen, die überhaupt problematisch sind, sind solche, die größer als die Standardgröße für Ganzzahlen sind.
Supercat
Ich verstehe nicht, warum (int)(strlen(string)) < -1funktioniert. Sie haben nur die linke Seite des Vergleichs geändert, die rechte Seite ist nach vorheriger Erklärung immer noch 4294967295. (cc @Xolve)
user13107
1
@ user13107 strlen(string)gibt ein size_tErgebnis zurück, das nicht als dargestellt werden kann int. Somit werden alle Operanden des Vergleichs zu befördert unsigned int.
Fabio Pozzi
10

Die hexadezimale Darstellung von -5ist:

  • 8-Bit, Zweierkomplement signed char:0xfb
  • 32-Bit, Zweierkomplement signed int:0xfffffffb

Wenn Sie eine vorzeichenbehaftete Nummer in eine vorzeichenlose Nummer konvertieren oder umgekehrt, tut der Compiler ... genau nichts. Was gibt es zu tun? Die Zahl ist entweder konvertierbar oder nicht. In diesem Fall folgt ein undefiniertes oder implementierungsdefiniertes Verhalten (ich habe nicht überprüft, welches), und das effizienteste implementierungsdefinierte Verhalten besteht darin, nichts zu tun.

Die hexadezimale Darstellung von (unsigned <type>)-5lautet also:

  • 8-Bit , unsigned char:0xfb
  • 32-Bit , unsigned int:0xfffffffb

Ähnlich aussehend? Sie sind Stück für Stück die gleichen wie die signierten Versionen.

Wenn Sie schreiben if (a == b), wo aund bvom Typ char, muss der Compiler tatsächlich lesen if ((int)a == (int)b). (Dies ist die "Integer-Promotion", auf die sich alle anderen einlassen.)

Also, was passiert , wenn wir konvertieren charzu int?

  • 8-Bit signed charbis 32-Bit signed int: 0xfb->0xfffffffb
    • Nun, das macht Sinn, weil es den Darstellungen von -5oben entspricht!
    • Es wird als "Vorzeichenerweiterung" bezeichnet, da das oberste Bit des Bytes, das "Vorzeichenbit", nach links in den neuen, breiteren Wert kopiert wird.
  • 8-Bit unsigned charbis 32-Bit signed int: 0xfb->0x000000fb
    • Diesmal wird eine "Null-Erweiterung" ausgeführt, da der Quelltyp nicht vorzeichenbehaftet ist und daher kein Vorzeichenbit zum Kopieren vorhanden ist.

Also a == bwirklich 0xfffffffb == 0x000000fb= = keine Übereinstimmung!

Und c == dstimmt wirklich 0xfffffffb == 0xfffffffbüberein =>!

ams
quelle
Das Konvertieren eines vorzeichenbehafteten Ganzzahltyps in einen vorzeichenlosen ist ein genau definiertes Verhalten gemäß C11 6.3.1.3/2"Otherwise, if the new type is unsigned, the value is converted by repeatedly adding or subtracting one more than the maximum value that can be represented in the new type until the value is in the range of the new type."
Lundin
Das Konvertieren einer vorzeichenlosen Ganzzahl in eine vorzeichenbehaftete Ganzzahl ist entweder genau definiert oder impliziert. definiert, C11 6.3.1.3/1 bzw. 6.3.1.3/3: When a value with integer type is converted to another integer type other than _Bool, if the value can be represented by the new type, it is unchanged./ - /"Otherwise, the new type is signed and the value cannot be represented in it; either the result is implementation-defined or an implementation-defined signal is raised."
Lundin
@ Lundin interessant. Ich kann nicht genau herausfinden, was es bedeutet, wiederholt einen Wert mehr als den Maximalwert des Typs zu addieren oder zu subtrahieren, obwohl ich mir vorstellen würde, dass das Ergebnis das ist, was ich oben gezeigt habe.
Ams
1
Hier ist eine Erklärung . Sie verwenden sogar -5 als Beispiel, zufällig :)
Lundin
1

Mein Punkt ist: Haben Sie beim Kompilieren keine Warnung erhalten, "signierten und nicht signierten Ausdruck zu vergleichen"?

Der Compiler versucht Ihnen mitzuteilen, dass er berechtigt ist, verrückte Sachen zu machen! :) Ich würde hinzufügen, verrücktes Zeug wird mit großen Werten passieren, nahe an der Kapazität des primitiven Typs. Und

 unsigned int d = -5;

weist d definitiv einen großen Wert zu, es ist äquivalent (auch wenn es wahrscheinlich nicht garantiert äquivalent ist) zu sein:

 unsigned int d = UINT_MAX -4; ///Since -1 is UINT_MAX

Bearbeiten:

Es ist jedoch interessant festzustellen, dass nur der zweite Vergleich eine Warnung ausgibt (überprüfen Sie den Code) . Dies bedeutet, dass der Compiler, der die Konvertierungsregeln anwendet, sicher ist, dass beim Vergleich zwischen unsigned charund keine Fehler auftreten char(während des Vergleichs werden sie in einen Typ konvertiert, der alle möglichen Werte sicher darstellen kann). Und in diesem Punkt hat er Recht. Dann werden Sie darüber informiert, dass dies nicht der Fall ist unsigned intund int: Während des Vergleichs wird eine der beiden in einen Typ konvertiert, der sie nicht vollständig darstellen kann.

Der Vollständigkeit halber habe ich es auch kurz überprüft : Der Compiler verhält sich genauso wie bei Zeichen, und wie erwartet treten zur Laufzeit keine Fehler auf.

.

Im Zusammenhang mit diesem Thema habe ich kürzlich diese Frage gestellt (noch C ++ -orientiert).

Antonio
quelle
Downvote weil ...? Es wäre konstruktiv, den Grund zu kennen.
Antonio
Weil du die Frage nicht beantwortet hast? Warum funktioniert es nicht mit char, sondern mit int?
Lundin
@Lundin Meine Motivation ist, dass dieser Code einen Fall auslöst, in dem das Verhalten undefiniert ist. Verschiedene Compiler geben unterschiedliche Ergebnisse (sind berechtigt zu geben). Sie können versuchen zu erraten, warum dieser bestimmte Compiler dieses Ergebnis geliefert hat, aber ich glaube nicht, dass dies einen Sinn hat: Undefinierte Verhaltensweisen sollten vermieden werden, und das ist es.
Antonio
@Lundin Und übrigens funktioniert es (es hat korrektes Verhalten) mit Zeichen und nicht mit int :)
Antonio