Sind die Verschiebungsoperatoren (<<, >>) in C arithmetisch oder logisch?


Antworten:


97

Laut K & R 2nd Edition sind die Ergebnisse implementierungsabhängig für die Verschiebung der vorzeichenbehafteten Werte nach rechts.

Wikipedia sagt, dass C / C ++ "normalerweise" eine arithmetische Verschiebung für vorzeichenbehaftete Werte implementiert.

Grundsätzlich müssen Sie entweder Ihren Compiler testen oder sich nicht darauf verlassen. Meine VS2008-Hilfe für den aktuellen MS C ++ - Compiler besagt, dass der Compiler eine arithmetische Verschiebung durchführt.


141

Beim Verschieben nach links gibt es keinen Unterschied zwischen arithmetischer und logischer Verschiebung. Beim Verschieben nach rechts hängt die Art der Verschiebung von der Art des zu verschiebenden Werts ab.

(Als Hintergrund für diejenigen Leser, die mit dem Unterschied nicht vertraut sind, verschiebt eine "logische" Rechtsverschiebung um 1 Bit alle Bits nach rechts und füllt das Bit ganz links mit einer 0. Eine "arithmetische" Verschiebung belässt den ursprünglichen Wert im Bit ganz links Der Unterschied wird wichtig, wenn es um negative Zahlen geht.)

Beim Verschieben eines vorzeichenlosen Werts ist der Operator >> in C eine logische Verschiebung. Beim Verschieben eines vorzeichenbehafteten Werts ist der Operator >> eine arithmetische Verschiebung.

Angenommen, eine 32-Bit-Maschine:

signed int x1 = 5;
assert((x1 >> 1) == 2);
signed int x2 = -5;
assert((x2 >> 1) == -3);
unsigned int x3 = (unsigned int)-5;
assert((x3 >> 1) == 0x7FFFFFFD);

57
So nah, Greg. Ihre Erklärung ist nahezu perfekt, aber das Verschieben eines Ausdrucks mit vorzeichenbehaftetem Typ und negativem Wert ist implementierungsdefiniert. Siehe ISO / IEC 9899: 1999, Abschnitt 6.5.7.
Robᵩ

12
@Rob: Tatsächlich ist das Verhalten für Linksverschiebung und vorzeichenbehaftete negative Zahl undefiniert.
JeremyP

5
Tatsächlich führt die Linksverschiebung auch zu einem undefinierten Verhalten für positiv vorzeichenbehaftete Werte, wenn der resultierende mathematische Wert (der nicht in der Bitgröße begrenzt ist) in diesem vorzeichenbehafteten Typ nicht als positiver Wert dargestellt werden kann. Die Quintessenz ist, dass Sie vorsichtig vorgehen müssen, wenn Sie einen vorzeichenbehafteten Wert nach rechts verschieben.
Michael Burr

3
@ Supercat: Ich weiß es wirklich nicht. Ich weiß jedoch, dass es dokumentierte Fälle gibt, in denen Code mit undefiniertem Verhalten dazu führt, dass ein Compiler sehr nicht intuitive Dinge ausführt (normalerweise aufgrund aggressiver Optimierung - siehe beispielsweise den alten Nullzeiger-Fehler des Linux-TUN / TAP-Treibers: lwn.net / Artikel / 342330 ). Sofern ich keine Vorzeichenfüllung bei Rechtsverschiebung benötige (was meines Erachtens ein implementierungsdefiniertes Verhalten ist), versuche ich normalerweise, meine Bitverschiebungen mit vorzeichenlosen Werten durchzuführen, auch wenn dies die Verwendung von Casts bedeutet, um dorthin zu gelangen.
Michael Burr

2
@MichaelBurr: Ich weiß, dass hypermoderne Compiler die Tatsache, dass Verhalten, das nicht durch den C-Standard definiert wurde (obwohl es in 99% der Implementierungen definiert wurde ), als Rechtfertigung verwendet, um Programme einzuschalten, deren Verhalten für alle vollständig definiert worden wäre Plattformen, auf denen zu erwarten ist, dass sie in wertlosen Bündeln von Maschinenanweisungen ohne nützliches Verhalten ausgeführt werden. Ich gebe jedoch zu (Sarkasmus), ich bin verwirrt darüber, warum Compilerautoren die massivste Optimierungsmöglichkeit verpasst haben: Lassen Sie jeden Teil eines Programms
weg

