Tipuri de date
Fiecare variabilă are un anumit tip. Și acest tip determină ce valori poate avea variabila, ce operații pot fi efectuate asupra ei și câți octeți va ocupa în memorie. În limbajul C++ sunt definite următoarele tipuri de date de bază: tipul logic bool, tipurile întregi, tipurile de numere cu virgulă mobilă, tipurile de caractere. Să analizăm aceste grupuri separat.
Tipul logic
Tipul logic bool poate stoca una dintre cele două valori: true (adevărat, corect) și false (fals, incorect). De exemplu, să definim o pereche de variabile de acest tip și să le afișăm valorile în consolă:
#include <iostream>
int main()
{
bool isAlive {true};
bool isDead {false};
std::cout << "isAlive: " << isAlive << "\n";
std::cout << "isDead: " << isDead << "\n";
}
La afișarea valorii de tip bool, aceasta este convertită în 1 (dacă este true) și 0 (dacă este false). De regulă, acest tip este utilizat în principal în expresii condiționale, care vor fi analizate mai târziu.
Valoarea implicită pentru variabilele de acest tip este false.
Tipuri întregi
Numerele întregi în limbajul C++ sunt reprezentate prin următoarele tipuri:
- signed char: reprezintă un singur caracter. Ocupă în memorie 1 byte (8 biți). Poate stoca orice valoare din intervalul de la -128 până la 127
- unsigned char: reprezintă un singur caracter. Ocupă 1 byte (8 biți). Interval: de la 0 până la 255
- char: reprezintă un caracter în codificarea ASCII. Ocupă 1 byte (8 biți). Poate stoca o valoare între -128 și 127 sau 0 și 255, în funcție de compilator. Deși are același interval ca signed char, nu este echivalent — poate fi tratat ca signed sau unsigned în funcție de compilator. Este destinat stocării codului numeric al unui caracter
- short: număr întreg între –32.768 și 32.767. Ocupă 2 bytes (16 biți). Sinonime: short int, signed short int, signed short
- unsigned short: număr întreg între 0 și 65.535. Ocupă 2 bytes. Sinonim: unsigned short int
- int: număr întreg. Pe arhitecturi de 16 biți: 2 bytes → interval: –32.768 până la 32.767. Pe arhitecturi de 32 biți: 4 bytes → interval: –2.147.483.648 până la 2.147.483.647. Dimensiunea trebuie să fie ≥ short și ≤ long. Sinonime: signed int, signed
- unsigned int: număr întreg pozitiv. 2 bytes → 0 – 65.535, 4 bytes → 0 – 4.294.967.295, Sinonim: unsigned.
- long: 4 bytes → interval: –2.147.483.648 până la 2.147.483.647, 8 bytes (pe unele arhitecturi) → –9.223.372.036.854.775.808 până la +9.223.372.036.854.775.807, Sinonime: long int, signed long int, signed long.
- unsigned long: Interval: 0 – 4.294.967.295, 4 bytes, Sinonim: unsigned long int
- long long: Interval: –9.223.372.036.854.775.808 până la +9.223.372.036.854.775.807, 8 bytes, Sinonime: long long int, signed long long int, signed long long
- unsigned long long: Interval: 0 – 18.446.744.073.709.551.615, De obicei, 8 bytes, Sinonim: unsigned long long int
Pentru reprezentarea numerelor în C++ se folosesc literali întregi cu sau fără semn, cum ar fi -10 sau 10. De exemplu, să definim o serie de variabile de tipuri întregi și să le afișăm în consolă:
#include <iostream>
int main()
{
signed char num1{ -64 };
unsigned char num2{ 64 };
short num3{ -88 };
unsigned short num4{ 88 };
int num5{ -1024 };
unsigned int num6{ 1024 };
long num7{ -2048 };
unsigned long num8{ 2048 };
long long num9{ -4096 };
unsigned long long num10{ 4096 };
std::cout << "num1 = " << num1 << std::endl;
std::cout << "num2 = " << num2 << std::endl;
std::cout << "num3 = " << num3 << std::endl;
std::cout << "num4 = " << num4 << std::endl;
std::cout << "num5 = " << num5 << std::endl;
std::cout << "num6 = " << num6 << std::endl;
std::cout << "num7 = " << num7 << std::endl;
std::cout << "num8 = " << num8 << std::endl;
std::cout << "num9 = " << num9 << std::endl;
std::cout << "num10 = " << num10 << std::endl;
}
Dar merită menționat că toți literalii întregi, în mod implicit, au tipul int. Astfel, în exemplele de mai sus, variabilelor de diferite tipuri li s-au atribuit diverse valori — 64, -64, 88, -88, 1024 ș.a.m.d. Însă toți acești literali întregi au, în esență, tipul int.
Totuși, putem folosi și literali întregi de alte tipuri. Literalii întregi fără semn (care corespund tipurilor unsigned) au sufixul u sau U. Literalii de tipurile long și long long au sufixele L/l și LL/ll, respectiv:
#include <iostream>
int main()
{
unsigned int num6{ 1024U }; // U - unsigned int
long num7{ -2048L }; // L - long
unsigned long num8{ 2048UL }; // UL - unsigned long
long long num9{ -4096LL }; // LL - long long
unsigned long long num10{ 4096ULL };// ULL - unsigned long long
std::cout << "num6 = " << num6 << std::endl;
std::cout << "num7 = " << num7 << std::endl;
std::cout << "num8 = " << num8 << std::endl;
std::cout << "num9 = " << num9 << std::endl;
std::cout << "num10 = " << num10 << std::endl;
}
Totuși, utilizarea sufixelor nu este obligatorie, deoarece, de regulă, compilatorul poate converti cu succes un literal întreg (care, tehnic, are tipul int) în tipul necesar, fără pierdere de informație.
Dacă numărul este mare, ne putem înșela ușor la introducerea lui. Pentru a îmbunătăți lizibilitatea numerelor, începând cu standardul C++14, în limbaj a fost adăugată posibilitatea de a separa cifrele unui număr folosind apostroful simplu '.
#include <iostream>
int main()
{
int num{ 1'234'567'890 };
std::cout << "num = " << num << "\n"; // num = 1234567890
}
Sisteme de numerație diferite
În mod implicit, toți literalii întregi standard reprezintă numere în sistemul zecimal, cu care suntem obișnuiți. Totuși, C++ permite utilizarea numerelor și în alte sisteme de numerație. Pentru a indica faptul că un număr este hexazecimal, se adaugă prefixul 0x sau 0X înaintea valorii. De exemplu:
int num1{ 0x1A }; // 26 – în sistem zecimal
int num2{ 0xFF }; // 255 – în sistem zecimal
int num3{ 0xFFFFFF }; // 16777215 – în sistem zecimal
Pentru a indica faptul că un număr este octal, se pune un zero (0) înaintea numărului. De exemplu:
int num1{ 034 }; // 26 – în sistem zecimal
int num2{ 0377 }; // 255 – în sistem zecimal
Literalii binari sunt precedați de prefixul 0b sau 0B:
int num1{ 0b11010 }; // 26 – în sistem zecimal
int num2{ 0b11111111 }; // 255 – în sistem zecimal
Toate aceste tipuri de literali acceptă, de asemenea, sufixele U / L / LL:
unsigned int num1{ 0b11010U }; // 26 – în sistem zecimal
long num2{ 0377L }; // 255 – în sistem zecimal
unsigned long num3{ 0xFFFFFFULL }; // 16777215 – în sistem zecimal
Numere cu virgulă mobilă
Pentru stocarea numerelor zecimale în C++ se folosesc numerele cu virgulă mobilă. Un număr cu virgulă mobilă constă din două părți: mantisa și exponentul. Ambele pot fi atât pozitive, cât și negative. Valoarea numărului este mantisa înmulțită cu zece la puterea exponentului.
De exemplu, numărul 365 poate fi scris ca număr cu virgulă mobilă în felul următor:
3.650000E02
În calitate de separator între partea întreagă și fracționară se folosește simbolul punctului. Mantisa aici are șapte cifre zecimale – 3.650000, exponentul – două cifre 02. Litera E înseamnă exponent, după ea se indică exponentul (puterea lui zece), cu care se înmulțește partea 3.650000 (mantisa), pentru a obține valoarea dorită. Adică, pentru a reveni la reprezentarea zecimală obișnuită, trebuie să efectuăm următoarea operație:
3.650000 × 10² = 365
Alt exemplu – să luăm un număr mic:
-3.650000E-03
În acest caz avem de-a face cu numărul –3.65 × 10⁻³, ceea ce este egal cu –0.00365. Aici observăm că, în funcție de valoarea exponentului, punctul zecimal „plutește”. De fapt, din acest motiv ele se numesc numere cu virgulă mobilă.
Totuși, deși o astfel de notare permite reprezentarea unui interval foarte mare de valori, nu toate aceste valori pot fi reprezentate cu precizie completă; numerele cu virgulă mobilă sunt, în general, o aproximare a valorii exacte. De exemplu, numărul 1254311179 ar arăta astfel: 1.254311E09. Totuși, dacă îl convertim înapoi în notație zecimală, va deveni 1254311000. Iar asta nu este același lucru cu 1254311179, deoarece am pierdut ultimele trei cifre.
În limbajul C++ există trei tipuri pentru reprezentarea numerelor cu virgulă mobilă:
- float: reprezintă un număr real în precizie simplă cu virgulă mobilă, în intervalul de ±3.4E−38 până la 3.4E+38. Ocupă în memorie 4 bytes (32 biți)
- double: reprezintă un număr real în precizie dublă cu virgulă mobilă, în intervalul de ±1.7E−308 până la 1.7E+308. Ocupă în memorie 8 bytes (64 biți)
- long double: reprezintă un număr real în precizie extinsă, cu virgulă mobilă, de cel puțin 8 bytes (64 biți). În funcție de dimensiunea ocupată în memorie, intervalul valorilor posibile poate varia
În reprezentarea binară internă, fiecare număr cu virgulă mobilă constă din un bit de semn, urmat de un număr fix de biți pentru exponent și un set de biți pentru stocarea mantisei. În numerele de tip float, 1 bit este pentru semn, 8 biți pentru exponent și 23 pentru mantisă, ceea ce în total oferă 32 de biți. Mantisa permite determinarea preciziei numărului în forma a 7 cifre zecimale.
În numerele de tip double, 1 bit de semn, 11 biți pentru exponent și 52 biți pentru mantisă, adică în total 64 de biți. O mantisă pe 52 de biți permite determinarea preciziei de până la 16 cifre zecimale.
Pentru tipul long double, structura depinde de compilatorul concret și implementarea acestui tip de date. Majoritatea compilatoarelor oferă precizie de până la 18–19 cifre zecimale (mantisă pe 64 de biți), în altele (precum Microsoft Visual C++) long double este echivalent cu tipul double.
În C++, literalii numerelor cu virgulă mobilă sunt reprezentate prin numere zecimale, care folosesc punctul ca separator între partea întreagă și fracționară:
double num {10.45};
Chiar și atunci când unei variabile i se atribuie un număr întreg, pentru a indica faptul că atribuim un număr cu virgulă mobilă, se folosește punctul:
double num1{ 1 }; // 1 – literal întreg
double num2{ 1. }; // 1. – literal al unui număr cu virgulă mobilă
Așadar, aici numărul 1. reprezintă un literal al unui număr cu virgulă mobilă și, în principiu, este echivalent cu 1.0.
În mod implicit, toate aceste numere cu punct sunt tratate ca fiind de tip double. Pentru a indica că un număr este de alt tip, se folosește sufixul f / F pentru float și l / L pentru long double:
float num1{ 10.56f }; // float
long double num2{ 10.56l }; // long double
Ca alternativă, se poate folosi și notația exponențială:
double num1{ 5E3 }; // 5E3 = 5000.0
double num2{ 2.5e-3 }; // 2.5e-3 = 0.0025
Dimensiuni ale tipurilor de date
La enumerarea tipurilor de date a fost indicată dimensiunea pe care acestea o ocupă în memorie, însă standardul limbajului stabilește doar valorile minime care trebuie respectate. De exemplu, pentru tipurile int și short, valoarea minimă este 16 biți, pentru long – 32 de biți, iar pentru long double – 64 de biți. În același timp, dimensiunea tipului long nu trebuie să fie mai mică decât cea a lui int, dimensiunea lui int nu trebuie să fie mai mică decât a lui short, iar dimensiunea lui long double trebuie să fie cel puțin egală cu cea a lui double. Producătorii de compilatoare pot alege dimensiunile maxime ale tipurilor în funcție de capabilitățile hardware ale computerului.
De exemplu, compilatorul g++ pe Windows folosește 16 bytes pentru long double. Compilatorul din Visual Studio, care tot rulează pe Windows, precum și clang++ pe Windows, folosesc 8 bytes pentru long double. Așadar, chiar și pe aceeași platformă, compilatoare diferite pot trata în mod diferit dimensiunile unor tipuri de date. Totuși, în general, se folosesc dimensiunile menționate mai sus în descrierea tipurilor.
Există însă situații în care trebuie să cunoaștem exact dimensiunea unui anumit tip. Pentru aceasta, în C++ există operatorul sizeof(), care returnează dimensiunea în bytes a memoriei ocupate de o variabilă:
#include <iostream>
int main()
{
long double number {2};
std::cout << "sizeof(number) =" << sizeof(number);
}
Ieșirea în consolă la compilare cu g++:
sizeof(number) = 16
Tipuri de caractere
În C++ există următoarele tipuri de date pentru caractere:
- char: reprezintă un singur caracter în codificarea ASCII. Ocupă 1 byte (8 biți) în memorie. Poate stoca orice valoare din intervalul de la -128 la 127 sau de la 0 la 255
- wchar_t: reprezintă un caracter extins. Pe Windows ocupă 2 bytes (16 biți), pe Linux – 4 bytes (32 biți). Poate stoca orice valoare din intervalul de la 0 la 65.535 (la 2 bytes) sau de la 0 la 4.294.967.295 (la 4 bytes)
- char8_t: reprezintă un caracter în codificarea Unicode. Ocupă 1 byte în memorie. Poate stoca orice valoare din intervalul de la 0 la 256
- char16_t: reprezintă un caracter în codificarea Unicode. Ocupă 2 bytes (16 biți). Poate stoca orice valoare din intervalul de la 0 la 65.535
- char32_t: reprezintă un caracter în codificarea Unicode. Ocupă 4 bytes (32 biți). Poate stoca orice valoare din intervalul de la 0 la 4.294.967.295
Variabilă de tip char
Variabila de tip char stochează codul numeric al unui singur caracter și ocupă un byte. Standardul limbajului C++ nu specifică ce codificare a caracterelor trebuie folosită pentru caracterele de tip char, așadar producătorii de compilatoare pot alege orice codificare, însă de obicei este folosită ASCII. Ca valoare, o variabilă de tip char poate primi un caracter între ghilimele simple sau codul numeric al caracterului:
#include <iostream>
int main()
{
char a1 {'A'};
char a2 {65};
std::cout << "a1: " << a1 << std::endl;
std::cout << "a2: " << a2 << std::endl;
}
În acest caz, variabilele a1 și a2 vor avea aceeași valoare, deoarece 65 este codul numeric al caracterului "A" în tabelul ASCII. La afișarea în consolă cu ajutorul cout, în mod implicit este afișat caracterul.
În plus, în C++ pot fi folosite secvențe speciale de control, care încep cu o bară oblică (\) și sunt interpretate într-un mod special. De exemplu, "\n" reprezintă un salt de linie, iar "\t" – un tab.
Totuși, ASCII este de obicei potrivit pentru seturi de caractere ale limbilor care folosesc alfabetul latin. Dacă este necesar să lucrăm cu caractere din mai multe limbi simultan sau din limbi care nu folosesc alfabetul englez, cele 256 de coduri posibile pot să nu fie suficiente. În acest caz se folosește Unicode.
Unicode este un standard care definește un set de caractere și punctele lor de cod, precum și mai multe codificări diferite pentru aceste puncte de cod. Cele mai utilizate codificări sunt: UTF-8, UTF-16 și UTF-32. Diferența dintre ele constă în modul în care este reprezentat punctul de cod al unui caracter; însă valoarea numerică a codului pentru orice caracter rămâne aceeași în oricare dintre aceste codificări. Principalele diferențe:
- UTF-8 reprezintă un caracter ca o secvență de lungime variabilă, de la unu până la patru bytes. Setul de caractere ASCII apare în UTF-8 ca coduri pe un singur byte, care au aceleași valori ca și în ASCII. UTF-8 este, în prezent, cea mai populară codificare Unicode.
- UTF-16 reprezintă caracterele ca una sau două valori de 16 biți.
- UTF-32 reprezintă toate caracterele ca valori de 32 de biți.
În C++ există patru tipuri pentru stocarea caracterelor Unicode: wchar_t, char8_t, char16_t și char32_t (char16_t și char32_t au fost adăugate în C++11, iar char8_t – în C++20).
Tipul wchar_t
Tipul wchar_t este tipul de bază destinat seturilor de caractere al căror volum depășește un singur byte. De aici provine și denumirea sa: wchar_t – de la wide char („caracter larg”), pentru că acest caracter este „mai larg” decât un caracter obișnuit de un byte. Valorile de tip wchar_t sunt definite la fel ca și caracterele char, cu excepția faptului că ele sunt precedate de caracterul "L":
wchar_t a1 {L'A'};
De asemenea, se poate transmite codul caracterului.
wchar_t a1 {L'\x41'};
Valoarea încadrată în ghilimele simple reprezintă codul hexazecimal al caracterului. Bara oblică inversă indică începutul unei secvențe de control, iar x după această bară înseamnă că urmează un cod în sistem hexazecimal.
Trebuie avut în vedere că, pentru a afișa în consolă caractere de tip wchar_t, nu se folosește std::cout, ci fluxul std::wcout:
#include <iostream>
int main()
{
char h = 'H';
wchar_t i {L'i'};
std::wcout << h << i <<'\n';
}
În același timp, fluxul std::wcout poate lucra atât cu char, cât și cu wchar_t. Iar fluxul std::cout, pentru o variabilă de tip wchar_t, va afișa codul numeric al caracterului în locul caracterului propriu-zis.
Problema cu tipul wchar_t constă în faptul că dimensiunea lui depinde foarte mult de implementare și de codificarea utilizată. Codificarea este de obicei aleasă în funcție de codificarea preferată a platformei țintă. Astfel, pe Windows wchar_t are de regulă o lățime de 16 biți și este codificat folosind UTF-16. Majoritatea celorlalte platforme stabilesc dimensiunea la 32 de biți și folosesc UTF-32 ca metodă de codificare. Pe de o parte, acest lucru permite o mai bună integrare cu platforma respectivă. Pe de altă parte, însă, complică scrierea unui cod portabil între platforme. De aceea, în general, este adesea recomandat să se folosească tipurile char8_t, char16_t și char32_t. Valorile acestor tipuri sunt destinate stocării caracterelor în codificările UTF-8, UTF-16 sau UTF-32, respectiv, iar dimensiunile lor sunt aceleași pe toate platformele răspândite.
Pentru definirea caracterelor de tip char8_t, char16_t și char32_t se folosesc, respectiv, prefixele u8, u și U:
char8_t c{ u8'l' };
char16_t d{ u'l' };
char32_t e{ U'o' };
Trebuie menționat că, pentru afișarea în consolă a valorilor de tip char8_t / char16_t / char32_t, deocamdată nu există instrumente integrate precum std::cout / std::wcout.
Specificatorul auto
Uneori este dificil să determinăm tipul unei expresii. În acest caz, putem lăsa compilatorul să deducă singur tipul obiectului. Pentru aceasta se folosește specificatorul auto. În același timp, dacă definim o variabilă cu specificatorul auto, acea variabilă trebuie neapărat să fie inițializată cu o valoare:
auto number = 5; // number are tipul int
auto sum {1234.56}; // sum are tipul double
auto distance {267UL}; // distance are tipul unsigned long
Pe baza valorii atribuite, compilatorul va deduce tipul variabilei. Variabilele neinițializate cu specificatorul auto nu sunt permise:
auto number;