Gli operatori di turno (<<, >>) sono aritmetici o logici in C?


Risposte:


97

Secondo la seconda edizione di K&R, i risultati dipendono dall'implementazione per il corretto spostamento dei valori firmati.

Wikipedia afferma che C / C ++ "di solito" implementa uno spostamento aritmetico sui valori con segno.

Fondamentalmente è necessario testare il compilatore o non fare affidamento su di esso. Il mio aiuto VS2008 per l'attuale compilatore MS C ++ dice che il loro compilatore fa uno spostamento aritmetico.


141

Quando si sposta a sinistra, non vi è alcuna differenza tra spostamento aritmetico e logico. Quando si sposta a destra, il tipo di spostamento dipende dal tipo di valore da spostare.

(Come sfondo per quei lettori che non hanno familiarità con la differenza, uno spostamento a destra "logico" di 1 bit sposta tutti i bit a destra e riempie il bit più a sinistra con uno 0. Uno spostamento "aritmetico" lascia il valore originale nel bit più a sinistra La differenza diventa importante quando si tratta di numeri negativi.)

Quando si sposta un valore senza segno, l'operatore >> in C è uno spostamento logico. Quando si sposta un valore con segno, l'operatore >> è uno spostamento aritmetico.

Ad esempio, supponendo una macchina a 32 bit:

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
Così vicino, Greg. La tua spiegazione è quasi perfetta, ma lo spostamento di un'espressione di tipo firmato e valore negativo è definito dall'implementazione. Vedere ISO / IEC 9899: 1999, sezione 6.5.7.
Robᵩ

12
@Rob: in realtà, per lo spostamento a sinistra e il numero negativo con segno, il comportamento non è definito.
JeremyP

5
In realtà, lo spostamento a sinistra comporta anche un comportamento indefinito per valori con segno positivo se il valore matematico risultante (che non è limitato nella dimensione dei bit) non può essere rappresentato come valore positivo in quel tipo con segno. La linea di fondo è che devi camminare con attenzione quando si sposta a destra un valore con segno.
Michael Burr,

3
@supercat: davvero non lo so. Tuttavia, so che ci sono casi documentati in cui il codice che ha un comportamento indefinito fa sì che un compilatore faccia cose molto non intuitive (di solito a causa di un'ottimizzazione aggressiva, ad esempio vedi il bug del puntatore nullo del vecchio driver TUN / TAP Linux: lwn.net / Articoli / 342330 ). A meno che non abbia bisogno di riempire il segno con lo spostamento a destra (che mi rendo conto sia un comportamento definito dall'implementazione), di solito cerco di eseguire i miei spostamenti di bit usando valori non firmati, anche se ciò significa usare i cast per arrivarci.
Michael Burr,

2
@MichaelBurr: so che i compilatori hypermodern usano il fatto che il comportamento non definito dallo standard C (anche se era stato definito nel 99% delle implementazioni ) come giustificazione per trasformare i programmi il cui comportamento sarebbe stato completamente definito su tutti piattaforme in cui ci si potrebbe aspettare che funzionino, in inutili mazzi di istruzioni della macchina senza comportamenti utili. Devo ammettere, però (sarcasmo) sono perplesso dal motivo per cui gli autori del compilatore hanno perso la più grande possibilità di ottimizzazione: omettere qualsiasi parte di un programma che, se raggiunto, comporterebbe il nidificazione delle funzioni ...
supercat

51

TL; DR

Considera ie ndi essere rispettivamente gli operandi sinistro e destro di un operatore di turno; il tipo di i, dopo la promozione di interi, essere T. Supponendo ndi essere [0, sizeof(i) * CHAR_BIT)- altrimenti non definito - abbiamo questi casi:

| 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                |

† la maggior parte dei compilatori implementa questo come spostamento aritmetico
‡ indefinito se il valore supera il tipo di risultato T; tipo di i promosso


Mutevole

La prima è la differenza tra i cambiamenti logici e quelli aritmetici da un punto di vista matematico, senza preoccuparsi della dimensione del tipo di dati. Gli spostamenti logici riempiono sempre i bit scartati di zeri mentre lo spostamento aritmetico lo riempie di zeri solo per lo spostamento a sinistra, ma per lo spostamento a destra copia l'MSB preservando così il segno dell'operando (presupponendo che la codifica del complemento a due per valori negativi).

