Reprezentare intern˘a. Operatori pe biti. Tablouri

bigfoot.cs.upt.ro

Reprezentare intern˘a. Operatori pe biti. Tablouri

Limbaje de programare

Reprezentare internă. Operatori pe bit¸i. Tablouri

29 octombrie 2012


Numerele nu sunt la fel în matematică ¸si C

În matematică:

numerele întregi Z ¸si reale IR au domeniu infinit de valori

numerele reale au precizie infinită (oricâte cifre zecimale)

În C:

numerele ocupă un loc finit în memorie

⇒ au domeniu de valori finit ¸si precizie finită (realii)

Pentru a lucra corect cu numere, trebuie să înt¸elegem:

cât loc ocupă ¸si cum se reprezintă în memorie

care sunt limitările de mărime ¸si precizie

ce erori de depă¸sire ¸si rotunjire pot apărea


Reprezentarea obiectelor în memorie

Orice valoare (constantă, parametru, variabilă) ocupă loc în memorie.

bit = cea mai mică unitate de memorare, are două valori (0 sau 1)

octet (byte) = grup de 8 bit¸i, destul pentru a memora un caracter

E cea mai mică unitate de memorie adresabilă direct

(se poate citi/scrie/folosi în expresii independent;

nu putem citi/scrie/calcula doar cu un bit)

Operatorul sizeof: dimensiunea în octet¸i a unui tip / unei valori

sizeof(tip) sau sizeof expresie

sizeof(char) e 1: un caracter ocupă (de obicei) un octet

Un întreg are sizeof(int) octet¸i ⇒ 8*sizeof(int) bit¸i

sizeof e un operator, evaluat la compilare. NU e o funct¸ie


Reprezentarea binară a numerelor

În memoria calculatorului, numerele se reprezintă în binar (baza 2)

Întreg fără semn, cu k cifre binare (bit¸i) k = 8 * sizeof(tip nr)

ck−1ck−2 . . . c1c0 (2) = ck−1 · 2 k−1 + . . . + c1 · 2 1 + c0 · 2 0

ck−1 = bitul cel mai semnificativ (superior)

c0 = bitul cel mai put¸in semnificativ (inferior)

Domeniul de valori: de la 0 la 2 k − 1 Ex: 11111111 e 255

c0 = 0 ⇒ număr par; c0 = 1 ⇒ număr impar

Întregi cu semn: reprezentat¸i în complement de 2

dacă bitul superior e 1, numărul e negativ

⇒ Domeniul de valori: de la −2 k−1 la 2 k−1 − 1

0ck−2 . . . c1c0 (2) = ck−2 · 2 k−2 + . . . + c1 · 2 1 + c0 · 2 0 (≥ 0)

1ck−2 . . . c1c0 (2) = −2 k−1 + ck−2 · 2 k−2 + . . . + c0 · 2 0 (< 0)

Exemple (pe 8 bit¸i):

11111111 e -1 111111110 e -2 10000000 e -128


Tipuri întregi

Înainte de int se pot scrie calificatori pentru:

dimensiune: short, long (în C99 ¸si long long)

semn: signed (implicit, dacă e omis), unsigned

Le putem combina; putem omite int. Ex: unsigned short

char: signed char [-128, 127] sau unsigned char [0, 255]

int, short: ≥ 2 octet¸i, acoperă sigur [−2 15 (-32768), 2 15 − 1]

long: ≥ 4 octet¸i, acoperă sigur [−2 31 (-2147483648) , 2 31 − 1]

long long: ≥ 8 octet¸i, acoperă sigur [−2 63 , 2 63 − 1]

Tipurile cu ¸si fără semn au aceea¸si dimensiune

sizeof(short)≤sizeof(int)≤sizeof(long)≤sizeof(long long)

Pentru limite există constante (macrouri) definite în limits.h

INT_MIN, INT_MAX, UINT_MAX (ex. 65535), la fel pt. CHAR, SHRT, LONG

C99: stdint.h are tipuri de dimensiune precizată (cu/fără semn)

int8_t, int16_t, int32_t, int64_t,