51

TL; DR

Betrachten iund nals linker bzw. rechter Operand eines Schichtoperators; die Art von i, nach ganzzahliger Heraufstufung, sein T. Unter nder Annahme , dass [0, sizeof(i) * CHAR_BIT)wir nicht definiert sind, haben wir folgende Fälle:

| Direction  |   Type   | Value (i) | Result                   |
| ---------- | -------- | --------- | ------------------------ |
| Right (>>) | unsigned |     0    | −∞  (i ÷ 2ⁿ)            |
| Right      | signed   |     0    | −∞  (i ÷ 2ⁿ)            |
| Right      | signed   |    < 0    | Implementation-defined  |
| Left  (<<) | unsigned |     0    | (i * 2ⁿ) % (T_MAX + 1)   |
| Left       | signed   |     0    | (i * 2ⁿ)                |
| Left       | signed   |    < 0    | Undefined                |

† Die meisten Compiler implementieren dies als arithmetische Verschiebung.
‡ undefiniert, wenn der Wert den Ergebnistyp T überschreitet. geförderte Art von i


Verschiebung

Erstens ist der Unterschied zwischen logischen und arithmetischen Verschiebungen aus mathematischer Sicht, ohne sich um die Größe des Datentyps zu kümmern. Logische Verschiebungen füllen verworfene Bits immer mit Nullen, während die arithmetische Verschiebung sie nur für die Linksverschiebung mit Nullen füllt, aber für die Rechtsverschiebung kopiert sie das MSB, wodurch das Vorzeichen des Operanden erhalten bleibt (unter der Annahme einer Zweierkomplementcodierung für negative Werte).

Mit anderen Worten, die logische Verschiebung betrachtet den verschobenen Operanden nur als einen Strom von Bits und verschiebt sie, ohne sich um das Vorzeichen des resultierenden Werts zu kümmern. Die arithmetische Verschiebung betrachtet sie als (vorzeichenbehaftete) Zahl und behält das Vorzeichen bei, wenn Verschiebungen vorgenommen werden.

Eine linksarithmetische Verschiebung einer Zahl X mit n entspricht der Multiplikation von X mit 2 n und entspricht somit der logischen Linksverschiebung; Eine logische Verschiebung würde auch das gleiche Ergebnis liefern, da MSB sowieso vom Ende fällt und es nichts zu bewahren gibt.

Eine rechtsarithmetische Verschiebung einer Zahl X durch n entspricht der ganzzahligen Division von X durch 2 n NUR, wenn X nicht negativ ist! Ganzzahlige Division ist nichts anderes als mathematische Division und rund auf 0 ( abgeschnitten ).

Bei negativen Zahlen, die durch die Zweierkomplementcodierung dargestellt werden, hat die Verschiebung nach rechts um n Bits den Effekt, dass sie mathematisch durch 2 n geteilt und in Richtung −∞ ( Etage ) gerundet wird . Daher ist die Rechtsverschiebung für nicht negative und negative Werte unterschiedlich.

für X ≥ 0 ist X >> n = X / 2 n = abgeschnitten (X ÷ 2 n )

für X <0 ist X >> n = Boden (X ÷ 2 n )

Wo ÷ist mathematische Division, /ist ganzzahlige Division. Schauen wir uns ein Beispiel an:

37) 10 = 100101) 2

37 ÷ 2 = 18,5

37/2 = 18 (Rundung 18,5 gegen 0) = 10010) 2 [Ergebnis der arithmetischen Rechtsverschiebung]

-37) 10 = 11011011) 2 (unter Berücksichtigung eines Zweierkomplements, 8-Bit-Darstellung)

-37 ÷ 2 = -18,5

-37 / 2 = -18 (Rundung 18,5 gegen 0) = 11101110) 2 [NICHT das Ergebnis einer arithmetischen Rechtsverschiebung]

-37 >> 1 = -19 (Rundung 18,5 in Richtung −∞) = 11101101) 2 [Ergebnis der arithmetischen Rechtsverschiebung]

