Operatori Bit a Bit in C++
- Gli operatori bit a bit in C++ consentono di manipolare i singoli bit di un valore intero.
- Gli operatori bit a bit includono NOT, AND, OR, XOR e shift a sinistra/destra.
- Gli operatori bit a bit operano su operandi di tipo intero, che possono essere signed o unsigned.
- L'uso di tipi unsigned è raccomandato per evitare comportamenti indefiniti con il bit del segno.
- Gli operatori di shift spostano i bit a sinistra o a destra, con i bit spostati oltre la fine che vengono scartati.
- La precedenza degli operatori bit a bit è inferiore a quella degli operatori aritmetici ma superiore a quella degli operatori relazionali.
Operatori Bit a Bit (Bitwise)
Gli operatori bit a bit prendono operandi di tipo intero e li usano come una collezione di bit.
Questi operatori ci permettono di testare e impostare singoli bit. Come vedremo nelle prossime lezioni, possiamo anche usare questi operatori su un tipo di libreria chiamato bitset
che rappresenta una collezione di bit di dimensione flessibile.
Come al solito, se un operando è un "intero piccolo," il suo valore viene prima promosso a un tipo intero più grande. Gli operandi possono essere sia signed che unsigned.
Operatore | Funzione | Uso | Precedenza |
---|---|---|---|
~ |
NOT bit a bit | ~expr |
1 |
<< |
shift a sinistra | expr1 << expr2 |
2 |
>> |
shift a destra | expr1 >> expr2 |
2 |
& |
AND bit a bit | expr1 & expr2 |
3 |
^ |
XOR bit a bit | expr1 ^ expr2 |
4 |
| |
OR bit a bit | expr1 | expr2 |
5 |
Se l'operando è signed
e il suo valore è negativo, allora il modo in cui il "bit del segno" viene gestito in un certo numero di operazioni bit a bit è dipendente dalla macchina. Inoltre, fare uno shift a sinistra che cambia il valore del bit del segno è indefinito.
Poiché non ci sono garanzie su come viene gestito il bit del segno, raccomandiamo fortemente di usare tipi unsigned
con gli operatori bit a bit.
Operatori di Shift Bit a Bit
Abbiamo già usato le versioni sovraccaricate degli operatori >>
e <<
che la libreria I/O definisce per fare input e output. Il significato built-in di questi operatori è che eseguono uno shift bit a bit sui loro operandi. Producono un valore che è una copia dell'operando sinistro (possibilmente promosso) con i bit spostati come indicato dall'operando destro. L'operando destro non deve essere negativo e deve essere un valore strettamente minore del numero di bit nel risultato. Altrimenti, l'operazione è indefinita. I bit vengono spostati a sinistra (<<
) o a destra (>>
). I bit che vengono spostati oltre la fine vengono scartati.
Per comprendere come funzionano gli operatori di shift, consideriamo il seguente esempio. Supponiamo di lavorare su una macchina in cui il tipo int
occupa 32 bit e che il tipo char
occupa 8 bit.
Dichiariamo, quindi, un char
come segue:
unsigned char bits = 0233;
Abbiamo definito questo char
usando un letterale ottale, 0233
, che rappresenta il valore decimale 155. In binario, questo valore è rappresentato come 10010111. Quindi, la disposizione in memoria della variabile bits
è rappresentata in figura:
Successivamente, eseguiamo l'operazione:
bits << 8;
Quando eseguiamo questa operazione, accadono due cose. In primo luogo la variabile bits
viene promossa a unsigned int
, quindi il suo valore rimane 155, ma ora è rappresentato come un intero a 32 bit e i bit aggiunti valgono 0. Questa operazione prende il nome di padding. Il risultato della promozione è rappresentato in figura:
Solo a questo punto viene eseguito lo shift a sinistra di 8 bit. Il risultato è rappresentato in figura:
Come si vede, i bit sono stati spostati a sinistra di 8 posizioni e i bit più a destra sono stati riempiti con 0.
Vediamo un altro esempio. Supponiamo di eseguire lo shift sempre della stessa variabile bits
, ma questa volta a sinistra di 31 bit. Come si può vedere dalla figura sottostante, i bit più a sinistra vengono scartati. Rimane un unico bit con valore 1, che è il bit più a destra del valore originale:
Infine, mostriamo un ultimo esempio in cui eseguiamo uno shift a destra di 3 bit. In questo caso, i bit più a destra vengono scartati e i bit più a sinistra vengono riempiti con 0:
L'operatore di shift a sinistra (l'operatore <<
) inserisce bit con valore 0 a destra. Il comportamento dell'operatore di shift a destra (l'operatore >>
) dipende dal tipo dell'operando sinistro: Se quell'operando è unsigned
, allora l'operatore inserisce bit con valore 0 a sinistra; se è un tipo signed
, il risultato è definito dall'implementazione, ossia vengono inserite copie del bit del segno o bit con valore 0 a sinistra.
Operatore NOT Bit a Bit
L'operatore NOT bit a bit (l'operatore ~
) genera un nuovo valore con i bit del suo operando invertiti. Ogni bit 1 viene impostato a 0; ogni bit 0 viene impostato a 1.
Chiariamo con un esempio. Supponiamo di avere la seguente dichiarazione:
unsigned char bits = 0227;
In questo caso, il valore di bits
in binario è 10010111, ossia 151. Quindi, la disposizione in memoria della variabile bits
è rappresentata in figura:
Adesso, se eseguiamo l'operazione:
~bits
Anche in questo caso, come per gli operatori di shift, il primo passo è la promozione di bits
a unsigned int
:
Qui, il nostro operando char viene prima promosso a int
. Promuovere un char
a int
lascia il valore invariato ma aggiunge bit 0 alle posizioni di ordine superiore. Quindi, promuovere bits
a int
aggiunge 24 bit di ordine superiore, tutti con valore 0.
Successivamente, viene eseguito il NOT bit a bit, ossia l'inversione di tutti i bit:
Operatori AND, OR e XOR Bit a Bit
Gli operatori AND (&
), OR (|
) e XOR (^
) generano nuovi valori con il pattern di bit composto dai suoi due operandi:
Nell'esempio della figura, abbiamo due variabili unsigned char
:
unsigned char b1 = 0145; // 10010101 in binario
unsigned char b2 = 0257; // 10101111 in binario
A queste variabili applichiamo gli operatori AND, OR e XOR bit a bit. Nota che abbiamo escluso i bit di padding che vengono aggiunti quando b1
e b2
vengono promossi a unsigned int
, ma bisogna ricordare che la promozione avviene prima dell'applicazione degli operatori bit a bit. Quindi in realtà i risultati delle operazioni bit a bit sono rappresentati come unsigned int
con 32 bit.
Per ogni posizione di bit nel risultato dell'operatore AND bit a bit (l'operatore &
) il bit è 1 se entrambi gli operandi contengono 1; altrimenti, il risultato è 0. Per l'operatore OR (or inclusivo) (l'operatore |
), il bit è 1 se uno o entrambi gli operandi contengono 1; altrimenti, il risultato è 0. Per l'operatore XOR (or esclusivo) (l'operatore ^
), il bit è 1 se uno ma non entrambi gli operandi contengono 1; altrimenti, il risultato è 0.
È un errore comune confondere gli operatori bit a bit e gli operatori logici. Ad esempio confondere il &
bit a bit con il &&
logico, il |
bit a bit con il ||
logico, e il ~
bit a bit con il !
logico.
Uso degli Operatori Bit a Bit
Come esempio di utilizzo degli operatori bit a bit, assumiamo che un insegnante abbia 30 studenti in una classe. Ogni settimana alla classe viene dato un quiz il cui risultato può essere "superato" o "non superato". Terremo traccia dei risultati di ogni quiz usando un bit per studente per rappresentare il voto superato o non superato in un dato test. Potremmo rappresentare ogni quiz in un valore intero unsigned
:
// useremo questo valore come collezione di bit
unsigned long quiz1 = 0;
Definiamo quiz1
come unsigned long
. Quindi, quiz1
avrà almeno 32 bit su qualsiasi macchina. Inizializziamo esplicitamente quiz1
per assicurarci che i bit partano con valori ben definiti.
L'insegnante deve essere in grado di impostare e testare singoli bit. Ad esempio, vorremmo essere in grado di impostare il bit corrispondente allo studente numero 27 per indicare che questo studente ha superato il quiz. Possiamo indicare che lo studente numero 27 ha superato la prova creando un valore che ha solo il bit 27 acceso. Se poi facciamo un OR bit a bit di quel valore con quiz1
, tutti i bit tranne il bit 27 rimarranno invariati.
Per lo scopo di questo esempio, conteremo i bit di quiz1
assegnando 0 al bit di ordine inferiore, 1 al bit successivo, e così via.
Possiamo ottenere un valore che indica che lo studente 27 ha superato usando l'operatore di shift a sinistra e un letterale intero unsigned long
pari a 1:
1UL << 27 // genera un valore con solo il bit numero 27 impostato
1UL
ha un 1 nel bit di ordine inferiore e (almeno) 31 bit zero. Abbiamo specificato unsigned long
perché gli int
sono garantiti avere solo 16 bit, e abbiamo bisogno di almeno 27. Questa espressione sposta il bit 1 a sinistra di 27 posizioni inserendo bit 0 dietro di esso.
Successivamente facciamo un OR di questo valore con quiz1
. Poiché vogliamo aggiornare il valore di quiz1
, usiamo un assegnamento composto:
// indica che lo studente numero 27 ha superato
// la prova
quiz1 |= 1UL << 27;
L'operatore |=
viene eseguito in modo analogo a come fa +=
. È equivalente a:
// equivalente a quiz1 |= 1UL << 27;
quiz1 = quiz1 | 1UL << 27;
Immagina che l'insegnante abbia riesaminato il quiz e scoperto che lo studente 27 in realtà non aveva superato il test. L'insegnante deve ora spegnere il bit 27. Questa volta abbiamo bisogno di un intero che ha il bit 27 spento e tutti gli altri bit accesi. Faremo un AND bit a bit di questo valore con quiz1 per spegnere solo quel bit:
// lo studente numero 27 non ha superato
quiz1 &= ~(1UL << 27);
Otteniamo un valore con tutti i bit tranne il 27 accesi invertendo il nostro valore precedente. Quel valore aveva bit 0 in tutti tranne il bit 27, che era un 1. Applicando il NOT bit a bit a quel valore spegnerà il bit 27 e accenderà tutti gli altri. Quando facciamo un AND bit a bit di questo valore con quiz1
, tutti tranne il bit 27 rimarranno invariati.
Infine, potremmo voler sapere come è andato lo studente alla posizione 27:
// come è andato lo studente numero 27?
bool stato = quiz1 & (1UL << 27);
Qui facciamo un AND di un valore che ha il bit 27 acceso con quiz1
. Il risultato è diverso da zero (cioè, true
) se il bit 27 di quiz1
è anche acceso; altrimenti, valuta a zero.
Gli Operatori di Shift (alias Operatori I/O) Sono Associativi a Sinistra
Sebbene molti programmatori non usino mai direttamente gli operatori bit a bit, la maggior parte dei programmatori usa versioni sovraccaricate di questi operatori per l'I/O. Un operatore sovraccaricato ha la stessa precedenza e associatività della versione built-in di quell'operatore. Pertanto, i programmatori devono comprendere la precedenza e l'associatività degli operatori di shift anche se non li usano mai con il loro significato built-in.
Poiché gli operatori di shift sono associativi a sinistra, l'espressione
cout << "ciao" << " a tutti" << endl;
viene eseguita come
( (cout << "ciao") << " a tutti" ) << endl;
In questa istruzione, l'operando "ciao"
viene raggruppato con il primo simbolo <<
. Il suo risultato viene raggruppato con il secondo, e poi quel risultato viene raggruppato con il terzo.
Gli operatori di shift hanno precedenza di livello medio: inferiore agli operatori aritmetici ma superiore agli operatori relazionali, di assegnamento e condizionali. Questi livelli di precedenza relativi significano che di solito dobbiamo usare le parentesi per forzare il raggruppamento corretto degli operatori con precedenza inferiore.
// OK: + ha precedenza più alta, quindi viene stampata la somma
cout << 42 + 10;
// OK: le parentesi forzano il raggruppamento inteso; stampa 1
cout << (10 < 42);
// ERRORE: tentativo di confrontare cout con 42!
cout << 10 < 42;
L'ultimo cout
viene interpretato come
(cout << 10) < 42;
che dice di "scrivere 10 su cout
e poi confrontare il risultato di quell'operazione (cioè, cout
) con 42."
Esercizi
-
Qual è il valore di
~'q' << 6
su una macchina conint
a 32 bit echar
a 8 bit, che usa il set di caratteri ASCII in cui'q'
ha il pattern di bit01110001
?Soluzione:
Per prima cosa
'q'
viene promosso aint
, quindi il suo valore diventa:00000000 00000000 00000000 01110001
Applicando il NOT bit a bit otteniamo:
11111111 11111111 11111111 10001110
Infine, facendo uno shift a sinistra di 6 otteniamo:
11111111 11111111 11100011 10000000
-
Nel nostro esempio di gestione quiz degli studenti in questa sezione, cosa succederebbe se usassimo
unsigned int
come tipo perquiz1
?Soluzione:
Su molte macchine,
unsigned int
ha 32 bit, quindi il codice funzionerebbe correttamente. Tuttavia, lo standard C++ garantisce solo cheunsigned int
abbia almeno 16 bit. Quindi, su alcune macchine,unsigned int
potrebbe avere solo 16 bit. In tal caso, il codice non funzionerebbe correttamente perché non ci sarebbero abbastanza bit per rappresentare i risultati di tutti e 30 gli studenti. -
Qual è il risultato di ciascuna di queste espressioni?
unsigned long ul1 = 3, ul2 = 7; ul1 & ul2 ul1 | ul2 ul1 && ul2 ul1 || ul2
Soluzione:
La prima espressione (
ul1 & ul2
) esegue un AND bit a bit tra i valori binari diul1
(ossia,0000...0011
) eul2
(ossia,0000...0111
). Il risultato è0000...0011
, che è 3.La seconda espressione (
ul1 | ul2
) esegue un OR bit a bit tra i valori binari diul1
eul2
. Il risultato è0000...0111
, che è 7.La terza espressione (
ul1 && ul2
) è un operatore logico che valuta se entrambi gli operandi sono diversi da zero. Poiché siaul1
cheul2
sono diversi da zero, il risultato ètrue
, che viene convertito in 1 quando assegnato a un tipo intero.La quarta espressione (
ul1 || ul2
) è un operatore logico che valuta se almeno uno degli operandi è diverso da zero. Poiché siaul1
cheul2
sono diversi da zero, il risultato ètrue
, che viene convertito in 1 quando assegnato a un tipo intero.