uint8_t, uint16_t, uint32_t, uint64_t


sizeof ¸si scrierea de programe portabile

Dimensiunea tipurilor depinde de sistem (procesor, compilator):

⇒ folosim sizeof ca să aflăm cât¸i octet¸i are un tip / o variabilă

NU scriem programe presupunând că un tip ar avea 2, 4, ... octet¸i

(programul va rula gre¸sit pe un sistem cu alte dimensiuni)

#include

#include

int main(void)

{

printf("Aici, intregii au %zu octeti\n", sizeof(int));

printf("Cel mai mic intreg negativ: %d\n", INT_MIN);

printf("Cel mai mare intreg fara semn: %u\n", UINT_MAX);

return 0;

}


Constante de tipuri întregi

Constante întregi: se pot scrie în program doar în baza 8, 10, 16

în baza 10: scrise obi¸snuit; ex. -5

în baza 8: cu prefix cifra zero; ex. 0177 (127 zecimal)

în baza 16: cu prefix 0x sau 0X; ex. 0xA9 (169 zecimal)

sufix u sau U pentru unsigned, ex. 65535u

sufix l sau L pentru long ex. 0177777L

Constante de tip caracter

caractere tipăribile, între ghilimele simple: ’0’, ’!’, ’a’

caractere speciale: ’\0’ nul ’\a’ alarm

’\b’ backspace ’\t’ tab ’\n’ newline

’\v’ vert. tab ’\f’ form feed ’\r’ carriage return

’\"’ ghilimele ’\’’ apostrof ’\\’ backspace

caractere scrise în octal (max. 3 cifre), ex: ’\14’

caractere scrise în hexazecimal (prefix x), ex. ’\xff’

Tipul caracter e tot un tip întreg (de dimensiuni mai mici).

O constantă caracter e convertită automat la int în expresii.


Reprezentarea numerelor reale

În baza 10, am învăt¸at reprezentarea în format ¸stiint¸ific:

6.022 · 10 23 , 1.6 · 10 −19 : o cifră, zecimale, 10 cu exponent

În calculator, se reprezintă în baza 2, cu semn, exponent, ¸si mantisă

(−1) semn ∗ 2 exp ∗ 1.mantisa (2)

Pe bit¸i: S EEEEEEEE MMMMMMMMMMMMMMMMMMMMMMM

float: 4 octet¸i: 1+8+23 bit¸i; double: 8 octet¸i: 1+11+52 bit¸i

pt. 0 < E < 255 obt¸inem numerele (−1) S ∗ 2 E−127 ∗ 1.M (2)

pt. E = 0, numere f. mici (denormalizate): (−1) S ∗ 2 −127 ∗ 0.M (2)

mai avem: reprezentări pentru ±0, ±∞, erori (NaN)

Precizia numerelor reale e relativă la modulul lor (“virgulă mobilă”)

Ex: cel mai mic float > 1 e 1 + 2 −23 (ultima poz. în mantisă 1)

La numere mari, imprecizia absolută cre¸ste:

De ex. 2 24 + 1 = 2 24 ∗ (1 + 2 −24 ), ultimul bit nu are loc în mantisă

⇒ va fi rotunjit; nu tot¸i întregii pot fi reprezentat¸i ca float


Tipuri reale

Numerele reale: reprezentate ca semn · (1 + mantisa) · 2 exponent

Domeniul de valori e simetric fat¸ă de zero

Precizia e relativă la mărimea numărului (în modul)

Exemple de limite (float.h, compilator gcc pe 32 bit¸i):

float: 4 octet¸i, între cca. 10 −38 ¸si 10 38 , 6 cifre semnificative

FLT_MIN 1.17549435e-38F FLT_MAX 3.40282347e+38F

FLT_EPSILON 1.19209290e-07F // nr.min. cu 1+eps > 1

double: 8 octet¸i, între cca. 10 −308 ¸si 10 308 , 15 cifre semnificative

DBL_MIN 2.2250738585072014e-308 DBL_MAX 1.7976931348623157e+308