Wie Guy Steele betonte, hat diese Diskrepanz zu Fehlern in mehr als einem Compiler geführt . Hier können nicht negative (mathematische) nicht vorzeichenbehaftete und vorzeichenlose nicht negative Werte (C) zugeordnet werden; beide werden gleich behandelt und die Verschiebung nach rechts erfolgt durch ganzzahlige Division.

Logisch und arithmetisch sind also bei der Linksverschiebung gleichwertig und bei der Negativverschiebung für nicht negative Werte. Bei der Verschiebung der negativen Werte nach rechts unterscheiden sie sich.

Operanden- und Ergebnistypen

Standard C99 §6.5.7 :

Jeder der Operanden muss ganzzahlige Typen haben.

Die ganzzahligen Heraufstufungen werden für jeden der Operanden durchgeführt. Der Typ des Ergebnisses ist der des heraufgestuften linken Operanden. Wenn der Wert des rechten Operanden negativ ist oder größer oder gleich der Breite des heraufgestuften linken Operanden ist, ist das Verhalten undefiniert.

short E1 = 1, E2 = 3;
int R = E1 << E2;

Im obigen Snippet werden beide Operanden int(aufgrund einer ganzzahligen Heraufstufung); Wenn E2negativ oder E2 ≥ sizeof(int) * CHAR_BITdann ist die Operation undefiniert. Dies liegt daran, dass das Verschieben von mehr als den verfügbaren Bits sicherlich überlaufen wird. Wurde Rals deklariert short, würde das intErgebnis der Schichtoperation implizit in konvertiert werden short; Eine verengte Konvertierung, die zu einem implementierungsdefinierten Verhalten führen kann, wenn der Wert im Zieltyp nicht darstellbar ist.

Linksverschiebung

Das Ergebnis von E1 << E2 sind E1 linksverschobene E2-Bitpositionen; Leerzeichen werden mit Nullen gefüllt. Wenn E1 einen vorzeichenlosen Typ hat, ist der Wert des Ergebnisses E1 × 2 E2 , modulo um eins mehr als der im Ergebnistyp darstellbare Maximalwert. Wenn E1 einen vorzeichenbehafteten Typ und einen nicht negativen Wert hat und E1 × 2 E2 im Ergebnistyp darstellbar ist, ist dies der resultierende Wert. Andernfalls ist das Verhalten undefiniert.

Da die Linksverschiebungen für beide gleich sind, werden die frei gewordenen Bits einfach mit Nullen gefüllt. Es heißt dann, dass es sich sowohl für vorzeichenlose als auch für vorzeichenbehaftete Typen um eine arithmetische Verschiebung handelt. Ich interpretiere es als arithmetische Verschiebung, da sich logische Verschiebungen nicht um den durch die Bits dargestellten Wert kümmern, sondern nur als einen Strom von Bits betrachtet werden. Der Standard spricht jedoch nicht von Bits, sondern von dem Wert, den das Produkt von E1 mit 2 E2 erhält .

Die Einschränkung hierbei ist, dass für vorzeichenbehaftete Typen der Wert nicht negativ sein sollte und der resultierende Wert im Ergebnistyp darstellbar sein sollte. Andernfalls ist die Operation undefiniert. Der Ergebnistyp ist der Typ des E1 nach Anwendung der integralen Heraufstufung und nicht der Zieltyp (die Variable, die das Ergebnis enthalten wird). Der resultierende Wert wird implizit in den Zieltyp konvertiert. Wenn es in diesem Typ nicht darstellbar ist, ist die Konvertierung implementierungsdefiniert (C99 §6.3.1.3 / 3).

Wenn E1 ein vorzeichenbehafteter Typ mit einem negativen Wert ist, ist das Verhalten der Linksverschiebung undefiniert. Dies ist ein einfacher Weg zu undefiniertem Verhalten, das leicht übersehen werden kann.

Verschiebung nach rechts

Das Ergebnis von E1 >> E2 sind E1 rechtsverschobene E2-Bitpositionen. Wenn E1 einen vorzeichenlosen Typ hat oder wenn E1 einen vorzeichenbehafteten Typ und einen nicht negativen Wert hat, ist der Wert des Ergebnisses der integrale Bestandteil des Quotienten von E1 / 2 E2 . Wenn E1 einen vorzeichenbehafteten Typ und einen negativen Wert hat, ist der resultierende Wert implementierungsdefiniert.

