Documente Academic
Documente Profesional
Documente Cultură
(c) )
1991-2001 Lucian Cucu – suport de curs pentru studentii sectiei de Informatica a Facultatii de Matematica,
Universitatea de Vest Timisoara
INTRODUCERE: PRELIMINARII............................................................................................................ 4
Modelul functional (simplificat) al unui calculator:.........................................................................................4
Etapele rezolvarii unei probleme cu calculatorul:............................................................................................5
Limbajul de programare C: Istoric. Legatura cu sistemul de operare UNIX...............................................5
Structura programelor C. Elemente constitutive.............................................................................................6
DATE...................................................................................................................................................... 8
Tipuri de date de baza in C...............................................................................................................................10
Specificarea constantelor...................................................................................................................................10
Declaratii.............................................................................................................................................................11
Declaratii de variabile (sintaxa).........................................................................................................................13
Clase de memorare.............................................................................................................................................13
Domeniul unui identificator..............................................................................................................................14
Vizibilitatea.........................................................................................................................................................14
Durata.................................................................................................................................................................14
Modificatori de declaratii..................................................................................................................................15
Initializarea variabilelor pe linia de declaratie...............................................................................................15
EXPRESII SI OPERATORI................................................................................................................... 16
Operatori.............................................................................................................................................................17
Conversii de tip..................................................................................................................................................20
Conversii implicite de tip...................................................................................................................................22
Conversii explicite de tip...................................................................................................................................23
POINTERI............................................................................................................................................. 40
Adrese..................................................................................................................................................................40
Pointerii sunt variabile. Declararea variabilelor pointer..............................................................................40
Operatorul &. Initializarea pointerilor. De ce?....................................................................................................41
Dereferenterea pointerilor. Operatorul *........................................................................................................41
Pointeri (void *)..................................................................................................................................................41
Pointeri ca argumente de functii.......................................................................................................................41
Operatii cu pointeri. Pointeri si tablouri. Indexarea si dereferentierea – notatii echivalente....................42
2
Pointeri si siruri de caractere............................................................................................................................42
Pointeri vs. tablouri............................................................................................................................................42
Eficienta utilizarii pointerilor...........................................................................................................................43
Pointeri la functii..................................................................................................................................................43
Tablouri multidimensionale. Tablouri de pointeri.........................................................................................43
Transmiterea argumentelor pe linia de comanda...........................................................................................44
STRUCTURI SI UNIUNI........................................................................................................................ 45
Structuri. Declaratie..........................................................................................................................................45
Operatii cu structuri. Accesul la membri........................................................................................................45
Structuri imbricate............................................................................................................................................46
Pointeri la structuri. Tablouri de structuri. Tablouri de pointeri la structuri.............................................46
Structuri cu autoreferire. Liste, arbori, tabele de cautare (hashing)............................................................47
Operatorul typedef.............................................................................................................................................48
Uniuni. Declaratie..............................................................................................................................................48
Operatii cu uniuni..............................................................................................................................................49
3
Introducere: preliminarii
A
L
U
Procesorul este locul/dispozitivul in care se efectueaza “calculele”. Operanzii, pastrati in registrii de date sunt
prelucrati conform instructiunii din registrul de instructiuni, rezultatul prelucrarii fiind depus intr-unul din
registrii de date.
Problema: numarul registrilor de date este limitat, de regula la cateva zeci, ori, cel mai adesea, rezolvarea unei
probleme implica mai multe date si rezultate partiale. In plus instructiunile/comenzile care descriu modul de
rezolvare pot fi la randul lor de ordinul miilor. Este evident necesara existenta unui spatiu pentru “depozitarea”
datelor si a instructiunilor pana in momentul cand sunt necesare in registri.
Memoria este locul dispzitivul in care se pastreaza datele si instructiunile unui program care se
“rezolva”(executa). Memoria este organizata in locatii capabile sa stocheze unitati de informatie (date sau
instructiuni). O data sau instructiune poate sa constea din una sau mai multe astfel de unitati de informatie,
stocate in locatii consecutive. Fiecare locatie de memorie are o adresa ( numarul de ordine fata de inceputul
memoriei). Regasirea unei date sau cod de instructiune in memorie se face pe baza adresei sale. Registrul de
adrese (din processor) este cel in care se incarca adresa datei sau a instructiunii care urmeaza sa fie aduse din
memorie intr-unul din registrii de date sau in registrul de instructiuni.
4
Problema: cum ajung in memorie datele si codurile de instructiuni care descriu un algoritm? Si cum se pot
“vedea” rezultatele programului? Penru aceasta sunt necesare dispozitivele de Intrare/Iesire (Input/Output – I/O).
Dispozitivele de I/O. Dispozitivele de intrare permit introducerea atat a succesiunii de coduri de instructiuni care
descriu algoritmul cat si a datelor problemei ce urmeaza a fi rezolvate conform acelui algoritm. Dispozitivele de
iesire au in principal rolul de a permite vizualizarea rezultatelor programului.
testarea programului
C-ul a aparut cu ocazia dezvoltarii noului sistem de operare UNIX pentru calculatorul PDP-11. Dennis
Ritchie si ulterior Brian Kernigham, in cautarea celui mai potrivit limbaj pentru scrierea sistemului de operare au
conceput in cele din urma (la inceputul anilor ’70) un limbaj nou: C. Filiatia: CPL, B, BCPL, C.
Peste 90% din sistemul de operare UNIX a fost scris in acest limbaj nou, restul (in general driverele de
dispozitive) fiind scris in limbaj de asamblare.
- C
- Limbaje de asamblare
- cod masina
5
Avantajele C-ului
- dimensiunea redusa:
- 27 cuvinte cheie
- inexistenta instructiunilor de I/O sau a celor pentru operatii matematice mai complicate
- structura lejera: orice functie de biblioteca poate fi rescrisa
- viteza: codul este foarte eficient
- slab tipizat: programatorul are libertate maxima pentru tratarea datelor in functie de necesitati
- limbaj structurat (chiar inainte de aparitia programarii structurate ca moda)
- permite (incurajeaza) programarea modulara
- interfata simpla cu limbjele de asamblare
- operatori pe biti
- variabile pointer
- structuri flexibile: tablourile sunt unidimensionale (dar pot consta din elemente de orice tip, inclusiv
…tablouri)
- utilizarea eficienta a memoriei
- portabilitate sporita
- biblioteci de functii speciale
Dezavantaje
- faptul ca este slab tipizat => posibilitate sporita de erori datorita conversiilor necontrolate
(trunchieri)
- controlul in timpul executiei este foarte sumar (de ex. nu se verifica depasirea limitelor unui tablou)
Standardul ANSI C
Adoptat in 1989 (pornind de la editia a doua a cartii lui B. Kernigham si D. Ritchie: The C Programming
Language) standardul ANSI C se conformeaza spiritului:
- ai incredere in programator
- nu-l impiedica pe programator sa faca ceea ce trebuie facut
- pastreaza limbajul "mic si simplu"
6
Un program C este alcatuit din urmatoarele elemente constitutive:
- directive de preprocesare (precompilare)
- declaratii:
- de variabile
- de functii
- definitii de functii
- comentarii
Specificarea unui program sursa (succesiune de secvente de caractere - litere, cifre, semne speciale - care
identifica datele si instructiunile) se face prin intermediul atomilor lexicali (tokens).
Reprezentarea unui algoritm intr-un limbaj de programare se face cu ajutorul elementelor recunoscute de
analizorul lexical - parte a compilatorului - specific pentru acel limbaj de programare.
urmatoarele elemente (atomi lexicali sau tokeni) trebuie recunoscute de catre analizorul lexical:
cuvintele cheie, care descriu de regula instructiunile limbajului, dar si alte elemente, ca de exemplu
denumirile tipurilor de date. Se pot include in aceasta categorie si operatorii, chiar daca nu sunt propriu-zis
cuvinte cheie, pentru ca descriu operatii
constantele
identificatorii, utilizati pentru denumirea variabilelor, functiilor, etichetelor
separatori
.
Ultimele doua clase de atomi lexicali din enumerarea de mai sus sunt cei cu ajutorul carora se descrie partea de
"date" (in sensul de materie prima asupra careia se actioneaza sau care rezulta) a unui program.
7
Date
Datele cu care opereaza un program C pot face obiectul mai multor tipuri de clasificari:
constante
o literale ("as is")
o simbolice("as defined")
variabile
intregi
o cu semn (reprezentare interna: complement fata de 2)
o fara semn (reprezentare interna: baza 2)
reale (reprezentare interna: virgula mobila)
numerice
caractere
adrese
scalare
agregate
o tablouri
o structuri
o uniuni
o bit-fields (campuri de biti)
siruri de caractere
enumerari
in functie de modul de reprezentare (intregi vs. reale, complement fata de doi vs. virgula flotanta)
in functie de interpretare (intregi vs. caractere, intregi vs.enumerari, intregi fara semn vs. pointeri)
Datele intregi sunt reprezentate in complement fata de doi. Din plaja totala de valori reprezentabile pe un
numar dat de biti, prima jumatate este rezervata valorilor pozitive iar cealalta jumatate (se recunosc dupa faptul
ca bitul cel mai semnificativ este egal cu 1) numerelor negative.
8
Reprezentare Valoare ca Valoare ca
binara numar fara semn numar cu semn
0000 0 0
0001 1 1
0010 2 2
>= 0 0011 3 3
0100 4 4
0101 5 5
0110 6 6
0111 7 7
1000 8 -8
1001 9 -7
1010 10 -6
1011 11 -5
< 0 1100 12 -4
1101 13 -3
1110 14 -2
1111 15 -1
10000- 24
0101 |-5| (adica 5)
------
1011 -5 (in reprezentarea in complement fata de 2)
Datele reale, caracterizate prin aceea ca pe langa o parte intreaga au (pot avea) si o parte fractionara, ridica
cateva probleme. Intrucat in gama de probleme de rezolvat prin utilizarea calculatorului pot apare atat numere
foarte mari (cu partea intreaga foarte mare) cat si numere foarte mici (cu parte fractionara reclamand multe pozitii
pentru a reprezenta cifrele semnificative), pentru a impaca ambele situatii ar fi necesar un spatiu mare (dublu) de
memorie pentru reprezentarea numerelor reale cel putin la nivelul numerelor intregi. In aceasta situatie insa
operatiile de manipulare (transfer memorie in/din registri) a acestor date ar fi foarte costisitoare ca timp si nici nu
s-ar justifica in majoritatea situatiilor. De exemplu, pentru numere cu cateva cifre la partea intreaga si cateva la
partea fractionara, nu ar fi necesar sa se risipeasca 64 de biti. Mai mult, in functie de problema concreta, de
obicei apar ca operanzi si rezultate numere din aceeasi "categorie": fie numere mari la care partea fractionara
lipseste sau este semnificativa doar pe primele pozitii de dupa virgula, fie numere mici, cu multe cifre
semnificative la partea fractionara si doar cateva la partea intreaga.
Solutia aleasa pentru reprezentarea numerelor reale presupune "mutarea"virgulei care desparte partea intreaga de
cea fractionara in functie de necesitati astfel ca pe un numar dat de pozitii sa se poata reprezenta si cel mai mare
numar intreg (fara cifre semnificative la partea fractionara) si cel mai mic numar subunitar ( fara parte intreaga
sau cu un numar minim de cifre la partea intreaga).
Aceasta reprezentare se numeste reprezentarea cu virgula mobila sau cu virgula flotanta (floating point). Ideea
este relativ simpla: se pastreaza cifrele semnificative ale numarului (toate de la parte intreaga si atatea cate incap
de la partea fractionara) si pozitia virgulei.
Cele trei elemente a caror reprezentare e necesara, in cazul datelor reale, sunt
semnul: 1 bit
exponentul 8 - 16 biti
mantisa (normalizata) 23 – 63 biti
9
S Exponent Mantisa(normalizata)
Relatia (“grosso modo”) dintre cele trei elemente si numarul N reprezentat este
(Detalii la laborator!)
Exista si diferente de interpretare a datelor intregi. De exemplu un numar intreg (mai mic decat 255) poate fi
interpretat si ca un caracter care are codul ASCII egal cu valoarea intregului. Invers, orice cod ASCII este un
numar intreg!
Problema consta doar in a putea preciza in cursul unui program modul in care se doreste interpretata o data.
Specificarea constantelor
Definirea constantelor simbolice face uz tot de constante literale pentru a asocia o valoare simbolului ce se
defineste.
10
Specificarea constantelor intregi
In mod implicit constantele intregi se reprezinta ca date de tipul int. In cazul in care valoarea constantei este
in afara plajei de valori pentru acest tip, se va utiliza tipul long.
Exemple:
-32768, -1, 0, 1 1000, 5000, 32767 constante intregi reprezentate ca int
-32769, 32768, 100000 constante intregi reprezentate ca long
Fortarea reprezentarii ca long, chiar daca valoarea sa n-ar justifica-o, se poate face prin utilizarea sufixului L
Exemple:
-1L, 0L, 1L
Fortarea reprezentarii unei constante pozitive ca un intreg fara semn se face prin utilizarea sufixului U
Exemple:
1U, 32768U, 65535U constante intregi reprezentate ca unsigned int
1UL, 50000UL 65536U constante intregi reprezentate ca unsigned long
Constantele intregi pot fi specificate si in baza de numeratie 8, respectiv 16 prin utilizarea prefixelor 0 respectiv
0x sau 0X.
Exemple:
01, 010, 01777 constante octale
0x1, 0XA5, 0XFF, 0xFFFF constante hexazecimale
Declaratii
Declaratii: - de variabile
- de functii
Terminologie (concepte):
- obiecte
- lvalue
- tipuri (de date)
- declaratii de tip
- clase de memorare
- domenii (scope)
- vizibilitate
- durata
- linkage
11
Definitie. Obiect: o regiune identificabila de memorie care poate pastra o valoare fixa (constanta ) sau
variabila.
OBS. Termenul de obiect definit mai sus nu trebuie confundat cu termenul omonim utilizat in programarea
orientata obiect!
Standardul ANSI C asigura unicitatea identificatorilor care sunt diferiti pe primiele 31 de caractere.
Prin tip:
- se determina spatiul de memorie necesar pt obiectul in cauza
- se precizeaza modul de reprezentare interna
Implicit, informatiile de mai sus determina plaja de valori pe care le poate retine obiectul respectiv.
Tipul unui obiect este utilizat in generarea codului necesar manipularii obiectului respectiv.
In sensul definitiei de mai sus, obiecte sunt variabilele (care nu sunt create in registri!) si functiile.
Lvalue
Un lvalue este un identificator sau o expresie care desemneaza un obiect.
Un lvalue modificabil este un lvalue care identifica un obiect care poate fi accesat si modificat.
Exemplu de lvalue care nu este modificabil: o variabila declarata const.: const float pi=3.14;
O declaratie:
- stabileste legatura intre identificator si obiectul pe care-l identifica.
- asociaza un identificator cu un tip de data
Declaratiile pot fi :
- de definire, care determina crearea unui obiect (alocarea de memorie si posibila sa initializare)
- de referire, care aduce la cunostinta compilatorului existenta si tipul obiectului accesibil prin
identificatorul precizat in declaratie.
OBS. Pot exista mai multe declaratii de referire a unui obiect (mai ales in programe constituite din mai
multe fisiere sursa), dar una singura de definire.
12
Declaratii de variabile (sintaxa)
Unde, in paranteze drepte sunt mentionate elementele optionale (care pot lipsi).
Tipurile de baza (fundamentale) in C sunt:
Clase de memorare
Asocierea identificatorilor cu obiecte presupune ca fiecare identificator sa aiba cel putin doua atribute
specificate:
- clasa de memorare
- tipul (datelor ce vor fi memorate - in cazul variabilelor, sau al datelor returnate - in cazul functiilor)
Clasa de memorare poate fi determinata de locul de plasare a declaratiei in fisierul sursa sau de utilizarea
modificatorilor de clasa de memorare.
In functie de locul in care apare declaratia in fisierul sursa clasa de memorare a unui obiect
- auto (sau register, daca compilatorul e setat astfel incat sa poata crea variabile registru din proprie
initiativa!) daca declaratia se face in interiorul unei functii (bloc – un bloc este u secventa de
declaratii si instructiuni cuprinsa intre acolade),
- static, daca declaratia este plasata in afara oricarei functii
Prin utilizarea modificatorilor de clasa de memorare, clasa de memorare a unui obiect poate fi:
- auto – obiectele cu aceasta clasa de memorare se creaza pe stiva. Variabilele decalarte in
interiorul unui bloc, numite si variabile locale au aceasta clasa de memorare.
- register (nu este imperativ ci orientaiv!)
- static – obiectele cu aceasta clasa de memorare se creaza in zona de date statice
- extern – aceasta clasa de memorare este tipica pentru obiectele declarate (create intr-un alt
fisier sursa)
13
Utilizarea modificatorului static in declaratia unei variabile globale (sau a unei functii) “ascunde”
variabila/functia respectiva in interiorul fisierului sursa in care apare.
Tipuri de domenii:
- bloc (local)
- functie
- prototip de functie
- fisier (global)
Domeniul unui identificator cu domeniul bloc (local) incepe din locul declaratiei si pana la sfarsitul
blocului in care a fost declarat. (acelasi domeniu il au si parametri din definitia unei functii)
Domeniul functie se refera doar la identificatorii utilizati ca etichete: etichetele pot fi utilizate in
instructiuni goto oriunde in functia in care e declarata eticheta. Etichetele trebuie sa fie unice intr-o functie.
Domeniul prototip de functie este propriu identificatorilor din lista de parametri din prototipul
(declaratia) unei functii si se termina la sfarsitul prototipului functiei.
Ex. Prototip de functie in care se folosesc denumiri de parametri diferite de cele din definitia aceleiasi
functii!
Domeniul fisier (global) este tipic identificatorilor declarati in afara oricarui bloc.
Spatiul unui nume reprezinta domeniul in care un identificator trebuie sa fie unic.
Vizibilitatea
unui identificator reprezinta regiunea dintr-un program sursa de unde se poate accesa obiectul asociat
identificatorului.
Durata
reprezinta perioada din executia unui program in care un identificator are asociat un obiect fizic (existent in
memorie).
Se poate distinge deasemenea si intre obiecte existente in timpul compilarii (compile-time objects - ex. typedef-
uri si tipuri) sau in timpul executiei (run-time objects- ex. variabile).
14
Prin linkage se precizeaza legatura (asocierea) dintre fiecare instanta (aparitie in programul sursa) a unui
identificator si obiectul care poate fi accesat prin intermediul lui. Un identificator poate avea linkage intern,
extern sau de nici un fel.
Modificatori de declaratii
Anumite atribute ale variabilelor pot fi modificate (fata de valoarea implicita) prin utilizarea modificatorilor de
declaratii. Acestia sunt cuvinte cheie care, utilizate intr-o declaratie modifica valoarea implicita a unui atribut al
variabilelor/functiilor precizate in declaratie.
Modificatorul const
Precizeaza ca valoarea variabila declarate (si initializate!) nu poate fi modificata.
Ex.
const int origin=0; /*corect*/
static
auto
register
extern
Variabilele pot fi initializate pe linia de declaratie prin utilizarea operatorului de asignare (=). Expresiile care se
folosesc pentru initializare trebuie sa fie de tipul declarat al variabilei care se initializeaza sau sa poata fi convertit
la acest tip. Eventualele variabile care apar in expresii folosite ca initializatori trebuie sa fi fost deja definite!
sau
15
TIP nume_tablou[expr_constanta] ={lista initializatori};
OBS lista trebuie sa nu aiba mai multe elemente decat valoarea expr_constanta iar elementele se separa prin
virgula.
In absenta expr_constanta, numarul de elemente ale listei de initalizatori determina numarul de elemente ale
tabloului.
Exemple:
int tab[10], a[5]={-2, 5, 8, 3, 9};
char judet[ ]=”Timis”;
Expresii si operatori
Definitie. Succesiune de identificatori, constante, apeluri de functii (care intorc o valoare) si operatori, care
respecta regulile sintatice.
Expresiile pot fi constante, atunci cand implica doar constante si operatori; astfel de expresii sunt evaluate la
momentul compilarii.
Orice expresie se evalueaza si produce un rezultat. Rezultatul evaluarii unei expresii are un tip care depinde de
tipul operanzilor si de operatorii din expresie.
Evaluarea expresiilor se face in concordanta cu anumite reguli care pot fi sintetizate in urmatoarea formulare:
In absenta parantezelor, evaluarea expresiilor se face in ordinea precedentei operatorilor, iar daca
operatorii au aceeasi precedenta in ordinea data de asociativitatea operatorilor (de regula de la stanga la
dreapta).
Operanzi pot fi orice valori ( constante, variabile, valori returnate de functii, valori rezultate in urma evaluarii
unei expresii) care se conformeaza tipului de opranzi asteptati de operatorii in cauza. (Contra)exemple: nu pot
figura ca operanzi intr-o expresie aritmetica structuri/uniuni/campuri de biti (in ansamblu); valori reale nu pot fi
operanzi in expresii cu operatorul %, operatorii logici pe biti, operatorii de deplasare.
In scopul evaluarii expresiilor operanzii simpli (care, la randul lor, nu sunt expresii) trebuie sa fie evaluati pentru
a li se obtine valoarea. Ordinea in care se face evaluarea operanzilor simpli nu este precizata de standardul tiple
ale aceluiasi operand in situatia in care cel putin una din aparitii este in contextul unei opratii cu efecte secundare.
Exemplu:
a[i++]=i;
16
Operatori
Tabela cu ordinea de precedenta a operatorilor limbajului C (preluata din B.Kernigham, D. Ritchie, - The C
Programming Language, 2nd ed.) este prezentrata mai jos. Precedenta scade de la prima linie pana la ultima. In
aceeasi linie sunt plasati operatori cu aceeasi precedenta.
OPERATORS ASSOCIATIVITY
( ) [ ] --> . left to right
! ~ ++ -- + - * & (type) sizeof right to left
* / % left to right
+ - left to right
<< >> left to right
< <= > >= left to right
== != left to right
& left to right
^ left to right
| left to right
&& left to right
|| left to right
?: right to left
= += -= *= /= %= &= ^= |= <<= >>= right to left
, left to right
Operatori aritmetici
+ adunare
- scadere
* inmultire
/ impartire
% rest modulo
Operatori de incrementare/decrementare
++ operatorul de incrementare
-- operatorul de decrementare
17
OBS. Cei doi operatori de mai sus sunt operatori unari (presupun un singur operand) si au particularitatea ca pot
fi folositi ca operatori prefixati sau postfixati. Pozitia lor in raport cu operatorul indica momentul in care se face
evaluarea: inainte, respectiv dupa de evaluarea operandului, evaluare care se face in eventualitatea ce operandul
respectiv face parte dintr-o expresie in care intervin si alti operatori (pe langa cei de incrementare/ decrementare
Ex. In expresia
a[i++] + b[++j]
se ia elementul de indice i din tabloul a si apoi se incrementeaza I, respectiv se incrementeaza j si apoi se ia
elementul corespunzator acestei noi valori a lui j din tabloul b pt evaluarea expresiei.
Operatori relationali
==
!=
<
>
<=
>=
Operatori logici
&& | 0 0 || | 0 0
0 | 0 0 0 | 0 1
0 | 0 1 0 | 1 1
! | 0 0
| 1 0
Operatori pe biti
Operatorii din aceasta categorie actioneaza asupra bitilor de pe aceasi pozitie din operanzi.
& - “si” pe biti
| - ”sau” pe biti
^ - “sau exclusiv” pe biti
~ - complementare
18
Tinand cont ca operatorii din aceasta categorie actioneaza de fapt asupra bitilor din operanzi, in descrierea
regulilor de evaloare, operanzii vor fi valorile posibile pentru un bit, adica 0 si respectiv 1:
& | 0 1 | | 0 1 ^ | 0 1
0 | 0 0 0 | 0 1 0 | 0 1
1 | 0 1 1 | 1 1 1 | 1 0
~ | 0 1
| 1 0
OBS. Operatorii pe biti se folosesc de obicei pentru “stergerea” (punerea pe 0) sau “setarea” unor biti. De obicei,
unul din operanzi e considerat masca prin care se precizeaza pozitiile din celalalt operand care trebuie sterse sau
setate. Operatorul & se utilizeaza pentru stergerea bitilor care in masca au valoarea 0 (si pastrarea neschimbata a
celor care au valoarea 1 in masca!) iar operatorul | se utilizeaza pentru setarea pe 1 a bitilor care in masca au
valoarea 1 (si pastrarea nemodificata a celor care in masca au valoarea 0!)
Operatori de deplasare
Sintaxa:
operand_1 >> nr_pozitii
operand_1 << nr_pozitii
Al doilea operand precizeaza numarul de pozitii cu care se deplaseaza spre dreapta sau spre stanga bitii primului
operand. Ambii operanzi trebuie sa fie de tip intreg.
Indiferent de tipul operandului_1 (cu sau fara semn) pozitiile eliberate in cazul deplasarii la stanga (cele mai
putin semnificative!) se completeaza cu 0.
Pozitiile eliberate in cazul deplasarii la dreapta (cele mai semnificative!) se completeaza in functie de tipul
operandului_1 (cu sau fara semn)
- cu bitul cel mai semnificativ, daca operandul e cu semn
- cu 0 , daca operandul este fara semn (unsigned)
Operatorul conditional
? :
Sintaxa:
Mod de evaluare: daca exp1 este adevarata atunci rezultatul intregii expresii este rezultatul produs de evaluarea
expresiei exp2, altfel cel produs de evaluarea lui exp3.
19
Operatori de asignare
= operatorul de asignare
op= operatorul de asignare compus. Efectul acestui operator este descris de
Conversii de tip
Conversiile (in sensul discutiei care urmeaza) reprezinta transformari dintr-un mod de reprezentare a datelor in
altul. Doua tipuri de conversii vor fi abordate:
Extern ( atunci cand sunt generate de dispozitive de intrare cu care utilizatorul interactioneaza direct - tastatura -
respectiv, pentru afisarea pe dispozitive de iesire destinate uzului utilizatorului uman - display, imprimanta)
datele sunt reprezentate ca succesiuni de coduri (de ex. ASCII), cate un cod pentru fiecare caracter (cifra, litera,
semn de punctuatie, etc).
Cand se citesc date de la tastatura, un program primeste o succesiune de coduri (de ex. ASCII) reprezentand
- numere
- intregi (ex. -1, 123, 1000) sau
- reale (ex. 0.5, -1.0, 3.14)
- "text" (orice altceva decat numere)
Datele numerice trebuie transformate in modul de reprezentare intern pentru a se putea efectua operatii cu ele.
Datele de tip text se pastreaza de obicei ca siruri de caractere (coduri ale caracterelor care compun textul)
Conversiile de date numerice in modul de reprezentare interna se realizeaza dupa regula generica:
c0 c1 ...c n conversie
c n b (c n 1 ...b (c1 b c0 )...)
Unde c0,c1,…cn sunt cifrele numarului intreg de convertit din reprezentarea externa in reprezentarea interna iar b
este baza de numeratie in care se reprezinta extern numarul (de obicei b=10).
20
(cea mai putin semnificativa) cifra si asa mai departe, pana cand catul impartirii devine mai mic decat b. Evident,
intrucat cifrele se obtin si se acumuleaza intr-un buffer in ordine inversa, este necesara o inversare a sirului de
caractere astfel obtinut.
Mai jos se dau codurile sursa pentru cateva rutine de conversie (din B.Kernigham & D.Ritchie - The C
Programming Language, 2nd edition - [K&R 88] ).
n = 0;
for (i = 0; s[i] >= '0' && s[i] <= '9'; ++i)
n = 10 * n + (s[i] - '0');
return(n);
}
#include <ctype.h>
21
/* itoa: convert n to characters in s */
void itoa(int n, char s[ ])
{
int i, sign;
Cand conversiile se fac intre diverse modalitati de reprezentare interna, se vorbeste de conversii de tip.
Conversiile de tip pot avea loc implicit sau explicit.
Atunci cand operanzii unei expresii sunt de tipuri diferite, valoarea operandului de tip “mai slab” se
converteste la tipul operandului de tip “mai tare”.
OBS. De convertit se converteste valoarea, deci, in cazul unui operand -variabila, tipul variabilei nu se schimba!
Prin tip mai tare vom intelege in continuare un tip care permite reprezentarea a mai multa informatie (plaja de
valori mai mare si/sau parte fractionara) decat un tip mai slab.
De exemplu tipul int este mai tare decat tipul char pentru ca plaja de valori reprezentabile este mai mare
(-32768 pana la 32767 fata de -128 pana la 127), tipul float este mai tare decat tipul long int pentru ca are
plaja de valori reprezentabile mai mare si in plus permite si reprezentarea partii fractionare.
Pe scurt regulile dupa care au loc conversiile implicite pentru operanzi fara semn sunt urmatoarele:
Daca unul din operanzi este long double, atunci si celalalt este convertit la long double.
altfel
daca unul din operanzi este double atunci si celalalt este convertit la double
altfel
daca unul din operanzi este float atunci si celalalt este convertit la float
altfel
se converteste operandul (operanzii) char si short la int
apoi
daca unul din operanzi este long int, atunci si celalalt operand se converteste la long int.
Daca unul din operanzi este unsigned , lucrurile se complica din cauza comparatiilor dintre valorile cu semn
si cele fara semn, care sunt dependente de implementare, intrucat depind de lungimea reprezentarii pentru tipurile
intregi.
Exista o exceptie de la regula enuntata anterior, privind conversiile implicite si anume, cand operatorul este
operatorul de asignare, atunci operandul din partea dreapta se converteste la tipul operandului din partea stanga,
22
chiar daca acest lucru presupune pierdere de informatie, intrucat in urma asignarii, valoarea operandului drept
trebuie stocata in zona de memorie ocupata de operandul stang, zona care este fixata (prin declaratia de tip!).
La conversia unei valori de tip intreg mai tare la un tip intreg mai slab se trunchiaza cifrele cele mai
semnificative. In cazul conversiei unei valori reale la un tip intreg se trunchiaza partea fractionara si, daca e
cazul, cifrele cele mai semnificative care nu pot fi reprezentate.
In cazul conversiei unei valori reale de tip mai tare la un tip real mai slab se face sau nu rotunjire, in functie de
implementare.
Exemple
Intrucat argumentele functiilor sunt la randul lor expresii, conversiile de tip au loc si atunci cand argumentul
actual e diferit de tipul specificat in prototipul (declaratia) functiei. In absenta prototipului unei functii
argumentele actuale de tip char si short se convertesc la double. De aceea parametrii functiilor care in mod
normal ar fi de tipul char/short se vor declara de tipul int (pentru a se economisi conversia implicita la int).
(tip)expresie
23
Controlul executiei (instructiuni)
Limbajul C este un limbaj “mic”, cu putine instructiuni. Aceasta caracteristica permite pe de o parte o
standardizare mai usoara cu efect direct asupra usurintei de implementare Instructiunile limbajului C sunt:
Instructiuni de test
If-else
Sintaxa:
if(expresie)
instructtiune_1;
else
instructiune_2;
Executie:
Daca expresie este adevarata (valoarea numerica este diferita de 0) se executa instructiune_1, altfel se
executa instructiune_2.
Ex.
1.
if(n>=0)
if(a>b)
z=a;
else
z=b;
else
printf(" n - negativ");
2.
if(n>=0) {
if(a>b)
z=a;
24
}
else
z=b;
Else-if
Sintaxa:
if(expresie_1)
instructiune_1;
else if(expresie_2)
instructiune_2;
…
else if(expresie_n)
instructiune_n;
else
instructiune;
Executie:
Daca expresie_1 este adevarata se executa instructiune_1, altfel, daca expresie_2 este adevarata se
executa instructiune_2, altfel,…, daca expresie_n este adevarata se executa instructiune_n altfel
se executa instructiune. Altfel spus, instructiunea else-if permite o selectie din mai multe variante
posibile (cea corespunzatoare primei expresii adevarate. Deasemenea, instructiunea else-if ofera si o varianta
implicita (ramura else instructie ), pentru cazul cand niciuna din expresiile expresie_i nu este
adevarata.
Ex.
if(a>0)
printf("a este pozitiv");
else if(a<0)
printf("a este negativ");
else
printf("a=0");
Instructiunea de selectie
Switch
Sintaxa:
switch(expresie){
case expresie_constanta_1: instructiune_1;
break;
case expresie_constanta_2: instructiune_2;
break;
…
25
Executie:
Se va executa instructiunea asociata cu expresie_constanta_i care are aceeasi valoare cu cea a
expresiei expresie (din switch). In cazul in care valoarea expresiei expresie nu coincide cu a nici uneia
din expresiile expresie_constanta_I se va executa instructiune (cazul implicit default). In cazul
in care secventa de instructiuni asociata cazului selectat nu se termina cu break se vor executa in continuare toate
instructiunile, pana la primul break.
Exemple:
Instructiuni de ciclare
For
Sintaxa:
Executie:
1. Se eevaluaeaza expresie_1
2. Se evalueaza expresie_2 si
Daca este falsa se paraseste ciclul for
Daca este adevarata se executa instructiune
3. Se evalueaza expresie_3
4. Se reia pasul 2
OBS. Daca expresie este falsa la prima evaluare, ciclul nu se executa niciodata!
Exemple:
While
Sintaxa:
while (expresie)
instructiune;
Executie:
Se evalueaza expresie si daca este adevarata se executa instructiune dupa care se revine la
evaluarea expresiei. Cand expresie devine falsa, se paraseste ciclul.
26
OBS. Daca expresie este falsa la prima evaluare, ciclul nu se executa niciodata!
Ex.
while((c=getchar( )) != EOF){ /* ciclu tipic de citire de la stdin (tastatura) */
…
}
while( 1 ) { /* ciclu infinit, din care se poate iesi fortat (cu break sau return)*/
…
}
Do-while
Sintaxa:
do {
instructiune;
} while(expresie);
Executie:
Se executa instructiune, apoi se evalueaza expresie si daca este adevarata se executa din nou
instructiune… Cand expresie devine falsa, se paraseste ciclul.
OBS. Ciclul se executa cel putin odata.
Exemplu:
Sintaxa:
break;
Executie:
Instructiunea se utilizeaza in contextul unei instructiuni de ciclare sau al unui switch si determina
parasirea fortata a acestuia.
Ex.
Continue
Sintaxa:
continue;
Executie:
Instructiunea se utilizeaza in contextul unei instructiuni de ciclare (nu si al unui switch!) si determina
trecerea la iteratia urmatoare.
27
Ex.
for( i=0; i<n ; i++){
if(t[i] < 0) continue;
…
}
Sintaxa:
return;
sau
return expresie;
Executie:
Instructiunea return transfera controlul executiei din functia apelata inapoi in functia apelanta, la
adresa de revenire salvata pe stiva, in contextul de apel. Daca tipul declarat al functiei este diferit de void,
atunci se utilizeaza forma a doua si expresie trebuie sa fie de tipul declarat al functiei sau convertibil la acesta.
In absenta unei instructiuni return, ultima acolada inchisa din definitia functiei joaca rolul instrucitunii return.
Ex.
Sintaxa:
goto eticheta;
Executie:
Controlul executiei se transfera la instructiunea (din aceeasi functie!) precedata de eticheta.
Eticheta este un identificator cu domeniul intreaga functie in care e definita si poate fi formata din caractere
alfanumerice. Definirea unei etichete se face ca mai jos:
eticheta:
Utilitatea instructiunii goto se manifesta in cazul in care se doreste parasirea mai multor cicluri imbricate intr-un
singur pas. {Cu instructiunea break se poate parasi doar ciclul in care se foloseste instructiunea!)
Ex.
…
for(…)
for(…)
for(…){
…
if(expr)
goto error;
}
…
error:
…
28
Structura programelor. Functii.
Filozofia (imprumutata din UNIX si) promovata de autorii C-ului ca si de standardul limbajului prevede
descompunerea sarcinilor de realizat de catre un program in unitati/module functionale numite functii. Avantajele
care deriva din aceasta filozofie se reflecta in
posibilitatea crescuta de refolosire a codului si
intr-o mai mare usurinta a modificarii/intretinerii codului aferent unei anumite sarcini.
Intr-un program C se pot distinge trei contexte/situatii in care se face referire la o functie:
declaratia functiei
definitia functiei
apelurile (apelarile) functiei
Declaratii de functii
Sintaxa unei declaratii de functie este:
Unde
TIP este tipul valorii pe care functia o intoarce/returneaza in urma unui apel.
Nume_functie este numele sub care se vor face referiri (apeluri) la functie.
In paranteza se precizeaza lista tipurilor argumentelor functiei.
OBS. 1. Daca tipul declarat al functiei (TIP) este void, aceasta inseamna ca functia nu intoarce nici o valoare.
2. Lista de argumente poate fi vida, ceea ce inseamna ca la apelul functiei nu i se trimite nici un
argument.
3. Nu este necesara (dar nici interzisa!) precizarea, in declaratie, de nume pentru argumentele functiei.
In definitia functiei:
Se precizeaza numele argumentelor formale
Se declara variabilele locale
Se descrie (printr-o secventa de instructiuni) actiunile de indeplinit la fiecare apel al functiei.
Se citeaza (intr-o instructiune return expresie) valoarea pe care functia o returneaza functiei
apelante (numai daca tipul declarat al functiei este diferit de void)
29
/* instructiuni */
Daca TIP este diferit de void, atunci ar trebui sa apara si o instructiune return in definitia functiei.
OBS. Definitia unei functii precizeaza CE SE FACE cu ARGUMENTELE primite la apel in scopul
obtinerii VALORII ce se va RETURNA functiei apelante.
Intr-un program poate apare o singura definitie pentru fiecare functie.
Apelurile unei functii reprezinta mecanismul prin care controlul executiei este transferat unei functii,
impreuna cu argumentele actuale, in scopul obtinerii unui anumit rezultat si/sau al efectuarii unor
anumite actiuni.
Sintaxa unui apel de functie:
Deasemenea apelul unei functii care intoarce o valoare poate apare in orice tip de expresii, inclusiv ca
argument in apelul altei functii. Exemplu:
putchar(getchar( ));
Argumentele se transmit de la functia apelanta la functia apelata prin intermediul stivei. Tot pe stiva se depune
adresa de revenire din apel, adica adresa instructiunii la care se transfera controlul executiei dupa ce s-a
terminat de executat functia apelata.
Argumentele se transmit prin valoare in ordinea inversa celei din lista de argumente, astfel incat primul
argument din lista sa fie intotdeauna sub adresa de revenire. Daca functia apelata are si variabile locale, acestea
se creaza tot pe stiva in ordinea declararii. Zona de pe stiva pe care se plaseaza valoarea argumentelor actuale,
adresa de revenire si variabilele locale poarta denumirea de context de apel.
Acest mod (ordine) de transmitere a argumentelor catre functia apelata poarta denumirea de conventie de apel C.
Exemplu de comunicare prin variabile globale: program de evaluare a expresiilor aritmetice (+, -, *, /) introduse
in notatie poloneza inversa (din [K&R], 4.3)
30
Textual, sarcina de indeplinit poate fi descrisa de urmatoarea secventa:
#include <stdio.h>
#include <math.h> /* for atof() */
31
break;
default;
printf("error: unknown command %s\n", s);
break;
}
}
return 0;
}
OBS. Deoarece operatorii de adunare (+) si inmultire (*) sunt comutativi ordinea in care se “coboara” operanzii
de pe stiva este irelevanta, in schimb, in cazul operatorilor de scadere (-) si impartire (/), care nu sunt comutativi,
in varful stivei se afla operandul al doilea (scazatorul, respectiv impartitorul) care trebuie “coborate” intr-o
variabila temporara (op2) pentru ca apoi ordinea operanzilor in expresie sa fie corecta (pop() – op2, respectiv
pop() / op2).
#include <ctype.h>
int getch(void);
void ungetch(int);
32
;
if (c == '.') /* collect fraction part */
while (isdigit(s[++i] = c = getch()))
;
s[i] = '\0';
if (c != EOF)
ungetch(c);
return NUMBER;
}
33
Gestionarea aplicatiilor complexe. Proiecte
In scopul dezvoltarii si al gestionarii ulterioare a modificarilor in aplicatii complexe, de dimensiuni care nu fac
rezonabila cantonarea tuturor functiilor intr-un singur fisier sursa, parti ale aplicatiei cu functionalitate distincta
se pastreaza in fisiere sursa separate. Aceste fisiere sursa separate se pot modifica si compila independent fiind
“legate” cu fisierele cod obiect rezultat din compilarea celorlalte componente in faza de link-editare. Ansamblul
de fisiere sursa care implementeaza functionalitatea unei aplicatii poarta, in majoritatea mediilor de dezvoltare,
denumirerea de proiect.
Exemplificare: programul anterior, care implementeaza un evaluator pentru expresii introduse de la stdin in
notatia poloneza inversa poate fi realizat intr-o versiune in care diversele functionalitati sunt implementate in cate
un fisier sursa separat. Pentru directivele de preprocesare si declaratiile necesare in mai multe fisiere sursa se
creaza un fisier header care va fi inclus in fisierele sursa care au nevoie de continutul sau. (Exemplul este preluat
din [K&R], 4.5).
calc.h:
----------------------
| #define NUMBER '0' |
| void push(double); |
| double pop(void); |
| int getop(char []);|
| int getch(void); |
| void ungetch(int); |
----------------------
34
Initializarea variabilelor
Atribuirea unei valori unei variabile se realizeaza cu ajutorul operatorului de asignare ( = ). Aceasta atribuire
poate avea loc si in declaratia variabilei. In continuare vom face distinctie intre atribuirea unei valori in linia de
decalaratie a unei variabile, pe care o vom numi initializare, si atribuirea intr-o expresie ulterioara declaratiei pe
care o vom desemna ca asignare (sau atribuire) a unei valori la variabila respectiva. In cazul initializarii vom
face distinctie intre initializarea automata (sau implicita) si initializarea explicita.
Initializarea automata (implicita ) se aplica variabilelor globale si variabilelor locale declarate static. Aceste
variabile, care au ca punct comun faptul ca au clasa de memorare static, se initializeaza in mod automat cu 0.
Variabilele cu clasa de memorare auto nu se initializeaza automat, valoarea lor intiala (in absenta initializareii
explicite) fiind valoarea prezenta, la momentul crearii variabilei, in zona de memorie (de pe stiva) unde aceasta e
creata. Evident aceasta valoare scapa controlului programatorului!
Initializarea explicita
Sintaxa pentru initializarea variabilelor scalare este:
In cazul variabilelor globale, expresie trebuie sa fie o expresie constanta, pe cand in cazul variabilelor locale
poate contine si variabile deja definite precum si parametri de apel ai functiei in care variabila este definita sau
chiar apeluri de functii.
Ex.
int val = 1;
int contor=val; /*gresit: initializatorul nu este o expresie constanta */
f(int n)
{
int a=2, x=2+n, p=pow(a,n);
…
}
Initializarea tablourilor
Ca si variabilele scalare, tablourile pot fi initializate. Elementele tablourilor declarate ca variabile globale sau ca
variabile locale statice, se initializeaza implicit cu 0.
35
Initializarea explicita a tablourilor se realizeaza prin utilizarea unei liste de initializatori. Lista este specificata
intre acolade, iar initializatorii sunt separati cu virgula.
Ex.
int a[5] = {15, 4, 3, 9, 21};
float b[ ]={1.5, -2.3, 5.0}; /*tablou dimensionat prin numarul initializatorilor la 3 elemente!*/
int c[3] = {1, 2, 3, 4}; /* greseala: prea multi initializatori!*/
int d[10] ={1, 2}; /* elementele neinitializate explicit se initializeaza implicit cu 0 */
Tablourile de caractere suporta doua modalitati de initializare. Una, comuna tuturor tablourilor:
OBS. Daca se doreste utilizarea continutului tabloului oras ca sir de caractere, atunci trebuie plasat si
terminatorul de sir (‘\0’):
In cazul in care valoarea unui tablou va fi utilizata ca sir de caractere, se initializeaza tabloul cu o constanta sir de
caractere:
char oras[30]=”BRASOV”;
Functii recursive
Functiile C se pot apela pe ele insele, cu alte cuvinte, in C se pot defini functii recursive.
Orice functie recursiva trebuie sa contina o conditie de oprire a apelului recursiv. In absenta acesteia, sau daca
aceasta nu e corecta, apelul recursiv va continua ducand la una din urmatoarele situatii:
a) daca programul a fost compilat cu o optiune care sa includa cod pentru verificarea depasirii stivei,
atunci, cand prin executia repetata a apelului recursiv contextele de apel succesive ajung sa epuizeze
spatiul alocat pentru stiva, programul se termina fortat cu un mesaj de eroare: “Stack overflow”.
b) Daca programul a fost compilat fara optiunea de mai sus, atunci apelul recursiv continua, cu crearea
contextelor de apel dincolo de limita zonei alocata pentru stiva distrugand eventualele date stocate in
zonele respective. In acest caz:
Daca datele distruse nu sunt critice, apelul recursiv va continua pana la inceputul segmentului de
date dupa care SP-ul (stack pointerul) vas indica din nou adresa de baza a stivei si procesul se
reia…
Daca datele distruse sunt critice (influenteaza executia in continuare a programului) programul se
termina anormal sau, mai frecvent, se blocheaza masina.
36
void printd(int n) /* printd: afiseaza n ca numar in baza 10 */
{
if (n < 0) {
putchar ('-');
n = -n;
}
if (n / 10)
printd(n / 10);
putchar(n % 10 + '0');
}
void qsort(int v[], int left, int right) /* qsort: sorteaza crescator v[left],…,v[right] */
{
int i, last;
void swap(int v[], int i, int j);
Functiile recursive sunt mai costisitoare (ca timp de executie si ca spatiu de memorie necesar) decat variantele
iterative ale acelorasi algoritmi. Acest dezavantaj provine din necesitatea crearii cate unui context de apel la
fiecare apel recursiv al functiei. Contextele de apel sunt prezente simultan in memorie – de unde consumul de
memorie – iar pe de alta parte, crearea contextului de apel, care implica deplasarea de date inspre stiva este
costisitoare ca timp.
Avantajul variantelor recursive ale unor algoritmi fata de variantele iterative rezida in naturaletea exprimarii,
concizia si chiar eleganta lor.
Preprocesorul C
Preprocesorul C este (in standardul ANSI C) componenta initiala a compilatorului care are sarcina de a
transforma codul sursa, conform unor directive de precompilare, inainte de compilarea prorpiu-zisa.
Directivele de precompilare se specifica pe linii separate care incep cu caracterlu #. Exemple de directive de
precompilare:
37
Fisierele citate (fisiere text, continand de obicei macrodefinitii, prototipuri de functii, mai rar definitii de functii)
se include in fisierul sursa in locul directivei inainte de si apoi noul fisier sursa rezultat este din nou supus
preprocesarii in vederea expandarii macrodefinitiilor.
OBS. Paranteza deschisa trebuie sa urmeze imediat (fara spatiu) dupa numele macroului!
Orice aparitie a NUMEMACRO(argactuali) in textul sursa va fi inlocuit cu sirul de substituie in care, in plus,
fiecare aparitie a lui argi este inlocuita cu argactuali. Procesul acesta de substitutie poarta denumirea de
expandare a macroului.
Ex.
#define PI 3.14159
#define TRUE 1
#define BEGIN {
dar si
#define max(A, B) ((A) > (B) ? (A) : (B))
#define square(x) (x)* (x)
Macrodefinitiile ofera o alternativa la definirea de functii, uneori utila prin economia de timp utilizata dar care
trebuie utilizata cu grija datorita posibilelor efecte colaterale. De exemplu , macroul
“apelat”
square(n+1);
se expandeaza ca
n+1*n+1;
ceea ce e cu totul altceva decat ceea ce se dorea, adica
(n+1)*(n+1);
Solutie: argumentele se plaseaza intre paranteze atunci cand sunt citate in sirul de substitutie.
a2=square(a++);
se va expanda ca
a2=(a++)*(a++);
ceea ce e diferit de
a2=(a)*(a);
a++:
38
Directive pentru conditionarea actiunilor preprocesorului/compilatorului
#idef NUME
…
#endif
#ifndef NUME
…
#endif
#if expresie_conditionala
…
#endif
#if !expresie_conditionala
…
#endif
Exemple:
#if !defined(HDR)
#define HDR
#endif
sau
#ifndef HDR
#define HDR
#endif
sau
#if SYSTEM == SYSV
#define HDR "sysv.h"
#elif SYSTEM == BSD
#define HDR "bsd.h"
#elif SYSTEM == MSDOS
#define HDR "msdos.h"
#else
#define HDR "default.h"
#endif
#include HDR
…
39
Pointeri
Prin locatie de memorie vom intelege, in continuare, cea mai mica unitate de memorie adresabila. De obicei,
unitatea minima de memorie adresabila este byte-ul. De obicei un byte este format din 8 biti (un bit este unitatea
minima de reprezentare a datelor), caz in care se vorbeste despre octet.
Locatiile de memorie sunt consecutive si numarul lor de ordine (numar intreg pozitiv!) reprezinta adresa lor .
Exista o stransa legatura intre dimensiunea memoriei adresabile si numarul de biti pe care se reprezinta adresele
locatiilor de memorie. De ex. cu adrese pe 2 octeti (16 biti) se poate adresa un spatiu de memorie de maximum
216=65536 locatii (64KB). Cu adrese pe 4 octeti se poate adresa un spatiu de memorie de 2 32=4294967296 locatii
(4GB).
Informatiile memorate de un calculator (fie ele date sau coduri de instructiuni sau chiar adrese) pot ocupa una sau
mai multe locatii succesive de memorie. Indiferent insa cate locatii de memorie ocupa o informatie ce trebuie
stocata, adresa de inceput a grupului de locatii ocupate este…o adresa. Altfel spus, informatiile de tip adresa
ocupa acelasi numar de octeti, indiferent de tipul informatiei stocate incepand cu acea adresa. De obicei adresele
se reprezinta pe doi sau patru octeti. Adresele reprezentate pe doi octeti sunt adrese relative in cadrul unor
segmente de memorie de 64KB, si sunt utilizate de obicei la calculatoare cu registri pe 16 biti, necesitand o
schema de prelucrare care sa permita accesarea unui spatiu de memorie mai mare decat 64KB.
In exemplul de mai jos, adresele (de inceput ale) celor trei date sunt cele cu font ingrosat. Data de tip long int
ocupa 4 bytes (de la 0100 la 0103, inclusiv), data de tip char ocupa un byte (la adresa 0106) iar data de tip int
ocupa 2 bytes (de la adresa 010A la 010B, inclusiv).
0100
0101
0102
0103
0104
0105
} long int (4 bytes)
Ca orice variabila, o variabila pointer ocupa spatiu in memorie. Spatiul ocupat de o variabila pointer este
dependent de implementare, este raportat corect de operatorul sizeof si este egal cu numarul de octeti necesari
pentru reprezentarea unei adrese, pentru ca:
40
Definitie2 (incompleta). Pointerii sunt variabile a caror valoare este interpretata ca adresa a unei
locatii de memorie.
unde NUME_TIP este orice tip de data fundamental sau definit de catre programator iar nume_variabila este un
identificator de variabila.
La ce sunt « buni » pointerii ? Un pointer permite accesul la o valoare, prin intermediul adresei de inceput a zonei
de memorie in care e stocata. (Adresa de inceput, pentru ca, cu exceptia datelor de tip char, toate celelalte
tipuri de date ocupa mai multi bytes in memorie !).
Numele de tip precizat in declaratia unei variabile pointer indica tipul (si deci si numarul de octeti al) valorii care
poate fi accesata prin intermediul pointerului care contine adresa sa.
Definitie3 (completa). Pointerii sunt variabile a caror valoare este interpretata ca adresa a unei
locatii de memorie unde poate fi accesata o valoare de tipul declarat al pointerului.
Pointerii, fiind variabile, au aceleasi atribute ca orice alta variabila : clasa de memorare, domeniu, vizibilitate,
durata, linkage.
La fel ca in cazul oricaror variabile, variabilele pointer globale sau cele locale declarate static se initializeaza
automat, cu valoarea NULL. NULL, ca valoare a unui pointer, este o adresa invalida! Ea se foloseste doar pentru
a indica faptul ca pointerul care are valoarea respectiva nu poate fi utilizat intr-o operatie de dereferentiere.
Ex.
TIP var, *pvar; /* var – variabila de tipul TIP; pvar – variabila “pointer la variabile de
tipul TIP” */
pvar = &var; /* valoarea lui pvar reprezinta adresa variabilei var */
OBS. TIP poate fi oricare din tipurile fundamentale sau definite de catre utilizator (inclusiv pointer!)
Exemplu:
TIP var=expresie,*pvar; /* var – variabila de tipul TIP; pvar – variabila “pointer la variabile de tipul TIP” */
pvar = &var; /* valoarea lui pvar reprezinta adresa varibilei var */
*pvar /* *pvar este o valoare de tipul TIP egala cu valoarea espresiei expresie*/
41
Exceptie : pointeri (void *)
Daca tipul declarat al unui pointer este void *, aceasta semnifica absenta oricarei informatii despre tipul valorii
memorate incepand cu adresa care reprezinta valoarea pointerului. Astfel de pointeri nu pot fi folositi decat
pentru memorarea unor adrese nu si pentreu accesul la datele de la adresele respective!
Asupra pointerilor de tip void * nu se pot face nici un fel de operatii (din cele admise asupra pointerilor) cu
exceptia asignarii unui astfel de pointer la alt pointer sau al asignarii unui pointer la un astfel de pointer.
Ex:
int v, *pi=&v;
char *pc;
void *p;
p=pi; /*corect! p contine adresa variabilei v */
*p… /* eronat! p nu poate fi dereferenetiat pt ca nu se cunoaste tipul datei de al adresa p*/
pc=p: /* corect! pc contine adresa primului octet (un char) de la adresa din p=pi deci
primul octet al lui v */
De ce pointeri ?
Pare fireasca intrebarea : de ce era nevoie de pointeri ? In fond, orice valoarea oricarei variabile este accesibila
utilizand numele (identificatorul) variabilei! Intrebarea are doua raspunsuri :
a) pentru ca permit sa se realizeze lucruri imposibil de realizat altfel
- accesul direct la variabile locale functiei apelante
- alocarea de spatiu in mod dinamic
b) pentru ca permit obtinerea de cod mai eficient
- in cazul sortarii unei multimi de siruri de caractere sau al unei multimi de structuri
- in cazul prelucrarii tablourilor
OBS. De altfel, la apelul unei astfel de functii, argumentul actual corespunzator argumentului formal declarat ca
tablou este, de obicei, numele unui tablou. Numele unui tablou este sinonim cu adresa de inceput a tabloului,
Q.E.D.!
Pe de alta parte daca functiei apelate i se transmite ca argument adresa unei variabile locale functiei apelante
(deci argumentul formal corespunzator este declarat ca pointer !) functia ape lata are acces direct la varibila a
carei adresa i-a fost transmisa !
42
Incrementarea/decrementarea unui pointer (atunci cand pointerul indica spre un element al unui
tablou)
Scaderea unui pointer din alt pointer (atunci cand ambii indica spre elemente ale aceluiasi
tablou)
Compararea a doi pointeri care indica spre elemente ale aceluiasi tablou
Operatii ilegale
Oricare din operatiile care nu sunt legale! ;-)
Ex.
TIP t[DIM], *p=t;
t[i] este tot una cu *(t+i) /* t+i reprezinta adresa celui de-al i+1 –lea element al tabloului t */
Pe de alta parte, intrucat evaluarea oricarei expresii reprezentand un element de tablou presupune un calcul de
adresa, un pointer poate fi utilizat ca nume de tablou.
Ex.
t[i] este totuna cu *(t + i*sizeof(TIP)) care este totuna cu *(p+i) si ca atare cu p[i]
OBS. Echivalenta dintre numele de tablouri si pointeri se opreste insa acolo unde e vorba de operatii care ar avea
ca urmare modificarea adresei.
Ex.
p++;
p=p+i;
t++;
t=t+i;
sunt ambele eronate, intrucat presupun modificarea lui t care este o constanta (adresa de memorie de la care
incepand sunt memorate elementele tabloului!
43
Ex.
OBS. Diferenta: o inmultire si o adunare la varianta cu tablou fata de o incrementare la varianta cu pointer. Chiar
daca presupunem ca incrementarea este echivalenta ca timp de executie cu adunarea (in nici un caz nu este mai
lunga !) in versiunea in care se foloseste pointerl se economiseste timpul necesar unei inmultiri la fiecare ciclu !
Daca in expresie mai apar si alte referiri la elemente ale tabloului diferenta creste.
Pointeri la functii
Desi functiile nu sunt variabile, ele ocupa totusi o zona de memorie, cu alte cuvinte, incep de la o anumita adresa!
De aceea, in C se pot declara pointeri la functii, prin intermediul carora se pot apela functiile a caror adresa o
contin. In mod similar numelor de tablouri, numele unei functii este sinonim cu adresa tabloului.
Ex.
int (*pf)(char *); /*pointer la functie care asteapta ca argument un char * si care intoarce un int */
pf=strlen; /* initializarea pointerului cu adresa unei functii de tipul declarat al pointerului */
Spre deosebire de alte limbaje, in C un tablou multidimensional este un tablou …unidimensional ale carui
elemente sunt …tablouri!
Ex.
TIP t[N][M];
In exemplul anterior, t este un tablou de N elemente, fiecare element fiind un tablou de M elemente de tipul TIP!
Aceasta inseamna ca t[i] este un tablou de M elemente de tipul TIP.
La fel ca si in cazul tablourilor unidimensionale, dimensiunile (precizate in declaratia) tablourilor
multidimensionale trebuie sa fie expresii constante.
In cazul in care un tablou multidimensional este transmis ca argument unei functii, prima dimensiune nu trebuie
precizata in declaratia functiei, in schimb toate celelalte dimensiuni sunt obligatorii, pentru a permite
compilatorului sa calculeze corect adresa unui element atunci cand se foloseste indexarea
Aceasta necesitate rezulta clar din modul in care o expresie desemnand un element de tablou este folosita pentru
a calcula adresa acestuia:
Elementul t[i][j][k] se afla la adresa t + ( i – 1)*N2*N3 + j.
TIP *ptr_tab[N];
44
Tablourile de pointeri reprezinta alternativa (economica) la utilizarea tablourilor bidin\mensionale, mai ales a
tablourilor bidimensionale de caractere.
Ex (reprez. Grafica)…
Un ui program C I se pot transmite anumite argumente (siruri de caractere) la lansarea in executie. Pentru
aceasta, functia main trebuie declarata ca asteptand doua argumente:
- argc – un intreg care are ca valoare numarul de argumente cu care a fost lansat in executie
programul (inclusiv numele acestuia)
- argv – un tablou de pointeri catre sirurile de caractere care reprezinta argumentele
Ex.
main( int argc, char *argv[ ])
{
/* definitia functiei main, in care se utilizeaza argumentele spre care pointeaza argv pentru a obtine
comportamente specifice, in functie de valoarea acestora */
…
}
45
46
Structuri si uniuni
Structuri. Declaratie.
Structurile sunt tipuri de date agregate care grupeaza mai multe date, in general de tipuri diferite – desi nu
obligatoriu! – care au o caracteristica comuna: descriu diverse atribute ale aceleiasi entitati.
Ex. Datele unui angajat dintr-o intreprindere, datele care definesc o adresa (domiciliu) coordonatele unui punct
din spatiul bi, tri, n-dimensional, etc. Structurile sunt echivalente cu tipuri de date similare din alte limbaje,
precum tipul record din Pascal.
Intrucat fiecare tip de “obiect” care se modeleaza printr-o structura are atribute distincte, structurile nu sunt de
fapt tipuri concrete ci permit definirea (de catre programator) a caracteristicilor tipului respectiv. Caracteristicile
respective alcatuiesc un “sablon”, o matrita care specifica atributele tuturor obiectelor de acelasi tip.
struct eticheta_tip {
TIP_membru1 nume_membru1;
TIP_membru2 nume_membru2;
...
TIP_membrun nume_membrun;
};
Cuvantul cheie struct precizeaza ca declaratia care urmeaza este pentru un nou tip de date ale carui atribute sunt
precizate de membrii structurii. Un anumit tip de structura poate fi precizat, de exemplu pentru a declara instante
ale acelei structuri (variabile), cu ajutorul etichetei care precizeaza ce tip de structura se declara
O structura poate fi initializata cu Ajutorul unei liste de initializatori, cate unul pentru fiecare membru. Exemplu
struct s{ /* declaratia tipului struct s */
char c;
int j;
float f;
char string[10];
} ;
struct s v={‘a’, 1, 3.14, “text”}; /*declaratia si initializarea variabilei v de tipul struct s*/
Variabilele de tip structura pot fi transmise ca argumente la functii si pot fi returnate ca valori ale unei functii.
struct s funct(struct s x); /*declaratie de functie care are un argument de tip struct s */
/* si returneaza o valoare de tip struct s */
u=funct(v); /*apelul functiei si asignarea valorii returnate la o variabila de tip struct s*/
Variabilele de tip structura nu sunt utile daca nu pot fi acesati, individual, membrii lor. Accesul la membrii unei
.
variabile de tip structura se face cu ajutorul operatorului (punct).
Generic:
nume_variabila.nume_membru /* valoarea membrului nume_membru al variabilei
/* cu numele nume_variabila */
Ex. v.c /* membrul c al variabilei v */
47
u.c /* membrul c al variabilei u */
Structuri imbricate
Ex.
struct point { int x, y;};
struct rectangle { struct point p1,p2};
struct s{ struct s m;}; /* eroare: membrii unei structuri nu pot fi structuri de acelasi tip*/
OBS. Interdictia/imposibilitatea declararii de structuri avand ca membri structuri de acelasi tip rezida din
caracterul recursiv al unei astfel de declaratii, ceea ce determina imposibilitatea rezolvarii uneia din sarcinile
legate de declararea variabilelor de tipul respectiv: rezervarea de spatiu! (La declararea oricarei variabile se
rezerva si spatiu pentru acea variabila!)
Ex.
struct s {
...
}v, *pv=&v;
Cu declaratia de mai sus, v este o variabila de tipul struct s, pv este un pointer la o structura de tipul struct s, iar
*pv este tot una cu v (daca pv are ca valoare adresa lui v) adica o structura de tip struct s.
Accesul la membrii unei structuri, prin intermediul unui pointer care contine adresa variabilei respective se face
in felul urmator:
(*pv).nume_membru
din cauza precedentei mai mari a operatorului de acces la membrii unei structuri (.) decat a operatorului de
dereferentiere (*).
Exista o alternativa pentru accesul la membrii unei structuri prin intermediul unui pointer si anume prin utilizarea
operatorului ->
Ex.
pv->nume_membru
t[i].nume_membru
48
Intrucat pointerii se folosesc cu precadere pentru a indica/accesa elemente
de tablou, in exemplul urmator se prezinta modalitatea de a accesa membrii
unui element al unui tablou de structuri prin intermadiul unui pointer.
Ex.
struct s t[DIM], *p=t;
...
(p+i)->nume_membru /*acces la un membru al elementului de indice i*/
p->nume_membru /*acces la un membru al elemnetului “curent”*/
Structurile reprezinta metoda preferata pentru implementarea listelor, arborilor, tablourilor de cautare (vezi si
discutia referitoare la Probleme care implica utilizarea tablourilor sau a listelor).
Structurile nu pot contine, ca membri, structuri de acelasi tip! Explicatia consta in imposibilitatea rezervarii de
spatiu pentru o variabila de acest tip, dupa cum rezulta din exemplul urmator:
struct s {
char c;
struct s n;
} v;
Oridecate ori compilatorul intalneste o declaratie (de definire) a unei variabile, una din actiunile intreprinse o
reprezinta generarea de cod pentru rezervarea de spatiu (“crearea variabilei”) pt variabila respectiva. In cazul unei
variabile structura, aceasta implica rezervarea de spatiu pentru fiecare membru in parte…
v.c - 1 byte
v.n - variabila structura
v.n.c - 1 byte
v.n.n - variabila structura
v.n.n.c - 1 byte
v.n.n.n - variabila structura…
…si procesul nu poate fi terminat!
In schimb, o structura poate contine ca membru un pointer la o structura de acelasi tip
struct s {
char c;
struct s *next;
} v;
v.c - 1 byte
v.next - 2 (4) bytes
Structurile care contin ca membri pointeri spre structuri de acelasi tip se numesc structuri cu autoreferire.
Structurile cu autoreferire reprezinta mijlocul de implementare a listelor, arborilor, tabelelor de cautare sau
hashing.
49
Operatorul typedef
Limbajul C permite definirea de noi nume de tipuri pentru tipuri de date existente. Aceasta facilitate este
accesibila prin operatorul typedef. Sintaxa operatorului este:
OBS. Noul nume de tip, creat cu typedef poate fi folosit in acelasi mod ca si vechiul nume, cu care este sinonim!
Ex.
typedef int Coord; /* Coord este un intreg */
typedef struct tnode * Treeptr; /*Treeptr este un pointer la struct tnode */
typedef char * String; /* String este un pointer la char */
OBS. In ultimul exemplu, desi String este sinonim cu un pointer la char si deci poate fi folosit (legal) in :
char c;
String p=&c;
Dar o astfel de utilizare ar fi improprie, intrucat, string inseamna “sir” (de caractere, in context!), ceea ce nu e …
cazul in exemplul anterior! Exemplul urmator indica o utilizare pertinenta pentru String:
char t[ ] = “Timisoara”;
String p = s, q;
Q = (String)malloc( strlen(p)+1);
intrucat, valoarea lui p este intradevar adresa unui sir (de caractere)!
Numele “create” cu typedef au aceeasi semnificatie ca si sinonimele lor si pot fi utilizate oriunde se poate utiliza
un nume de tip predefinit!
Uniuni. Declaratie
Uniunile sunt tipuri de date care permit partajarea aceluiasi spatiu de memorie de catre mai multe variabile (de
tipuri diferite) care nu trebuie sa coexiste (simultan). Sintaxa declaratiei unei uniuni:
union eticheta {
/*
declaratii membri
*/
};
Ca si in cazul structurilor, declaratia unei uniuni precizeaza un “model”/”sablon” si ca atare nu este insotita de
rezervare de spatiu. Spatiu se rezerva doar in momentul declararii unei variabile de tip uniune.
Spatiul necesar unei variabile uniune trebuie sa fie (este) suficient pentru a pastra cel mai mare (care are nevoie
de cel mai mult spatiu) membru.
50
Initializarea (pe linia de declaratie a) unei variabile uniuni se poate face doar cu o valoare corespunzatoare
primului membru.
Operatii cu uniuni
Ca si in cazul structurilor, variabilele uniune pot fi asignate/copiate (inclusiv transmise ca argumente actuale la
apelul unei functii, respectiv returnate de o functie), li se poate prelua adresa si membri sai pot fi accesati
individual. Operatorul de acces la membri este acelasi ca la structuri, adica .
Cade in sarcina programatorului sa urmareasca care dintre membri variabilei uniune este memorat in aceasta.
Exista o modalitate standard pentru a realiza aceasta urmarire.
Ex.
#define INT 0
#define FLOAT 1
#define STRING 2
union u {
int ival;
float fval;
char *sval;
; v;
int v_type;
Oridecate ori se memoreaza un membru in variabila uniune v, actiuneaeste insotita de actualizarea valorii
variabilei v_type:
v.ival=val_intreaga;
v_type=INT;
…
v.fval=val_flotanta;
v_type=FLOAT;
…
v.sval=adresa_sir_caractere;
v_type=STRING;
Oridecate ori trebuie “exploatata” valoarea variabilei uniune, se verifica tipul valorii memorate curent in v prin
“interogarea” variabilei v_type:
if(v_type == INT)
printf(“ %d “, v.ival; /* “exploateaza” pe v.ival */
else if (v-type == FLOAT)
printf(“ %f “, v.fval); /* “exploateaza” pe v.fval */
else if(v_type == STRING)
printf(“ %s “, v.sval; /* “exploateaza” pe v.sval */
else
printf(“bad union member! “);
51
Probleme care implica utilizarea tablourilor sau a listelor
Cazul 1
Se cunoaste dimensiunea tabloului (volumul de date) la momentul elaborarii/proiectarii/codificarii programului.
Solutie: tablou cu dimensiunea “inghetata” in fisierul sursa (eventual in fisierul sursa sau intr-un fisier header
se defineste cu directiva define dimensiunea tabloului , astfel incat daca se doreste o versiune a aplicatiei care sa
permita un volum diferit de date se modifica definitia).
Cazul 2
Se cunoaste volumul de date de abia la inceputul executiei programului (pentru fiecare executie volumul de date
e posibil sa difere).
Solutie: “tablou” pentru care se aloca spatiu in mod dinamic si accesul la elementele tabloului se face de regula
prin pointeri.
Cazul 3
Volumul de date nu se cunoaste nici macar la inceputul executiei ci doar la sfarsitul fazei de creere/citire a
datelor.
Solutie: datele se implementeaza sub forma de lista/arbore/graf/retea (in functie de problema concreta)
52
Intrari si iesiri (I/O). Fisiere.
Intrari/iesiri standard
Stdin si respectiv stdout pot fi redirectate astfel incat intrarea sa se faca dintr-un fisier, respectiv iesirea sa se
faca intr-un fisier.
Fisiere. Modele
Doua modele:
Modelul UNIX: fisiere, in care accesul se face la nivel de byte, comanda dispozitivelor fizice se
face de catre sistemul de operare (SO), singura legatura a programului cu fisierul fiind handle-
ul (un numar intreg nenegativ returnat de functia de deschidere fisier – open). Caracteristici:
simplitate, eleganta dar si eficienta mai scazuta. Se pot folosi pentru a “construi” functii de I/O
de nivel superior.
Modelul cu streamuri: streamuri (dispozitive logice de I/O), pentru care functia de deschidere
(fopen) pregateste un bloc de control al fisierului (FCB, o structura de tip FILE, prin
intermediul caruia se efectueaza/gestioneaza toate operatiile asupra fisierului) si intoarce un
pointer (file pointer) spre acest bloc de control. Blocul de control al fisierelor se pastreaza in
memoria programului ceea ce constituie principalul dezavantaj al modelului: din eroare, se
poate altera continutul FCB, pentru ca este accesibil! Avantaj: este mai eficient si este adaptabil
diverselor tipuri de SO.
Pe de alta parte operatiile de acces la continutul unui fisier se poate face buferizat (prin intermediul unui buffer)
sau nebuferizat (direct, byte cu byte).
53
Fisere text, care sunt “colectii” de linii de text, fiecare linie terminata cu un caracter newline (intern,
pentru ca extern/fizic, terminatorul de linie poate fi altul si e sarcina implementarii sa asigure
compatibilitatea/conversia).
Fisiere binare, care sunt colectii de bytes, a caror interpretare cade exclusiv in sarcina programului.
54