In altre parole, lo spostamento logico considera l'operando spostato come solo un flusso di bit e li sposta, senza preoccuparsi del segno del valore risultante. Il turno aritmetico lo considera come un numero (firmato) e conserva il segno mentre vengono fatti i turni.

Uno spostamento aritmetico sinistro di un numero X per n equivale alla moltiplicazione di X per 2 n ed è quindi equivalente allo spostamento sinistro logico; un cambiamento logico darebbe anche lo stesso risultato poiché MSB cade comunque alla fine e non c'è nulla da preservare.

Uno spostamento aritmetico di destra di un numero X per n equivale alla divisione intera di X per 2 n SOLO se X non è negativo! La divisione intera non è altro che divisione matematica e arrotondata a 0 ( trunc ).

Per i numeri negativi, rappresentati dalla codifica in complemento a due, sposta verso destra di n bit ha l'effetto di dividere matematicamente da 2 n e arrotondamento verso -∞ ( piano ); quindi il giusto spostamento è diverso per i valori non negativi e negativi.

per X ≥ 0, X >> n = X / 2 n = trunc (X ÷ 2 n )

per X <0, X >> n = piano (X ÷ 2 n )

dove ÷è divisione matematica, /è divisione intera. Diamo un'occhiata a un esempio:

37) 10 = 100101) 2

37: 2 = 18.5

37/2 = 18 (arrotondamento 18,5 verso 0) = 10010) 2 [risultato dello spostamento aritmetico a destra]

-37) 10 = 11011011) 2 (considerando un complemento a due, rappresentazione a 8 bit)

-37: 2 = -18,5

-37 / 2 = -18 (arrotondamento 18,5 verso 0) = 11101110) 2 [NON il risultato dello spostamento destro aritmetico]

-37 >> 1 = -19 (arrotondamento 18,5 verso −∞) = 11101101) 2 [risultato dello spostamento aritmetico a destra]

Come ha sottolineato Guy Steele , questa discrepanza ha portato a bug in più di un compilatore . Qui i valori non negativi (matematica) possono essere associati a valori non negativi (C) non firmati e firmati; entrambi sono trattati allo stesso modo e il loro spostamento a destra avviene per divisione di numeri interi.

Quindi logico e aritmetico sono equivalenti nello spostamento a sinistra e per valori non negativi nello spostamento a destra; è nel giusto spostamento dei valori negativi che differiscono.

Operando e tipi di risultati

Standard C99 §6.5.7 :

Ciascuno degli operandi deve avere tipi interi.

Le promozioni di numeri interi vengono eseguite su ciascuno degli operandi. Il tipo di risultato è quello dell'operando di sinistra promosso. Se il valore dell'operando di destra è negativo o è maggiore o uguale alla larghezza dell'operando di sinistra promosso, il comportamento non è definito.

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

Nel frammento di cui sopra, entrambi gli operandi diventano int(a causa della promozione di numeri interi); se E2era negativo o E2 ≥ sizeof(int) * CHAR_BITquindi l'operazione non è definita. Questo perché lo spostamento di più dei bit disponibili sta sicuramente per traboccare. Era Rstato dichiarato come short, il intrisultato dell'operazione di cambio sarebbe stato convertito implicitamente in short; una conversione restrittiva, che può portare a comportamenti definiti dall'implementazione se il valore non è rappresentabile nel tipo di destinazione.

Tasto maiuscolo di sinistra

Il risultato di E1 << E2 è E1 posizioni bit E2 spostate a sinistra; i bit vuoti vengono riempiti con zeri. Se E1 ha un tipo senza segno, il valore del risultato è E1 × 2 E2 , riducendo il modulo uno in più rispetto al valore massimo rappresentabile nel tipo di risultato. Se E1 ha un tipo con segno e un valore non negativo ed E1 × 2 E2 è rappresentabile nel tipo di risultato, allora quello è il valore risultante; in caso contrario, il comportamento non è definito.