Die Rechtsverschiebung für vorzeichenlose und vorzeichenlose nicht negative Werte ist ziemlich einfach. Die freien Bits werden mit Nullen gefüllt. Für vorzeichenbehaftete negative Werte ist das Ergebnis der Rechtsverschiebung implementierungsdefiniert. Die meisten Implementierungen wie GCC und Visual C ++ implementieren jedoch die Rechtsverschiebung als arithmetische Verschiebung, indem das Vorzeichenbit beibehalten wird.

Fazit

Im Gegensatz zu Java, die einen besonderen Betreiber hat >>>für logische Verschiebung abgesehen von den üblichen >>und <<, C und C ++ nur arithmetische Verschiebung haben mit einigen Bereichen und die Implementierung definierte nicht definiert. Der Grund, warum ich sie als arithmetisch betrachte, liegt in der Standardformulierung der Operation mathematisch, anstatt den verschobenen Operanden als einen Strom von Bits zu behandeln; Dies ist vielleicht der Grund, warum diese Bereiche nicht / implementierungsdefiniert bleiben, anstatt nur alle Fälle als logische Verschiebungen zu definieren.


1
Gute Antwort. In Bezug auf die Rundung (im Abschnitt Verschieben ) - Rechtsverschiebung rundet -Infsowohl für negative als auch für positive Zahlen. Das Runden einer positiven Zahl auf 0 ist ein privater Fall des Rundens auf 0 -Inf. Beim Abschneiden lassen Sie immer positiv gewichtete Werte fallen, daher subtrahieren Sie vom ansonsten präzisen Ergebnis.
Ysap

1
@ysap Ja, gute Beobachtung. Grundsätzlich ist die Runde gegen 0 für positive Zahlen ein Sonderfall der allgemeineren Runde gegen −∞; Dies ist in der Tabelle zu sehen, in der sowohl positive als auch negative Zahlen als rund in Richtung −∞ bezeichnet wurden.
Legends2k

17

In Bezug auf die Art der Verschiebung, die Sie erhalten, ist die Art des Werts, den Sie verschieben, wichtig. Eine klassische Fehlerquelle ist, wenn Sie ein Literal verschieben, um beispielsweise Bits zu maskieren. Wenn Sie beispielsweise das am weitesten links stehende Bit einer Ganzzahl ohne Vorzeichen löschen möchten, können Sie dies als Maske versuchen:

~0 >> 1

Leider werden Sie dadurch in Schwierigkeiten geraten, da für die Maske alle Bits gesetzt sind, weil der zu verschiebende Wert (~ 0) vorzeichenbehaftet ist und somit eine arithmetische Verschiebung durchgeführt wird. Stattdessen möchten Sie eine logische Verschiebung erzwingen, indem Sie den Wert explizit als vorzeichenlos deklarieren, dh indem Sie Folgendes tun:

~0U >> 1;

16

Hier sind Funktionen, die eine logische Rechtsverschiebung und eine arithmetische Rechtsverschiebung eines int in C gewährleisten:

int logicalRightShift(int x, int n) {
    return (unsigned)x >> n;
}
int arithmeticRightShift(int x, int n) {
    if (x < 0 && n > 0)
        return x >> n | ~(~0U >> n);
    else
        return x >> n;
}

7

Wenn Sie dies tun - Linksverschiebung mit 1 multiplizieren Sie mit 2 - Rechtsverschiebung mit 1 dividieren Sie durch 2

 x = 5
 x >> 1
 x = 2 ( x=5/2)

 x = 5
 x << 1
 x = 10 (x=5*2)

In x >> a und x << a, wenn die Bedingung a> 0 ist, lautet die Antwort x = x / 2 ^ a, x = x * 2 ^ a bzw. Was wäre die Antwort, wenn a <0 ist?
JAVA

@sunny: a darf nicht kleiner als 0 sein. Es ist undefiniertes Verhalten in C.
Jeremy

4

Nun, ich habe es auf Wikipedia nachgeschlagen und sie haben folgendes zu sagen:

