Sunteți pe pagina 1din 34

Limbaje de programare

Cuprins
Reprezentare internă
Operatori pe biți
Preprocesorul C
Recapitulare
Bibliografie selectivă
Reprezentare internă
Să ne amintim:
În matematică:
numerele întregi și reale 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 și precizie finită (numere reale)
Pentru a lucra corect cu numere, trebuie să înțelegem:
cât loc ocupă și cum se reprezintă în memorie
care sunt limitările de mărime și precizie
ce erori de depășire și rotunjire pot apărea
Reprezentare internă
Orice valoare (parametru, variabilă) ocupă loc în memorie.
◦ bit = cea mai mică unitate de memorare, are două valori posibile: 0 sau 1
◦ octet (byte) = grup de 8 biți, cea mai mică unitate care se poate scrie/citi în/din memorie

Operatorul sizeof ne dă dimensiunea în octeți (bytes) a unui tip sau a unei valori.
Exemplu: sizeof(’A’)==sizeof(char)==1 (un caracter se reprezintă pe 1 octet)
Dimensiunea tipurilor depinde de sistem (procesor, compilator):
ex. sizeof(int) poate fi 2, 4, 8, ...

sizeof este un operator, evaluat la compilare. NU este o funcție!


Reprezentare internă
În memorie, numerele se reprezintă în binar (baza 2)
Un întreg fără semn, cu k cifre binare (biți):
ck−1ck−2...c2c1c0
= 2k−1∗ ck−1 + 2k−2 ∗ ck−2 + . . . + 22 ∗ c2 + 2 ∗ c1 + 20 ∗ c0
= Σk−1 0 2i ∗ ci
Unde ck−1 e cifra binară (bitul) cea mai semnificativă, iar c0 cea mai puțin
semnificativă.
Ex: 110(2) = 6(10) 1100(2) = 12(10) 11101(2) = 29(10)
Reprezentare internă
În memorie, numerele se reprezintă în binar (baza 2).
Un întreg cu semn, se reprezintă în complement de 2:
◦ dacă este ≥ 0 (la fel ca un întreg fără semn) :
ck−1ck−2...c2c1c0
= 2k−1∗ ck−1 + 2k−2 ∗ ck−2 + . . . + 22 ∗ c2 + 2 ∗ c1 + 20 ∗ c0 = Σk−1 0 2i ∗ ci
◦ dacă este < 0:
1ck−2...c2c1c0
= -2k−1 + 2k−2 ∗ ck−2 + . . . + 22 ∗ c2 + 2 ∗ c1 + 20 ∗ c0 = -2k−1 +Σk−1 0 2i ∗ ci

Matematic reprezentarea unui număr negativ X în complement față de doi este:


2n-|X|
Reprezentare internă
În memorie, numerele se reprezintă în binar (baza 2).
Exemple de valori întregi cu semn (pe 8 biți):
11111111(2) = −1(10) 11111110(2) = −2(10) 10000000(2) = −128(10)
00000001(2) = 1(10) 00000010(2) = 2(10)
Domeniul de reprezentare pentru 8 biți cu semn este [-128;127] .
Reprezentare internă
În baza 10, cunoaștem reprezentarea în format științific:
6.022 · 1023, 1.6 · 10−19: o cifră, zecimale, 10 cu exponent

În calculator, se reprezintă în baza 2, cu semn, exponent, și mantisă:


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

Pe biți: S EEEEEEEE MMMMMMMMMMMMMMMMMMMMMMM


float – simplă precizie, 32 de biți:
1 bit de semn, 8 biți exponent, 23 mantisă. pt. 0 < E < 255: nr. = (−1) S ∗ 2 E−127 ∗ 1.M în rest: 0

double – dublă precizie, 64 de biți:


1 bit de semn, 11 biți exponent, 52 mantisă
Reprezentare internă
Avem reprezentări pentru ±0, ±∞, erori (NaN)
Precizia numerelor reale e relativă la modulul lor (“virgulă mobilă”)
Ex: cel mai mic float > 1 este 1 + 2−23 (ultima poziție în mantisă 1)
La numere mari, imprecizia absolută crește: De ex. 224 + 1 = 224 ∗ (1 + 2−24), ultimul
bit nu are loc în mantisă ⇒ va fi rotunjit;
Nu toți întregii pot fi reprezentați ca float.
Operatori pe biți
La ce folosesc operatorii pe biți ?
Pentru a accesa reprezentarea internă a numerelor și a reprezenta/codifica și
prelucra eficient diverse tipuri de date.
Operatorii pe biți:
◦ Oferă acces la reprezentarea binară a datelor în memorie
◦ Se pot folosi doar pentru operanzi de tip întreg
◦ Lucrează simultan pe toți biții operanzilor
Operatori pe biți
Operator Descriere Asociativitate
~ Complement faţă de 1 pe biţi dreapta-stânga
<< si >> Deplasare stânga/dreapta a biţilor stânga-dreapta
& ŞI pe biţi stânga-dreapta
^ SAU-EXCLUSIV pe biţi stânga-dreapta
| SAU pe biţi stânga-dreapta
&= și |= Atribuire cu ŞI/SAU dreapta-stânga
^= Atribuire cu SAU-EXCLUSIV dreapta-stânga
<<= şi >>= Atribuire cu deplasare de biţi dreapta-stânga
Operatori pe biți
Exemple de operații pe biți
ȘI SAU SAU EXCLUSIV
10110101 & 10110101 | 10110101∧
01110001 01110001 01110001
= 00110001 = 11110101 = 11000100
Complement pe biți:
∼ 10110101 = 01001010 ∼ 01110001 = 10001110
Deplasare la stânga:
10110101 << 1 = 01101010 10110101 << 2 = 11010100
Deplasare la dreapta:
10110101 >> 1 = 01011010 10110101 >> 2 = 00101101
Operatori pe biți
Proprietăți ale operatorilor pe biți:
◦ n << k are valoarea n · 2 k (dacă nu apare depășire)
◦ n >> k are valoarea n/2 k (pentru n fără semn; împărțire întreagă)
Deci, 1 << k ar doar bitul k pe 1 ⇒ este 2k pentru k < 8*sizeof(int)
◦ ~(1 << k) are doar bitul k pe 0, restul pe 1
Operatori pe biți
Proprietăți ale operatorilor pe biți:
◦ & cu 1 păstrează valoarea, & cu bitul 0 este întotdeauna 0
◦ n & (1 << k) testează dacă bitul k din n este 1
◦ n & ~(1 << k) resetează (pune pe 0) bitul k în rezultat
◦ | cu 0 păstrează valoarea, | cu bitul 1 este întotdeauna 1
◦ n | (1 << k) setează (pune pe 1) bitul k în rezultat
◦ ^ cu 0 păstrează valoarea, ^ cu 1 schimbă valoarea bitului în rezultat
◦ n ^ (1 << k) schimbă valoarea bitului k în rezultat
Operatori pe biți
Cum verificăm dacă bitul i dintr-un număr n este setat?
Pentru a verifica valoarea bitului i din numărul n, practic noi ar trebui să privim numărul astfel:
b7 b6 … bi … b1 b0
n * * … ? … *
Detectarea bitului:
pas 1: se aplică următoarea operația x = n & mask , unde mask = (1 « i)
b7 … bi+1 bi bi−1 … b0
n * … * ? * … *
mask 0 … 0 1 0 … 0 &
x 0 … 0 ? 0 … 0
pas 2: deoarece “?” poate avea două valori, dacă x == 0 , atunci bitul i este 0, dacă x > 0 , atunci
bitul i este 1
Operatori pe biți #include <stdio.h>
int is_set(char byte, int i) {
int mask = (1 << i);
Exemplu de verificare dacă return (byte & mask) != 0;
un bit i este setat. }

int main() {
char mybyte=7,i=2;
if (is_set(mybyte, i)) {
printf("bitul %d din octetul %d este setat!\n", i, mybyte);
} else {
printf("bitul %d din octetul %d NU este setat!\n", i, mybyte);
}

return 0;
}
Operatori pe biți #include <stdio.h>
Exemplu de setare a unui bit char set(char byte, int i) {
int mask = (1 << i);
return (byte | mask);
}

int main() {
char mybyte=4,i=3;
printf("Numarul este:%d\n",mybyte);
mybyte=set(mybyte,i);
printf("Numarul nou este:%d\n",mybyte);
return 0;
}
Operatori pe biți #include <stdio.h>
Exemplu de resetare a unui char reset(char byte, int i) {
bit int mask = (1 << i);
mask=~mask;
return (byte & mask);
}

int main() {
char mybyte=4,i=2;
printf("Numarul este:%d\n",mybyte);
mybyte=reset(mybyte,i);
printf("Numarul nou este:%d\n",mybyte);
return 0;
}
Preprocesorul C
Preprocesorul:
◦ este o componentă din cadrul compilatorului C
◦ realizează preprocesarea fișierelor sursă