Poiché gli spostamenti a sinistra sono uguali per entrambi, i bit lasciati liberi vengono semplicemente riempiti di zeri. Quindi afferma che sia per i tipi non firmati che per quelli firmati si tratta di uno spostamento aritmetico. Lo sto interpretando come spostamento aritmetico poiché gli spostamenti logici non si preoccupano del valore rappresentato dai bit, ma lo guardano semplicemente come un flusso di bit; ma lo standard non parla in termini di bit, ma definendolo in termini di valore ottenuto dal prodotto di E1 con 2 E2 .

L'avvertenza qui è che per i tipi con segno il valore dovrebbe essere non negativo e il valore risultante dovrebbe essere rappresentabile nel tipo di risultato. Altrimenti l'operazione non è definita. Il tipo di risultato sarebbe il tipo di E1 dopo aver applicato la promozione integrale e non il tipo di destinazione (la variabile che manterrà il risultato). Il valore risultante viene convertito implicitamente nel tipo di destinazione; se non è rappresentabile in quel tipo, la conversione è definita dall'implementazione (C99 §6.3.1.3 / 3).

Se E1 è un tipo con segno con un valore negativo, il comportamento dello spostamento a sinistra non è definito. Questo è un percorso facile verso comportamenti indefiniti che possono essere facilmente trascurati.

Maiusc destro

Il risultato di E1 >> E2 è E1 posizioni bit E2 spostate a destra. Se E1 ha un tipo senza segno o se E1 ha un tipo con segno e un valore non negativo, il valore del risultato è la parte integrante del quoziente di E1 / 2 E2 . Se E1 ha un tipo con segno e un valore negativo, il valore risultante è definito dall'implementazione.

Lo spostamento a destra per valori non negativi non firmati e firmati è piuttosto semplice; i bit vuoti sono riempiti di zeri. Per valori negativi con segno, il risultato del giusto spostamento è definito dall'implementazione. Detto questo, la maggior parte delle implementazioni come GCC e Visual C ++ implementano lo spostamento a destra come spostamento aritmetico preservando il bit di segno.

Conclusione

A differenza di Java, che ha un operatore speciale >>>per lo spostamento logico oltre al solito >>e <<, C e C ++ hanno solo lo spostamento aritmetico con alcune aree lasciate indefinite e definite dall'implementazione. Il motivo per cui li considero aritmetici è dovuto alla formulazione standard dell'operazione matematicamente piuttosto che al considerare l'operando spostato come un flusso di bit; questo è forse il motivo per cui lascia quelle aree non definite / implementate invece di definire tutti i casi come cambiamenti logici.


1
Bella risposta. Per quanto riguarda l'arrotondamento (nella sezione Shifting ) - lo spostamento a destra viene arrotondato verso -Infper i numeri sia negativi che positivi. L'arrotondamento verso 0 di un numero positivo è un caso privato di arrotondamento verso -Inf. Quando si troncano, si rilasciano sempre valori ponderati positivamente, quindi si sottrae dal risultato altrimenti preciso.
ysap

1
Sì, buona osservazione. Bascialmente, il round verso 0 per i numeri positivi è un caso speciale del round più generale verso −∞; questo può essere visto nella tabella, dove sia i numeri positivi che quelli negativi l'avevo notato come rotondo verso −∞.
legends2k

17

In termini di tipo di spostamento ottenuto, l'importante è il tipo di valore che si sta spostando. Una classica fonte di bug è quando si sposta letteralmente, per esempio, mascherando i bit. Ad esempio, se si desidera eliminare il bit più a sinistra di un numero intero senza segno, è possibile provare questo come maschera:

~0 >> 1

Sfortunatamente, questo ti metterà nei guai perché la maschera avrà tutti i suoi bit impostati perché il valore da spostare (~ 0) è firmato, quindi viene eseguito uno spostamento aritmetico. Invece, si vorrebbe forzare uno spostamento logico dichiarando esplicitamente il valore come non firmato, ovvero facendo qualcosa del genere:

~0U >> 1;

16

Ecco le funzioni per garantire lo spostamento destro logico e lo spostamento destro aritmetico di un int in C:

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