C hat jedoch nur einen Rechtsschieber, >>. Viele C-Compiler wählen die auszuführende Rechtsverschiebung aus, je nachdem, welche Art von Ganzzahl verschoben wird. Oft werden vorzeichenbehaftete Ganzzahlen mithilfe der arithmetischen Verschiebung verschoben, und vorzeichenlose Ganzzahlen werden mithilfe der logischen Verschiebung verschoben.

Es klingt also so, als ob es von Ihrem Compiler abhängt. Beachten Sie auch in diesem Artikel, dass die Linksverschiebung für arithmetisch und logisch gleich ist. Ich würde empfehlen, einen einfachen Test mit einigen vorzeichenbehafteten und vorzeichenlosen Zahlen im Grenzfall durchzuführen (High-Bit-Satz natürlich) und zu sehen, was das Ergebnis auf Ihrem Compiler ist. Ich würde auch empfehlen, es zu vermeiden, je nachdem, ob es das eine oder das andere ist, da C anscheinend keinen Standard hat, zumindest wenn es vernünftig und möglich ist, eine solche Abhängigkeit zu vermeiden.


Obwohl die meisten C-Compiler eine arithmetische Linksverschiebung für vorzeichenbehaftete Werte hatten, scheint dieses hilfreiche Verhalten veraltet zu sein. Die gegenwärtige Compiler-Philosophie scheint darin zu bestehen, anzunehmen, dass die Leistung einer Linksverschiebung für eine Variable einen Compiler dazu berechtigt, anzunehmen, dass die Variable nicht negativ sein darf, und daher jeglichen Code wegzulassen, der für ein korrektes Verhalten erforderlich wäre, wenn die Variable negativ wäre .
Supercat

0

Linksverschiebung <<

Dies ist irgendwie einfach und wenn Sie den Schaltoperator verwenden, ist dies immer eine bitweise Operation, sodass wir ihn nicht mit einer Doppel- und einer Float-Operation verwenden können. Immer wenn wir eine Null verschieben, wird sie zum niedrigstwertigen Bit ( LSB) addiert .

Bei der Rechtsverschiebung müssen >>wir jedoch eine zusätzliche Regel befolgen, die als "Vorzeichenbitkopie" bezeichnet wird. Die Bedeutung von "Vorzeichenbitkopie" ist, wenn das höchstwertige Bit ( MSB) gesetzt ist, dann nach einer erneuten Verschiebung nach rechts MSBgesetzt wird, wenn es zurückgesetzt wurde, dann wird es erneut zurückgesetzt, bedeutet, wenn der vorherige Wert Null war, dann nach erneutem Verschieben, das Bit ist Null, wenn das vorherige Bit Eins war, dann ist es nach der Verschiebung wieder Eins. Diese Regel gilt nicht für eine Linksverschiebung.

Das wichtigste Beispiel für die Rechtsverschiebung, wenn Sie eine negative Zahl in die Rechtsverschiebung verschieben. Nach einer gewissen Verschiebung erreicht der Wert schließlich Null und danach, wenn Sie diese -1 verschieben, bleibt der Wert beliebig oft gleich. Bitte prüfen.


0

Verwendet normalerweise logische Verschiebungen für vorzeichenlose Variablen und für Linksverschiebungen für vorzeichenbehaftete Variablen. Die arithmetische Rechtsverschiebung ist wirklich wichtig, da sie die Variable vorzeichenweise erweitert.

wird dies gegebenenfalls verwenden, wie es andere Compiler wahrscheinlich tun.


-1

GCC tut es

  1. für -ve -> Arithmetische Verschiebung

  2. Für + ve -> Logische Verschiebung


-7

Nach Meinung vieler Compiler:

  1. << ist eine arithmetische Linksverschiebung oder eine bitweise Linksverschiebung.
  2. >> ist eine arithmetische Rechtsverschiebung oder eine bitweise Rechtsverschiebung.

3
"Arithmetische Rechtsverschiebung" und "bitweise Rechtsverschiebung" sind unterschiedlich. Das ist der Punkt der Frage. Die Frage lautete: "Ist >>arithmetisch oder bitweise (logisch)?" Sie antworteten " >>ist arithmetisch oder bitweise." Das beantwortet die Frage nicht.
wchargin

Nein, <<und >>Operatoren sind logisch und nicht arithmetisch
shjeff
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.