DBL_EPSILON 2.2204460492503131e-16 // nr.min. cu 1+eps > 1

long double: pentru precizie ¸si mai mare (12 octet¸i)

Constante reale: pot fi scrise în următoarele forme:

cu punct zecimal; optional semn ¸si exponent (prefix e sau E)

în mantisă, partea reală sau zecimală pot lipsi: 2. .5

Tip implicit: double; sufix f, F: float; l, L: long double

Se recomandă double pentru precizie suficientă în calcule.

funct¸iile din math.h: tip double, variante cu sufix: sin, sinf, sinl


Atent¸ie la depă¸siri ¸si precizie!

int (chiar long) au domeniu de valori mic (32 bit¸i: ± 2 miliarde)

Pentru multe calcule cu întregi mari (factorial, etc.), e insuficient

⇒ folosim reali (double): domeniu de valori mare

Realii au precizie limitată: dincolo de 1E16 tipul double nu mai

distinge doi întregi consecutivi !

O valoare zecimală nu e reprezentată neapărat precis în baza 2,

poate fi o fract¸ie periodică: 1.2 (10) = 1.(0011) (2)

printf("%f", 32.1f); va scrie 32.099998

În calcule: pierderi de precizie ⇒ rezultatul poate diferi de cel exact

⇒ înlocuim x==y cu fabs(x - y) < ceva foarte mic

pentru ceva foarte mic ales în funct¸ie de specificul problemei

Diferent¸e mai mici de limita preciziei nu se pot reprezenta

pentru x < DBL_EPSILON (cca. 10 −16 ) avem 1 + x == 1


La ce folosesc operatorii pe bit¸i ?

Pentru a accesa reprezentarea internă a numerelor

¸si a reprezenta/codifica ¸si prelucra eficient diverse tipuri de date.

O mult¸ime: reprezentată cu un bit pentru fiecare element posibil

(1 = este; 0 = nu este în mult¸ime)

⇒ mult¸ime de numere naturale mici (< 32): pe un uint32 t

Operat¸ii:

intersect¸ie (într-o mult¸ime S¸I în cealaltă),

reuniune (într-o mult¸ime SAU în cealaltă),

adăugare element (reuniune cu { element }) etc.

Data curentă se poate reprezenta pe 16 bit¸i:

ziua: 1-31 (5 bit¸i)

luna: 1-12 (4 bit¸i)

anul (de la 1900 la 2027): 7 bit¸i

⇒ ne trebuie operat¸ii ca să extragem ziua/luna/anul dintr-o

valoare de 16 bit¸i (short sau uint16 t)


Operatori pe bit¸i

Oferă acces la reprezentarea binară a datelor în memorie

facilităt¸i apropiate limbajului ma¸sină (de asamblare)

Pot fi folosit¸i doar pentru operanzi de orice tip întreg

& S¸I bit cu bit (1 doar când ambii bit¸i sunt 1)

| SAU bit cu bit (1 dacă cel put¸in un bit e 1)

^ SAU exclusiv bit cu bit (1 dacă exact unul din bit¸i e 1)

~ complement bit cu bit (valoarea opusă: 1 pt. 0, 0 pt. 1)

> deplasare la dreapta cu număr indicat de bit¸i

(se introduc la stânga bit¸i de 0 dacă numărul e fără semn)

altfel depinde de implementare (ex. se repetă bitul de semn)

⇒ cod neportabil pe alt sistem, nu folosit¸i pt. nr. cu semn!

Tot¸i operatorii lucrează simultan pe tot¸i bit¸ii operanzilor.

nu modifică operanzii, ci dau un rezultat (ca ¸si alt¸i operatori uzuali)


Proprietăt¸i ale operatorilor pe bit¸i

n > k are valoarea n/2 k (pentru n fără semn; împărt¸ire întreagă)

Deci 1


Crearea ¸si selectarea unor tipare de bit¸i

& cu 1 nu schimbă & cu 0 face 0

| cu 0 nu schimbă | cu 1 face 1

Valoarea dată de bit¸ii 0-3 din n: S¸I cu 0 . . . 01111 (2) n & 0xF