Quando lo fai - spostamento a sinistra per 1 si moltiplica per 2 - spostamento a destra per 1 si divide per 2

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

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

In x >> ae x << a se la condizione è a> 0, la risposta è rispettivamente x = x / 2 ^ a, x = x * 2 ^ a quindi Quale sarebbe la risposta se <0?
JAVA,

@sunny: a non deve essere inferiore a 0. È un comportamento indefinito in C.
Jeremy,

4

Bene, l'ho cercato su Wikipedia , e hanno questo da dire:

C, tuttavia, ha un solo operatore di spostamento a destra, >>. Molti compilatori C scelgono quale spostamento destro eseguire a seconda del tipo di intero che viene spostato; spesso gli interi con segno vengono spostati utilizzando lo spostamento aritmetico e gli interi senza segno vengono spostati utilizzando lo spostamento logico.

Quindi sembra che dipenda dal tuo compilatore. Sempre in quell'articolo, nota che lo spostamento a sinistra è lo stesso per l'aritmetica e la logica. Consiglierei di fare un semplice test con alcuni numeri firmati e non firmati sulla custodia del bordo (ovviamente set di bit alto) e vedere quale risultato ha il tuo compilatore. Vorrei anche raccomandare di evitare a seconda che sia l'uno o l'altro poiché sembra che C non abbia standard, almeno se è ragionevole e possibile evitare tale dipendenza.


Sebbene la maggior parte dei compilatori C avesse un turno aritmetico a sinistra per i valori con segno, tale comportamento utile sembra essere stato deprecato. L'attuale filosofia del compilatore sembra essere quella di supporre che le prestazioni di uno spostamento a sinistra su una variabile autorizzino un compilatore ad assumere che la variabile debba essere non negativa e quindi omettere qualsiasi altro codice che sarebbe necessario per un comportamento corretto se la variabile fosse negativa .
supercat

0

Tasto maiuscolo di sinistra <<

Questo è in qualche modo facile e ogni volta che usi l'operatore di cambio, è sempre un'operazione un po 'saggia, quindi non possiamo usarla con un'operazione double e float. Ogni volta che abbiamo lasciato shift di uno zero, viene sempre aggiunto al bit meno significativo ( LSB).

Ma nel turno giusto >>dobbiamo seguire una regola aggiuntiva e quella regola si chiama "copia copia bit". Il significato di "copia bit segno" è se MSBviene impostato il bit più significativo ( ), quindi dopo uno spostamento a destra di nuovo MSBverrà impostato se è stato ripristinato, quindi viene nuovamente ripristinato, significa che se il valore precedente era zero quindi dopo lo spostamento di nuovo, il bit è zero se il bit precedente era uno, quindi dopo lo spostamento è di nuovo uno. Questa regola non è applicabile per uno spostamento a sinistra.

L'esempio più importante sullo spostamento a destra se si sposta qualsiasi numero negativo sullo spostamento a destra, quindi dopo alcuni spostamenti il ​​valore raggiunge infine lo zero e quindi dopo se si sposta questo -1 in qualsiasi numero di volte il valore rimarrà lo stesso. Si prega di controllare.


0

utilizzerà in genere turni logici su variabili senza segno e per turni a sinistra su variabili con segno. Lo spostamento aritmetico a destra è veramente importante perché firmerà per estendere la variabile.

userà questo quando applicabile, come è probabile che facciano altri compilatori.


-1

GCC lo fa

  1. per -ve -> Spostamento aritmetico

  2. Per + ve -> Spostamento logico


-7

Secondo molti compilatori:

  1. << è uno spostamento aritmetico a sinistra o spostamento a sinistra bit a bit.
  2. >> è uno spostamento destro aritmetico a destra bit per bit.

3
"Spostamento destro aritmetico" e "spostamento destro bit a bit" sono diversi. Questo è il punto della domanda. La domanda è: "È >>aritmetica o bit a bit (logica)?" Hai risposto " >>è aritmetico o bit a bit". Questo non risponde alla domanda.
mercoledì

No, <<e gli >>operatori sono logici, non aritmetici
shjeff
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.