Wie wird der Schichtoperator in C ausgewertet?

Lesezeit: 8 Minuten

Benutzer-Avatar
Chouaib

Ich habe kürzlich ein (seltsames) Verhalten festgestellt, als ich Operationen mit Schicht durchgeführt habe >> <<!

Um es zu erklären, lassen Sie mich diesen kleinen ausführbaren Code schreiben, der zwei Operationen ausführt, die identisch sein sollen (nach meinem Verständnis), aber ich bin mit unterschiedlichen Ergebnissen überrascht!

#include <stdio.h>

int main(void) {
    unsigned char a=0x05, b=0x05;

    // first operation
    a = ((a<<7)>>7);

    // second operation
    b <<= 7;
    b >>= 7;

    printf("a=%X b=%X\n", a, b);
    return 0;
} 

Als lief, a = 5 und b = 1. Ich erwarte, dass beide gleich 1 sind! Kann mir jemand freundlicherweise erklären, warum ich ein solches Ergebnis erhalten habe?

PS: In meinem Umfeld die Größe von unsigned char ist 1Byte

  • Lesen über numerische Förderung.

    – Irgendein Programmierer-Typ

    23. September 2014 um 7:32 Uhr

  • So a = ((a<<31)>>31); werde bekommen, was ich will, richtig? (Int-Größe ist 4 Bytes)

    – Chouaib

    23. September 2014 um 7:46 Uhr

  • @JoachimPileborg: warum im Fall von a Würde der Compiler nicht einfach die Operation optimieren, weglassen a unverändert?

    – David C. Rankin

    23. September 2014 um 8:18 Uhr

  • @DavidC.Rankin Natürlich hindert nichts den Compiler daran, die einzige Möglichkeit, sicher zu gehen, besteht darin, sich den generierten Assemblercode anzusehen. Aber in der Allgemeines In diesem Fall ist das Ergebnis das, was es aufgrund der numerischen Beförderung ist.

    – Irgendein Programmierer-Typ

    23. September 2014 um 8:34 Uhr

  • a & 1 führt zum gleichen Ergebnis wie Ihre zweite Operation (und ist sinnvoller).

    – Sean Latham

    23. September 2014 um 9:13 Uhr


Im ersten Beispiel:

  • a wird in ein umgewandelt intnach links, dann nach rechts verschoben und dann wieder in umgewandelt usigned char.

Dies führt zu a=5 offensichtlich.

Im zweiten Beispiel:

  • b umgewandelt wird intnach links verschoben und dann wieder in umgewandelt unsigned char.
  • b umgewandelt wird intnach rechts verschoben und dann wieder in umgewandelt unsigned char.

Der Unterschied besteht darin, dass Sie im zweiten Beispiel während der Konvertierung Informationen verlieren unsigned char

  • Aha! Das ist ein bisschen verwirrend, wenn man lange Codes schreibt. Nehmen wir nun an, ich möchte diese Informationen verlieren, wie kann ich das in einer Zeile tun? ist es in Ordnung, auf char zu werfen a = ((char)(a<<7)>>7) ?

    – Chouaib

    23. September 2014 um 7:37 Uhr

  • Ich denke, ich habe es verstanden, ein Zeilencode wird sein a = ((a<<31)>>31); Recht ? Nun, vorausgesetzt, sizeof(int) = 4 Bytes

    – Chouaib

    23. September 2014 um 7:42 Uhr


  • a = ((char)a<<7)>>7; wird es tun (verschiebt, schneidet ab und verschiebt sich zurück) (achten Sie auf Ihre Klammern). Obwohl viel einfacher zu tun a = a & 0x01;

    – Baldrickk

    23. September 2014 um 7:44 Uhr


  • @Baldrickk @thumbmunkeys: Casting zu char wird es ruinieren, alles zu geben 0xFF also denke ich, dass es eher geeignet ist, um zu werfen unsigned char.

    – Chouaib

    23. September 2014 um 7:56 Uhr

  • @chouaib noch besser, um es damit zu maskieren a = a & 0x01; Punktieren Sie die Operation, die Sie ausführen möchten, wenn verfügbar, und nutzen Sie keine “Macken” des Systems aus, es sei denn, Sie möchten Ihren Code aus irgendeinem Grund absichtlich verschleiern.

    – Baldrickk

    23. September 2014 um 8:06 Uhr

