Poate că nu doar o dată te-ai întrebat care este diferența dintre declararea variabilelor cu semn (signed) și declararea variabilelor fără semn (unsigned). Poate că te întrebi de ce ai folosi o variabilă unsigned. Și poate că nu îți este foarte clară folosirea tipurilor de date în C.
În această săptămână dorim să clarificăm declararea variabilelor în C și folosirea tipurilor de date. Mai mult, punctăm acele probleme frecvent întâlnite atunci când variabilele signed interacționează cu variabilele unsigned în cod. De ce obținem rezultate dubioase atunci când realizăm SHIFT LEFT/RIGHT cu cel puțin unul dintre operanzi număr negativ? De ce se comportă ciudat programul atunci când comparăm o variabilă signed cu o variabilă unsigned? Ce este situația de overflow și cum o putem preveni? Să începem prin a discuta despre cele mai folosite tipuri de date în C.
Tipuri de date în C
După cum am văzut atunci când am detaliat reprezentarea numerelor în calculator și lucrul pe biți, numerele sunt reprezentate în calculator în binar. Ele sunt o înșiruire de 8, 16, 32 sau 64 de biți și dimensiunea maximă a unui număr depinde de arhitectura procesorului.
De asemenea, ne amintim că atunci când un număr întreg este reprezentat cu semn, cel mai semnificativ bit (primul bit de la stânga la dreapta) este considerat bit de semn. Dacă acesta are valoarea 0, atunci numărul este pozitiv, iar în caz contrar, numărul este considerat negativ. Majoritatea calculatoarelor folosesc complementul față de 2 pentru a reprezenta un număr negativ, întrucât facilitează realizarea operațiilor aritmetice.
Întrucât în reprezentarea numerelor întregi cu semn primul bit reprezintă semnul, acesta nu face parte din valoarea numărului. Acest lucru înseamnă că dacă numărul are N biți, doar N-1 dintre aceștia vor fi folosiți pentru a desemna valoarea acestuia. Astfel, atunci când o variabilă este declarată fără semn, aceasta poate acoperi o mulțime mai mare de numere naturale.
Intervalul de valori al unui tip de date
Fiecare tip de date are un interval din cadrul căruia îi pot fi asignate valori, iar acesta depinde de numărul de biți pe care este reprezentat numărul. Fiecare bit poate lua valoarea 0 sau 1, așadar 2 valori. Având N biți, pentru o variabilă există 2 * 2 * … * 2 (de N ori) = 2 N valori diferite pe care le poate lua.
Să urmărim exemplul reprezentării unui număr pe 3 biți în cele două cazuri, atunci când variabila este unsigned și atunci când variabila este signed.
Variabilă unsigned
Care este valoarea minimă pe care o poate lua?
0002 = 010
Care este valoarea maximă pe care o poate lua?
1112 = 710
Variabilă signed – folosim reprezentarea în complement față de 2
Care este valoarea minimă pe care o poate lua?
1002 = -410
Care este valoarea maximă pe care o poate lua?
0112 = 310
Observăm că:
7 = 23 – 1
-4 = -23 – 1
3 = 23 – 1 – 1
Folosind aceste exemple, putem deduce valorile minime și maxime posibile pentru numere reprezentate pe N biți.
În cazul variabilelor unsigned, cea mai mică valoare posibilă de asignat este 0, ceea ce înseamnă că cea mai mare valoare la care poate ajunge este 2N– 1.
În cazul variabilelor signed, intervalul este distribuit de la valoarea -2N-1 la 2N – 1– 1. Vom exemplifica în cele ce urmează acest aspect.
În mod automat, atunci când o variabilă este declarată fără cuvântul cheie unsigned, aceasta este considerată ca fiind cu semn. Astfel, ea poate lua atât valori pozitive, cât și negative și deci este ca și cum am folosi în declarare cuvântul cheie signed.
Regăsim în următorul tabel câteva tipuri de date uzuale din C:
Tip de date | Dimensiune | Interval de valori |
char | 8 biți (1 byte) | [-127, 128] |
unsigned char | 8 biți (1 byte) | [0, 255] |
short | 16 biți (2 bytes) | [-32 768, 32 767] |
unsigned short | 16 biți (2 bytes) | [0, 65 535] |
int | 32 biți (4 bytes) | [-2 147 483 648, 2 147 483 647] |
unsigned int | 32 biți (4 bytes) | [0, 4 294 967 295] |
Este de menționat faptul că în arhitecturile vechi ale calculatoarelor, pe când procesoarele erau pe 16 biți, dimensiunea tipului de date int era de 2 bytes. Acest lucru depinde de implementarea din compilator. Totuși, în prezent, int este preponderent folosit ca având 4 bytes.
Drept concluzie, pe caz general, o variabilă reprezentată pe N biți poate lua valori astfel:
cu semn: [-2N – 1, 2N – 1– 1]
fara semn: [0, 2N– 1]
Să luăm exemplul numărului 42 și să vedem cum arată acesta în calculator în funcție de tipul de date:
unsigned short : 00000000 00101010
unsigned int : 00000000 00000000 00000000 00101010
Dacă acesta ar fi negativ, cum ar arăta în reprezentarea în complement față de 2 în calculator?
- short :
Luăm valoarea fără semn a numărului
00000000 00101010
Scădem 1 și obținem:
00000000 00101001
Aplicăm operația NOT pe fiecare bit:
11111111 11010110
Așadar, -4210 = 11111111 110101102
- int :
Analog, aplicăm pașii de mai sus și obținem:
-4210 = 11111111 11111111 11111111 110101102
Situația de Integer overflow
Ce este fenomenul de integer overflow?
Este situația în care valoarea unei variabile depășește valoarea minimă sau valoarea maximă a intervalului pe care este definită. Consecința acestui lucru este faptul că se păstrează doar atâți biți cât pot fi folosiți în reprezentarea variabilei, de la dreapta spre stânga.
Să considerăm numărul 3, reprezentat pe 4 biți:
310 = 00112
Doresc să îl înmulțesc cu 8. Mai țineți minte din articolul trecut cum puteam realiza înmulțirea cu un număr putere a lui 2 rapid? Folosim operația SHIFT LEFT.
8 = 23 => 3 * 8 = 3 * 23 = 0011 << 3 = 1 1000
Se observă faptul că rezultatul obținut nu poate fi reprezentat pe 4 biți și astfel, se produce fenomenul de overflow.
Cum gestionează fenomenul de integer overflow C?
Pentru numerele signed, această situație va produce un comportament nedefinit al programului (undefined behavior). Astfel, în funcție de implementarea compilatorului, variabila poate lua orice valoare.
Pentru numerele unsigned, se va aplica operația de modulo aritmetic. Astfel, rezultatul reprezintă restul împărțirii numărului cu overflow la 2N.
Folosim exemplul de mai sus și avem numărul cu overflow 1 101111012 = 44510
Numărul trebuie reprezentat pe 8 biți, deci împărțim acest număr la 28 = 256.
Rezultatul se obține astfel:
445 % 256 = 189
Această metodă de lucru este numită wrap around.
Compararea unei variabile unsigned cu o variabilă signed
O problemă des întâlnită în programe este cea a comparării greșite a variabilelor. Pentru început să analizăm output-ul următorului program:
OUTPUT:
2 este mai mic decat -2.
Weird, right? După cum se observă, programul afișează faptul că 2 este mai mic decât -2.
Explicația este aceea că atunci când una dintre variabilele comparate este declarată unsigned, ambele variabile sunt văzute ca fiind unsigned. Astfel, deși în aritmetică 2 este mai mare ca -2, numărul negativ este convertit în variabilă fără semn și va căpăta o valoare foarte mare.
Urmărind reprezentarea în complement față de 2, folosită de majoritatea calculatoarelor în prezent, -2 va fi următoarea secvență de biți:
11111111 11111111 11111111 11111110
Atunci cănd este văzut drept un număr fără semn, valoarea lui va fi 4294967294.
Prin comparație, când ambele variabile sunt signed, programul se va comporta cum ne așteptăm:
OUTPUT:
2 este mai mare decat -2.
Acest lucru este valabil și la celelalte operații efectuate între o variabilă signed și o variabilă unsigned. Așa cum am menționat mai sus, compilatorul va transforma valoarea variabilei cu semn într-o valoare fără semn.
Shiftarea folosind numere negative
Atunci când se realizează operația de SHIFT LEFT sau SHIFT RIGHT având cel puțin unul dintre operanzi negativ, programul are un comportament nedefinit. Modul în care este gestionat acest lucru depinde de implementarea din compilator.
Să observăm câteva exemple ce au un comportament nedefinit la rularea programului:
134 << -1
-12 >> 2
-256 << -4
Nice to know
Când este indicat să folosim variabila signed?
- Vrem să folosim numere negative
- Știm că în urma efectuării unor instrucțiuni din program valoarea variabilei va putea fi negativă
Când este indicat să folosim variabila unsigned?
- Vrem să reprezentăm numere naturale
- Avem nevoie să acoperim un interval mai mare de valori naturale
- Lucrul cu indecși (ex. Index în instrucțiuni repetitive, sau la accesarea elementelor în vectori
Sumar. Cuvinte cheie
Ca o scurtă recapitulare, cele mai importante aspecte sunt:
- Variabilele signed se folosesc de primul bit pentru a marca semnul numărului.
- Intervalul valorilor pe care le poate lua o variabilă pe N biți este:
- [0, 2N – 1], pentru numere cu semn
- [-2N-1, 2N-1 – 1], pentru numere fără semn
- Situația de integer overflow este gestionată folosind operatorul modulo pentru variabilele unsigned și are un comportament nedefinit pentru variabilele signed.
- La compararea sau efectuarea de operații aritmetice între o variabilă signed și o variabilă unsigned, calculatorul va interpreta variabilele ca fiind ambele unsigned.
- Folosirea operațiilor de SHIFT LEFT sau SHIFT RIGHT având cel puțin unul dintre operanzi număr negativ are comportament nedefinit.