Resetăm bit¸ii 2, 3, 4: S¸I cu ˜0 . . . 011100 (2) n &= ~0x1C

Setăm bit¸ii 1-4: SAU cu 11110 (2) n = n | 0x1E n |= 036

Schimbăm bit¸ii 0-2 din n: XOR cu 0 . . . 0111 (2) n = n ^ 7

⇒ alegem operat¸ia ¸si masca (valoarea, scrisă u¸sor în hexa/octal)

Întregul cu tot¸i bit¸ii 1: ~0 (cu semn) sau ~0u (fără semn)

k bit¸i din dreapta 0, restul 1: ~0


Conversii explicite ¸si implicite de tip

Conversii implicite: în expresii, char, short se convertesc la int

Tipul de mărime mai mică e convertit la cel de mărime mai mare

La dimensiuni egale, tipul cu semn e convertit la tipul fără semn

În expresii mixte întreg-real, întregii sunt convertit¸i la reali

Conversii la atribuire: se trunchiază când membrul stâng e mai mic!

char c; int i; c = i; // pierde bit¸ii superiori din i

!!! Partea dreaptă e evaluată întâi, independent de cea stângă

unsigned eur_rol = 43000, usd_rol = 31000 // curs valuta

double eur_usd = eur_rol / usd_rol; // rezultatul e 1 !!!

(împăt¸ire întreagă înainte de conversia prin atribuire la real)

Atribuind real la întreg, se trunchiază spre zero (partea fract¸ionară)

Conversia explicită (type cast): ( numetip )expresie

converte¸ste expresia ca ¸si prin atribuire la o valoare de tipul dat

eur_usd = (double)eur_rol / usd_rol // real/intreg dă real


Atent¸ie la semn ¸si depă¸sire

ATENT¸IE char poate fi signed sau unsigned, depinde de sistem

⇒ valori diferite dacă bitul 7 e 1, ¸si în conversia la int

getchar/putchar lucrează cu unsigned char convertit la int

ATENT¸IE: practic orice operat¸ie aritmetică poate provoca depă¸sire!

printf("%d\n", 1222000333 + 1222000333); // -1850966630

(rezultatul are cel mai semnificativ bit 1, ¸si e considerat negativ)

printf("%u\n", 2154000111u + 2154000111u); // trunchiat: 4032926

ATENT¸IE la comparat¸ii ¸si conversii cu semn / fără semn

if (-5 > 4333222111u) printf("-5 > 4333222111 !!!\n");

pentru că -5 convertit la unsigned are valoare mai mare !

Comparat¸ii corecte între int i ¸si unsigned u:

if (i < 0 || i < u) respectiv if (i >= 0 && i >= u)

(compară i cu u doar dacă i e nenegativ)


Declararea tablourilor

Tablou (vector) = un ¸sir de elemente de acela¸si tip de date

Tabloul x asociază la un indice n, o valoare x[n]

În matematică, acela¸si lucru face un ¸sir xn sau o funct¸ie x(n)

Declarare: tip nume-tablou[nr-elem];

double x[20]; int mat[10][20];

Init¸ializare: între acolade, cu virgule:

int a[4] = { 0, 1, 4, 9 };

Dimensiunea tabloului (nr. de elemente) = o constantă pozitivă

C99 acceptă ¸si dimensiuni variabile, cu valoare cunoscută în momentul

declarării

void f(int n) { int tab[n]; /* n e cunoscut la apel */ }

Sintaxa declarat¸iei: tip a[dim]; spune că a[indice] are tipul tip


Folosirea tablourilor

Un element de tablou nume-tab[indice] e folosit ca orice variabilă

are o valoare, poate fi folosit în expresii, poate fi atribuit

x[3] = 1; n = a[i]; t[i] = t[i + 1]

Indicele poate fi orice expresie cu valoare întreagă

ATENT¸IE! În C, indicii de tablou sunt de la zero la dimensiune - 1

int a[4]; cont¸ine a[0], a[1], a[2], a[3], NU există a[4]