Ausführliche Erläuterung der Vorgänge zwischen den Zeilen:

Fall a:

  • Im Ausdruck a = ((a<<7)>>7);, a<<7 wird zuerst ausgewertet.
  • Der C-Standard besagt, dass jeder Operand der Shift-Operatoren implizit ganzzahlig hochgestuft wird, was bedeutet, dass sie, wenn sie vom Typ bool, char, short usw. sind (zusammen die “kleinen ganzzahligen Typen”), zu einem hochgestuft werden int.
  • Dies ist bei fast allen Operatoren in C Standard. Stattdessen hat das Ergebnis einer Verschiebung immer den Typ des hochgestuften linken Operanden. In diesem Fall int.
  • So a wird zum Typ befördert int, die noch den Wert 0x05 enthält. Das 7 literal war bereits vom Typ int wird also nicht gefördert.
  • Als du gegangen bist, verschiebe das int bei 7 erhalten Sie 0x0280. Das Ergebnis der Operation ist vom Typ int.
  • Beachten Sie, dass int ist ein vorzeichenbehafteter Typ. Hätten Sie die Daten also weiter in die Vorzeichenbits verschoben, hätten Sie ein undefiniertes Verhalten hervorgerufen. Wäre entweder der linke oder der rechte Operand ein negativer Wert gewesen, würden Sie ebenfalls ein undefiniertes Verhalten hervorrufen.
  • Sie haben jetzt den Ausdruck a = 0x280 >> 7;. Für den nächsten Schiebevorgang finden keine Weiterleitungen statt, da beide Operanden bereits int sind.
  • Das Ergebnis ist 5 und vom Typ int. Sie wandeln dieses int dann in ein Zeichen ohne Vorzeichen um, was in Ordnung ist, da das Ergebnis klein genug ist, um hineinzupassen.

Fall b:

  • b <<= 7; ist äquivalent zu b = b << 7;.
  • Wie vorher, b wird zu einem befördert int. Das Ergebnis ist wieder 0x0280.
  • Sie versuchen dann, dieses Ergebnis in einem unsigned char zu speichern. Es wird nicht passen, also wird es abgeschnitten, um nur das niederwertigste Byte zu enthalten 0x80.
  • In der nächsten Zeile, b wird wieder zu einem int befördert, das 0x80 enthält.
  • Und dann verschieben Sie 0x80 um 7 und erhalten das Ergebnis 1. Dies ist vom Typ int, kann aber in ein Zeichen ohne Vorzeichen passen, sodass es in b passt.

Guter Rat:

  • Verwenden Sie niemals bitweise Operatoren für vorzeichenbehaftete Integer-Typen. Dies macht in 99% der Fälle keinen Sinn, kann aber zu verschiedenen Fehlern und schlecht definiertem Verhalten führen.
  • Wenn Sie bitweise Operatoren verwenden, verwenden Sie die Typen in stdint.h anstelle der primitiven Standardtypen in C.
  • Wenn Sie bitweise Operatoren verwenden, verwenden Sie explizite Umwandlungen in den beabsichtigten Typ, um Fehler und unbeabsichtigte Typänderungen zu verhindern, aber auch, um deutlich zu machen, dass Sie tatsächlich verstehen, wie implizite Typumwandlungen funktionieren, und dass Sie nicht nur den Code zum Laufen gebracht haben ausversehen.

Eine bessere und sicherere Art, Ihr Programm zu schreiben, wäre gewesen:

#include <stdio.h>
#include <stdint.h>    