În urma utilizării, toate instrucţiunile de preprocesare sunt înlocuite (substituite),


pentru a genera cod C „pur”.
Preprocesarea este o prelucrare exclusiv textuală a fişierului sursă.
=> Nu se fac nici un fel de verificări sintactice sau semantice asupra codului sursă, ci
doar sunt efectuate substituţiile din text.
=> Preprocesorul va prelucra şi fişiere fără nici un sens în C.
Preprocesorul C
Incluziune
Probabil cea mai des folosită instrucţiune de preprocesare este cea de incluziune,
de forma
#include <nume_fişier>
sau
#include "nume_fisier"
care are ca rezultat înlocuirea sa cu conţinutul fişierului specificat de nume_fişier.
Diferenţa dintre cele două versiuni este că cea cu paranteze unghiulare caută
nume_fişier doar în directorul standard de fişiere antet (numit deobicei include), iar
cea cu ghilimele caută atât în directorul include cât şi în directorul curent.
Preprocesorul C
Definirea de simboluri
Definirea de simboluri este cel mai des folosită în conjuncţie cu instrucţiunile de procesare
condiţionată, fiind folosită pentru activarea şi dezactivarea unor segmente de cod în funcţie
de prezenţa unor simboluri. Definirea unui simbol se face în cod cu instrucţiunea
#define SIMBOL
sau se poate realiza şi la compilare, prin folosirea flagului -D al compilatorului
Un simbol poate fi de asemenea „şters” folosind instrucţiunea
#undef SIMBOL
în cazul în care nu se mai doreşte prezenţa simbolului de preprocesor ulterior definirii sale.
Preprocesorul C #include <stdio.h>

// am definit VALOARE ca fiind 1


Exemplu: #define VALOARE 1

Dacă se simbolul CHANGE este definit // Verific daca este definit simbolul CHANGE
se va afișa 0, altfel se va afisa 1 #ifdef CHANGE
#undef VALOARE // sterg VALOARE
#define VALOARE 0 // redefinesc simbolul cu alta valoare
#endif

int main() {
printf("VALOARE = %d\n", VALOARE);
return 0;
}
Preprocesorul C
Definirea de macro-uri
Instrucţiunile de preprocesare mai pot fi folosite #define NMAX 100
şi pentru definirea de constante simbolice şi #define MMAX 10
macroinstrucţiuni. De exemplu #define LMAX 10000
#define IDENTIFICATOR valoare ...
int a[NMAX][MMAX];
va duce la înlocuirea peste tot în cadrul codului
sursă a şirului IDENTIFICATOR cu şirul valoare. ...
Înlocuirea nu se face totuşi în interiorul şirurilor int v[LMAX];
de caractere. ...
Un exemplu foarte bun este definirea unor
macro-uri pentru dimensiunile tablourilor alocate
static.
Preprocesorul C
Definirea de macro-uri
O macroinstrucţiune:
◦ este similară unei constante simbolice, ca definire, dar acceptă parametrii.
◦ este folosită în program în mod asemănător unei funcţii
◦ la compilare, ea este înlocuită în mod textual cu corpul ei.
În plus, nu se face nici un fel de verificare a tipurilor. Spre exemplu:
#define MAX(a, b) a > b ? a : b
va returna maximul dintre a şi b, iar
#define DUBLU(a) 2*a
va returna dublul lui a.
Preprocesorul C
Definirea de macro-uri
Deoarece preprocesarea este o prelucrare textuală a codului sursă, în cazul exemplului anterior,
macroinstrucţiunea în forma prezentată nu va calcula întotdeauna dublul unui număr.
Astfel, la o folosire de forma:
DUBLU(a + 3)
în pasul de preprocesare se va genera expresia
2*a+3
care bineînţeles că nu realizează funcţia dorită.
Pentru a evita astfel de probleme, este bine ca întotdeauna în corpul unui macro, numele
„parametrilor” să fie închise între paranteze:
#define SQUARE(a) (a)*(a)
Preprocesorul C
Instrucțiuni de compilare condiționată
Instrucţiunile de compilare condiţionată sunt folosite pentru a „ascunde” fragmente de cod
în funcţie de anumite condiţii. Formatul este următorul:
#if conditie
....
#else
....
#endif
unde conditie este este o expresie constantă întreagă.
#if conditie
...

Preprocesorul C #elif co#if conditie


...
#elif conditie2
Instrucțiuni de compilare condiționată ...
Pentru realizarea de expresii cu mai multe opţiuni #elif conditie3
se poate folosi şi forma #elif: ...
#else
De obicei condiţia testează existenţa unui simbol. ...
Scenariile tipice de folosire sunt:
#endif
◦ dezactivarea codului de debug o dată ce nditie2
problemele au fost remediate
...
◦ compilare condiţionată în funcţie de platforma #elif conditie3
de rulare
...
◦ prevenirea includerii multiple a fişierelor antet #else
...
#endif
Preprocesorul C
Instrucțiuni de compilare condiționată #ifndef _NUME_FISIER_ANTET_
Prevenirea includerii multiple a fişierelor antet se #define _NUME_FISIER_ANTET_
realizează astfel: /* corpul fisierului antet */
/* prototipuri de functii,
Astfel, la prima includere a fişierului antet, simbolul
declaratii de tipuri si de constante */
_NUME_FISIER_ANTET_ nu este definit.
Preprocesorul execută ramura #ifndef în care este #endif
definit simbolul _NUME_FISIER_ANTET_ şi care
conţine şi corpul - conţinutul util - al fişierului
antet.
La următoarele includeri ale fişierului antet
simbolul _NUME_FISIER_ANTET_ va fi definit iar
preprocesorul va sări direct la sfârşitul fişierului
antet, după #endif.
//pc.h.

Preprocesorul C #ifndef PC_H // daca este prima oara


// cand acest fiser este inclus
#define PC_H // defineste simbolul pentru a nu
Exemplu de program cu doua fisiere: // nu permite o includere ulterioara
pc.h si main.c
void write_in_c() {
printf("write code in C\n");
}
#include <stdio.h>
#include "pc.h" void pc() {
write_in_c();
int main() { }
pc();
return 0; #endif // sfarsitul codului care depinde de
} // existenta simbolului PC_H
Preprocesorul C
Exemplu de program cu trei fisiere:
pc.h, pc.c si main.c //pc.h.
#ifndef PC_H // daca este prima oara
//pc.c // cand acest fiser este inclus
#include "pc.h" #define PC_H // defineste simbolul pentru a nu
#include <stdio.h> #include <stdio.h> // nu permite o includere ulterioara
#include "pc.h" void write_in_c() {
printf("write code in C\n"); void write_in_c();
int main() { } void pc();
pc(); void pc() {
return 0; write_in_c(); #endif // sfarsitul codului care depinde de
} } // existenta simbolului PC_H
Recapitulare
În C, putem declara variabile de următoarele tipuri:
◦ elementare: int, char, double, float, unsigned etc.
inclusiv adrese de orice tip: tip *var
◦ tablouri: elemente de același tip
◦ structuri: elemente cu tipuri posibil diferite

Un șir are tipul char * (este reprezentat prin adresa șirului)


Recapitulare
În C (și în limbaje de programare în general) putem folosi valori:
◦ în atribuire ... = v
◦ ca parametri la funcții: ... f(v, ...) ... operatorii din expresii sunt similari funcțiilor
◦ ca rezultat returnat din funcție: return v;

În C, putem folosi astfel valori elementare (inclusiv adrese) și structuri


NU putem folosi astfel un tablou (ca tot unitar, valoare compusă)
Când folosim un tablou, folosim de fapt adresa lui
Recapitulare
Când folosim (atribuim, transmitem, returnăm) valori (elementare sau structuri)
se creează copii are acestora.
Transmiterea parametrilor se face prin valoare
O funcție NU poate modifica o variabilă a cărei valoare e transmisă ca parametru

Un pointer (referință) reprezintă unic un obiect, prin adresa lui


Dacă modificăm o valoare de la o anumită adresă această modificare este vizibilă peste tot
Bibliografie selectivă
◦ Kernighan, B. W., & Ritchie, D. ”The C programming language - Second
edition”, 1988 Prentice Hall Software Series
◦ Minea, M., Limbaje de programare, materiale de curs
http://staff.cs.upt.ro/~marius/curs/lp/index.html
◦ Holotescu, C., Limbaje de programare, materiale de curs
http://labs.cs.upt.ro/~oose/pmwiki.php/LP/Lectures
◦ Programarea calculatoarelor (OpenCourseWare
https://ocw.cs.pub.ro/courses/programare/laboratoare/)

S-ar putea să vă placă și