Exemplu de traversare ¸si atribuire a unui tablou:

int a[10]; for (int i = 0; i < 10; ++i) a[i] = i + 1;


Constante simbolice ca dimensiuni de tablou

E util să folosim un nume de constantă (macro) pentru dimensiune

#define NUME val

Preprocesorul C înlocuie¸ste NUME în sursă cu val înaintea compilării

Constantele definite astfel se scriu deobicei cu litere MARI

#define LEN 30

double t[LEN];

for (int i = 0; i < LEN; ++i) { // tabelam sin cu pas 0.1

t[i] = sin(0.1*LEN); printf("%f ", t[i]);

}

Programul e mai u¸sor de citit, e clar că LEN e lungimea tabloului.

Dacă vrem altă dimensiune, modificăm programul doar într-un loc

⇒ evităm gre¸selile din neatent¸ie sau uitare


Exemplu: Calculul primelor numere prime

#include

#define MAX 100 // preprocesorul inlocuieste MAX cu 100

int main(void) {

unsigned p[MAX] = {2}; // 2 e intaiul prim

unsigned cnt = 1, n = 3; // un numar prim, 3 e candidat

do {

for (int j = 0; n % p[j]; ++j) // cat nu se imparte

if (p[j]*p[j] > n) { // nu mai pot fi alti divizori

p[cnt++] = n; break; // memoreaza, iese din ciclu

}

n += 2; // incearca urmatorul numar impar

} while (cnt < MAX); // pana nu e plin tabloul

for (int j = 0; j < MAX; ++j)

printf("%d ", p[j]);

putchar(’\n’);

return 0;

}


Variabile ¸si adrese

Orice variabilă x are o adresă: acolo e memorată valoarea ei

Operatorul prefix & dă adresa operandului: &x e adresa variabilei x

Operandul lui &: orice lvalue (destinat¸ie de atribuiri): variabile,

elemente de tablou. NU au adrese: alte expresii, constantele

Numele unui tablou e chiar adresa tabloului.

Fie int a[6]; Numele a reprezintă adresa tabloului.

Numele a NU reprezintă toate elementele împreună!

O adresă poate fi tipărită (în baza 16) cu formatul %p în printf

#include

int main(void) {

double d; int a[6];

printf("Adresa lui d: %p\n", &d); // folosim operatorul &

printf("Adresa lui a: %p\n", a); // a e adresa, nu trebuie &

return 0;

}


Tablouri ca parametri la funct¸ii

Declarat¸ia unui tablou alocă ¸si memorie pentru elementele sale

dar numele reprezintă adresa sa ¸si nu tabloul ca tot unitar

⇒ numele tabloului NU poartă informat¸ii despre dimensiunea lui

except¸ie: sizeof(numetab) este nr-elem * sizeof(tip-elem)

La funct¸ii se transmit numele tabloului (adresa) S¸I lungimea sa

NU scriem lungimea între [] la parametru, nu se ia în

considerare

#include

void printtab(int t[], unsigned len) {

for (int i = 0; i < len; ++i) printf("%d ", t[i]);

putchar(’\n’);

}

int main(void) {

int prim[10] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29 };

printtab(prim, 10); // ATENTIE: NU prim[10], NU prim[]

return 0;

}


Tablouri ca parametri la funct¸ii

Transmiterea parametrilor în C se face prin valoare

⇒ un parametru tablou e transmis prin valoarea adresei sale

Având adresa, funct¸ia poate citi S¸I scrie elementele tabloului

void sumvect(double a[], double b[], double r[], unsigned len)

{

for (unsigned i = 0; i < len; ++i) r[i] = a[i] + b[i];

}

#define LEN 3 // macro pt. lungimea tablourilor

int main(void) {

double a[LEN] = {0, 1.41, 1}, b[LEN] = {1, 1.73, 1}, c[LEN];

sumvect(a, b, c, LEN);

return 0;

}

Init¸ializare

Tablourile neinit¸ializate au elemente de valoare necunoscută.

Tablourile init¸ializate part¸ial au restul elementelor nule.

More magazines by this user
Similar magazines