int main(void) {
    uint8_t a=0x05;
    uint8_t b=0x05;
    uint32_t tmp;

    // first operation
    tmp = (uint32_t)a << 7;
    tmp = tmp >> 7;
    a = (uint8_t)tmp;

    // second operation
    tmp = (uint32_t)b << 7;
    tmp = tmp >> 7;
    b = (uint8_t)tmp;

    printf("a=%X b=%X\n", a, b);
    return 0;
} 

  • Vielen Dank für die ausführliche Erklärung und die vielen Informationen und Tipps, ich bin nur ein bisschen enttäuscht, dass ich das nicht in einem Zeilencode tun kann (Operation 1)

    – Chouaib

    23. September 2014 um 12:36 Uhr

  • @chouaib Alles in eine Codezeile zu schreiben, erfüllt keinen eigenen Zweck. Ich habe es nur aus Gründen der Lesbarkeit in mehrere Zeilen aufgeteilt – der generierte Maschinencode ist immer noch derselbe. Du könntest auch schreiben a = (uint8_t)(((uint32_t)a<<7)>>7); aber das ist ein unlesbares Durcheinander.

    – Ludin

    23. September 2014 um 12:39 Uhr


  • Auswählen uint32_t erscheint willkürlich. Es gibt nichts Besonderes an 32-Bit-Breite oder ist dies nur ein Beispiel, wo uint162_t oder uint64_t würde auch funktionieren? Casting zu unsigned oder uintmax_t hätte aber eine gewisse Relevanz.

    – chux – Wiedereinsetzung von Monica

    23. September 2014 um 13:45 Uhr

  • @chux Es gibt etwas Besonderes bei uint32_t, nämlich dass es auf keinem bekannten System der Welt einer der kleinen Integer-Typen ist. Ich habe es ausgewählt, um sicherzustellen, dass dieser Beispielcode auf allen Systemen einwandfrei funktioniert. Hätte ich zum Beispiel abgeholt uint16_t, es hätte auf 8- und 16-Bit-Systemen gut funktioniert, aber auf einem 32-Bit-System hätte ich immer noch eine Integer-Promotion bekommen. Anstelle von uint32_t können Sie jedoch jeden anderen “groß genugen” unsigned Integer-Typ verwenden, z uint_least32_t oder uint64_t.

    – Ludin

    23. September 2014 um 14:04 Uhr

  • @chux Casting zu unsigned ist keine gute Idee, da es eine unbekannte Größe hat und der Code daher nicht portierbar wird. Casting zu uintmax_t macht auch keinen Sinn, da es wahrscheinlich einen unnötig großen Typ ergibt.

    – Ludin

    23. September 2014 um 14:05 Uhr

Benutzer-Avatar
versternen

Die Verschiebungsoperationen würden ganzzahlige Beförderungen zu ihren Operanden und in Ihrem Code das Ergebnis machen int wieder umgewandelt wird char so was:

// first operation
a = ((a<<7)>>7); // a = (char)((a<<7)>>7);

// second operation
b <<= 7; // b = (char) (b << 7);
b >>= 7; // b = (char) (b >> 7);

Zitat aus dem N1570-Entwurf (der später zum Standard von C11 wurde):

6.5.7 Bitweise Verschiebungsoperatoren:

  1. Jeder der Operanden muss vom Typ Integer sein.
  2. Die ganzzahligen Heraufstufungen werden an jedem der Operanden durchgeführt. Der Typ des Ergebnisses ist der des heraufgestuften linken Operanden. Wenn der Wert des rechten Operanden negativ oder größer oder gleich der Breite des heraufgestuften linken Operanden ist, ist das Verhalten nicht definiert.

Und es wird vermutet, dass es in C99 und C90 ähnliche Aussagen gibt.

  • Verkohlen ist ein ganzzahliger Typ.

    – Oliver Charlesworth

    23. September 2014 um 8:14 Uhr

  • @OliverCharlesworth Danke und es war mein Problem, die Idee nicht richtig auszudrücken. Ich habe die Antwort bearbeiten lassen.

    – versternen

    23. September 2014 um 8:39 Uhr

  • Es ist sehr schwer, eine Antwort zu akzeptieren, da alle “akzeptabel” sind. Ich habe die 3 positiv bewertet, aber ich habe mich aufgrund des Hinzufügens des Zitats entschieden, die von Starify zu akzeptieren, aber um ehrlich zu sein, wünschte ich, ich könnte mehr als eine akzeptieren;)

    – Chouaib

    20. Oktober 2014 um 0:00 Uhr

1343270cookie-checkWie wird der Schichtoperator in C ausgewertet?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy