Upcoming SlideShare
×

Thanks for flagging this SlideShare!

Oops! An error has occurred.

×
Saving this for later? Get the SlideShare app to save on your phone or tablet. Read anywhere, anytime – even offline.
Standard text messaging rates apply

# Carte C 2003

10,670
views

Published on

Basic C programming book

Basic C programming book

Published in: Education, Business, Technology

3 Likes
Statistics
Notes
• Full Name
Comment goes here.

Are you sure you want to Yes No
Your message goes here
• Be the first to comment

Views
Total Views
10,670
On Slideshare
0
From Embeds
0
Number of Embeds
2
Actions
Shares
0
487
0
Likes
3
Embeds 0
No embeds

No notes for slide

### Transcript

• 1. CUPRINS CUPRINS............................................................................................I Capitolul I...........................................................................................1 INTRODUCERE ÎN..........................................................................1 ARHITECURA SISTEMELOR DE CALCUL...............................1 1.1. Importanţa limbajului C....................................................1 1.2 Arhitectura de bază a unui calculator.................................4 1.2.1 Microprocesorul.........................................................7 1.2.2 Memoria.....................................................................8 1.2.3 Echipamentele periferice..........................................10 1.3. Programarea calculatorului.............................................13 1.3.1. Sistemul de operare.................................................14 1.3.2. Tipuri de fişiere.......................................................18 1.3.3. Construirea fişierului executabil.............................19 Capitolul II.......................................................................................24 REPREZENTAREA DATELOR ÎN CALCULATOR.................25 2.1. Reprezentarea internă/externă a numerelor....................26 2.2. Reprezentarea externă a numerelor.................................27 2.2.1. Reprezentarea externă a numerelor întregi..............28 2.2.2. Reprezentarea externă a numerelor reale................31 2.3 Reprezentarea internă a numerelor..................................33 2.3.1. Reprezentarea internă a numerelor întregi..............33 2.3.2 Adunarea, scăderea şi înmulţirea numerelor întregi.35 2.3.3 Reprezentarea internă a numerelor reale..................37 Game de reprezentare pentru numerele reale....................49 2.3.5. Codificare BCD.......................................................50 Capitolul III......................................................................................52 ELEMENTELE DE BAZĂ ALE LIMABJULUI C.....................52 3.1. Crearea şi lansarea în execuţie a unui program C...........52 3.2. Structura unui program C................................................54 3.3. Mulţimea caracterelor ....................................................56 3.3.1. Litere şi numere.......................................................57 I
• 2. 3.3.2. Caractere whitespace...............................................57 3.3.3. Caractere speciale şi de punctuaţie..........................57 3.3.4. Secvenţe escape.......................................................58 3.4. Identificatori....................................................................59 3.5. Cuvintele cheie ale limbajului C.....................................60 3.6. Constante.........................................................................60 3.6.1. Constante caracter...................................................61 3.6.2. Constante întregi.....................................................61 3.6.3. Constante în virgulă mobilă....................................61 3.6.4. Constante şir............................................................62 3.6.5. Constanta zero.........................................................63 3.6.6. Obiecte constante....................................................63 3.6.7. Enumerări ...............................................................64 Capitolul IV......................................................................................64 ..........................................................................................................64 Operanzi şi operatori în C...............................................................64 4.1. Operanzi .........................................................................65 4.2. Operatori ........................................................................65 4.2.1. Operatori aritmetici.................................................66 4.2.2. Operatori de incrementare şi decrementare.............67 4.2.3. Operatori relaţionali................................................67 4.2.4. Operatori logici.......................................................68 4.2.5. Operatori logici la nivel de bit.................................69 4.2.6. Operatorul de atribuire............................................74 4.2.7. Operatorul sizeof.....................................................74 4.2.8. Operatorul ternar ?.................................................75 4.2.9. Operatorul virgulă...................................................76 4.2.10. Operatorul de forţare a tipului sau de conversie explicită (expresie cast)........................................77 4.2.11. Operatorii paranteză..............................................77 4.2.12. Operatorul adresă..................................................78 4.2.13. Alţi operatori ai limbajului C................................78 4.2.14. Regula conversiilor implicite şi precedenţa operatorilor...........................................................78 Capitolul V.......................................................................................80 ..........................................................................................................80 II
• 3. Instrucţiuni.......................................................................................80 5.1. Instrucţiuni etichetate (instrucţiunea goto).....................80 5.2. Instrucţiuni expresie........................................................81 5.3. Instrucţiuni compuse.......................................................82 5.4. Instrucţiuni de selecţie....................................................82 5.4.1. Instrucţiunea if.........................................................82 5.4.2. Instrucţiuni de selecţie multiplă: if - else if.............84 5.4.3. Instrucţiunea switch.................................................85 5.5. Instrucţiuni repetitive......................................................87 5.5.1. Instrucţiunea for......................................................87 5.5.2. Instrucţiunea while..................................................90 5.5.3. Instrucţiunea do-while.............................................92 5.5.4. Bucle încuibate........................................................94 5.5.5. Instrucţiunea break..................................................96 5.5.6. Instrucţiunea continue.............................................97 Capitolul VI......................................................................................97 ..........................................................................................................97 TIPURI DE DATE STRUCTURATE ...........................................97 6.1. Tablouri unidimensionale...............................................98 6.1.1. Constante şir............................................................99 6.1.2. Iniţializarea vectorilor de caractere.......................100 6.1.3. Funcţii pentru prelucrarea şirurilor (fişierul antet string.h)...............................................................102 6.2. Tablouri cu două dimensiuni (matrice).........................104 6.2.1. Iniţializarea matricelor..........................................105 6.2.2. Tablouri bidimensionale de şiruri.........................106 6.3. Tablouri multidimensionale..........................................106 6.4. Structuri.........................................................................107 6.4.1. Tablouri de structuri..............................................109 6.4.2. Introducerea structurilor în funcţii........................115 6.4.3. Tablouri şi structuri în structuri.............................119 6.5. Uniuni...........................................................................119 6.6. Enumerări......................................................................121 Capitolul VII..................................................................................123 ........................................................................................................123 POINTERI.....................................................................................123 III
• 4. 7.1. Operatori pointer...........................................................123 7.1.1. Importanţa tipului de bază.....................................125 7.1.2. Expresii în care intervin pointeri...........................125 7.2. Pointeri şi tablouri.........................................................130 7.2.1. Indexarea pointerilor.............................................131 7.2.2. Pointeri şi şiruri ....................................................133 7.2.3. Preluarea adresei unui element al unui tablou.......134 7.2.4. Tablouri de pointeri...............................................134 7.2.5. Pointeri la pointeri.................................................135 7.2.6. Iniţializarea pointerilor..........................................136 7.2.7. Alocarea dinamică a memoriei..............................137 7.2.8. Pointeri la structuri................................................139 7.2.9. Structuri dinamice liniare de tip listă...................142 Capitolul VIII.................................................................................155 FUNCŢII.......................................................................................155 8.1. Forma generală a unei funcţii.......................................155 8.2. Reîntoarcerea dintr-o funcţie........................................158 8.3. Valori returnate.............................................................159 8.4. Domeniul unei funcţii...................................................160 8.4.1. Variabile locale.....................................................160 8.4.2. Parametri formali...................................................162 8.4.3. Variabile globale...................................................163 8.5. Apelul funcţiilor............................................................167 8.6. Apelul funcţiilor având ca argumente tablouri.............168 8.7. Argumentele argc şi argv ale funcţiei main()...............172 8.8. Funcţii care returnează valori neîntregi........................173 8.9. Returnarea pointerilor...................................................174 8.10. Funcţii de tip void.......................................................177 8.11. Funcţii prototip............................................................178 8.12. Funcţii recursive..........................................................180 8.13. Clase de memorare (specificatori sau atribute)...........181 8.14. Pointeri la funcţii.........................................................187 Capitolul IX....................................................................................188 PREPROCESAREA.....................................................................189 9.1. Directive uzuale............................................................189 9.2. Directive pentru compilare condiţionată.......................191 IV
• 5. 9.3. Modularizarea programelor.........................................195 Capitolul X.....................................................................................200 ........................................................................................................200 INTRĂRI/IEŞIRI...........................................................................200 10.1. Funcţii de intrare şi ieşire - stdio.h..............................200 10.2. Operaţii cu fişiere........................................................203 10.3. Nivelul inferior de prelucrare a fişierelor...................206 10.3.1. Deschiderea unui fişier........................................206 10.3.2. Scrierea într-un fişier...........................................210 10.3.3. Citirea dintr-un fişier...........................................212 10.3.4. Închiderea unui fişier...........................................214 10.3.5. Poziţionarea într-un fişier....................................214 10.3.6 Ştergerea unui fişier.............................................216 10.3.7. Exemple de utilizare a funcţiilor de intrare/ieşire de nivel inferior.......................................................217 10.4. Nivelul superior de prelucrare a fişierelor..................222 10.4.1. Funcţia fopen()....................................................223 10.4.2. Funcţia fclose()....................................................224 10.4.3. Funcţiile rename() şi remove()...........................225 10.4.4. Funcţii de tratare a erorilor..................................225 10.4.5. Funcţii cu acces direct.........................................226 10.4.6. Funcţii pentru poziţionare...................................228 10.4.7. Ieşiri cu format....................................................229 10.4.8. Intrări cu format..................................................232 10.4.9. Funcţii de citire şi scriere a caracterelor..............235 Capitolul XI....................................................................................239 ........................................................................................................239 Utilizarea ecranului.......................................................................239 în mod text.....................................................................................239 11.1. Setarea ecranului în mod text......................................240 11.2. Definirea unei ferestre.................................................241 11.3. Ştergerea unei ferestre.................................................241 11.4. Deplasarea cursorului..................................................241 11.5. Setarea culorilor..........................................................242 11.6. Funcţii pentru gestiunea textelor.................................243 Capitolul XII..................................................................................246 V
• 6. ........................................................................................................246 Utilizarea ecranului.......................................................................246 în mod GRAFIC............................................................................246 12.1. Iniţializarea modului grafic.........................................247 12.2. Gestiunea culorilor......................................................249 12.3. Setarea ecranului.........................................................251 12.4. Utilizarea textelor în mod grafic.................................251 12.5. Gestiunea imaginilor...................................................253 12.6. Desenarea şi colorarea figurilor geometrice...............254 Capitolul XIII.................................................................................260 Funcţii matematice .......................................................................260 13.1 Funcţii trigonometrice..................................................261 13.2 Funcţii trigonometrice inverse.....................................261 13.3 Funcţii hiperbolice.......................................................261 13.4 Funcţii exponenţiale şi logaritmice..............................262 13.5 Generarea de numere aleatoare....................................262 13.6 Alte tipuri de funcţii matematice.................................263 Capitolul XIV.................................................................................264 ELEMENTE DE PROGRAMARE AVANSATĂ.......................264 14.1 Gestionarea memoriei..................................................264 14.1.1 Memoria convenţională........................................264 14.1.2 Memoria expandată..............................................267 14.1.3 Memoria extinsă...................................................268 14.1.4 Stiva......................................................................268 14.2 Servicii DOS şi BIOS..................................................269 14.2.1 Serviciile BIOS....................................................270 14.2.2 Serviciile DOS......................................................274 14.3 Bibliotecile C...............................................................277 14.3.1 Reutilizarea unui cod obiect.................................278 14.3.2 Lucrul cu fişiere bibliotecă...................................278 14.3 Fişierele antet...............................................................279 BIBLIOGRAFIE............................................................................279 VI
• 7. Capitolul I INTRODUCERE ÎN ARHITECURA SISTEMELOR DE CALCUL 1.1. Importanţa limbajului C Creat în anul 1972 de programatorii de sistem Dennis M. Ritchie şi Brian W. Kernighan de la Bell Laboratories cu scopul de a asigura implementarea portabilă a sistemului de operare UNIX, C-ul este astăzi unul din cele mai cunoscute şi puternice limbaje de programare. Eficient, economic şi portabil, C-ul este o alegere bună pentru realizarea oricărui tip de programe, de la editoare de texte, jocuri cu facilităţi grafice, programe de gestiune şi pentru calcule ştiinţifice, până la programe de sistem, constituind unul dintre cele mai puternice instrumente de programare. Adesea referit ca limbaj portabil, C-ul permite transferul programelor între calculatoare cu diferite procesoare şi în acelaşi timp facilitează utilizarea caracteristicilor specifice ale maşinilor particulare, programele scrise în C fiind considerate cele mai portabile. Dacă evoluţia limbajelor de programare a adus în prim plan nume ca FORTRAN, LISP, COBOL, ALGOL-60 sau PASCAL, unele cu răspândire mai mult ”academică” – fiind folosite pentru a prezenta conceptele de bază sau conceptele avansate de programare – ca de pildă ALGOL-60 sau PASCAL, altele cu răspândire industrială masivă – ca de pildă FORTRAN şi COBOL – limbajul C a pătruns mai lent, dar foarte sigur. Realitatea arată clar că, în momentul de faţă, piaţa producătorilor de programe este dominată net de C şi de variantele evoluate ale acestuia. Elementele principale care au contribuit la succesul C-ului sunt următoarele: - modularizarea programelor – ce dă posibilitatea unui singur programator să stăpânească relativ uşor programe de zeci de mii de linii de sursă; 1
• 8. - capacitatea de programare atât la nivel înalt cât şi la nivel scăzut – ceea ce dă posibilitatea utilizatorului de a programa fie fără a ”simţi” sistemul de operare şi maşina de calcul, fie la un nivel apropiat de sistemul de operare ceea ce permite un control foarte bun al eficienţei programului din punct de vedere viteză/memorie; - portabilitatea programelor – ce permite utilizarea programelor scrise în C pe o mare varietate de calculatoare şi sisteme de operare; - facilităţile de reprezentare şi prelucrare a datelor – materializate printr-un număr mare de operatori şi funcţii de bibliotecă ce fac programarea mult mai uşoară. Prin anii ’80 interesul pentru programarea orientată pe obiecte a crescut, ceea ce a condus la apariţia de limbaje care să permită utilizarea ei în scrierea programelor. Limbajul C a fost dezvoltat şi el în această direcţie şi în anul 1980 a fost dat publicităţii limbajul C++, elaborat de Bjarne Stroustrup de la AT&T. La ora actuală, majoritatea limbajelor de programare moderne au fost dezvoltate în direcţia programării orientate pe obiecte. Limbajul C++, ca şi limbajul C, se bucură de o portabilitate mare şi este implementat pe o gamă largă de calculatoare începând cu microcalculatoare şi până la cele mai mari supercalculatoare. Limbajul C++ a fost implementat pe microcalculatoarele compatibile IBM PC în mai multe variante. Cele mai importante implementări ale limbajelor C++ pe aceste calculatoare sunt cele realizate de firmele Microsoft şi Borland. Conceptele programării orientate pe obiecte au influenţat în mare măsură dezvoltarea limbajelor de programare în ultimul deceniu. De obicei, multe limbaje au fost extinse astfel încât ele să admită conceptele mai importante ale programării orientate pe obiecte. Uneori s-au făcut mai multe extensii ale aceluiaşi limbaj. De exemplu, limbajul C++ are ca extensii limbajul E ce permite crearea şi gestiunea obiectelor persistente, lucru deosebit de important pentru sistemele de gestiune a bazelor de date, limbajul O ce încearcă să îmbine facilităţile de nivel înalt cu cele ale programării de sistem, limbajul Avalon/C++ destinat calculului distribuit, şi nu în ultimul rând binecunoscutul de acum limbaj Java, specializat în aplicaţii Internet. Interfeţele utilizator au atins o mare dezvoltare datorită facilităţilor oferite de componentele hardware ale diferitelor 2
• 9. calculatoare. În principiu, ele simplifică interacţiunea dintre programe şi utilizatorii acestora. Astfel, diferite comenzi, date de intrare sau rezultate pot fi exprimate simplu şi natural utilizând diferite standarde care conţin ferestre, bare de meniuri, cutii de dialoguri, butoane, etc. Cu ajutorul mouse-ului toate acestea pot fi accesate extrem de rapid şi uşor fără a mai fi nevoie să cunoşti şi să memorezi o serie întreagă comenzi ale sistemului de operare sau ale limbajului de programare. Toate acestea conduc la interfeţe simple şi vizuale, accesibile unui segment foarte larg de utilizatori. Implementarea interfeţelor este mult simplificată prin utilizarea limbajelor orientate spre obiecte, aceasta mai ales datorită posibilităţii de a utiliza componente standardizate aflate în biblioteci specifice. Importanţa aplicării conceptului de reutilizare a codului rezultă din faptul că interfeţele utilizator adesea ocupă 40% din codul total al aplicaţiei. Firma Borland comercializează o bibliotecă de componente standardizate care pot fi utilizate folosind limbajul C++, bibliotecă cunoscută sub numele Turbo Vision. De obicei, interfeţele utilizator gestionează ecranul în mod grafic. O astfel de interfaţă utilizator se numeşte interfaţă utilizator grafică. Una din cele mai populare interfeţe utilizator grafice pentru calculatoarele IBM PC este produsul Windows oferit de firma Microsoft. Windows este un mediu de programare ce amplifică facilităţile oferite de sistemul de operare MS-DOS. Aplicaţiile Windows se pot dezvolta folosind diferite medii de dezvoltare ca: Turbo C++ pentru Windows, Pascal pentru Windows, Microsoft C++, Microsoft Visual Basic, Visual C şi Visual C++. Componentele Visual permit specificarea în mod grafic a interfeţei utilizator, a unei aplicaţii, folosind mouse-ul, iar aplicaţia propriu-zisă se programează într-un limbaj de tip Basic, C sau C++. Dacă în ani ’70 se considera că o persoană este rezonabil să se poată ocupa de o aplicaţie de 4-5 mii de instrucţiuni, în prezent, în condiţiile folosirii limbajelor de programare orientate pe obiecte, această medie a ajuns la peste 25 de mii de instrucţiuni. Un limbaj de programare trebuie privit nu doar la suprafaţa sa – sintaxă şi mod de butonare a calculatorului pentru o implementare particulară – ci mai ales în profunzime, prin conceptele pe care se bazează, prin stilul de programare, prin modul de structurare a 3
• 10. aplicaţiei şi, implicit, a programului, prin filozofia de rezolvare a problemelor folosind limbajul. Din aceste puncte de vedere, C-ul nu poate lipsi din cultura unui programator, iar pentru un profesionist C- ul este, şi mai mult, o necesitate vitală, acesta fiind piatra de temelie pentru înţelegerea şi utilizarea eficientă a limbajelor de nivel înalt orientate pe obiecte şi Visual. 1.2 Arhitectura de bază a unui calculator Calculatoarele de tip PC (calculatoare personale) reprezintă cele mai răspândite şi mai utilizate dintre calculatoare, datorită gradului de accesibilitate şi preţului relativ scăzut. Indiferent de tipul calculatorului, modul general de concepţie, de alcătuire şi funcţionare este acelaşi. Calculatorul este o maşină programabilă. Două dintre principalele caracteristici ale unui calculator sunt: 1. Răspunde la un set specific de instrucţiuni într-o manieră bine definită. 2. Calculatorul poate executa o listă preînregistrată de instrucţiuni, numită program. Calculatoarele moderne sunt electronice şi numerice.  Partea de circuite electrice şi electronice precum şi conexiunile fizice dintre ele se numeşte hardware.  Totalitatea programelor precum şi datele aferente acestor programe poartă denumirea de software. 4
• 11. Echipamente de ieşire Echipamente de stocare date (HDD, FDD, CD-ROM, etc.) UPC Unitatea de procesare şi control Echipamente de intrare Fig.1.1 Configuraţia standard pentru utilizator Partea hardware a unui calculator este formată din totalitatea componentelor sale fizice. Toate calculatoarele de uz general necesită următoarele componente hardware:  memorie: Permite calculatorului să stocheze, cel puţin temporar, date şi programe.  dispozitive de stocare externe: Permit calculatoarelor să stocheze permanent programe şi mari cantităţi de date. Cele mai uzuale dispozitive de stocare externă sunt HDD (hard disk drives), FDD (floppy disk drive) şi CD-ROM (Compact Disk-Read Only Memory) sau CD-R/W (Compact Disk- Read/Write).  dispozitive de intrare : În mod uzual sunt reprezentate de tastatură (keyboard) şi de mouse. Aceste dispozitive reprezintă calea uzuală de introducere a datelor şi instrucţiunilor care gestionează funcţionarea unui calculator.  dispozitive de ieşire: Reprezintă modalitatea prin care calculatorul transmite utilizatorului uman rezultatele execuţiei programelor. Ecranul monitorului sau imprimanta sunt astfel de dispozitive uzuale.  unitatea de procesare şi control (UPC) : Este partea principală a unui calculator deoarece este componenta care execută instrucţiunile. În mod uzual această unitate de procesare şi control este reprezentată de un microprocesor care se 5
• 12. plasează pe placa de bază (mainboard) a calculatorului împreună cu memoria internă RAM. În plus faţă de aceste componente orice calculator este prevăzut cu o magistrală (bus) prin care se gestionează modalitatea de transmitere a datelor între componentele de bază ale calculatorului. Magistrala reprezintă o colecţie de trasee electrice care leagă microprocesorul de dispozitivele de intrare/ieşire şi de dispozitivele interne/externe de stocare a datelor. Putem distinge magistrala de date, magistrala de adrese şi magistrala de comandă şi control. În figura 1.2 este prezentată interacţiunea dintre componentele HARD principale ale unui calculator. Calculatoarele pot fi în general clasificate după dimensiuni sau putere de calcul. Nu se poate face însă la ora actuală o distincţie netă între următoarele categorii de calculatoare: PC (Personal Computer): Un calculator de dimensiuni mici, monoutilizator (single-user), bazat pe un microprocesor. În plus acesta este dotat standard cu tastatură, mouse, monitor şi dispozitive periferice de stocare a datelor. Memoria secundară Echipament UNITATEA Echipament de intrare CENTRALĂ de ieşire Memoria principală Fig. 1.2 Arhitectura minimală a unui sistem de calcul  staţii de lucru (workstation): Un calculator monoutilizator de mare putere. Aceasta este asemănător unui PC dar are un microprocesor mai puternic şi un monitor de înaltă calitate (rezoluţie mai mare). 6
• 13.  minicalculator (minicomputer): Un calculator multiutilizator (multi-user) capabil să lucreze simultan cu zeci sau chiar sute de utilizatori.  mainframe: Un calculator multiutilizator capabil să lucreze simultan cu sute sau chiar mii de utilizatori.  supercomputer: Un computer extrem de rapid care poate executa sute de milioane de operaţii într-o secundă. 1.2.1 Microprocesorul Microprocesorul este cea mai importantă şi cea mai scumpă componentă a unui calculator de performanţele acesteia depinzând în mare măsură rezultatele întregului sistem. Din punct de vedere fizic, microprocesorul este un cip ce conţine un circuit integrat complex ce îi permite să prelucreze informaţii prin executarea unor operaţii logice şi matematice diverse (adunări, scăderi, înmulţiri, împărţiri, comparări de numere). El este compus din două părţi importante: unitatea de execuţie (EU – Execution Unit) şi unitatea de interfaţă a magistralei de date (BIU – Bus Interface Unit). Prima componentă realizează efectiv operaţiile, iar cea de-a doua are funcţia de transfer a datelor de la şi înspre microprocesor. Microprocesorul reprezintă de fapt unitatea centrală a unui calculator şi îndeplineşte o serie de activităţi specifice cum ar fi: execută operaţii aritmetice şi logice, decodifică instrucţiuni speciale, transmite altor cipuri din sistem semnale de control. Toate aceste operaţii sunt executate cu ajutorul unor zone de memorie ale microprocesorului, numite registre. Orice microprocesor are un set finit de instrucţiuni pe care le recunoaşte şi pe care le poate executa. Calculatoarele IBM PC folosesc procesoare INTEL sau compatibile, realizate de alte companii cum ar fi: AMD, NexGen, CYRIX. Numele microprocesorului este folosit la identificarea calculatorului. Se folosesc frecvent expresii de tipul calculator 386, calculator 486, calculator Pentium II, etc. În 1971 firma Intel a fost abordata de o companie Japoneza, acum dispărută, pentru a construi un circuit dedicat pentru un nou calculator. Designerul Ted Hoff a propus o soluţie programabilă, de uz general, şi astfel s-a născut circuitul Intel 4004. Au urmat la scurt timp chipurile 4040 si 8008 dar lor le lipseau multe din caracteristicile microprocesoarelor aşa cum le ştim noi azi. În 1974 Intel a prezentat 7
• 14. pentru prima oară circuitul Intel 8080 care a fost folosit in sistemele Altair şi IMSAI. Curând după aceea au apărut procesoarele Motorola 6800 şi 6502 de la MOS Technology. Doi dintre proiectanţii de la Intel au părăsit firma, creând corporaţia ZILOG care a produs chipul Z80 (compatibil cu 8080 dar cu set de instrucţiuni mai puternic şi de două ori mai rapid. Cipul Intel 4004 a fost primul procesor comercial, lansat la sfârşitul anului 1971. La un preţ de circa 200\$ şi înglobând 2300 de tranzistori, cipul 4004 dezvolta mai multă putere de calcul decât ENIAC, primul calculator electronic, cu 25 de ani în urma. Faţă de cele 18.000 de tuburi cu vacuum ce ocupau 900 metri cubi, procesorul 4004 putea dezvolta 60.000 de operaţii pe secundă. Această invenţie a contribuit la revoluţionarea domeniilor de aplicaţii ale computerelor, dând startul unui adevărat galop de inovaţii tehnologice. Următorul pas a fost în 1980, când IBM a inclus un procesor Intel în arhitectura primului PC. Astăzi PC-urile sunt pretutindeni în jurul nostru. Un copil care lucrează la o maşina ce incorporează un procesor Pentium Pro beneficiază de mult mai multă putere de calcul decât dispunea guvernul SUA în perioada lansării primelor echipaje umane către Lună. Într-un număr aniversar al publicaţiei Communications of the ACM, Gordon Moore, co-fondator al companiei Intel, era optimist în privinţa evoluţiei PC-urilor şi a microprocesoarelor: "complexitatea microprocesoarelor, care se măsoară prin numărul de tranzistori pe cip, s-a dublat aproape constant la fiecare 18 luni, de la apariţia primului prototip 4004. Aceasta evoluţie exponenţială a determinat o continuă creştere a performanţelor PC-urilor şi o scădere a costului procesului de calcul. Pe când în 1991 un PC bazat pe procesorul Intel 486 costa aproape 225\$ pentru o performanţă de un milion de instrucţiuni pe secundă (MIPS), astăzi, un sistem desktop ce utilizează un cip Pentium Pro este evaluat la circa 7\$ pe MIPS. Nu se întrevede nici o dificultate care să frâneze această rată de dezvoltare". 1.2.2 Memoria Microprocesorul are capacitatea de a memora date care urmează a fi prelucrate, cât şi rezultatele intermediare. Se observă că rolul său principal este de a prelucra şi transmite informaţiile şi rezultatele şi deci capacitatea sa de memorare este mică neputând stoca programe. 8
• 15. De aceea, un calculator necesită şi o memorie care să găzduiască date şi programe. Memoria este formată din punct de vedere fizic din cipuri ce stochează informaţia sub forma a două niveluri de tensiune ce corespund valorilor 0 şi 1 din sistemul de numeraţie. Celulele de bază ale memoriei (ce pot avea valoarea 0 sau 1) se numesc biţi şi ele reprezintă particulele cele mai mici de informaţie din calculator. Pentru citirea informaţiilor nu se folosesc biţi în mod individual ci aceştia sunt grupaţi într-o succesiune. Astfel o succesiune de 8 biţi formează un octet (sau un byte) aceasta reprezentând unitatea de măsură a capacităţii de memorie. Deoarece reprezentarea numerelor în calculator se face în baza 2 şi nu în baza 10, aşa cum suntem obişnuiţi în mod normal să lucrăm, şi multiplii unui byte vor fi puteri ale lui 2, astfel: 1 KB=210B=1024 B 1 MB=210KB=1 048 576 B 1 GB=210MB=230 B Abrevierile K (kilo), M (mega), G (giga) se scriu de obicei cu litere mari şi reprezintă mii, milioane şi, respectiv, miliarde. Memoria unui calculator are două componente: memoria principală (internă) şi memoria secundară (externă). Memoria internă este memoria ce poate fi accesată în mod direct de către microprocesor şi în care sunt încărcate programele înainte de a fi executate de către microprocesor. Dacă primele procesoare puteau accesa doar 1 MB de memorie astăzi un procesor Pentium poate accesa peste 256 MB. Memoria principală este formată din două tipuri de circuite: cip-uri ROM şi cip-uri RAM. Circuitele de tip ROM (Read Only Memory) au memorate programele care controlează iniţial calculatorul (sistemul de operare). Aceste memorii pot fi doar citite (conţinutul lor nu poate fi modificat). Înscrierea conţinutului acestor memorii se face de către fabricant, iar operaţiunea de înscriere cu programe se mai numeşte „arderea memoriilor”. Circuitele de tip RAM (Random Acces Memory ) sunt memorii la care utilizatorul are acces şi al căror conţinut se şterge la deconectarea calculatorului. În memoria RAM informaţia este stocată temporar. De exemplu, programele de aplicaţii curente şi datele asociate acestor aplicaţii sunt încărcate în memoria RAM înainte de a fi prelucrate de către microprocesor. 9
• 16. Deoarece capacitatea de memorie a unui calculator nu poate fi atât de mare încât să poată păstra toate programele pe vrem să le executăm, a apărut necesitatea existenţei unor memorii externe, care să fie solicitate la nevoie. Rolul acestora îl joacă discurile şi ele pot fi asemănate cu cărţile dintr-o bibliotecă pe care le putem consulta ori de câte ori avem nevoie de anumite informaţii. Primele discuri apărute pentru PC-uri, numite şi dischete, floppy disk-uri sau discuri flexibile, permiteau stocarea a maximum 160 KB de informaţie. Astăzi mai există doar dischete cu diametrul de 3.5 inch cu capacitatea de 1,44 MB. Existenţa acestora pare a fi pusă însă în pericol de apariţia CD-urilor reinscriptibile a căror capacitate de memorare depăşeşte 700 MB iar evoluţia tehnologică, din ce în ce mai rapidă, dă semne că lucrurile nu se vor opri aici. Dischetele folosesc metode magnetice de memorare a informaţiei motiv pentru care ele se mai numesc şi suporturi magnetice de informaţie. Principala componentă a unui calculator utilizată pentru memorarea programelor o reprezintă hard discul. Acesta poate fi asemănat cu o dischetă de mare capacitate, integrată într-o unitate încapsulată. Iniţial, puţine PC-uri prezentau hard discuri, dar cum preţurile acestora au scăzut considerabil, iar performanţele şi capacităţile au crescut, în prezent toate calculatoarele prezintă acest dispozitiv. În clipa de faţă, capacitatea de memorare a unui hard disc a depăşit valoarea de 40 de GB. 1.2.3 Echipamentele periferice Comunicarea om-maşină se realizează cu ajutorul echipamentelor periferice prin intermediul cărora utilizatorul poate programa sau da anumite comenzi calculatorului sau poate vizualiza rezultatele obţinute de către anumite programe. Principalele echipamente periferice ale unui calculator sunt următoarele: tastatura, mouse-ul, scanner-ul, monitorul şi imprimanta. Ele pot fi grupate în echipamente de intrare – cele prin care calculatorul primeşte informaţii sau comenzi (tastatură, mouse, scanner) - şi echipamente de ieşire – cele prin care calculatorul transmite informaţii în exterior (monitor, imprimantă). În continuare sunt prezentate câteva caracteristici ale fiecărui echipament. Tastatura – este principalul dispozitiv de intrare al calculatorului prin intermediul căruia se transmit comenzi către 10
• 17. unitatea centrală. Cuplarea la calculator a tastaturii se face prin intermediul unui cablu de conectare. Din punct de vedere al dispunerii tastelor, tastatura se aseamănă destul de mult cu cea a unei maşini de scris dar are şi părţi care o individualizează. Primele tastaturi au avut 83/84 de taste, pentru ca, ulterior, ele să fie îmbogăţite prin dublarea tastelor existente sau adăugarea altora noi, ajungându-se la 101/102 taste. Din punct de vedere al funcţionalităţii lor ele pot fi împărţite în patru categorii: - taste alfanumerice; - taste cu scopuri speciale; - taste direcţionale şi numerice; - taste funcţionale. Tastele alfanumerice conţin literele, cifrele şi semnele de punctuaţie şi ocupă partea centrală a tastaturii. Acţionarea unei astfel de taste determină apariţia caracterului corespunzător pe ecranul calculatorului. Tastele cu scopuri speciale sunt aşezate în acelaşi bloc cu tastele alfanumerice şi determină efectuarea anumitor acţiuni fără înscrierea de caractere pe ecran. Tastele de mişcare se află situate în partea dreaptă a tastaturii şi ele funcţionează în două moduri, care pot fi comutate prin acţionarea altei taste, aflate deasupra lor, pe care scrie NumLock. Dacă ledul corespunzător acestei taste este aprins, modul de lucru este numeric, în caz contrar fiind comutat pe semnificaţia direcţională. Tastele funcţionale sunt un grup de 12 taste situate în partea de sus a tastaturii având pe ele litera F urmată de un număr între 1 şi 12. Acţionarea acestor taste determină efectuarea unor operaţii specifice de la program la program. Mouse-ul – este tot un echipament de intrare mai uşor de manevrat decât tastatura dar care poate efectua mai puţine operaţii. Totuşi, foarte multe aplicaţii (în special aplicaţiile grafice) nu mai pot fi concepute fără mouse. Un mouse are aspectul unei bucăţi de săpun, uşor manevrabil, având dedesubt o bilă poziţionabilă, cu sensibilitate şi viteză reglabile. Mişcarea maouse-ului pe o suprafaţă plană este corelată cu deplasarea pe ecran a unui cursor cu o formă deosebită: cruciuliţă, săgeată, etc. Declanşarea unei acţiuni se face prin poziţionarea cursorului în zona corespunzătoare şi apăsarea unuia dintre butoanele aflate pe partea posterioară. Iniţial un mouse avea două sau trei 11
• 18. butoane. Acum există mouse-uri cu 5 butoane şi 2 rotiţe ce îndeplinesc o serie de funcţii corespunzătoare unor taste speciale. Folosirea mouse-ului uşurează mult munca utilizatorilor, nemaifiind necesar ca aceştia să memoreze numărul relativ mare de comenzi corespunzător fiecărui produs, ca în situaţia în care se foloseşte numai tastatura. Utilitatea mouse-ului este şi mai evidentă în cazul aplicaţiilor grafice. De altfel, WINDOWS este un sistem de operare creat special pentru lucrul cu mouse-ul. Scanner-ul – reprezintă dispozitive care se cuplează la un PC şi cu care, prin intermediul unui software adecvat, se pot capta imagini, fotografii, texte etc., în vederea unei prelucrări ulterioare. Astfel se pot manevra imagini foto, se pot crea efecte grafice speciale, care nu se pot obţine prin metode tradiţionale. După captare, imaginea poate fi prelucrată, mutată, mărită, micşorată, rotită, colorată, umbrită, suprapusă cu altă imagine etc. Cu un software de recunoaştere optică a caracterelor datele sau documentele tipărite pe coli de hârtie pot fi transformate în fişiere, putându-se realiza chiar o stocare a lor sub formă de arhivă. Monitorul - Sistemul video este format din două părţi: un adaptor (placă) video şi un monitor sau display. Adaptorul video reprezintă dispozitivul care realizează legătura (interfaţa) cu calculatorul şi se află în interiorul acestuia. El va fi corespunzător tipului de monitor video care îi este ataşat. Adaptorul video realizează o rezoluţie orizontală şi una verticală. Rezoluţia reprezintă numărul de elemente, în cazul de faţă puncte – pixeli – care pot fi afişate pe ecran. De exemplu, un monitor VGA, în mod video, are o rezoluţie de 640 x 480 pixeli. Standardul VGA (Video Graphics Array) a fost introdus de către IBM o dată cu calculatoarele PS/2, iar modurile video VGA reprezintă un superset al standardelor video anterioare, CGA (Color Graphics Adapter) şi EGA (Enhanced Graphics Adapter). Cea mai importantă îmbunătăţire adusă de standardul VGA a fost rezoluţia superioară a caracterelor în modul text, precum şi posibilitatea de a afişa 256 de culori la un moment dat. Monitorul, denumit uneori şi display, permite vizualizarea datelor introduse de la tastatură sau rezultate în urma execuţiei unor comenzi sau programe, fiind încadrat în categoria echipamentelor periferice de ieşire. Ca piesă principală, monitorul conţine un tub de 12
• 19. vacuum, similar cu cel de la televizor şi trei tunuri de electroni (corespunzătoare celor trei culori fundamentale). Standardele video MDA, CGA şi EGA folosesc monitoare digitale. Datele care descriu culorile pixelilor sunt trimise de adaptorul video la monitor sub forma unor serii de semnale digitale care sunt echivalente unor serii de biţi. Standardul VGA a introdus un nou tip de monitor care utilizează semnale analogice pentru transferul informaţiilor privind culoarea de la adaptorul video la monitor. Dacă semnalele digitale prezintă niveluri care indică prezenţa sau absenţa unui bit, semnalele analogice pot prezenta orice valoare între una minimă şi una maximă. Imprimanta - Reprezintă un dispozitiv care poate fi ataşat unui calculator, cu scopul tipăririi de texte şi grafică, putând fi considerată un fel de maşină de scris automată. Până în prezent au fost realizate un număr destul de mare de tipuri de imprimante pentru PC-uri, ele diferind atât prin performanţe, cât şi prin modalităţile tehnice de ralizare. Fiecare dintre ele prezintă avantaje şi dezavantaje, ideal fiind a o folosi pe cea care corespunde cel mai bine tipului de lucrări executate. În funcţie de modul în care este realizată imprimarea se disting următoarele tipuri de imprimante: - imprimante matriceale cu 9, 18 sau 24 de ace – realizează imprimarea prin impactul acelor peste o bandă de hârtie; - imprimante cu jet de cerneală – funcţionează prin pulverizarea fină a unor picături de cerneală pe hârtia de imprimat; - imprimante laser – ce utilizează o rază laser sau mici diode luminiscente care încarcă electrostatic un tambur de imprimare, corespunzător caracterului care urmează a fi imprimat; - imprimante rapide de linii – ce imprimă mai multe linii odată, fiind folosite mai mult la sisteme de calcul de dimensiuni mari. Calitatea imprimării creşte de la primul la ultimul tip prezentat, dar în mod corespunzător şi preţul echipamentului. 1.3. Programarea calculatorului 13
• 20. Programele de calculator, cunoscute sub numele de software, sunt constituite dintr-o serie de instrucţiuni pe care le execută calculatorul. Când se creează un program, trebuie specificate instrucţiunile pe care calculatorul trebuie să le execute pentru a realiza operaţiile dorite. Procesul de definire a instrucţiunilor pe care le execută calculatorul se numeşte programare. Programele executate pe un calculator pot fi împărţite în trei categorii: • programe de aplicaţie – sunt acele programe care interacţionează direct cu utilizatorul, specializate în realizarea unei categorii de prelucrări. Editoarele de texte, programele pentru gestiunea bazelor de date, programele de tehnoredactare asistată de calculator, de grafică etc. sunt programe de aplicaţie. • utilitare – programe, care la fel ca programele de aplicaţie, interacţionează direct cu utilizatorul, dar, spre deosebire de acestea, realizează prelucrări de uz general. Utilitarele realizează o serie de operaţii de „gospodărie” cum ar fi: copierea fişierelor, pregătirea discurilor magnetice pentru utilizare, crearea de copii de salvare, testarea echipamentului, etc. • programe de sistem – realizează legătura între componentele electronice ale calculatorului şi programele de aplicaţie şi utilitare. Rolul programului de sistem este acela de a uşura sarcina programatorului, simplificând îndeplinirea acelor sarcini care sunt comune marii majorităţi a programelor de aplicaţie: alocarea memoriei, afişarea caracterelor pe ecran şi la imprimantă, citirea caracterelor de la tastatură, accesul la informaţiile stocate pe disc magnetic, etc. 1.3.1. Sistemul de operare Sistemul de operare este o parte componentă a software-ului unui calculator, care mai cuprinde un număr variabil de programe utilitare selectate conform cu necesităţile programatorilor. Sistemul de operare este un program cu funcţii de coordonare şi control asupra resurselor fizice ale calculatorului şi care intermediază dialogul om-calculator. Sistemul de operare permite rularea programelor şi păstrarea informaţiilor pe disc. În plus, fiecare sistem 14
• 21. de operare pune la dispoziţia aplicaţiilor o serie de servicii care permit programelor să aloce memorie, să acceseze diferite echipamente periferice, cum ar fi imprimanta, şi să gestioneze alte resurse ale calculatorului. Un sistem de operare trebuie să aibă capacitatea de a se adapta rapid la modificările tehnologice, rămânând în acelaşi timp compatibil cu hardware-ul anterior. Lanţul de comunicare utilizator – calculator este prezentat în Figura 1.3: Sistemul de operare este cel mai important program care rulează pe un calculator. Orice calculator de uz general este dotat cu un sistem de operare care permite execuţia altor programe. Sistemele de operare execută operaţiuni de bază precum: recunoaşterea unei intrări de la tastatură (preluare caracter), trimiterea unui caracter pentru afişare pe ecranul monitorului, gestionarea fişierelor şi a directoarelor pe disc (floppy-disk sau hard-disk), controlul fluxului de date cu echipamentele periferice ca drivere de disc sau imprimante. CALCULATOR SISTEM DE OPERARE APLICAŢII UTILIZATOR Fig. 1.3. Comunicarea utilizator - calculator 15
• 22. Aplicaţie Disk-drive Sistem de operare Mouse Monitor Tastaturã Imprimantã Fig. 1.4 Rolul sistemului de operare Sistemul de operare al unui calculator este partea de software necesară şi suficientă pentru execuţia oricăror alte aplicaţii dorite de utilizator. Un calculator nu poate funcţiona decât sub gestiunea unui sistem de operare. Orice aplicaţie lansată în execuţie de către un utilizator apelează la resursele puse la dispoziţie de către sistemul de operare. Sistemul de operare interfaţează calculatorul cu operatorul uman de o manieră cât mai transparentă cu putinţă astfel încât utilizatorul nu trebuie să facă eforturi mari de adaptare dacă lucrează cu arhitecturi hardware diferite. Pentru sisteme mai mari, sistemele de operare au responsabilităţi şi capabilităţi şi mai mari. Ele acţionează ca un gestionar al traficului de date şi al execuţiei programelor. În principal sistemul de operare asigură ca diferite programe şi diferiţi utilizatori să nu interfereze unele cu altele. Sistemul de operare este de asemenea responsabil cu securitatea, asigurând inaccesibilitatea persoanelor neautorizate la resursele sistemului. Sistemele de operare se pot clasifica după cum urmează:  multi-user: Permit ca doi sau mai mulţi utilizatori să ruleze în acelaşi timp programe (utilizatori concurenţi). Anumite sisteme de operare permit sute sau chiar mii de utilizatori concurenţi.  multiprocesor: Permit execuţia unui program pe mai mult de un microprocesor. 16
• 23.  multitasking: Permit mai multor programe să ruleze în acelaşi timp (execuţie concurentă).  multithreading: Permit diferitelor părţi ale unui program să fie executate concurent.  timp real (real time): Răspund instantaneu la diferite intrări. Sistemele de operare de uz general, ca DOS sau UNIX nu sunt sisteme de operare de timp real. Sistemele de operare furnizează o platformă software pe baza căreia alte programe, numite programe de aplicaţie, pot rula (pot fi executate). Programele de aplicaţie trebuie să fie scrise pentru a rula pe baza unui anumit sistem de operare. Alegerea unui anumit sistem de operare determină în consecinţă mulţimea aplicaţiilor care pot fi rulate pe calculatorul respectiv. Pentru PC-uri, cele mai populare sisteme de operare sunt DOS, OS/2 sau Windows, dar mai sunt disponibile şi altele precum Linux. Ca utilizator se interacţionează cu sistemul de operare prin intermediul unor comenzi. Spre exemplu, sistemul de operare DOS acceptă comenzi precum COPY sau RENAME pentru a copia fişiere sau pentru a le redenumi. Aceste comenzi sunt acceptate şi executate de o parte a sistemului de operare numită procesor de comenzi sau interpretor de linie de comandă. Interfaţele grafice cu utilizatorul (GUI, Graphical user interfaces) permit introducerea unor comenzi prin selectarea şi acţionarea cu mouse-ul a unor obiecte grafice care apar pe ecran. Spre exemplu, sistemul de operare Windows are un desktop ca intefaţă garfică cu utilizatorul. Pe acest desktop (birou) se află diferite simboluri grafice (icoane, icons) ataşate diferitelor aplicaţii disponibile pe calculatorul respectiv. Utilizatorul are multiple posibilităţi de configurare a acestei intefeţe grafice. Primul sistem de operare creat pentru calculatoare a fost CP/M (Control Program for Microcomputers), realizat pentru calculatoarele pe 8 biţi. O dată cu perfecţionarea componentelor HARD s-a impus şi necesitatea dezvoltării unui SOFT adecvat. Astfel, în 1981, a apărut prima versiune a sistemului de operare MS-DOS. Sistemul de operare MS–DOS (MicroSoft Disk Operating System) este destinat gestionării resurselor software si hardware ale microcalculatoarelor cu o arhitectura de tip IBM – PC sau compatibilă cu aceasta şi echipate cu procesoare 8086 sau 80x86, Pentium. Odată cu creşterea 17
• 24. capabilităţilor hardware ale calculatoarelor, acesta s-a transformat, prin dezvoltări succesive, în Windows. Indiferent de sistemul de operare utilizat, din punctul de vedere al utilizatorului, informaţiile sunt scrise pe disc sub forma unor fişiere. Un fişier este o colecţie de informaţii grupate sub acelaşi nume. Un fişier poate fi un program executabil, un text, o imagine, un grup de comenzi sau orice altceva. Un fişier este identificat prin numele său. Numele unui fişier este format dintr-un şir de caractere (care în funcţie de sistemul de operare este limitat la un anumit număr maxim de caractere), urmate eventual de semnul punct (.) şi de încă maximum 4 caractere, numite extensie, ca de exemplu: nume.ext. Pentru a putea avea acces rapid la fişiere, sistemul de operare creează nişte fişiere speciale, numite directoare, care pot fi asemănate cu cuprinsul unei cărţi, deoarece ele conţin numele fişierelor şi adresa de început a acestora. De asemenea, un director poate conţine la rândul său alte directoare creându-se astfel o structură arborescentă de directoare în care poate fi găsit foarte repede un anumit fişier. 1.3.2. Tipuri de fişiere Fişierele se pot împărţi în două categorii – executabile şi neexecutabile. În prima categorie intră acele fişiere al căror nume scris în dreptul prompterului (în cazul sistemului de operare DOS) determină executarea unor activităţi de către sistemul de operare. O parte dintre fişierele executabile sunt programe şi sunt recunoscute prin extensia lor care poate fi EXE sau COM, altele fiind constituite în fişiere de comenzi proprii sistemului de operare, a căror extensie este BAT. Fişierele COM, numite adesea şi comenzi, conţin informaţii în formatul imagine de memorie. Ele sunt mai compacte şi mai rapide decât fişierele EXE, dar lungimea lor nu poate să depăşească 64 K. Fişierele EXE pot să ajungă la dimensiuni mai mari prin segmentarea programului în fragmente a căror dimensiune să fie de maximum 64K. Dintre fişierele neexecutabile vom aminti câteva mai importante: • fişiere text ; • fişiere cu extensia SYS sau DRV, cunoscute sub numele de driver-e şi care conţin instrucţiuni despre modul în care 18
• 25. sistemul de operare trebuie să controleze diferite componente hardware; • surse de programe scrise în diferite limbaje (cu extensiile PAS – limbajul Pascal, C – limbajul C, CPP – limbajul C++, etc.); • fişiere care conţin informaţii intermediare între cele în limbaj sursă şi cele executabile (extensiile OBJ, OVL); • fişiere ce conţin imagini (extensiile JPEG, GIF, BMP); • fişiere ce conţin sunete (extensiile WAV, MIDI, MP3) etc. 1.3.3. Construirea fişierului executabil Instrucţiunile pe care le execută un calculator sunt de fapt grupuri de 1 ş 0 (cifre binare) care reprezintă semnale electronice produse în interiorul calculatorului. Pentru a programa primele calculatoare (în anii 1940-1950), programatorii trebuiau să înţeleagă modul în care calculatorul interpreta diferitele combinaţii de 0 şi 1, deoarece programatorii scriau toate programele folosind cifre binare. Cum programele deveneau din ce în ce mai mari, acest mod de lucru a devenit foarte incomod pentru programatori. De aceea au fost create limbaje de programare care permit exprimarea instrucţiunilor calculatorului într-o formă mai accesibilă programatorului. După ce programatorul scrie instrucţiunile într-un fişier - numit fişier sursă, un al doilea program – numit compilator, converteşte instrucţiunile limbajului de programare în şirurile 1 şi 0 – cunoscute sub numele de cod maşină. Pentru a obţine un program executabil, orice program sursă trebuie eventual translatat (tradus) în limbaj cod maşină sau cod obiect pe care îl poate înţelege microprocesorul. În urma acestui proces, alături de fişierul sursă apare şi fişierul cod obiect (object file.) Această translatare sau traducere este efectuată de către compilatoare, interpretoare sau asambloare. Compilatorul este folosit pentru transformarea codului sursă, adică a programului scris într-un limbaj de programare de nivel înalt, în cod obiect (object code). Acest cod obiect va fi transformat în faza de editare de legături în cod maşină executabil de microprocesorul sistemului de calcul. Programatorii scriu programe într-o formă numită cod sursă. Acest cod sursă parcurge apoi câţiva paşi înainte de a deveni program executabil. 19
• 26. Pe scurt, un compilator este un program special care procesează instrucţiuni scrise într-un limbaj de programare particular şi le transformă în limbaj maşină sau cod maşină pe care îl poate executa microprocesorul. La ora actuală un limbaj de programare este inclus într-un mediu de programare mai complex care include un editor de texte pentru introducerea instrucţiunilor în limbajul de programare de nivel înalt, un compilator şi un editor de legături folosite pentru translatarea codului sursă în cod maşină. În mod tipic, un programator scrie declaraţii într-un limbaj precum Pascal, C sau MATLAB folosind un editor. Se creează astfel un fişier numit fişier cod sursă ce conţine o colecţie de instrucţiuni şi declaraţii scrise în limbajul respectiv. Primul pas este prelucrarea codului sursă de către compilator, care translatează instrucţiunile de nivel înalt într-o serie de instrucţiuni cod obiect. Când este lansat în execuţie compilatorul acesta, într-o primă etapă, lansează un analizor sintactic, gramatical, numit parser. Acesta parcurge şi analizează sintactic, secvenţial, în ordinea în care au fost introduse, toate instrucţiunile scrise în limbajul de nivel înalt. O instrucţiune de nivel înalt se translatează într-una sau mai multe instrucţiuni specifice microprocesorului pentru care a fost conceput compilatorul. Aceste instrucţiuni ale microprocesorului sunt înlocuite cu codurile lor binare, fiecare instrucţiune a microprocesorului fiind codificată de către constructor. Codurile binare ale instrucţiunilor microprocesorului împreună cu reprezentările interne ale datelor manipulate formează codul obiect. Deci în unul sau mai multe faze (parserul este una dintre faze) din codul sursă de intrare se produce un cod de ieşire, numit în mod tradiţional cod obiect. Este foarte important ca referiri la alte module de cod să fie corect reprezentate în acest cod obiect. Pasul final în producerea programului executabil, după ce compilatorul a produs codul obiect, este prelucrarea codului obiect de către un editor de legături (link-editor sau linker). Acest linker combină diferitele module (le leagă) şi dă valori reale, efective, tuturor adreselor simbolice existente în codul obiect. În urma acestei prelucrări se obţine codul maşină, salvat într-un fişier cu extensia .exe. Acest cod maşină poate fi executat secvenţial, instrucţiune cu instrucţiune, de către microprocesor. 20
• 27. Cu alte cuvinte, un program executabil (executable program - aflat pe disc cu extensia .exe) se obţine prin salvarea pe disc a codului maşină obţinut prin prelucrarea succesivă a fişierului cod sursă de către compilator (compiler) şi apoi de către link-editor (linker). Fig. 1.5 Procesul de elaborare a unui program executabil Procesul de obţinere a unui executabil este prezentat în figura de mai jos. Blocurile tridimensionale reprezintă entităţile principale ale mediului de programare: editorul de texte, compilatorul (compiler) şi editorul de legături (linker). Blocurile dreptunghiulare reprezintă fişierele rezultate în urma aplicării celor trei utilitare de sistem:  în urma utilizării editorului de texte obţinem fişierul text sursă cod cu numele generic “nume”. Dacă folosim limbajul de programare C spre exemplu, se obţine fişierul nume.c care se va salva pe disc.  în urma lansării în execuţie a compilatorului, acesta preia fişierul sursă şi îl prelucrează corespunzător, semnalizându-se toate erorile fatale pentru program sau avertismente utile programatorului în procesul de depanare. În cazul în care compilarea se efectuează cu succes, se obţine un fişier cod obiect, salvat pe disc sub numele nume.obj  în urma lansării în execuţie a editorului de legături, se preia fişierul cod obiect nume.obj şi se leagă cu toate modulele necesare (inclusiv funcţii de bibliotecă sau alte module externe), obţinându-se un program executabil (cod maşină) cu 21
• 28. numele nume.exe la care adresele nu mai sunt simbolice ci absolute relativ la adresa de început a programului. La lansarea în execuţie a programului fluxul de informaţie este complet controlat de către microprocesor, toate salturile de adresă fiind făcute corespunzător. Interpretorul (interpreter) este un program care execută instrucţiuni scrise într-un limbaj de nivel înalt. Numai anumite limbaje de nivel înalt, spre exemplu BASIC, LISP sau MATLAB, sunt prevăzute cu un interpretor. Există două modalităţi de a executa un program scris în limbaj de nivel înalt. Cel mai comun mod este acela de a compila programul. Cealaltă modalitate este “pasarea” programului unui interpretor. Un interpretor translatează instrucţiunile de nivel înalt într-o formă intermediară care este apoi executată. Prin contrast, un compilator translatează instrucţiunile de nivel înalt direct în limbaj maşină (cod maşină). Programele compilate rulează în general mai rapid decât cele interpretate. Un alt avantaj al programelor compilate este acela al desprinderii din context în sensul că programele executabile generate în urma procesului de compilare pot fi executate direct sub sistemul de operare al calculatorului. Un program interpretat se execută sub mediul în care a fost creat. Spre exemplu, pentru a rula un program scris în limbajul BASIC se lansează în execuţie mediul BASIC, apoi se deschide fişierul sursă-BASIC corespunzător şi se lansează interpretorul de BASIC pentru execuţia sa. Avantajul unui interpretor este acela al evitării procesului de compilare consumator de timp în cazul în care avem programe de mari dimensiuni. Interpretorul poate executa imediat programele sursă. Pentru acest motiv interpretoarele se folosesc mai ales în procesul de dezvoltare al programelor, când programatorul doreşte adăugarea unor mici porţiuni de program pe care să le testeze rapid. De asemenea, interpretoarele permit o programare interactivă fiind des folosite în procesul de instrucţie. În mediul de programare MATLAB, mediu interpretor, orice comandă utilizator se execută imediat. Se pot edita şi fişiere script, care conţin secvenţe de comenzi care se execută secvenţial. 22
• 29. Programele de descriere a paginii (Page Description Languages) ca PostScript spre exemplu folosesc un interpretor. Fiecare imprimantă PostScript are incorporat un interpretor care execută instrucţiuni PostScript. Asamblorul (assembler) este un program care face translaţia unui program scris în limbaj de asamblare (limbaj de nivel scăzut, corespunzător microprocesorului sistemului de calcul) în limbaj cod maşină. Putem spune că asamblorul reprezintă pentru limbajul de asamblare ceea ce reprezintă compilatorul pentru limbajele de nivel înalt. Cum limbajul de asamblare conţine instrucţiuni mai puţin complexe decât cele de nivel înalt, asamblorul face practic o convertire biunivocă între mnemonicele limbajului de asamblare şi codurile binare corespunzătoare acestor mnemonice (instrucţiuni). 23
• 30. Instrucţiunile în limbajul de nivel înalt se introduc de la tastaturã. Tot ce se introduce de la tastaturã este vizibil pe monitor Editor de texte (eventual incorporat în mediu) Fişier text (fişier sursã) cu extensia adecvatã: nume.pas (limbaj Pascal) nume.c (limbaj C) nume.cpp (limbaj C++) nume.bas (limbaj BASIC), etc. Fişierul sursã cu numele “nume” şi extensia corespunzãtoare se salveazã din memoria RAM pe harddisk Compilator Se compileazã fişierul sursã Se obţine fişierul cod obiect: nume.obj Se salveazã pe harddisk fişierul nume.obj Link-editare (legarea tuturor modulelor necesare) Se obţine fişierul cod maşinã (executabil): nume.exe Se salveazã pe harddisk fişierul nume.exe Lansarea în execuţie de cãtre sistemul de operare a executabilului nume.exe Fig. 1.6 Detalierea procesului de generare a unui executabil Capitolul II 24
• 31. REPREZENTAREA DATELOR ÎN CALCULATOR Se ştie că un calculator numeric prelucrează numere binare. Acest lucru ţine de suportul fizic de manipulare, transport şi stocare a datelor interne, mai bine zis este legat de faptul că semnalul fizic purtător de informaţie este o tensiune continuă cu două valori: una înaltă (High) şi una joasă (Low). Acestor două valori li se asociază natural două valori logice: T (true, adevărat) şi F (false, fals) sau cele două cifre binare1 şi 0. Tensiune High=’1’ Low=’0’ timp Ca urmare a acestei asocieri spunem, prin abuz de limbaj, că un calculator numeric prelucrează numere binare. Ca şi un număr zecimal, un număr binar are mai multe cifre binare. Sistemul de numeraţie binar folosit pentru reprezentarea informaţiei în calculatoare este un sistem de numeraţie ponderal, întocmai ca sistemul de numeraţie zecimal. Reprezentarea naturală a numerelor la nivelul percepţiei umane este cea zecimală, pe când reprezentarea proprie maşinilor de calcul este cea binară. De aici rezultă necesitatea compatibilizării sau interfaţării între aceste două moduri de reprezentare a numerelor. Cum cele două sisteme de numeraţie sunt ponderale, o primă diferenţă este aceea că sistemul zecimal foloseşte ca ponderi puterile întregi (pozitive sau negative) ale lui 10 (zece) iar sistemul binar va folosi puterile întregi (pozitive sau negative) ale lui 2. În altă ordine de idei, dacă pentru reprezentarea externă sunt semnificative simbolurile de reprezentare (cifre, semnele + sau -, punct zecimal sau binar, mantisă sau exponent), pentru reprezentarea 25
• 32. internă sunt necesare convenţii de reprezentare: indiferent de tipul datelor, acestea vor fi colecţii sau şiruri de cifre binare cărora, prin convenţie, li se atribuie semnificaţii. Într-o primă instanţă, este foarte important să facem o distincţie între tipurile de date recunoscute de un calculator (sau mai bine zis de microprocesorul cu care este dotat calculatorul personal) şi formatele de reprezentare ale acestor date ce reprezintă convenţii pentru reprezentarea tipurilor de date, atât la nivel intern (în memoria calculatorului) cât şi la nivel extern, al percepţiei umane. Din punctul de vedere al tipurilor de date care sunt implementate în limbajul C putem spune că distingem două mari categorii, date de tip întreg (integer) şi date de tip real (float). Formatele de reprezentare internă/externă vor fi prezentate în cele ce urmează. Cel mai simplu de reprezentat sunt numerele naturale. Se face apoi trecerea la numerele întregi negative şi apoi la numerele reale care au o parte întreagă şi una fracţionară. 2.1. Reprezentarea internă/externă a numerelor Reprezentarea internă a numerelor se referă la modul în care se stochează datele în memoria RAM a calculatorului sau în regiştrii microprocesorului. În acest format se prelucrează numerele pentru implementarea diverselor operaţii aritmetice. La nivelul calculatorului informaţia nu poate fi decât binară. În această reprezentare putem scrie numere întregi pozitive sau negative sau numere reale. Există un standard IEEE care reglementează modul de reprezentare internă a datelor. Reprezentarea externă este reprezentarea numerelor la nivelul utilizatorului uman, deci în principiu se poate folosi orice bază de numeraţie pentru reprezentarea numerelor. La nivel de reprezentare externă se foloseşte semnul “-” în faţa unui număr în cazul în care acesta este negativ sau punctul care separă partea întreagă de cea fracţionară. De asemenea, numerele întregi interpretate fără semn se pot afişa şi în format binar, octal sau hexazecimal, deci în bazele 2, 8 sau 16. În cele ce urmează ne vom pune următoarele probleme: - cum se reprezintă extern un număr natural - cum se reprezintă intern un număr natural - cum se reprezintă extern un număr întreg negativ 26
• 33. - cum se reprezintă intern un număr întreg negativ - cum se face conversia de la reprezentarea externă la cea internă - cum se face conversia de la reprezentarea internă la cea externă 2.2. Reprezentarea externă a numerelor În ceea ce priveşte reprezentarea externă, nu sunt nici un fel de dificultăţi deoarece fiecare este familiarizat cu reprezentarea zecimală a numerelor naturale sau reale. Trebuie menţionat de la început că orice tip de reprezentare pe care o vom folosi este ponderală în sensul că poziţia cifrelor în număr nu este întâmplătoare ci conformă cu o pondere corespunzătoare unei puteri a bazei de numeraţie. O caracteristică a reprezentărilor externe este folosirea unor convenţii de format unanim acceptate şi de altfel foarte naturale pentru un utilizator uman. Spre exemplu, pentru a exprima numere negative se foloseşte semnul “-” iar pentru reprezentarea numerelor reale se foloseşte punctul “.” pentru delimitarea părţii întregi de cea fracţionară. De asemenea, suntem familiarizaţi şi cu notaţia ştiinţifică în care intervine mantisa şi exponentul (în virgulă mobilă). Reprezentarea zecimală este cea mai naturală pentru utilizatorul uman. Vom oferi în continuare câteva exemple de reprezentări zecimale externe: Număr Reprezentare Reprezentare normală ştiinţifică 37 37 0.37x102 -37 -37 -0.37x102 0.375 0.375 0.375x100 -0.375 -0.375 -0.375x100 0.00375 0.00375 0.375x10-2 -0.00375 -0.00375 -0.375x10-2 12.375 12.375 0.12375x102 -12.375 -12.375 -0.12375x102 În general dorim să obţinem rezultatele numerice ale programelor pe care le concepem într-o formă de reprezentare accesibilă. Totuşi, calculatorul trebuie informat asupra formatului de reprezentare în care dorim să se afişeze datele necesare. Aceasta înseamnă că va trebui să specificăm câte cifre se vor folosi la partea 27
• 34. întreagă şi câte la partea fracţionară sau dacă dorim reprezentare ştiinţifică sau nu. De altfel şi operatorul uman face aceleaşi convenţii 1 de reprezentare. Spre exemplu ştim că numărul nu poate fi exact 3 reprezentat ca un număr zecimal, deci fixăm un format de reprezentare. Dacă formatul ale se limitează la 4 cifre zecimale, atunci 1 vom scrie ≅ 0.3333 3 Limbajul C are o serie de funcţii de reprezentare cu format a datelor numerice sau alfanumerice prin care programatorul poate impune un format extern cu care se manipulează datele. 2.2.1. Reprezentarea externă a numerelor întregi Numerele naturale se pot reprezenta fie în baza de numeraţie 10, fie în orice altă bază. În general, un număr întreg în baza b se poate reprezenta cu un număr predeterminat de cifre ci ∈ B = { 0,1,2,....., b − 2, b − 1} . Mulţimea B reprezintă mulţimea cifrelor sau simbolurilor de reprezentare. Spre exemplu: b = 2 ⇒ B = { 0,1} b = 7 ⇒ B = { 0,1,2,3,4,5,6} b = 10 ⇒ B = { 0,1,2,3,4,5,6,7,8,9} Noi suntem obişnuiţi să folosim mulţimea cifrelor zecimale. Dacă totuşi se foloseşte o bază de reprezentare mai mare decât 10, atunci mulţimea cifrelor zecimale nu mai este suficientă pentru reprezentarea numerelor în acea bază. Spre exemplu să considerăm baza b = 16 care va folosi 16 cifre hexazecimale (sau mai simplu hexa). Prin convenţie, cele 16 cifre hexazecimale vor fi: Cifra Simbol Cifra Simbol 0 0 8 8 1 1 9 9 2 2 10 A 3 3 11 B 4 4 12 C 5 5 13 D 6 6 14 E 7 7 15 F 28
• 35. Forma generală de reprezentare externă a numerelor întregi este de forma:  N b = ±c n −1c n − 2 ......c 2 c1c 0  c k ∈ B = { 0,1,2,....., b − 2, b − 1} Valoarea numerică zecimală a numărului N b va fi: ( ) ∑ ck ⋅ b k n −1 N b = ± c n−1 ⋅ b n −1 + c n − 2 ⋅ b n− 2 + ... + c1 ⋅ b1 + c 0 ⋅ b 0 = ± k =0 În continuare vom studia următoarele probleme: - cum se face conversia unui număr din baza b = 10 în baza b=2 - cum se face conversia inversă, din baza b = 2 în baza b = 10 - cum se face conversia dintr-o bază oarecare b1 în altă bază b2 Pentru a reprezenta un număr natural din baza 10 în baza 2, se împarte succesiv numărul la 2 şi se utilizează resturile la aceste împărţiri în ordinea inversă de cum au fost obţinute. a) Conversia din baza 10 în baza 2 şi invers Fie de exemplu numărul zecimal 37. Reprezentarea sa binară va fi obţinută astfel: 3710 = 1001012 37 2 36 18 2 1 18 9 2 0 8 4 2 1 4 2 2 0 2 1 0 Conversia inversă, din baza 2 în baza 10 este simplă şi utilizează ponderea 2: 25 24 23 22 21 20 1001012 = 1 0 0 1 0 1 = 1x25 + 1x22 + 1x20=37 Cu aceste numere naturale putem face o serie de operaţii aritmetice. Adunarea numerelor naturale binare se face întocmai ca la cele în reprezentare în baza 10, după regula: 0+0=0 29
• 36. 0+1=1 1+0=1 1+1=0, transport 1 spre rangul următor Astfel, să facem adunarea 37+25 în binar: 37 1 0 0 1 0 1+ 25 11001 62 111110 Se observă cum se obţine rezultatul corect. Înmulţirea se face în mod asemănător, ca o adunare repetată. Spre exemplu, să calculăm 37x25 37 1 0 0 1 0 1x 25 11001 100101 100101 100101 925 1110 011101 11100111012 = 1x20 + 1x22 + 1x23 +1x24 +1x27 +1x28+1x29 = 1+4+8+16+128+256+512 = 92510 b) Conversia dintr-o bază oarecare b1 într-o altă bază b2 . Fie spre exemplu numărul 4911 care se doreşte scris în baza 13. Pentru a realiza această conversie, vom folosi baza intermediară 10. Vom converti mai întâi 4A11 în baza 10 şi apoi numărul zecimal obţinut îl vom trece în baza 13. Se observă cum un număr în baza 11 poate conţine şi cifra A=10 iar un număr în baza 13 poate conţine cifrele A=10, B=11, C=12. 4 A11 = 10 ⋅110 + 4 ⋅111 = 44 + 10 = 5410 54 13 52 4 13 2 0 0 4 5310 = 4213 4 A11 = 4213 30
• 37. 2.2.2. Reprezentarea externă a numerelor reale Semnificativă pentru utilizatorul uman este reprezentarea zecimală (în baza b=10) a numerelor reale, cu care suntem obişnuiţi. Faţă de reprezentarea numerelor întregi, la numerele reale intervine simbolul punct “.” care delimitează partea întreagă de partea fracţionară. Cu alte cuvinte, cu ajutorul numerelor reale putem reprezenta şi numere care nu sunt întregi. Forma generală a unui număr real reprezentat într-o bază oarecare b este:  N b = ±c n −1c n − 2 ...c1c 0 • c −1c − 2 ...c − m +1c − m  c k ∈ B = { 0,1,2,..., b − 2, b − 1} Valoarea zecimală a numărului de mai sus va fi: ( ) ∑ ck ⋅ b k n− 1 N10 = ± cn − 1b n − 1 + cn − 2b n − 2 + c1b1 + c0b 0 + c− 1b − 1 + c− 2 ⋅ b − 2 + c− m + 1b − m + 1 + c− m b − m = ± k= −m Se observă cum punctul delimitează partea întreagă (exprimată printr-o combinaţie de puteri pozitive ale bazei b) şi partea fracţionară (exprimată printr-o combinaţie de puteri negative ale bazei b). Semnificaţie pentru programator şi pentru producătorii de software sau microprocesoare au bazele de reprezentare b = 10 şi b = 2 , deoarece baza 10 este naturală pentru reprezentarea externă a numerelor iar baza 2 este naturală pentru reprezentarea binară, internă, a numerelor. În formulele de mai sus avem o reprezentare a unui număr real cu n cifre pentru partea întreagă şi m cifre pentru partea fracţionară. Aşa cum în sistemul zecimal reprezentăm cu un număr finit de cifre zecimale numerele reale, acelaşi lucru se va întâmpla şi în sistemul binar. Punctul binar va avea o semnificaţie asemănătoare cu punctul zecimal, care face separarea între partea întreagă şi cea fracţionară. Cifrele binare situate după punctul binar vor corespunde puterilor negative ale lui 2. Astfel, în general, un număr real va avea reprezentarea binară: ( N 2 = ± bm bm− 1...b1b0 .b− 1b− 2 ...b− n = bm 2 m + bm− 1 2 m− 1 + b1 21 + b0 20 + b− 1 2 − 1 + b− 2 2 − 2 + ... + b− n 2 − n ) Spre exemplu, numărul 12.25 va avea reprezentarea binară: 12.2510 = 1100.01 = 2 3 + 2 2 + 2 −2 31
• 38. Partea întreagă a unui număr real se reprezintă binar precum numerele întregi (cu sau fără semn). Pentru a determina partea fracţionară, se procedează în mod invers ca la partea întreagă. Astfel, dacă partea fracţionară zecimală se reprezintă binar, atunci aceasta se înmulţeşte succesiv cu 2. Dacă rezultatul depăşeşte valoarea 1, atunci se înscrie un bit 1. Se continuă mai departe cu dublarea valorii care depăşeşte 1. Dacă rezultatul nu depăşeşte valoarea 1, atunci se înscrie un bit 0 şi se continuă multiplicarea cu 2. Spre exemplificare, vom vedea cum se obţine reprezentarea binară a lui 12.25. Partea întreagă este 12. Ea se reprezintă binar prin împărţiri succesive la 2 şi considerarea resturilor. Partea fracţionară este 0.25 Partea P.F. x 2 Noua Bitul fracţionară P.F. înscris P.F. 0.25 0.5 0 0.5 1 0 1 0 Obţinem exact rezultatul căutat: 12.25 = 1100.01 Să mai considerăm un alt exemplu. Să reprezentăm numărul 5.37 Partea întreagă are reprezentarea 510 =1012 Partea P.F. x 2 Noua Bitul fracţionară P.F. P.F. înscris 0.37 0.74 0.74 0 0.74 1.48 0.48 1 0.48 0.96 0.96 0 0.96 1.92 0.92 1 0.92 1.84 0.84 1 0.84 1.68 0.68 1 0.68 1.36 0.36 1 0.36 0.72 0.72 0 0.72 1.44 0.44 1 Etc.. Etc.. Obţinem: 5.3710 = 101.010111101...2 Cu cât mai multe cifre binare vom reţine după punctul binar, cu atât vom fi mai aproape de valoarea exactă 5.37. Obţinem un rezultat foarte important: Deşi un număr zecimal poate avea un număr finit de cifre zecimale după punctul zecimal, reprezentarea sa binară internă poate avea un număr infinit de cifre binare. Este valabilă şi reciproca: un număr real zecimal cu un 32
• 39. număr infinit de cifre se poate reprezenta într-o altă bază pe un 1 număr finit de cifre ( ex: = 0.3333...3...10 = 0.13 ). Cum orice 3 reprezentare binară internă este pe un număr finit de biţi, numărul poate să nu fie reprezentat exact în calculator, ci cu o anumită aproximaţie. Acest lucru este decisiv pentru a înţelege importanţa lungimii reprezentării numerelor în calculator. Cu cât un număr binar se reprezintă pe un număr mai mare de biţi, cu atât precizia de reprezentare creşte. 2.3 Reprezentarea internă a numerelor Deoarece semnalul intern purtător de informaţie într-un calculator este de tip binar, un număr zecimal (întreg sau real) se va reprezenta intern în baza 2 cu ajutorul unui număr binar. O cifră binară se numeşte bit (Binary Digit) şi poate fi fie 0 fie 1. În reprezentarea externă a numerelor am văzut că se poate folosi orice bază de numeraţie (cu cifrele corespunzătoare). De asemenea, numerele pot fi prefixate cu un simbol de semn ± şi pot include în reprezentare şi punctul de separaţie între partea întreagă şi cea fracţionară. În reprezentarea internă acest lucru nu mai este posibil deoarece semnele plus (+), minus (-) sau punct (.) nu au nici o semnificaţie pentru calculator. Orice număr (orice tip de dată) este reprezentat la nivel intern de un număr prestabilit de biţi. Specialiştii din industria software au ajuns la un consens de reprezentare concretizat prin standardul IEEE 754 de reprezentare a internă a numerelor reale în computere. Reprezentarea internă a numerelor a impus în limbajul C definirea aşa-numitelor tipuri de date. Tipul unei date reprezintă modul în care microprocesorul stochează în memorie şi prelucrează cu ajutorul regiştrilor interni o dată. Tipul unei date se referă la lungimea sa de reprezentare (pe câţi biţi se reprezintă data) precum şi ce semnificaţie au anumite câmpuri de biţi din cadrul reprezentării. 2.3.1. Reprezentarea internă a numerelor întregi 33
• 40. Un număr binar este o colecţie de cifre binare ponderate fiecare cu o putere a lui 2. Bitul corespunzător ponderii celei mai mari, situat cel mai în stânga, se numeşte MSB (Most Significand Bit) iar cel corespunzător ponderii celei mai mici, situat cel mai în dreapta, se numeşte LSB (Less Significand Bit). În cazul reprezentării binare a numerelor naturale, reprezentarea externă (cea percepută de operatorul uman) şi cea internă (cea prelucrată de procesorul calculatorului) sunt asemănătoare. Cum pentru operatorul uman operatorii ‘+’ sau ‘-‘ semnifică faptul că un număr este pozitiv sau negativ, este necesară o convenţie pentru reprezentarea internă a numerelor întregi negative. Această convenţie prevede folosirea MSB pentru reprezentarea semnului numerelor întregi. Dacă numărul este pozitiv, se adaugă în poziţia MSB bitul de semn ‘0’, iar dacă numărul este negativ se utilizează în poziţia MSB bitul de semn ‘1’. Mai mult, numerele negative se reprezintă în aşa numitul complement faţă de 2. Reprezentarea numerelor întregi negative în complement faţă de 2 Această formă de reprezentare a numerelor negative necesită parcurgerea următorilor paşi: pas1. Se reprezintă modulul numărului negativ, folosind bit de semn (egal cu 0, evident) pas2. Se complementează toţi biţii numărului astfel obţinut. Complementarea înseamnă transformarea bitului 0 în bitul 1 şi a bitului 1 în bitul 0. pas3. Numărul astfel obţinut se adună cu 1. De exemplu, să reprezentăm numărul -37. 3710 = 1001012 = [ 0] 100101 pas1. |-37| = 37 bit semn pas2. 0100101---->1011010 pas3. 1011010 + 1 = 1011011 => -3710 = 10110112 Evident, MSB este bitul de semn şi este egal cu 1. La o primă vedere, este posibil să credem că prin utilizarea complementului faţă de 2 putem pierde semnificaţia numărului negativ. Pentru a vedea ce număr negativ este reprezentat, putem repeta procedeul de mai sus şi obţinem reprezentarea numărului pozitiv dat de modulul său. O modalitate mai simplă este alocarea ponderii corespunzătoare bitului de semn dar pe care o considerăm că reprezintă un număr negativ. Astfel: 34
• 41. 10110112 = -1x26 + 1x24 + 1x23 + 1x21 + 1x20 = -64 + 27 = -37 2.3.2 Adunarea, scăderea şi înmulţirea numerelor întregi Aceste operaţii se execută folosind reprezentarea în complement faţă de 2 a numerelor întregi, sau, mai bine zis, se execută folosind în algoritmi bitul de semn ca pe un bit obişnuit. De exemplu, dorim să calculăm: 37-25 25-37 (-25)x37 (-25)x(-37) Pentru efectuarea acestor calcule, vom scrie reprezentările cu bit de semn ale numerelor implicate: 2510 = 110012 = 011001 − 25 = 100111  10 2  3710 = 1001012 = 0100101 − 3710 = 1011011  Se observă că 25 şi (-25) se reprezintă pe 6 biţi iar 37 şi (-37) pe 7 biţi. Deoarece am observat că biţii unui întreg cu semn nu au toţi aceeaşi semnificaţie, este nevoie să reprezentăm numerele cu care lucrăm pe un acelaşi număr de biţi. La adunări sau scăderi, biţii de semn se vor afla în aceeaşi poziţie (vor avea aceeaşi pondere) şi vom obţine astfel rezultate corecte. Pentru a avea o scriere pe un acelaşi număr de biţi, se adaugă (completează) la stânga bitul de semn de un număr corespunzător de ori. Astfel: 37 − 25 = 37 + (−25) = 0100101 + 1100111 0100101 + − 2510 = 1001112 = 1100111  1100111 25 = 0110012 = 0011001 −−−−−− 0001100 = 1210 35
• 42. 25 − 37 = 25 + (−37) = 0011001 + 1011011 0011001 + − 37 = 1011011  1011011 25 = 0110012 = 0011001 −−−−−− 1110100 = −64 + 52 = −12 În continuare vom pune în evidenţă importanţa gamei de reprezentare, adică a domeniului de valori ale datelor. Să considerăm, spre exemplu, adunarea a două numere cu semn reprezentate pe un octet (8 biţi). Aceste numere sunt cuprinse în gama [− 2 , 2 − 1] 7 7 = [ − 128, 127] . Dacă vom dori să adunăm două numere din acest domeniu şi să reprezentăm rezultatul tot pe un octet, putem avea surprize. De exemplu, să considerăm operaţiile (117-12) şi (117+12). Se observă că operanzii sunt în gama de reprezentare a numerelor cu semn pe 8 biţi. Prin prima scădere, ne aşteptăm să obţinem un rezultat, 105, în aceeaşi gamă de reprezentare. 117-12=117+(-12) = 01110101+11110100 = 01101001 = 10510, rezultat corect. 117+12 = 01110101+00001100 = 10000001 = -12710, rezultat evident incorect. Incorectitudinea provine de la faptul că rezultatul a depăşit gama de reprezentare. Dacă rezultatul este interpretat pe 9 biţi de exemplu, gama de reprezentare devine [ − 256, 255] şi rezultatul va fi 117+12 = 001110101+000001100 = 010000001 = 12910, rezultat corect. Ca o concluzie preliminară, reţinem că pentru a obţine rezultate corecte este necesar să precizăm dacă se lucrează sau nu cu bit de semn şi pe câţi biţi se face reprezentarea, pentru că numai în acest context interpretarea rezultatelor este corectă. În ceea ce priveşte înmulţirea numerelor întregi cu semn (cu bit de semn), aici problema nu mai are o rezolvare asemănătoare, în sensul că nu putem trata biţii de semn la fel cu cei de reprezentare ai valorii. Astfel, procesorul studiază biţii de semn şi ia o decizie în privinţa semnului rezultatului. De fapt, se realizează funcţia logică XOR a biţilor de semn. Numerele negative se vor lua în modul, iar operaţiile de înmulţire se vor face numai cu numere pozitive. La final, 36
• 43. funcţie de semnul rezultatului, se ia decizia reprezentării corecte a rezultatului. Spre exemplu, să calculăm (-25)x37. Pentru aceasta, procesorul va primi pentru procesare următoarele două numere: 37 x(−25) = [ 0]100101 × [1]100111 Se analizează separat biţii de semn şi se ia decizia că rezultatul va fi negativ, deci, la final, se va reprezenta în complement faţă de 2. Mai departe se va lucra cu 25, modulul numărului (-25), care se obţine prin complementarea faţă de 2 a numărului binar 1100111: 11001110011000+1=0011001 Se va reţine pentru procesare numai numărul (fără semn) 11001, care se va înmulţi cu numărul (fără semn) 100101, obţinând, aşa cum am arătat mai sus, valoarea 1110011101. Mai departe, se adaugă bitul de semn, 0 pentru numere pozitive, obţinându-se 01110011101. Acest ultim număr se va complementa faţă de 2, obţinându-se 10001100010+1=[1]0001100011, adică valoarea -1024+99 = -925, valoarea corectă. Ca o concluzie, pentru a furniza rezultate corecte, procesorul va trebui informat în permanenţă despre ce fel de numere prelucrează (cu sau fără semn) şi care este lungimea lor de reprezentare (toate trebuie să aibă aceeaşi lungime). Reprezentarea în complement faţă de 2 se poate folosi şi pentru numerele reale negative, bitul de semn fiind MSB de la partea întreagă. Astfel, -12.25 poate avea reprezentarea: 12.2510 = 1100.012 → 01100.01 01100.01 → 10011.10 + 0.01 = 10011.11 10011.112 = −2 4 + 21 + 2 0 + 2 −1 + 2 −2 = −16 + 3 + 0.75 = −12.2510 Pentru înmulţirea numerelor reale rămân valabile considerentele de la numere întregi. În cazul de mai sus, problema reprezentării numărului negativ a fost rezolvată cu ajutorul bitului de semn dar problema reprezentării punctului binar va avea altă rezolvare. 2.3.3 Reprezentarea internă a numerelor reale Din considerentele de la reprezentarea externă a datelor putem trage alte concluzii importante din punct de vedere al reprezentării 37
• 44. interne. Numerele binare întregi fără semn au aceeaşi reprezentare atât externă cât şi internă. Numerele întregi cu semn (care în reprezentare externă sunt prefixate cu ± ) au ca reprezentare internă un bit de semn, dar care se tratează deosebit de ceilalţi biţi ai reprezentării. Toţi întregii cu semn, care au MSB=1, sunt reprezentaţi intern în complement faţă de 2. Numerele reale se pot reprezenta identic cu cele întregi cu semn, cu o precizare: nu se face o deosebire netă între biţii reprezentării părţii întregi şi cei ai reprezentării părţii fracţionare. Acest tratament nediferenţiat provine de la reprezentarea ştiinţifică uzuală cu mantisă şi exponent. Fie, spre exemplu, reprezentarea binară a numărului 12.25: 12.2510 = 1100.01 = 0.110001 x 2 4 Calculatorul poate reprezenta şirul de biţi 110001 şi reţine faptul că punctul se pune după primii 4 biţi ai reprezentării. Acest lucru se întâmplă şi în realitate. Deci, singura deosebire între reprezentarea numerelor reale şi a celor întregi constă în faptul că numerele reale necesită o informaţie suplimentară despre aşa numitul exponent, în cazul nostru numărul pozitiv 4. În cele ce urmează, vom prezenta tipurile de bază pe care le pot avea datele în reprezentarea internă. Tipul unei date determină modul în care procesorul stochează şi prelucrează data respectivă. Cum primele procesoare care au condus la apariţia pe piaţă a primelor calculatoare pentru neprofesionişti (aşa numitele Home Computers) au fost procesoare capabile să prelucreze şi să transmită în paralel 8 biţi, a fost naturală gruparea a 8 biţi într-o entitate numită byte. 1B = 8b (adică un byte reprezintă 8 biţi) Procesoarele au evoluat, ajungându-se în prezent la procesoare pe 64 de biţi. Cum evoluţia lor s-a făcut trecându-se succesiv prin multipli de 8 biţi, s-au impus şi alte entităţi de reprezentare a informaţiei, pe care le vom prezenta sintetic în tabelul de mai jos. Denumire Dimensiune Denumire Notaţie echivalentă Nr. Nr. byte biti Byte 1B 8b octet B Word 2B 16 b cuvânt W 38
• 45. Denumire Dimensiune Denumire Notaţie echivalentă Nr. Nr. byte biti Double_Words 4B 32 b Cuvânt dublu DW Quad_Words 8B 64 b Cuvânt cvadruplu QW Ten_Words 10B 80 b TW A determina reprezentarea internă înseamnă să determinăm lungimea reprezentării (de obicei în multipli de octeţi), modul de interpretare al biţilor ce compun reprezentarea şi gama de reprezentare, adică să determinăm magnitudinea (valorile minime şi maxime pozitive şi negative) ce pot fi reprezentate în formatul respectiv. În limbajul C, există două tipuri de reprezentare pe care le putem numi principale: tipul întreg şi tipul real, fiecare având şi anumite particularizări. Astfel, tipul întreg (int) include şi tipul caracter (char) iar tipul real (float) include şi tipul real extins (double). Tipurile de date le vom reprezenta de la simplu la complex, în ordinea char, int, float, double. Tipurile de bază sunt char, int, float, double şi cu ajutorul modificatorilor de tip putem obţine diverse particularizări. Modificatorii pot fi signed, unsigned, short, long. Ca o generalitate, numerele sunt reprezentate intern luându-se în considerare bitul de semn, deci implicit numerele întregi sau reale au MSB bit de semn. Dacă se specifică explicit, prin modificatorul unsigned, nu se mai consideră (interpretează) bitul de semn. 2.3.3.1 Tipul char Codul ASCII (American Standard Code for Information Interchange) este un cod de reprezentare a caracterelor. Prin caracter înţelegem unităţile de bază care se pot tasta (intrări de la tastatură), tipări la imprimantă sau afişa pe ecran. Tastatura reprezintă, de exemplu, dispozitivul de intrare care conţine de fapt o întreagă colecţie de caractere ce pot fi emise prin apăsarea unei taste. Pentru a fi receptat, emis sau prelucrat de către calculator, fiecare caracter are asociat un cod binar (o combinaţie de biţi) care îl identifică în mod unic. Cum cu un octet putem codifica 2 8 = 256 caractere, octetul s-a 39
• 46. dovedit o entitate suficientă pentru codificarea caracterelor utilizate în informatică. În 256 de coduri distincte se pot include literele mari şi mici ale alfabetului anglo-saxon (inclusiv litere specifice diverselor alfabete precum cel chirilic sau particularităţi ale diferitelor ţări: ş, ţ, â, î, Ş... în română, de exemplu). Se mai pot include caractere ce reprezintă numere, semne de punctuaţie sau alte caractere de control. Codul ASCII a standardizat această codificare, astfel încât el este folosit în cvasitotalitatea calculatoarelor (doar mainframe-urile IBM mai folosesc un alt cod, mai vechi, numit EBCIDIC). Dacă se declară o dată de tip char, ea este considerată explicit de tipul signed char (cu MSB bit de semn), deci reprezentarea internă este de forma: S b6 b5 b4 b3 b2 b1 b0 Bit de semn Gama de reprezentare este cuprinsă între max = 27 − 1 = 127   ⇒ [ − 128, 127 ] min = −27 = −128  Dacă se declară tipul unsigned char, atunci nu se mai consideră (interpretează) bitul de semn şi data se consideră întreagă pozitivă, în gama max = 2 8 − 1 = 255   ⇒ [ 0, 255] min = 0  Tabelele de mai sus conţin codurile ASCII ale primelor 128 de caractere. Coloana D semnifică valoarea zecimală (decimal) a octetului, coloana H reprezintă aceeaşi valoare reprezentată în format hexazecimal (baza 16) iar în coloana Sym se reprezintă simbolul afişat pe monitoarele PC. Întregul alfabet al limbajului C se regăseşte în mulţimea primelor 128 de caractere ASCII. Restul de 128 de caractere se mai numeşte şi set de caractere extins ASCII şi poate fi vizualizat printr-un program simplu. Trebuie menţionat faptul că reprezentarea datelor în format hexazecimal este foarte răspândită în tehnica programării calculatoarelor. Avantajul reprezentării interne a datelor în format 40
• 47. hexazecimal constă în folosirea unui număr mai mic de cifre (de 4 ori mai mic decât numărul de cifre binare). Reprezentarea unui număr natural în format hexazecimal se realizează cu metoda împărţirii succesive la 16 sau, mai simplu, pornind de la reprezentarea binară a numărului. Cum mulţimea cifrelor hexa conţine 16 simboluri (0…9 şi A… F), pentru codificarea celor 16 cifre avem nevoie de 4 cifre binare ( 2 4 = 16 ). Pentru a reprezenta un octet vom avea nevoie de 2 cifre hexazecimale şi vom proceda astfel: - se divide octetul în două grupe de câte 4 biţi - se înlocuieşte fiecare grup de 4 biţi cu cifra hexazecimală pe care o codifică. De exemplu, să presupunem că avem numărul 217. 21710 = 110110012 = 1101.10012 = D916 = 13 ⋅161 + 9 ⋅16 0 = 208 + 9 = 217 În acest mod, dacă un număr are o reprezentare internă pe un număr de k octeţi, se poate reprezenta simplu cu ajutorul a 2k cifre hexazecimale. În tabelele de mai jos se prezintă codificarea ASCII a caracterelor. Codurile corespunzătoare simbolurilor alfanumerice din tabel sunt exact semnalele binare care se transmit în reprezentarea internă. Cu alte cuvinte, dacă la tastatură se tastează simbolul “a”, atunci circuitele corespunzătoare transmit spre calculator semnale binare corespunzătoare codului 1010 0001, adică 61H sau 97 în zecimal. La fel se întâmplă când se lucrează cu procesoare de text sau când se tipăreşte un document la imprimantă. Sistemul de calcul manevrează codurile ASCII corespunzătoare literelor şi cifrelor pe care utilizatorul le poate interpreta. D H Sym D H Sym D H Sym D H Sym 0 0 Null 1 1 ► 3 2 4 3 0 6 0 2 0 8 0 1 1 ☺ 1 1 ◄ 3 2 ! 4 3 1 7 1 3 1 9 1 2 2 ☻ 1 1 ↕ 3 2 " 5 3 2 8 2 4 2 0 2 3 3 ♥ 1 1 ‼ 3 2 # 5 3 3 9 3 5 3 1 3 41
• 48. 4 4 ♦ 2 1 ¶ 3 2 \$ 5 3 4 0 4 6 4 2 4 5 5 ♣ 2 1 § 3 2 % 5 3 5 1 5 7 5 3 5 6 6 ♠ 2 1 ▬ 3 2 & 5 3 6 2 6 8 6 4 6 7 7 2 1 ↨ 3 2 ' 5 3 7 3 7 9 7 5 7 8 8 2 1 ↑ 4 2 ( 5 3 8 4 8 0 8 6 8 9 9 2 1 ↓ 4 2 ) 5 3 9 5 9 1 9 7 9 1 a LF 2 1a → 4 2a * 5 3a : 0 6 2 8 1 b ♂ 2 1 ← 4 2 + 5 3 ; 1 7 b 3 b 9 b 1 c ♀ 2 1c ∟ 4 2c , 6 3c < 2 8 4 0 1 d CR 2 1 ↔ 4 2 - 6 3 = 3 9 d 5 d 1 d 1 e ♫ 3 1e ▲ 4 2e . 6 3e > 4 0 6 2 1 f ☼ 3 1f ▼ 4 2f / 6 3f ? 5 1 7 3 D H Sy D H Sy D H Sy D H Sym m m m 6 4 @ 8 5 P 96 60 ` 11 7 p 4 0 0 0 2 0 6 4 A 8 5 Q 97 61 a 11 7 q 5 1 1 1 3 1 6 4 B 8 5 R 98 62 b 11 7 r 6 2 2 2 4 2 6 4 C 8 5 S 99 63 c 11 7 s 7 3 3 3 5 3 6 4 D 8 5 T 10 64 d 11 7 t 8 4 4 4 0 6 4 42
• 49. 6 4 E 8 5 U 10 65 e 11 7 u 9 5 5 5 1 7 5 7 4 F 8 5 V 10 66 f 11 7 v 0 6 6 6 2 8 6 7 4 G 8 5 W 10 67 g 11 7 w 1 7 7 7 3 9 7 7 4 H 8 5 X 10 68 h 12 7 x 2 8 8 8 4 0 8 7 4 I 8 5 Y 10 69 i 12 7 y 3 9 9 9 5 1 9 7 4a J 9 5a Z 10 6a j 12 7a z 4 0 6 2 7 4 K 9 5 [ 10 6b k 12 7 { 5 b 1 b 7 3 b 7 4c L 9 5c 10 6c L 12 7c | 6 2 8 4 7 4 M 9 5 ] 10 6d M 12 7 } 7 d 3 d 9 5 d 7 4e N 9 5e ^ 11 6e n 12 7e ~ 8 4 0 6 7 4f O 9 5f _ 11 6f o 12 7f ⌂ 9 5 1 7 2.3.3.2 Tipul int Acest tip se foloseşte pentru reprezentarea numerelor întregi cu sau fără semn. Odată cu standardizarea ANSI C din 1989, s-a trecut la modul de reprezentare a întregilor impus de noul procesor Intel 80386 dotat şi cu coprocesorul matematic Intel 80387. MSB S b30 Octetul 1 Octetul 2 Octetul 3 b0 Octetul 4 LSB 43
• 50. Tipul int este identic cu signed int şi utilizează o reprezentare pe 4B a numerelor întregi cu semn. Reprezentarea pe 4 octeţi duce la posibilitatea măririi gamei de reprezentare astfel: max = 231 −1   ( ) 3 3 ( ) ; 231 = 2 ⋅ 230 = 2 ⋅ 210 ≅ 2 ⋅ 103 ≅ 2 ⋅109 min = −231  Rezultă că putem reprezenta numere întregi în gama: [± 2.1475 ⋅10 ] ≅ [− 2 ⋅10 , 2 ⋅10 ] 9 9 9 unsigned int nu va mai lua în considerare bitul de semn, astfel încât reprezentarea internă este de forma din figura de mai jos. Evident,  max = 232 −1 32  min = 0 3 ( ) 3 ; 2 = 4 ⋅ 230 = 4 ⋅ 210 ≅ 4 ⋅ 103 ≅ 4 ⋅109( )  Gama de reprezentare se poate schimba cu ajutorul modificatorilor short sau long. MSB S b14 b0 LSB short int se va reprezenta pe 2B, sub forma max = 215 − 1   ; 215 = 2 5 ⋅ 210 = 32 ⋅ 210 ⇒ [ − 32768, 32767] . min = −215  unsigned short int va schimba gama de reprezentare în [ 0, 65535] long int se va reprezenta pe 8B şi va conduce la o gamă imensă de reprezentare a numerelor întregi, lucru dovedit de ( ) 6 ± 2 63 = ±2 3 ⋅ 210 ≅ ±8 ⋅1018 = ±9.2234 ⋅1018 unsigned long int va considera numai numere întregi pozitive în [ gama 0, 1.844 ⋅1019 .] 2.3.3.2 Tipul float Acest tip de reprezentare este de tip real, fiind cunoscut şi ca reprezentare în virgulă mobilă (floating point). Acest tip descrie mecanismul de bază prin care se manipulează datele reale. Conceptul fundamental este acela de notaţie ştiinţifică, prin care orice număr se 44
• 51. poate exprima ca un număr zecimal (deci, cu punct zecimal) multiplicat cu o putere a lui zece sau ca un număr real binar (cu punct binar) multiplicat cu o putere a lui 2. 5.2510 = 101.01 = 1.0101 x 2 2 = 0.10101 x 2 3 Se observă cum stocarea în calculator a unei date floating-point necesită trei părţi: - bitul de semn (sign) - mantisa, fracţia (significand) - exponent (exponent) Folosind formatul specific I80386, în limbajul C se disting trei tipuri de date reale: - float , cu reprezentare pe 4 octeţi (32 biţi, double word) - double, cu reprezentare pe 8 octeţi (64 biţi, quad word) - long double, cu reprezentare pe 10 octeţi (80 biţi, ten word) MSB b31 b30 b0 LSB 45
• 52. Exponent Significand S biased 31 30 23 22 0 Exponent = 8b Significand = 23b float S Bias = 7FH=127 63 62 52 51 0 Exponent = 11b Significand = 52b double S Bias = 3FFH=1023 79 78 64 63 0 Exponent = 15b Significand = 52b long S Bias = 3FFFH=16383 double Tipurile float şi double sunt formate pentru numere reale ce există numai în memorie. Când un astfel de număr este încărcat de procesor în stiva pentru numere reale (flotante) pentru prelucrare sau ca rezultat al prelucrării, el este automat convertit la formatul long double (sau extended). În cazul în care acest număr se stochează în memorie, el se converteşte la tipul float sau double. Toate cele trei subtipuri reale au un format comun, care va fi prezentat în continuare. Ceea ce le deosebeşte este numărul de biţi alocaţi pentru exponent şi pentru mantisă, precum şi interpretarea biţilor mantisei (significand). Semnul are alocat în toate formatele un singur bit: 0 pentru numere pozitive şi 1 pentru numere negative. Mărimea câmpului exponent variază cu formatul şi valoarea sa determină câţi biţi se mută la dreapta sau la stânga punctului binar. Câmpul significand este analogul mantisei în notaţia ştiinţifică. El conţine toţii biţii semnificativi ai reprezentării, deci biţii semnificativi atât ai părţii întregi cât şi ai părţii fracţionare cu singura restricţie ca aceşti biţi să fie consecutivi. Deoarece punctul binar este mobil, cu cât sunt mai mulţi biţi alocaţi părţii întregi, cu atât vor fi mai puţini pentru partea fracţionară şi invers. Cu cât formatul este mai larg, cu atât se vor reprezenta mai precis numerele. Pentru a salva un spaţiu preţios de stocare, nici unul dintre cele trei formate float nu stochează zerouri nesemnificative. De exemplu, pentru numărul 0.0000101 = 0.101x 2 −4 câmpul significand va stoca 46
• 53. numărul 101, nu şi cele 4 zerouri nesemnificative ale părţii fracţionare. Pentru a salva şi mai mult spaţiu, pentru formatele float şi double câmpul significand nu va conţine primul bit semnificativ care obligatoriu este 1. Câştigând acest bit (numit bit phantom), se dublează gama de reprezentare. Formatul long double va conţine totuşi bitul de semn 1 cel mai semnificativ. Punctul binar se pune exact înaintea primului bit din câmpul significand, adică după bitul 1 implicit (phantom). În cazul long double, se aplică după primul bit 1. Pentru a uşura operarea cu aceste numere, câmpul exponent nu este stocat ca un număr întreg cu semn, ci este decalat (normalizat, cu bias) pentru a reprezenta numai numere pozitive (deci exponentul este interpretat ca număr natural fără semn). Biasul adăugat se scade pentru a afla exponentul exact. Avantajul exponentului decalat constă, pe lângă faptul că nu mai are nevoie de bit de semn, în faptul că pentru a compara două numere reale putem începe prin compararea biţilor pornind de la MSB către LSB, cel mai mare fiind cel care are 1 la primul bit diferit. Se decide astfel foarte rapid care număr este cel mai mare. Ca exemplu, să considerăm un format float în care se stochează: Sign = 0 Exponent = 10000010 = 13010 Significand = 1001000…00 Valoarea reală a exponentului va fi 130 - 127 = 3 Biţii câmpului significand se obţin adăugând MSB phantom, deci aceştia vor fi 11001000...00 Numărul real care s-a stocat este: 0.110010...00 x 24 = 1100.1 =12.5 Reprezentarea internă a numărului 12.5, pe 4 octeţi (float), este următoarea: Semn 0 1 0 0 0 0 0 1 0 1 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 LSB Cu alte cuvinte, putem spune că reprezentarea internă a numărului real 12.5 este (în format hexazecimal): 47
• 54. 12.510 = 4148000016 În cazul în care dorim să reprezentăm numărul negativ –12.5, singurul bit care se va modifica va fi bitul de semn, care devine 1. Astfel, reprezentarea internă în format float a numărului negativ real –12.5 este: Semn 1 1 0 0 0 0 0 1 0 1 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 LSB − 12.510 = C148000016 Dacă numărul 12.5 se reprezintă în formatul double, deci pe 8 octeţi, atunci reprezentarea sa internă se va realiza astfel: - bitul de semn va fi 0 - exponentul nu va mai fi pe 8 biţi ca la tipul float, ci pe 11 biţi, deci se va schimba şi bias, care va fi 1023. Atunci: 3 + 1023 = 1026 = 1024 + 2 = 10000000010 exponent −1 bias - significand va fi acelaşi ca la tipul float, dar reprezentat pe 52 de biţi Semn 0 1 0 0 0 0 0 0 0 0 1 0 1 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 LSB 12.510 = 402900000000000016 48
• 55. Reţinem că la numere reale numai bitul de semn indică dacă numărul este pozitiv sau negativ, mantisa şi exponentul se reprezintă ca numere naturale fără bit de semn. Formatele prezentate mai sus respectă standardul IEEE 754 de reprezentare a internă a numerelor reale în computere. Se poate pune o întrebare legitimă: de ce bias-ul în cazul float spre exemplu este 127? Pentru a răspunde la această întrebare, putem face următorul raţionament: - exponentul cu semn este reprezentat pe 8 biţi, deci este în gama de reprezentare [ − 128, + 127] . - pentru a obţine un exponent pozitiv, adăugăm numărul 128. - deoarece bitul phantom nu este reprezentat, exponentul trebuie micşorat cu o unitate pentru a indica unde anume se poziţionează exact punctul binar. - Exponent pozitiv = exponent +128 – 1 = exponent + bias de unde rezultă evident faptul că bias = 127 în cazul tipului float. În final să analizăm un exemplu de procesare a produsului a două numere reale. Vrem să calculăm valoarea 5.25 x 1.5. Pentru aceasta, vom scrie cei doi factori ai produsului în forma: .10101 × .11 5.2510 = 101.012 = .10101 × 23  −−−−−  1 1.510 = 1.12 = .11 × 2 ; 10101  5.25 × 1.5 = [ ( .10101) × ( .11) ] × 2 3+1 10101  −−−−− .0111111 ⇒ 5.25 × 1.5 = .0111111 × 24 = 111.111 = 7.875 Se observă cum câmpurile exponent şi significand sunt procesate separat, în final corelându-se forma de reprezentare internă. Game de reprezentare pentru numerele reale Gama de reprezentare pentru fiecare din tipurile reale prezentate mai sus se calculează luând în considerare cel mai mare număr şi cel mai mic număr posibil a fi scris în respectiva reprezentare. Astfel, exponentul este decisiv pentru gama de reprezentare. 49
• 56. La tipul float, avem exponent _ biasmax = 255 ⇒ exponent _ realmax = 255 − 127 = 128 ( )12 nmax = 2128 = 28 ⋅ 210 = 256 ⋅ (1024 )12 ≅ 256 ⋅1036 = 2.56 ⋅1038 Valoarea maximă exactă, calculată fără a aproxima ca mai sus: 38 210 = 1024 ≅ 1000 = 103 este n max = 3.4028 ⋅10 exponent _ biasmin = 0 ⇒ exponent _ realmin = 0 − 127 = −127 ( ) nmin = 2−127 = 2− 7 ⋅ 210 −12 ( ) = 23 ⋅ 2−10 ⋅ 210 −12 ( ) = 8 ⋅ 210 −13 ≅ 8 ⋅ 10− 39 Valoarea pozitivă minimă exactă este n min = 5.8775 ⋅10 −39 La tipul double vom obţine: exponent _ biasmax = 2047 ⇒ exponent _ realmax = 2047 − 1023 = 1024 ( ) nmax = 21024 = 24 ⋅ 210 102 = 16 ⋅ (1024 ) 306 ≅ 16 ⋅ 10306 = 1.6 ⋅ 10307 Valoarea maximă exactă este n max = 1.7 ⋅10 308 exponent _ biasmin = 0 ⇒ exponent _ realmin = 0 − 1023 = −1023 nmin = 2−1023 = 2−3 ⋅ 210( ) −102 ≅ .125 ⋅ 10−306 Valoarea pozitivă minimă exactă este n min = 1.1125 ⋅ 10 −308 Efectuând aceleaşi consideraţii şi calcule pentru tipul long double, n  max = 1.1 ⋅ 10 4932 vom obţine  n min = 3.4 ⋅ 10 −4932  2.3.5. Codificare BCD Procesorul I80386 este considerat primul procesor care are capacitatea de a procesa operaţii aritmetice asupra unor numere reprezentate în zecimal codificat binar (BCD, binary-coded decimal) în locul formatelor binare standard. Reprezentarea numerelor în cod BCD este folosită pentru a face numerele binare mai accesibile operatorului uman. Neajunsul acestei reprezentări este faptul că numerele BCD ocupă spaţiu de stocare mai mare decât numerele binare. Ele sunt mai uşor de interpretat de către programatorul uman, pentru computer neavând nici un fel de relevanţă. Procesorul 80386 50
• 57. poate manevra două tipuri de formate BCD: împachetat şi neîmpachetat (packed BCD şi unpacked BCD). În formatul unpacked BCD, o cifră zecimală se stochează pe un octet. Spre exemplu, cifra zecimală 5 va fi reprezentată intern sub forma 00001001. Formatul packed BCD stochează două cifre zecimale pe un octet, crescând capacitatea de stocare internă precum şi gama de reprezentare pe un acelaşi număr de octeţi. Ambele codificări folosesc reprezentarea pe 4 biţi a cifrelor zecimale. Spre exemplu, numărul 9817 se stochează pe 4 octeţi în format unpacked BCD şi pe 2 octeţi în format packed BCD: unpacked BCD: 9817 = 0000 1001 0000 1000 0000 0001 0000 0111 packed BCD: 9817 = 1001 1000 0001 0111 Se observă cum valoarea maximă care se poate stoca pe un octet este 9 pentru unpacked BCD, 99 pentru packed BCD şi 255 pentru codificarea binară fără semn standard. Toate formatele reale prezentate se conformează standardului IEEE 754 pentru reprezentarea numerelor în virgulă mobilă în format binar. Ca o concluzie la acest capitol, decisiv pentru înţelegerea dezvoltărilor ulterioare, putem sintetiza următoarele:  Reprezentarea externă a numerelor se referă la modul în care operatorul uman acceptă schimbul de date cu calculatorul. Acest schimb de date are dublu sens: de la operatorul uman către calculator şi invers. Reprezentarea externă este de obicei zecimală şi are un format aproape identic cu formatul matematic uzual: simbol de semn prefixat, punct zecimal, mantisă sau exponent. Numerele naturale se mai pot reprezenta şi în format octal sau hexazecimal. În format extern se introduc datele de la tastatură pentru prelucrare şi se obţin pe monitor sau la imprimantă rezultatele oferite de calculator.  Reprezentarea internă a numerelor se referă la modul în care se stochează datele în memoria RAM a calculatorului şi respectiv în regiştrii interni ai microprocesorului. Această reprezentare internă este legată de noţiunea de tip de dată.  Tipul de dată întreg (integer) se reprezintă intern pe 2, 4 sau 8 octeţi în complement faţă de 2, cu cel mai semnificativ bit (MSB) bit de semn: 1 pentru numere întregi negative şi 0 pentru numere întregi pozitive. Un caz particular de dată de tip întreg este tipul character, interpretat ca întreg pe un octet. 51
• 58.  Tipul de dată real (float) se reprezintă intern pe 4, 8 sau 10 octeţi şi conţine 3 câmpuri de biţi distincte: bit de semn, câmp mantisă şi câmp exponent, de lungimi corespunzătoare. Dacă se specifică explicit, toate numerele se pot defini fără semn (unsigned), caz în care calculatorul nu mai interpretează bitul de semn (MSB) diferit ci îl include în câmpul de reprezentare al mărimii, crescând gama de reprezentare. Capitolul III ELEMENTELE DE BAZĂ ALE LIMABJULUI C 3.1. Crearea şi lansarea în execuţie a unui program C Prezentăm câteva comenzi simple pentru a lansa în execuţie un program C folosind compilatorul BORLANDC v3.1 (versiunea pentru sistemul de operare DOS): • după setarea pe directorul corespunzător se tastează - bc – pentru a intra în mediul BorlandC; - <Alt>-F – pentru a selecta meniul File; - N – pentru a deschide un fişier nou. • se editează programul sursă folosind editorul mediului BorlandC; Exemplu: 52
• 59. #include <stdio.h> void main (void) { printf("Primul program in C!"); } • se acţionează tasta F2 şi se indică numele fişierului (cu extensia .c sau .cpp) pentru salvarea programului sursă pe disc – de exemplu mesaj.c - (se recomandă salvarea pe disc după efectuarea oricărei modificări în programul sursă pentru evitarea pierderii accidentale a acesteia); • se realizează compilarea, link-editarea (realizarea legăturilor) şi lansarea în execuţie a programului executabil mesaj.exe tastând <CTRL>-F9; • pentru a vizualiza rezultatele execuţiei programului se tastează <Alt>-F5; • se revine în fereastra de editare a mediului acţionând o tastă oarecare; • pentru a închide un fişier sursă se tastează <Alt>-F3 iar pentru a ieşi din program în mediul de operare se tastează <Alt>-X. La fel de simplu poate fi utilizată şi varianta pentru sistemul de operare WINDOWS a compilatorului BORLANDC v3.1: • se lansează în execuţie programul bcw.exe; • se selectează din meniul File opţiunea New, creându-se fişierul noname00.cpp; • în fereastra noname00.cpp se introduce codul programului; • din meniul File se selectează opţiunea Save As… sau Save iar în căsuţa de dialog care apare se va salva fişierul program cu extensia .cpp; • din meniul Compile se selectează opţiunea Build All ce va afişa caseta de dialog Compiling (compilare); • dacă operaţia de compilare se încheie cu succes (nu există erori de sintaxă în program) compilatorul va afişa mesajul Press any key, caz în care compilatorul va crea fişierul executabil; • lansarea în execuţie a fişierului executabil se poate realiza folosind opţiunea Run din meniul Run sau combinaţia de taste <CTRL>-F9. 53
• 60. Fiecare limbaj de programare are un set de reguli, denumite reguli sintactice, reguli ce trebuie respectate la editarea unui cod sursă. Dacă este încălcată o regulă sintactică, programul nu va fi compilat cu succes. În acest caz, pe ecran va fi afişat un mesaj de eroare ce specifică linia ce conţine eroarea, precum şi o scurtă descriere a erorii. În exemplul următor programului îi lipseşte caracterul punct şi virgulă după utilizarea funcţie printf: #include <stdio.h> void main (void) { printf("Primul program in C!") } La compilare pe ecran vor apare următoarele mesaje de eroare: Compiling NONAME00.CPP: Error NONAME00.CPP 5: Statement missing ; Error NONAME00.CPP 5: Compound statement missing } Cu toate că în codul sursă există doar o eroare, compilatorul de C va afişa două mesaje de eroare. Lipsa caracterului punct şi virgulă provoacă o serie de erori în cascadă. Pentru a corecta erorile sintactice se merge cu ajutorul cursorului în linia indicată de către mesajul de eroare şi se corectează instrucţiunea respectivă. 3.2. Structura unui program C Conceptul de bază folosit în structurarea programelor scrise în limbajul C este funcţia. Astfel, un program în C este compus din cel puţin o funcţie şi anume funcţia main() sau funcţia principală. La rândul ei, funcţia main() poate apela alte funcţii definite de utilizator sau existente în bibliotecile ce însoţesc orice mediu de dezvoltare de programare în C. Structura generală a unei funcţii C este de forma: tip nume_funcţie (param_1, param_2, ...,param_n) − − − − − − − −  − − − − − − − − Instrucţiuni declarare tip parametri − − − − − − − −  { − − − − − − −  − − − − − − − Corp funcţie=secvenţă de instrucţiuni sau apel de funcţii − − − − − − −  54
• 61. } unde: - tip reprezintă tipul de dată returnat de funcţie (în mod implicit o funcţie returnează tipul int); - nume_funcţie reprezintă numele sub care funcţia este cunoscută în program; - param_1,...,param_n - parametrii cu care funcţia este apelată şi al căror tip poate fi declarat direct în această listă, sau prin instrucţiuni separate plasate imediat după lista parametrilor. Corpul funcţiei este definit ca o secvenţă de instrucţiuni şi/sau apeluri de funcţii şi este delimitat de restul funcţiei prin paranteze acolade. În limbajul C există două categorii de funcţii. O primă categorie este formată de funcţiile ce returnează o valoare la revenirea din ele în punctul de apel, tipul acestei valori fiind definit de de tipul funcţiei. Cealaltă categorie conţine funcţiile ce nu returnează nici o valoare la revenirea din ele , pentru aceste funcţii fiind utilizat cuvântul cheie void în calitate de tip. El semnifică lipsa unei valori returnate la revenirea din funcţie. Exemple: 1) void f(void) { ………… } Funcţia f nu are parametri şi nu returnează nici o valoare. 2) double g(int x) { ………… } Funcţia g are un parametru x de tipul int şi returnează la revenirea în programul principal o valoare flotantă în dublă precizie. Funcţiile C sunt în general unităţi independente, compilabile separat. Instrucţiunile, la rândul lor, pot defini tipul unor date folosite în program, sau operaţii ce trebuie executate prin program. Din punct de vedere sintactic, orice instrucţiune trebuie terminată cu caracterul ";", iar grupurile de instrucţiuni pot fi delimitate prin caracterele { şi } pentru a forma unităţi sintactice noi de tip bloc. Funcţiile apelate vor primi valori pentru argumentele (parametrii) lor şi pot returna către funcţia apelantă valori de un anumit tip. 55
• 62. Cu aceste precizări generale, dacă avem un program compus din două funcţii, şi anume funcţia principală şi o funcţie apelată f(), atunci structura acestuia va fi de forma: tip main( )  {   --------    Functia principala f( ); / * apelul functiei f() * /  --------   }   ___________ f( )  {    − − − − − − Functia f( ) − − − − − −  }   Programul începe cu execuţia funcţiei main(). Aceasta funcţie este folosită în general fără parametri. La rândul lor, funcţiile apelate pot fi scrise în limbaj C, sau realizate în alte limbaje de programare: asamblare, Fortran, Pascal etc. 3.3. Mulţimea caracterelor În programele C pot fi utilizate două mulţimi de caractere: mulţimea caracterelor C şi mulţimea caracterelor C reprezentabile. Mulţimea caracterelor C se compune din litere, cifre, semne de punctuaţie care au o semnificaţie specifică pentru compilatorul C. Programele C sunt formate din combinaţii ale caracterelor din mulţimea de caractere C constituite în instrucţiuni semnificative. Mulţimea caracterelor C este o submulţime a mulţimii caracterelor C reprezentabile. Mulţimea caracterelor reprezentabile este formată din totalitatea literelor, cifrelor şi simbolurilor grafice. Dimensiunea mulţimii de caractere reprezentabile depinde de tipul de terminal, consolă etc. Fiecare caracter din mulţimea caracter din mulţimea caracterelor C are un înţeles explicit pentru compilatorul C. Compilatorul dă mesaje de eroare când întâlneşte caractere întrebuinţate greşit sau caractere care nu aparţin mulţimii caracterelor 56
• 63. C. În continuare sunt descrise caracterele şi simbolurile din mulţimea caracterelor C şi utilizarea acestora. 3.3.1. Litere şi numere Mulţimea caracterelor C include literele mari şi mici ale alfabetului englez şi cifrele zecimale din sistemul de numere arabe. Literele mari şi mici ale alfabetului englez sunt următoarele: ABCDEFGHIJKLMNOPRSTUVWXYZ abcdefghijklmnoprstuvwxyz iar cifrele zecimale: 0 1 2 3 4 5 6 7 8 9. Aceste litere şi cifre pot fi folosite pentru a forma constante, identificatori şi cuvinte cheie. Compilatorul C prelucrează litere mari şi mici în mod distinct. 3.3.2. Caractere whitespace Spaţiul, tab-ul, linefeed (linie nouă), carriage return (revenire la capătul rândului), form feed, tab-ul vertical şi newline sunt numite caractere whitespace deoarece servesc pentru spaţiere între cuvinte, aliniere la o nouă coloană, salt la linie nouă. Aceste caractere separă instrucţiuni definite de utilizator, constante şi identificatori, de celelalte instrucţiuni dintr-un program. Compilatorul C ignoră caracterele whitespace dacă nu sunt folosite ca separatori sau drept componente de constante, sau ca şiruri de caractere. Caracterele whitespace sunt utilizate pentru a face programele mai lizibile. Comentariile sunt de asemenea tratate ca whitespace. 3.3.3. Caractere speciale şi de punctuaţie Caracterele speciale şi de punctuaţie din mulţimea caracterelor C sunt folosite pentru mai multe scopuri. Tabelul următor prezintă aceste caractere. Aceste caractere au o semnificaţie specială pentru compilatorul de C. Caracterele de punctuaţie din setul de caractere reprezentabile C care nu apar în acest tabel pot fi utilizate numai în şiruri, constante caracter şi comentarii. Caracte Nume Caracte Nume 57
• 64. r r , Virgulă ! Semnul exclamării . Punct | Bară verticală ; Punct şi virgulă / Slash : Două puncte Backslash ? Semnul ~ Tilda întrebării ’ Apostrof _ Underscore ” Ghilimele # Diez ( Paranteză % Procent stânga ) Paranteză & Ampersand dreapta [ Paranteză ^ Săgeată sus dreaptă stânga ] Paranteză * Asterisc dreaptă dreapta { Acoladă stânga - Minus } Acoladă dreapta = Egal > Mai mare + Plus < Mai mic 3.3.4. Secvenţe escape Secvenţele escape sunt combinaţii speciale de caractere formate din whitespace şi caractere negrafice constituite în şiruri şi constante caracter. Ele sunt în mod tipic utilizate pentru a specifica acţiuni precum carriage return şi tab pe terminale şi imprimante şi pentru a furniza reprezentarea caracterelor care normal au înţeles special, cum ar fi ghilimelele (”). O secvenţă escape constă dintr-un backslash urmat de o literă sau combinaţii de cifre. Setul complet de secvenţe escape cuprinde: a caracterul BEL - activare sunet b caracterul BS (backspace) - revenire cu un spaţiu f caracterul FF (form feed) - salt de pagină la imprimantă n caracterul LF (line feed) - rând nou r caracterul CR (carriage return) - revenire la coloana 1 t caracterul HT (horizontal tab) - tab orizontal v caracterul VT (vertical tab) - tab vertical 58
• 65. caracterul (backslash) " caracterul " (double qoute) - ghilimele ' caracterul ' (single qoute) - apostrof 0 caracterul NULL ooo - constantă octală xhh - constantă hexazecimală Backslash-ul care precede un caracter neinclus în lista de mai sus este ignorat şi acest caracter este reprezentat ca un literal. De exemplu, forma „c” reprezintă caracterul c într-un literal sau într-o constantă caracter. Secvenţele ooo şi xdd permit scrierea oricărui caracter din setul ASCII ca un număr octal format din trei cifre sau ca un număr hexagesimal format din două cifre. Exemplu: '6' 'x6' 6 ASCII '60' 'x30' 48 ASCII '137' 'x5f' 95 ASCII Numai cifrele octale (de la 0 la 7) pot apare într-o secvenţă escape octală şi trebuie să apară cel puţin o cifră. De exemplu, caracterul backspace poate fi scris ca „10” în loc de „010”. Similar, o secvenţă hexagesimală poate să conţină cel puţin o cifră, iar a doua cifră poate fi omisă. Totuşi, când se utilizează secvenţe escape în şiruri, este indicat să se scrie toate cele trei cifre ale secvenţei. Altfel, caracterul care urmează după secvenţa escape ar putea fi interpretat ca o parte a secvenţei, dacă se întâmplă să fie o cifră octală sau hexagesială. De exemplu, secvenţa 0331 este interpretată drept ESC şi 1. Dacă am scrie 331, omiţând primul zero, atunci am avea o interpretare greşită. Secvenţele escape permit caractere de control negrafice pentru a fi transmise către display. Caracterele negrafice trebuie totdeauna reprezentate ca secvenţe escape. Plasând necorespunzător un caracter negrafic în programe C, el are rezultat imprevizibil. 3.4. Identificatori Identificatorii sunt nume ce sunt date variabilelor, funcţiilor şi etichetelor utilizate în program. Un nume este o succesiune de litere şi eventual cifre, primul caracter fiind literă. În calitate de litere se pot utiliza literele mici şi mari ale alfabetului englez, precum şi caracterul 59
• 66. subliniere (_). Numărul de caractere care intră în componenţa unui nume nu este limitat. Numele sunt utilizate pentru a defini diferite variabile sau funcţii într-un program C. În mod implicit, numai primele 32 de caractere dintr-un nume sunt luate în considerare, adică două nume sunt diferite dacă ele diferă în primele 32 de caractere ale lor. Exemple de nume: a, b1, a1b2c3, Fs, _hG, Nume, nUME, … Se recomandă ca numele să fie sugestive, adică ele să sugereze pe cât posibil scopul alegerii lor sau a datei pe care o reprezintă. 3.5. Cuvintele cheie ale limbajului C În limbajul C există un număr de cuvinte care au o utilizare predefinită, numite cuvinte cheie. Utilizatorul nu poate să utilizeze aceste cuvinte pentru a denumi variabile sau funcţii într-un program. Tabelul următor prezintă cuvintele cheie ale limbajului C: Cuvintele cheie ale limbajului C auto default float register struct volatile break do for return switch while case double goto short typedef char else if signed union const enum int sizeof unsigned continue extern long static void 3.6. Constante În C, constantele se referă la valori fixe pe care programul nu le poate modifica. Constantele pot fi: întregi, în virgulă mobilă sau reale, constante-caracter, constante-şir sau enumerări. Zero poate fi folosit ca o constantă pentru tipurile pointer, iar şirurile de caractere sunt de fapt constante de tip char[]. Este posibil, de asemenea, să se specifice constante simbolice. O constantă simbolică este un nume a cărui valoare nu poate fi modificată în domeniul său. În C există trei feluri de constante simbolice: 1. orice valoare de orice tip poate fi folosită ca şi constantă prin adaugarea cuvântului cheie const la definirea sa; 2. un set de constante întregi definite ca o enumerare; 3. orice nume de vector sau funcţie. 60
• 67. 3.6.1. Constante caracter O constantă caracter este un caracter inclus între apostrofuri. De exemplu, 'a', 'A' şi '%' sunt constante caracter. Valoarea unei constante caracter este chiar valoarea numerică corespunzătoare caracterului dat în setul de caractere al maşinii. De pildă, dacă pe un calculator caracterele se reprezintă în cod ASCII, atunci constanta '1' are valoarea 0618, 4910 sau 3116. Constantele caracter pot fi folosite în operaţii de calcul exact ca şi întregii. Caracterele pot fi reprezentate şi prin secvenţe escape (de exemplu, prin constanta 'n' se introduce caracterul newline). 3.6.2. Constante întregi Constantele întregi se reprezintă în 4 forme: zecimale, octale, hexazecimale şi constante caracter. Constantele zecimale sunt cel mai frecvent folosite şi se reprezintă ca şiruri de cifre zecimale. O constantă care începe cu zero urmat de x (0x) este un număr hexazecimal, iar o constantă care începe cu zero este un număr octal. Pentru a reprezenta cifrele hexazecimale 10,...,15 se folosesc literele a,...,f sau literele mari corespunzătoare. Notaţiile octale şi hexazecimale sunt utile în exprimarea succesiunilor de biţi. Exemplu: int hex = 0xFF; /* numărul 255 în zecimal */ int oct = 011 ; /* numărul 9 în zecimal */ 3.6.3. Constante în virgulă mobilă O constantă în virgulă mobilă are tipul float, double sau long double. Compilatorul, ca şi în cazul constantelor întregi, trebuie să semnaleze eroare în cazul în care constantele sunt prea mari pentru a putea fi reprezentate. Exemple de constante în virgulă mobilă: 123.23 .23 0.23 1.0 1. 1.2e10 1.256-15 Observaţie. În interiorul constantelor întregi sau reale nu pot apare spaţii albe. De exemplu, 56.62 e - 17 nu este o constantă în virgulă mobilă, ci sunt de fapt 4 atomi lexicali: 56.62, e, -, 17 şi se va genera o eroare de sintaxă. Dacă se doreşte o constantă de tip float, aceasta se poate defini astfel: const float pi8 = 3.14159265; 61
• 69. Tipul unui şir este vector de un număr de caractere a.i. "asaf" are tipul char[5]. Şirul vid se notează prin " " şi are tipul char[1]. De notat că, pentru fiecare şir s, strlen(s) == sizeof(s) - 1, deoarece funcţia strlen() nu numără şi terminatorul 0. În interiorul unui şir se poate folosi convenţia de notaţie cu . Aceasta face posibilă reprezentarea caracterului ghilimele (") şi în interiorul unui şir. Cel mai frecvent caracter folosit este caracterul 'n'=newline (NL). De exemplu, instrucţiunea: printf ("beep at end of message007n"); determină scrierea unui mesaj, a caracterului BEL şi a caracterului NL. O secvenţă de forma n într-un şir nu determină introducerea unui caracter NL în şir, ci este o simplă notaţie (n este caracter neafişabil). Nu este permisă continuarea şirurilor de caractere de pe o linie pe alta. Atunci când se include o constantă numerică într-un şir de caractere utilizând notaţia octală sau hexazecimală este recomandat să se folosească 3 cifre pentru număr. Exemplu: char v1[] = "ax0fah0129";//'a' 'x0f' 'a' 'h' '012' '9' char v2[] = "axfah 129"; /* 'a' 'xfa' 'h' '12' '9' */ char v3[] = "axfad127"; /* 'a' 'xfa' 'd' '127' */ 3.6.5. Constanta zero Zero poate fi utilizat ca o constantă pentru tipurile întregi, în virgulă mobilă sau pointer. Nu se recomandă alocarea unui obiect la adresa zero. Tipul lui zero va fi determinat de context. 3.6.6. Obiecte constante Cuvântul cheie const poate fi inclus într-o declaraţie a unui obiect pentru a determina ca tipul acestui obiect să fie constant şi nu variabil. Exemplu : const int model = 145; const int v[ ] = {1, 2, 3, 4}; Deoarece nu i se poate atribui o valoare, o constantă poate fi doar iniţializată. Declarând ceva ca fiind constant, ne asigurăm că valoarea sa nu se modifică în domeniul său. Astfel instrucţiunile: model = 165; /* eroare */ model++; /* eroare */ vor determina apariţia unor mesaje de eroare corespunzătoare. 63
• 70. De notat că const modifică un tip ceea ce înseamnă că restricţionează felul în care se poate utiliza un obiect, şi nu modul de alocare. Pentru o constantă, compilatorul nu rezervă memorie deoarece i se cunoaşte valoarea (precizată la iniţializare). Mai mult, iniţializatorul pentru o expresie constantă este, de obicei (dar nu întotdeauna), o expresie constantă. Dacă este aşa, aceasta poate fi evaluată în timpul compilării. 3.6.7. Enumerări Folosirea cuvântului cheie enum este o metodă alternativă pentru definirea constantelor întregi, ceea ce este uneori mult mai util decât utilizarea lui const. De exemplu, enum {ASM , AUTO , BREAK }; defineşte 3 constante întregi denumite enumeratori şi le atribuie valori. Deoarece valorile enumeratorilor sunt atribuite implicit, începând cu 0, aceasta este echivalentă cu: const ASM = 0; const AUTO = 1; const BREAK = 2; O enumerare poate avea nume. De exemplu, enum Keyword {ASM , AUTO , BREAK }; defineşte o enumerare cu numele Keyword. Numele enumerării devine sinonim cu int şi nu cu un nou tip. Declararea unei variabile Keyword în loc de int poate oferi atât utilizatorului, cât şi compilatorului, o sugestie asupra modului de utilizare. De exemplu, enum Keyword Key; //declara var. Key de tip enum Keyword switch (Key) { case ASM: ........... break; case BREAK: ........... break; } determină compilatorul să iniţieze un avertisment deoarece sunt folosite numai două din cele trei valori ale lui Key. Capitolul IV OPERANZI ŞI OPERATORI ÎN C 64
• 71. 4.1. Operanzi O expresie, în limbajul C, este formată dintr-un operand sau mai mulţi legaţi prin operatori. Un operand poate fi: - o constantă; - o constantă simbolică; - numele unei variabile; - numele unui tablou; - numele unei structuri; - numele unui tip; - numele unei funcţii; - elementele unui tablou; - elementele unei structuri; - o expresie inclusă între paranteze rotunde. Unui operand îi corespunde un tip şi o valoare. Dacă tipul operandului este bine precizat la compilare, valoarea operandului se determină fie la compilare, fie la execuţie. Exemple: 1. 6353 – este o constantă întreagă zecimală de tip int şi reprezintă un operand constant de tip int. 2. float x2 – reprezintă declaraţia variabilei x2, iar numele x2 reprezintă un operand de tipul float. 3. 0xa13d – este o constantă întreagă hexazecimală de tip unsigned şi reprezintă un operand de tipul unsigned. 4. produs(a,b) – este un apel al funcţiei produs. Această funcţie reprezintă un operand al cărui tip coincide cu tipul valori returnate de funcţia produs. 4.2. Operatori Operatorii pot fi unari sau binari în funcţie de numărul de operanzi cărora li se aplică. Un operator unar se aplică unui singur operand, iar un operator binar se aplică la doi operanzi. Operatorul binar se aplică la operandul care îl precede imediat şi la care îl urmează imediat. Operatorii limbajului C nu pot avea ca operanzi constante şir (şiruri de caractere). C are mai multe clase generale de operatori: 65
• 72. aritmetici, relaţionali şi logici, operatori pentru prelucrare biţi, precum şi câţiva operatori speciali pentru sarcini particulare. La scrierea unei expresii se pot utiliza operatori din toate clasele. La evaluarea unei astfel de expresii este necesar să se ţină seama de priorităţile operatorilor care aparţin diferitelor clase de operatori, de asociativitatea operatorilor de aceeaşi prioritate şi de regula conversiilor implicite. 4.2.1. Operatori aritmetici Lista operatorilor aritmetici este următoarea: + reprezintă operatorul plus unar sau binar, în funcţie de context - reprezintă operatorul minus unar sau binar, în funcţie de context * reprezintă operatorul de înmulţire (binar) / reprezintă operatorul de împărţire (binar) % reprezintă operatorul modulo (binar) Operandul operatorului unar plus trebuie să fie de tip aritmetic sau pointer, iar rezultatul este valoarea operandului. Un operand întreg presupune o promovare a întregilor. Operandul operatorului unar minus trebuie să fie de tip aritmetic, iar rezultatul este numărul negativ corespunzător. Un operand întreg presupune promovarea întregilor. Operanzii operatorilor * şi / trebuie să fie de tip aritmetic, iar ai lui % trebuie să fie de tip întreg. Operatorul binar / reprezintă câtul, iar % oferă restul împărţirii primului operand la al doilea. Dacă al doilea operand al operatorului / sau % este zero, rezultatul este nedefinit. Pentru operanzi de tip întreg este adevărată egalitatea: (a / b) * b + a % b = a În expresii operatorii binari + şi - au aceeaşi precedenţă, care însă este mai mică decât a grupului *, / şi %. Precedenţa ultimului grup este mai mică decât cea a operatorilor unari + şi -. Folosirea parantezelor în expresii poate schimba precedenţa între operatori în timpul evaluării acestora. Exemplu: Dacă a, b, c, d sunt variabile de tip int, atunci: - expresia d * b % a este echivalentă cu (d * b) % a; - expresia -a / d este echivalentă cu (-a) / d; - expresia a=b=c=d-15 este echivalentă cu a=(b=(c=(d -15))); - expresia a%-b*c este echivalentă cu (a%(-b))*c; 66
• 73. 4.2.2. Operatori de incrementare şi decrementare În C, operaţiile de forma i = i+1 şi j = j-1 pot fi programate folosind doi operatori unari specifici şi anume ++ pentru incrementare cu 1 şi -- pentru decrementare cu 1. Aceşti operatori pot fi folosiţi atât ca prefix pentru variabile (de exemplu, ++i, --j) sau ca sufix (i++, j--). Între aceste moduri de utilizare există diferenţe. Astfel, în expresia + +i, variabila i este incrementată înainte de a-i folosi valoarea, în timp ce în expresia i++, variabila i este incrementată după întrebuinţarea valorii acesteia. Exemplu: Considerăm secvenţa: x = 10; y = ++x; Dacă se afişează y, atunci vom găsi y = 11 deoarece mai întâi se incrementează x şi apoi se atribuie valoarea lui y. Dacă scriem: x = 10; y = x++; vom găsi y=10 (mai întâi se face atribuirea lui x la y şi apoi incrementarea lui x). Precedenţa tuturor operatorilor aritmetici este: Înaltă ++ -- + - (unari) * / % Scăzută + - (binari) Operatorii de aceeaşi precedenţă sunt evaluaţi de al stânga la dreapta. 4.2.3. Operatori relaţionali Operatorii relaţionali permit compararea a două valori şi luarea unei decizii după cum rezultatul comparării este adevărat sau fals. Dacă rezultatul operaţiei este fals, atunci valoarea returnată este zero, iar dacă este adevărat, valoarea returnată este 1. Operatorii relaţionali folosiţi în C sunt: 67
• 74. == egal != diferit < mai mic strict <= mai mic sau egal > mai mare strict >= mai mare sau egal Operatorii relaţionali au o precedenţă mai mică decât operatorii aritmetici, astfel o expresie de forma a < b + c este interpretată ca a<(b+c). 4.2.4. Operatori logici Operatorii logici binari && (ŞI, AND) şi || (SAU, OR) precum şi operatorul logic unar de negare “!“ (NOT), atunci când sunt aplicaţi unor expresii, conduc la valori întregi 0 şi 1, cu semnificaţia fals şi adevărat. Semantica acestor operatori se deduce din tabelul următor, unde e1 şi e2 sunt două expresii: e1 e2 e1&&e2 e1||e2 ! e1 zero zero 0 0 1 zero diferit de zero 0 1 1 diferit de zero zero 0 1 0 diferit de zero diferit de zero 1 1 0 Expresiile legate prin operatori logici binari sunt evaluate de la stânga la dreapta. Precedenţa operatorilor logici şi relaţionali este următoarea: Înaltă ! > >= < <= == != && Scăzută || Astfel, expresia: 10>5 && !(10<9) || 3<4 este adevarată; expresia: 1 && !0 || 1 este adevarată; expresia; 1 && ! (0 ||1) este falsă. Programul următor tipăreşte numerele pare cuprinse între 0 şi 100. # include <stdio.h> main () 68
• 75. { int i; for (i = 0; i <= 100; i++) if (! (i%2)) printf ("%d" , i); } Operatorii logici şi relaţionali sunt utilizaţi în formarea instrucţiunilor repetitive precum şi a instrucţiunii if. 4.2.5. Operatori logici la nivel de bit Ne reîntoarcem la cei trei operatori de tip booleean & (AND, §I), | (OR, SAU) şi ~ (NOT) precum şi la un al patrulea operator, denumit SAU-EXCLUSIV ^ (EXCLUSIVE-OR). Aceşti operatori se aplică la nivel de bit sau grupuri de biţi, după tabelele: AND x OR x NOT EXCLUSIVE-OR x & 0 1 | 0 1 ~ ^ 0 1 y 0 0 0 y 0 0 1 y 0 1 y 0 0 1 1 0 1 1 1 1 1 0 1 1 0 În C, aceşti operatori se aplică în paralel biţilor corespunzători aflaţi în orice poziţie. Din această cauză ei se mai numesc şi operatori logici pe bit. Trebuie făcută distincţia faţă de operatorii logici, care folosesc notaţii dublate: &&, ||, sau !. Operatorii logici au aceleaşi denumiri, dar ei tratează întregul operator ca pe o singură valoare, adevărată sau falsă. În scriere, se mai foloseşte şi denumirea bit-and, bit-or, bit-negate sau exclusive-or. Ca exemplu, considerăm operaţia bit-not. Fie numărul binar: N2 = 0000000000000111 = 0x0007 = 710 Negarea sa pe bit se realizează cu instrucţiunea ~0x7 sau ~07 sau ~7 şi valoarea sa va fi ~N2 = 1111111111111000 = 0xFFF8 sau 0177770 pe un computer cu întreg pe 16 biţi sau 0xFFFFFFF8 pe un computer cu întreg pe 32 de biţi. Exemplul următor realizează un SAU şi un ŞI pentru două caractere: ‘a’ | ’c’ = 0110 0001 | 0110 0011 = 0110 0011 = ‘c’ ‘a’ & ’c’ = 0110 0001 & 0110 0011 = 0110 0010 = ‘a’ 69
• 76. Deoarece limbajul C a fost gândit să înlocuiască limbajul de asamblare în majoritatea operaţiilor de programare, acesta trebuie să aibă capacitatea să suporte toţi (sau cel puţin mulţi) operatorii utilizaţi în asamblare. Operatorii pentru prelucrarea biţilor se aplică biţilor dintr-un byte sau cuvânt, ambele variabile de tip char şi short int. Aceşti operatori nu se aplică tipurilor float, double, long double, void sau altor tipuri mai complexe. Operatorii pentru prelucrarea biţilor utilizaţi în C sunt: & AND | OR ^ exclusive OR (XOR) ~ complement faţă de unu (NOT) >> deplasare dreapta << deplasare stânga Operaţiile pe biţi sunt utilizate de obicei în drivere, pentru testarea şi mascarea anumitor biţi. De exemplu, operaţia AND poate fi folosită pentru ştergerea unor biţi dintr-un byte sau dintr-un cuvânt, OR poate fi folosită pentru setarea unor biţi, iar XOR pentru complementarea unor biţi şi testarea egalităţii a 2 bytes. Observaţie: Operatorii relaţionali şi logici (&&, ||, !,...) produc totdeauna un rezultat care este fie 0, fie 1, pe când operatorii similari destinaţi prelucrării biţilor pot produce orice valoare arbitrară, în concordanţă cu operaţia specifică. Operatorii >> şi << deplasează toţi biţii dintr-o variabilă la dreapta, respectiv la stânga. Forma generală a operaţiilor de deplasare este: variabilă >> număr_de_poziţii_bit - pentru deplasare dreapta. variabilă << număr_de_poziţii_bit - pentru deplasare stânga. În poziţiile rămase libere, după deplasare, se introduc zerouri. Operaţiile de deplasare pot fi utile când se decodifică perifericele de intrare cum ar fi convertoarele D/A (digital/analogice) şi când se citesc informaţii de stare. Operatorii de deplasare se pot utiliza şi pentru realizarea cu rapiditate a operaţiilor de înmulţire şi împărţire. Se ştie că o deplasare stânga cu 1 bit realizează înmulţirea cu 2, iar o deplasare dreapta cu 1 bit realizează o împărţire cu 2. Exemplu: 70
• 77. x = 7; 0 0 0 0 0 1 1 1 7 x << 1; 0 0 0 0 1 1 1 0 14 x << 3; 0 1 1 1 0 0 0 0 112 x << 2; 1 1 0 0 0 0 0 0 192 x >> 1; 0 1 1 0 0 0 0 0 96 x >> 2; 0 0 0 1 1 0 0 0 24 Următorul exemplu evidenţiază efectul operatorilor de deplasare: # include <stdio.h> void disp_binary(); /* prototipul functiei disp_binary() */ void main() { int i = 1, t; for (t=0;t<8;t++) { disp_binary(i); i=i<<1;} printf (" n"); for (t=0;t<8;t++) { i=i>>1; disp_binary(i);}} void disp_binary(int i) /* se defineste functia disp_binary() */ /* care afiseaza bitii dintr-un byte */ {register int t; for (t=128;t>0;t=t/2) if (i&t) printf("1"); else printf("0"); printf("n");} Programul produce următoarea ieşire: 0 0 0 0 0 0 0 1 0 0 0 0 0 0 1 0 . . . . . . . . . . . . 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 . . . . . . . . . . . . 0 0 0 0 0 0 0 1 Deşi limbajul C nu conţine un operator de rotire, se poate realiza o funcţie care să efectueze această operaţie. De exemplu rotirea la stânga cu o poziţie a numărului 10101010 ne conduce la numărul 01010101 şi se realizează după schema: 71
• 78. 1 0 1 0 1 0 1 0 0 1 0 1 0 1 0 1 O posibilitate de realizare a operaţiei de rotire necesită utilizarea unei uniuni cu două tipuri de date diferite. De exemplu utilizând uniunea: union rotate { char ch[1]; unsigned int i; } rot; Următoarea funcţie realizează o rotire cu 1 bit. void rotate_bit(union rotate *rot) {rot->ch[1]=0; rot->i=rot->i<<1; if (rot->ch[1]) rot->i=rot->i|1;} Atât întregul i cât şi cele două caractere ch[0] şi ch[1] partajează primii doi octeţi din cei 4 rezervaţi de uniune. Numărul de rotit se introduce (pe 8 biţi) în ch[0]. Se roteşte apoi întregul i (deci se rotesc toţi cei 4 octeţi care îi corespund). Se testează MSB al lui ch[0] care se găseşte în urma rotirii în poziţia LSB din ch[1]. Dacă este 1, atunci se setează la 1 LSB din ch[0], realizându-se astfel operaţia de rotaţie. Un exemplu de program care să utilizeaze această funcţie: # include <stdio.h> union rotate { char ch[1]; unsigned int i; } rot; void disp_binary(); void rotate_bit(); void main() { register int t; rot.ch[0]=147; for (t=0;t<7;t++) { disp_binary(rot.i); rotate_bit(&rot);}} /* se defineste functia rotate_bit() */ void rotate_bit(union rotate *rot) {rot->ch[1]=0; rot->i=rot->i<<1; if (rot->ch[1]) rot->i=rot->i|1;} 72
• 79. /* se defineste functia disp_binary() */ void disp_binary(int i) {register int t; for (t=128;t>0;t=t/2) if (i&t) printf("1"); else printf("0"); printf("n");} Acest program realizează rotirea numărului 14710=100100112 cu 6 poziţii 10010011 00100111 01001110 10011100 00111001 01110010 11100100 Programul de mai sus funcţionează pentru numere reprezentabile pe un octet (mai mici de 255). Dacă dorim să facem o rotire pe doi octeţi, atunci se poate modifica programul de mai sus după cum urmează: # include <stdio.h> union rotate { char ch[3]; unsigned int i; } rot; void disp_binary(); void rotate_bit(); void main() { register int t; rot.i=17843; for (t=0;t<7;t++) { disp_binary(rot.i); rotate_bit(&rot);}} /* se defineste functia rotate_bit() */ void rotate_bit(union rotate *rot) {rot->ch[2]=0; rot->i=rot->i<<1; if (rot->ch[2]) rot->i=rot->i|1;} /* se defineste functia disp_binary() */ void disp_binary(int i) {register int t; for (t=32768;t>0;t=t/2) if (i&t) printf("1"); else printf("0"); printf("n");} 73
• 80. Operatorul " ~ " realizează complementul faţă de 1. O utilizare interesantă a complementului faţă de 1 este aceea că ne permite să vedem setul caracterelor extinse implementate în calculator: # include <stdio.h> # include <conio.h> void main() {char ch; do {ch = getch(); printf ("%c %cn", ch, ~ch);} while (ch != 'q');} 4.2.6. Operatorul de atribuire În C, operatorul de atribuire (asignare) este semnul egal (=). Valoarea expresiei din dreapta se atribuie variabilei din stânga operatorului "=". În C, forma: suma = a + b + c; trebuie privită ca o nouă expresie, numită expresie de asignare. Valoarea ei este chiar valoarea expresiei din dreapta operatorului de atribuire. Dacă într-o expresie se fac mai multe atribuiri, atunci evaluarea se face de la dreapta la stânga: x = y = z = 0 este echivalentă cu (x=(y=(z=0))); O expresie de atribuire de forma x = x + 5 în care variabila din stânga apare imediat după operatorul = se poate scrie într-o formă compactă de tipul x += 5, unde operatorul += este tot un operator de atribuire. Majorităţii operatorilor binari le corespund operatori de atribuire de forma "op = " unde op poate fi : +, -, *, %, <<, >>, &, ^ , Operatorii de atribuire (asignare) sunt: = , += , -= , *= , /= , %= , <<= , >>= , &= , ^= , |= Deci, o expresie de asignare de forma : var = (var) op (expr) unde var este o variabilă şi expr este o expresie, admite o reprezentare compactă de forma: var op= expresie Într-o formă compactă, ca mai sus, var este evaluată o singură dată. 4.2.7. Operatorul sizeof Operatorul sizeof returnează numărul de octeţi necesar memorării variabilei sau tipului datei care este operandul său. Dacă 74
• 81. sizeof operează asupra unui tip de date, atunci tipul trebuie să apară între paranteze. De exemplu, sizeof(char) va fi 1, sizeof(int) va fi 4 etc., deci rezultatul este un număr întreg, fără semn. Fişierul standard stddef.h defineşte tipul size_t al rezultatului oferit de operatorul sizeof. Dacă sizeof se aplică unui tablou, rezultatul este numărul total de octeţi din tablou. Exemplul din programul următor prezintă dimensiunea principalelor tipuri de date: # include <stdio.h> # include <stddef.h> void main(){ printf("nTip caracter pe %d octet",sizeof(char)); printf("nTip short int pe %d octeti",sizeof(short int)); printf("nTip int pe %d octeti",sizeof(int)); printf("nTip long int pe %d octeti",sizeof(long int)); printf("nTip float pe %d octeti",sizeof(float)); printf("nTip double pe %d octeti",sizeof(double)); printf("nTip long double pe %d octetin", sizeof(long double));} În urma execuţiei acestui program, se va afişa (rezultatele depind de tipul de procesor sau de compilator): Tip caracter pe 1 octet Tip short int pe 2 octeti Tip int pe 4 octeti Tip long int pe 4 octeti Tip float pe 4 octeti Tip double pe 8 octeti Tip long double pe 8 octeti 4.2.8. Operatorul ternar ? Operatorul " ? " poate fi utilizat pentru a înlocui instrucţiunea if / else având forma: if (conditie) expresie1 else expresie2 Operatorul ternar " ? " necesită trei operanzi şi are forma generală: Expr1 ? Expr2 : Expr3 unde Expr1, Expr2 şi Expr3 sunt expresii. Se evaluează expresia Expr1. Dacă este adevărată, se evaluează Expr2, care devine valoarea întregii expresii. Dacă Expr1 este falsă, se 75
• 82. evaluează Expr3, iar valoarea acesteia devine valoarea întregii expresii: Exemplu: x = 10; y = x > 9 ? 100 : 200; Cum 10 > 9, valoarea lui y va fi 100. Dacă x ar fi mai mic decât 9, y va primi valoarea 200. Acelaşi program scris cu if /else va fi: x = 10; if (x > 9) y = 100; else y = 200; În alcătuirea expresiilor din declaraţia operatorului ternar " ? " pot fi folosite şi funcţii: Exemplu: # include <stdio.h> f1(); f2(); // prototipurile functiilor f1() si f2() void main() { int t; printf (": "); scanf("%d",&t); // se introduce numarul intreg t t?f1()+f2(t): printf(" S-a introdus zeron");} f1() {printf ("S-a introdus "); } f2(int n) {printf ("%dn", n);} Dacă se introduce zero, atunci va fi apelată printf() şi va afişa " S-a introdus zero". Dacă se introduce alt număr, atunci programul va executa atât funcţia f1(), cât şi funcţia f2(). 4.2.9. Operatorul virgulă Operatorul virgulă se utilizează într-un şir în care se introduc mai multe expresii. Astfel, instrucţiunea: x = (y = 3, y+1), are că efect atribuirea valorii 4 variabilei x. Deci expresiile separate prin virgulă sunt evaluate de la stânga la dreapta, prima expresie evaluată căpătând valoarea void. Dacă se utilizează un operator de atribuire, valoarea atribuită variabilei din stânga operatorului de atribuire este valoarea ultimei expresii din dreapta, după evaluare. Exemplu: y = 10; x = (y = y - 5, 30 / y); Variabila x va căpăta valoarea 6. 76
• 83. Observaţie Deoarece operatorul virgulă are o precedenţă mai mică decât operatorul de atribuire, pentru ca atribuirile să se facă corect, trebuie utilizate paranteze. 4.2.10. Operatorul de forţare a tipului sau de conversie explicită (expresie cast) Adesea se doreşte specificarea conversiei valorii unui operand spre un tip dat. Acest lucru este posibil folosind o construcţie de forma: (tip) operand Printr-o astfel de construcţie valoarea operandului se converteşte spre tipul indicat în paranteze. În construcţia de mai sus (tip) se consideră că este un operator unar. Acest operator este cunoscut sub numele de operator de forţare a tipului sau de conversie explicită. De cele mai multe ori însă este utilizată denumirea engleză a operatorului şi anume expresie cast. Exemplu: Presupunem că o funcţie oarecare f are un parametru de tip double. Pentru ca această funcţie să poată fi apelată cu un parametru int n (n este un parametru de tip întreg) acesta trebuie mai întâi convertit la tipul double. Acest lucru se poate realiza printr-o atribuire: double x f(x=n) Un alt mod mai simplu de conversie a parametrului întreg spre tipul double este utilizarea unei expresii cast: f((double)n) Operatorul de forţare a tipului fiind unar, are aceeaşi prioritate ca şi ceilalţi operatori unari ai limbajului C. 4.2.11. Operatorii paranteză Parantezele rotunde se utilizează fie pentru a include o expresie, fie la apelul funcţiilor. O expresie inclusă în paranteze rotunde formează un operand. În acest mod se poate impune o altă ordine în efectuarea operaţiilor, decât cea care rezultă din prioritatea şi asociativitatea operatorilor. Operanzii obţinuţi prin includerea unei expresii între paranteze impun anumite limite asupra operatorilor. De exemplu, la un astfel de operand nu se pot aplica operanzii de incrementare şi decrementare sau operatorul adresă. Astfel construcţiile: (a-5+b)++ --(a+b) &(a*b) sunt eronate. 77
• 84. La apelul unei funcţii, lista parametrilor efectivi se include între paranteze rotunde. În acest caz se obişnuieşte să se spună că parantezele rotunde sunt operatori de apel de funcţie. Parantezele pătrate include expresii care reprezintă indici. Ele se numesc operatori de indexare. Parantezele sunt operatori de prioritate maximă. Operatorii unari au prioritatea imediat mai mică decât parantezele. 4.2.12. Operatorul adresă Operatorul adresă este unar şi se notează prin caracterul &. El se aplică pentru a determina adresa de început a zonei de memorie alocată unei date. În forma cea mai simplă, acest operator se utilizează în construcţii de forma: &nume unde nume este numele unei variabile simple sau al unei structuri. În cazul în care nume este numele unui tablou, atunci acesta are ca valoare chiar adresa de început a zonei de memorie alocată tabloului respectiv şi, în acest caz, nu se mai utilizează operatorul adresă &. 4.2.13. Alţi operatori ai limbajului C În limbajul C se mai utilizeză şi operatorii: „ * ” , „ . ” şi „->” Operatorul „ * ” unar (a nu se confunda cu operatorul aritmetic binar de înmulţire) se utilizează pentru a face acces la conţinutul unei zone de memorie definită prin adresa ei de început. Se obişnuieşte să se spună că operatorul de adresă & este operator de referenţiere, iar operatorul „ * ” este operator de dereferenţiere. Operatorii „ . ” şi „ -> ” se utilizează pentru a se accesa componentele unei structuri. Ei au prioritate maximă, având aceeaşi prioritate cu parantezele. 4.2.14. Regula conversiilor implicite şi precedenţa operatorilor Regula conversiilor implicite se aplică la evaluarea expresiilor. Ea acţionează atunci când un operator binar se aplică la doi operanzi de tipuri diferite. În acest caz, operandul de tip inferior se converteşte spre tipul superior al celuilalt operand şi rezultatul este de tip superior. Înainte de toate se convertesc operanzii de tip char şi enum în tipul int. Dacă operatorul curent se aplică la operanzi de acelaşi tip, atunci se execută operatorul respectiv, iar tipul rezultatului coincide cu tipul comun al operanzilor. Dacă rezultatul aplicării operatorului 78
• 85. reprezintă o valoare în afara limitelor tipului respectiv, atunci rezultatul este eronat (are loc o „depăşire”). Exemplu: Rezultatul împărţiirii 7/3 este 2 şi nu 2.5 deoarece cei doi operanzi sunt de tip întreg şi prin urmare rezultatul (care este de tip real) este şi el convertit la tipul întreg. Dacă operatorul binar se aplică la operanzi de tipuri diferite, atunci se face o conversie înainte de execuţia operatorului, conform algoritmului umător: 1. Dacă unul din operanzi este de tip long double, atunci celălalt operand se converteşte spre tipul long double iar tipul rezultatului aplicării operatorului este de asemenea de tip long double. 2. Altfel, dacă unul din operanzi este de tip double atunci celălalt operand se converteşte spre tipul double iar tipul rezultatului aplicării operatorului este de asemenea de tip double. 3. Altfel, dacă unul din operanzi este de tip float atunci celălalt operand se converteşte spre tipul float iar tipul rezultatului aplicării operatorului este de asemenea de tip float. 4. Altfel, dacă unul din operanzi este de tip unsigned long atunci celălalt operand se converteşte spre tipul unsigned long iar tipul rezultatului aplicării operatorului este de asemenea de tip unsigned long. 5. Altfel, dacă unul din operanzi este de tip long atunci celălalt operand se converteşte spre tipul long iar tipul rezultatului aplicării operatorului este de asemenea de tip long. 6. Altfel, unul din operanzi trebuie sa fie de tip unsigned, celălalt de tip int şi acesta se converteşte spre tipul unsigned, iar tipul rezultatului aplicării operatorului este de tip unsigned. Precedenţele operatorilor C sunt prezentate în tabelul următor. Operatorii aflaţi pe aceeaşi linie au aceeaşi prioritate. Ei se asociază de la stânga la dreapta, exceptând operatorii unari, condiţionali şi de atribuire, care se asociază de la dreapta la stânga. Precedenţa Operatorul 79
• 86. Înaltă () [ ] -> . ! ~ ++ -- - (type) * & sizeof * / % + - << >> < <= > >= == != & ^ | && || ?: Scăzută = += -= *= /= , Capitolul V INSTRUCŢIUNI Limbajul C posedă un set variat de instrucţiuni, set care îi permite să realizeze principalele compuneri de operaţii: secvenţierea, repetiţia cu test final, repetiţia cu test iniţial, repetiţia cu număr cunoscut de paşi, decizia şi selecţia, saltul necondiţionat, ieşirea prematură dintr-un ciclu. Instrucţiunile pot fi clasificate în: instrucţiuni etichetate, instrucţiuni expresie, instrucţiuni compuse, instrucţiuni de selecţie, instrucţiuni repetitive, instrucţiuni de salt. 5.1. Instrucţiuni etichetate (instrucţiunea goto) Instrucţiunile etichetate posedă etichete ca prefixe şi au forma: etichetă: instrucţiune 80
• 87. Eticheta formată dintr-un identificator defineşte identificatorul ca destinaţie pentru o instrucţiune de salt, singura utilizare a sa fiind ca destinaţie a unei instrucţiuni goto. Etichetele nu pot fi redeclarate. Etichetele sunt locale în corpul funcţiei în care sunt definite. Instrucţiunea goto are următorul format: goto etichetă La întâlnirea instrucţiunii goto, se realizează un salt la instrucţiunea prefixată de eticheta aflată după instrucţiunea goto. Deoarece o etichetă este locală în corpul unei funcţii rezultă că ea este nedefinită în afara corpului funcţiei respective, deci, o instrucţiune goto nu poate face salt la o instrucţiune din afara corpului funcţiei în care este definită. Nu este recomandată utilizarea abuzivă a acestei instrucţiuni deoarece programul devine mai puţin lizibil şi pot apare erori logice în program foarte greu de detectat. Instrucţiunea goto se utilizează în special pentru ieşirea din mai multe cicluri imbricate. Exemplu: Următorul program utilizează instrucţiunea goto pentru a afişa numerele de la 1 la 100: #include <stdio.h> void main(void) { int nr=1; eticheta: printf(”%d”, nr++); if (nr<=100) goto eticheta; } 5.2. Instrucţiuni expresie Cele mai multe instrucţiuni sunt instrucţiunile expresie, care au forma: [ expresie ]; unde expresie este opţională. Majoritatea instrucţiunilor expresie sunt atribuiri sau apeluri de funcţii. Deci, o instrucţiune expresie constă dintr-o expresie urmată de caracterul ";". Exemplu: a = b * c + 3; printf ("FAC. DE AUTOMATICA"); 81
• 88. Dacă expresia expresie de mai sus lipseşte, construcţia “;“ se numeşte instrucţiune vidă. Aceasta nu are nici un efect, dar este utilizată pentru a înlocui un corp vid al unei iteraţii sau pentru a plasa o etichetă. 5.3. Instrucţiuni compuse O instrucţiune compusă este o posibilă listă de declaraţii şi/sau instrucţiuni închise între acolade. Exemplu: { a = b + 2; b++; } O instrucţiune compusă se numeşte bloc. Un bloc ne permite să tratăm mai multe instrucţiuni ca pe una singură. Corpul unei funcţii este o instrucţiune compusă. Domeniul de vizibilitate al unui identificator declarat într-un bloc se întinde din punctul declaraţiei până la sfârşitul blocului. Identificatorii utilizaţi într-un bloc pot fi ascunşi prin declaraţii de acelaşi nume în blocurile interioare blocului iniţial. Exemplu: # include <stdio.h> int x = 34; /* x este global */ void main(void) { int *p = &x; /*p preia adresa variabilei globale*/ int x1, x2; printf("x = %dn", x); {int x; /*x este local si il ascunde pe cel global */ x = 1; /* atribuirea se face la cel local */ x1 = x; printf("x = %dn", x1);} { int x; /* se ascunde prima variabila locala */ x = 2; /* se atribuie valoarea 2 acestui x */ x2 = x; printf("x = %dn",x2); } printf("x = %d %d %d n",x,x1,x2); } 5.4. Instrucţiuni de selecţie 5.4.1. Instrucţiunea if O instrucţiune if cu care în C se implementează o structură de control de selecţie sau o structură alternativă, are următorul format general: if (conditie) instructiune1; else instructiune2; 82
• 89. unde conditie este orice expresie care prin evaluare conduce la o valoare întreagă. Dacă valoarea expresiei este diferită de zero (condiţie adevărată), atunci se execută instructiune1; altfel, dacă valoarea expresiei este zero (condiţie falsă), se execută instructiune2. În ambele cazuri, după executarea lui instructiune1 sau instructiune2, controlul este transferat la instrucţiunea ce urmează după if. Aici, prin instructiune1 sau instructiune2 se înţelege o instrucţiune simplă, o instrucţiune compusă (un bloc) sau o instrucţiune vidă. Porţiunea else instructiune2; este opţională, în acest fel putându- se obţine o structură de selecţie cu o ramură vidă de forma: if (conditie) instructiune; Exemplu: Următorul program citeşte două numere şi afişează pe cel mai mare dintre ele. # include <stdio.h> void main (void) { int x, y; printf("Introduceti doua numere intregi: n"); scanf ("%d %d", &x, &y); if (x > y) printf ("Cel mai mare este : %dn",x); else printf("Cel mai mare este : %dn", y); } Deoarece partea else dintr-o instrucţiune if este opţională, apare o ambiguitate atunci când else este omis dintr-un if inclus (încuibat). În C acest lucru se rezolvă prin asocierea lui else cu cel mai apropiat if. De exemplu, în secvenţa: if (x) if (y) printf ("1"); else printf ("2"); else este asociat cu instrucţiunea if(y). Dacă dorim ca else să fie asociat cu if(x) trebuie să utilizăm acolade, astfel: if (x) { if (y) printf ("1"); } else printf ("2"); Secvenţa anterioară este echivalentă cu: if (x) { if (y) printf ("1"); else ;} else printf ("2"); 83
• 90. 5.4.2. Instrucţiuni de selecţie multiplă: if - else if Într-o instrucţiune if se poate include, pe o ramură, o altă instrucţiune if. În acest fel se creează posibilitatea de a codifica structuri de selecţie multiplă, folosindu-se perechi else if. O asemenea construcţie este de forma: if (conditie1) instructiune1; else if (conditie2) instructiune2; else if (conditie3) instructiune3; . . . . . . . . . . . . . . . . else if (conditieN) instructiuneN; else instructiuneN+1; În acest caz, condiţiile sunt testate în ordine. Dacă una din ele este adevărată, atunci este executată instrucţiunea corespunzătoare, după care controlul este transferat la instrucţiunea următoare din program. Codul pentru fiecare alternativă poate fi format dintr-o instrucţiune simplă (inclusiv instrucţiunea vidă) sau dintr-un bloc delimitat prin { şi }. Dacă nici una dintre expresii nu este adevărată, atunci se execută secvenţa corespunzătoare ultimei alternative introdusă prin else. Această ultimă alternativă nu este obligatorie, structura putându-se încheia după secvenţa notată cu instructiuneN. Exemplu: Considerăm un program care realizează conversiile inch-cm şi cm-inch. Presupunem că indicăm unitatea intrării cu i pentru inch şi c pentru centimetru: # include <stdio.h> # include <conio.h> void main(void) { const float fact = 2.54; float x,in,cm; char ch = 0; printf ("nIntroduceti numarul: n"); scanf("%f",&x); printf("nIntroduceti unitatea: n"); ch = getche(); /* se introduce un caracter de la tastatura care se afiseaza pe ecran */ if (ch == 'i') { in = x; cm = x * fact;} else if(ch == 'c') { 84
• 91. in = x/fact; cm = x; } else in = cm = 0; printf("n%5.2f in = %5.2f cm n",in,cm); } 5.4.3. Instrucţiunea switch Într-o instrucţiune de selecţie switch, se compară, pe rând, o valoare cu constantele dintr-o mulţime şi în momentul găsirii unei coincidenţe se execută instrucţiunea sau blocul de instrucţiuni asociate acelei constante. Forma generală a instrucţiunii switch este: switch (variabila) { case constanta1 : secventa_instructiuni_1 break; case constanta2 : secventa_instructiuni_2 break; case constanta3 : secventa_instructiuni_3 break; . . . . . . . . . . . . . . . . . . . case constantaN : secventa_instructiuni_N break; default : secventa_instructiuni_N+1 } Instrucţiunea switch realizează transferul controlului la una din secvenţele de instrucţiuni dacă valoarea variabila ce trebuie să aibă tipul întreg coincide cu una din constantele de dupa case. Secvenţa de instrucţiuni se execută pâna se întâlneşte break, după care se trece la instrucţiunea imediat următoare după switch. Dacă nu se găseşte nici o coincidenţă, se execută secvenţa de instrucţiuni de după default, iar dacă default lipseşte, deoarece prezenţa acesteia este opţională, se trece la instrucţiunea următoare. Exemplu: Decizia din exemplul anterior poate fi realizată şi astfel: # include <stdio.h> # include <conio.h> void main(void) { const float fact = 2.54; float x, in, cm; char ch = 0; printf ("nIntroduceti numarul: n"); scanf("%f", &x); 85
• 92. printf("nIntroduceti unitatea: n"); ch = getche(); switch(ch) { case 'i': in = x; cm = x * fact; break; case 'c': in = x/fact; cm = x; break; default: in = cm = 0; break; } printf("n%5.2f in = %5.2f cm n",in,cm); } Observaţie: Constantele case trebuie sa fie distincte. Pentru a ieşi din instrucţiunea switch se foloseşte instrucţiunea break. Exemplu: # include <stdio.h> void main (void) { int t; for (t = 0; t < 10; t++) switch (t) { case 1 : printf ("Now"); break; case 2 : printf (" is "); break; case 3 : case 4 : printf (" the "); printf (" time for all good men n"); break; case 5 : case 6 : printf (" to "); break; case 7 : case 8 : case 9 : printf (" . "); break; } } Rulând acest program, vom obţine: Now is the time for all good men the time for all good men to to . . . Instrucţiunea switch este foarte eficientă în scrierea programelor care afişează pe ecran o listă de opţiuni (un meniu) din 86
• 93. care utilizatorul alege câte una şi o execută. Instrucţiunile switch pot fi şi incluse (încuibate) una în alta. 5.5. Instrucţiuni repetitive 5.5.1. Instrucţiunea for Forma generală a instrucţiunii for este: for (initializare; conditie; incrementare) instructiune; unde: - initializare este o instrucţiune de atribuire utilizată pentru iniţializarea variabilei de control a ciclului. Nu există nici o restricţie privitoare la tipul său; - condiţie este o expresie relaţională care se testează înaintea fiecărei iteraţii: dacă condiţia este adevărată (diferită de 0), ciclul se continuă; dacă condiţia este falsă (egală cu 0), instrucţiunea for se încheie; - incrementare se evaluează după fiecare iteraţie specificând astfel reiniţializarea ciclului. Exemplu: Următorul program afişează pe ecran numerele de la 1 la 100. # include <stdio.h> void main (void) { int x; for (x = 1; x <= 100; x++) printf("%d ", x); } Nu întotdeauna ciclul for trebuie să se desfăşoare în sensul creşterii variabilei de control. Putem crea cicluri for în care variabila de control se decrementează. Exemplu: Programul următor afişează numerele de la 100 la 1. # include <stdio.h> void main (void) { int x; for (x =100; x > 0; x--) printf("%d", x); } Nu există restricţii în incrementarea sau decrementarea variabilei de control a ciclului. Exemplu: Următorul program afişează pe ecran numerele de la 0 la 100 din 5 în 5: # include <stdio.h> void main (void) { int x; for (x = 0; x <= 100; x = x + 5) printf ("%d", x); } 87
• 94. Instructiunea instrucţiune din declaraţia ciclului for poate fi o instrucţiune simplă sau un bloc (un grup de instrucţiuni delimitate de acolade) care va fi executat repetitiv. Exemplu: Programul următor afişează pe ecran numerele de la 0 la 99, precum şi pătratul acestora: # include <stdio.h> void main (void) { int i; for (i = 0; i < 100; i++) { printf (" Acesta este i : %3d", i); printf (" si i patrat : %5d n", i*i); } } Exemplu: Calculul factorialului unui număr: n! = 123...n # include <stdio.h> void main (void) { int n, i; long int factorial; printf ("Introduceti n : "); scanf ("%d", &n); factorial = 1; for (i = 1; i <= n; i++) factorial *= i; printf (" %d ! = %ld n", n, factorial); } Instrucţiunile for pot fi incluse una în alta. Exemplu: Programul următor parcurge un şir de caractere de la stânga la dreapta, afişând subşirurile ce au ca bază primul caracter. # include <stdio.h> # include <string.h> void main (void) { int l, n, i; char sir[81]; puts ("Tastati un sir terminat cu <CR> : "); gets (sir); l = strlen (sir); for (n = 0; n <= l; ++n) { for (i = 0; i < n; ++i) putchar (sir[i]); putchar ('n'); } } Variante ale ciclului for: Limbajul C permite mai multe variante ale ciclului for care determină creşterea flexibilităţii acestuia în diferite situaţii. Una din cele mai utilizate variante constă în folosirea a mai multe variabile de control a ciclului. În exemplul următor, atât x cât şi y sunt variabile de control a ciclului: 88
• 95. # include <stdio.h> void main (void) { int x, y; for (x = 0, y = 0; x + y < l00; x++, y++) printf ("%d ", x + y); } Acest program tipăreşte numerele de la 0 la 98 din 2 în 2. Se observă că iniţializările şi incrementările celor două variabile sunt separate prin virgulă. Terminarea ciclului presupune testarea nu numai a variabilei de control cu anumite valori prestabilite, ci condiţia de terminare a ciclului poate fi orice expresie C corectă. Exemplu: Considerăm un program pentru antrenarea unui copil în exerciţiile de adunare. Dacă copilul vrea să se oprească se apesa tasta T, atunci când calculatorul îl întreabă dacă să continue. # include <stdio.h> # include <conio.h> void main (void) { int i, j, raspuns; char terminare = ' '; for (i=1; i<100; i++) { for (j=1; j<100 && terminare !='t';j++) { printf ("nCit este %d + %d ? ",i,j); scanf("%d",&raspuns); if (raspuns != i+j) printf("nGresit !"); else printf("nCorect !"); printf(" Continuam ? "); terminare = getchar(); } /* Pentru terminare se apasa tasta t */ } } O altă caracteristică interesantă a ciclului for este aceea că nu se impune definirea tuturor celor trei parametri ai ciclului for, oricare dintre ei putând fi opţionali. De exemplu, ciclul următor se va executa până când de la tastatura se introduce numărul 123: for (x = 0; x != 123;) scanf("%d", &x); Deoarece instrucţiunea de incrementare a lui x lipseşte, de fiecare dată când ciclul se repetă, programul testează ca x să fie egal cu 123, dar nu modifică pe x în nici un fel. Dacă de la tastatură se introduce 123, condiţia buclei devine falsă şi ciclul se termină. Exemplu: O variantă de calcul a lui n! ar fi următoarea: # include <stdio.h> void main (void) { int n, i; long int factorial; 89
• 96. printf ("Introduceti n : "); scanf ("%d", &n); factorial = 1; for (i = 1; i <= n;) { factorial *= i ++; printf (" %d ! = %ld n", n, factorial); } } Bucle infinite: Una din cele mai interesante utilizări ale ciclului for constă în crearea de bucle infinite. Dacă nici una din cele trei expresii care formează ciclul for nu sunt precizate, se obţine o buclă fără sfârşit, ca în exemplul următor în care se consideră că elementul condiţie are valoarea adevărat: for (;;) printf ("Aceasta bucla va rula la nesfirsit. n "); Ieşirea dintr-o bucla for: Pentru terminarea unei bucle for, chiar şi a buclei for(; ;) se foloseşte instrucţiunea break care se plasează oriunde în corpul ciclului şi determină încheierea imediată a ciclului (la întâlnirea acesteia), programul continuându-se cu instrucţiunea ce urmează după instrucţiunea for. Exemplu: Acest program va rula până când de la tastatura se apasă tasta A: # include <stdio.h> # include <ctype.h> void main (void) { for (; ;) { ch = getche (); if (ch == 'a') break; } printf (" Ai apasat tasta A "); } Utilizarea ciclurilor for fără corp (instrucţiune) : Pentru crearea unor întârzieri de timp se pot folosi cicluri for cu corp vid de forma: for (t = 0; t < O_ANUMITA_VALOARE; t++); Observaţie: De obicei, instrucţiunea for este legată de parcurgerea unor structuri de date de tip tablou. 5.5.2. Instrucţiunea while Forma generală a instrucţiunii repetitive while este: while (conditie) instructiune; unde instructiune poate fi o instrucţiune vidă, o instrucţiune simplă sau un bloc de instrucţiuni ce vor fi executate repetitiv. În timpul 90
• 97. execuţiei se evaluează mai întâi condiţia buclei a cărei valoare trebuie să fie întreagă. Dacă valoarea calculată este diferită de 0 (condiţie adevărată), atunci instructiune se execută. Dacă, după o evaluare (inclusiv prima) rezultă o valoare 0 (condiţie falsă), atunci controlul este transferat la instrucţiunea ce urmează după while. Astfel, instrucţiunea asociată cu while se execută repetat, cât timp valoarea asociată condiţiei este diferită de 0 sau condiţia este adevărată. Exemplu: Programul următor calculează c.m.m.d.c. pentru o pereche x, y de numere întregi pozitive. # include <stdio.h> void main (void) { int xi, yi, x, y; printf (" Introduceti doua numere pozitive: n"); scanf ("%d %d", &xi, &yi); x = xi; y = yi; while (x != y) if (x > y) x -= y; else y -= x; printf (" C.m.m.d.c. (%d, %d) = %d", xi, yi, x); } Metoda de calcul se bazează pe faptul că: ♦ daca x > y, atunci cmmdc (x, y) = cmmdc (x-y, x); ♦ daca x < y, atunci cmmdc (x, y) = cmmdc (x, y-x); ♦ daca x = y, atunci cmmdc (x, y) = x =y . De exemplu, cmmdc (14, 21) = 7. Deoarece instrucţiunea while realizează testarea condiţiei la începutul instrucţiunii, aceasta instrucţiune este bună de utilizat în situaţiile în care nu se doreşte execuţia buclei, evident dacă condiţia nu este adevărată. Exemplu: Programul următor realizează centrarea unui text pe ecran: # include <stdio.h> # include <ctype.h> void main (void) { char sir[255]; printf(" Introduceti un sir de caractere: n"); gets (sir); centreaza (strlen (sir)); printf (sir); } /* Se calculează numărul de spaţii pentru centrarea unui şir de caractere cu lungimea lung */ centreaza (lung) int lung; { lung = (80 - lung)/2; 91
• 98. while (lung > 0) { printf (" "); lung--; } } Dacă dorim să programăm un ciclu infinit, atunci se poate găsi o expresie care ramâne tot timpul adevărată. Un exemplu uzual este următorul: while (1) { Corpul ciclului } Ieşirea din ciclu, în acest caz, se asigură prin mecanisme de tip break, goto sau return. Corpul ciclului while poate conţine şi numai instrucţiunea vidă. De exemplu, while ((ch = getche ()) != 'A'); este o buclă simplă care se execută până când de la tastatură se va introduce caracterul "A". Observaţie: Instrucţiunea while reprezintă mecanismul sintactic de bază pentru a programa cicluri în C. Reamintim că instrucţiunea for se foloseşte după următorul format general: for (initializare; conditie; incrementare) instructiune; care este echivalentă semantic cu secvenţa: initializare; while (conditie) { instructiune; incrementare; } 5.5.3. Instrucţiunea do-while Spre deosebire de ciclurile programate cu while sau for, unde condiţia de ciclare este verificată la început, în cazul folosisii mecanismului do-while, condiţia se evaluează după execuţia secvenţei de instrucţiuni ce reprezintă corpul ciclului. Forma generală a buclei do-while este: do { instructiune; } while (conditie); Semantic, do-while este echivalentă cu secvenţa: instructiune; while (conditie) instructiune; Deşi acoladele nu sunt necesare când instructiune este o instrucţiune simplă, de obicei se utilizează pentru a evita confuzia cu while. Se remarcă faptul că instructiune ce reprezintă corpul ciclului (adică, o instrucţiune simplă, o instrucţiune compusă sau o instrucţiune vidă) este executată cel puţin odată. Celelalte execuţii sunt condiţionate de valoarea întreagă rezultată din evaluarea 92
• 99. condiţiei. Dacă această valoare este 0 (condiţie falsă), atunci controlul se transferă la următoarea instrucţiune din program; în caz contrar se execută corpul ciclului şi se reevaluează condiţia. Exemplu: Următoarea secvenţă asigură preluarea corectă a unei valori întregi între 1 şi 10: # include <stdio.h> void main (void) { int num; do { printf("nnIntrod. un intreg între 1 si 10: "); scanf ("%d", &num); printf (" Numarul introdus este : %d ", num); } while (num < 1 || num > 10); } Un caz tipic de utilizare a instrucţiunii do-while este oferit de programele interactive în care selecţia unei opţiuni se face pe baza unui meniu afişat pe ecranul terminalului. Exemplu: Următorul program implementează o versiune a unui meniu de verificare a corectitudinii ortografice într-un text: # include <stdio.h> # include <ctype.h> void main (void) { char ch; printf ("1. Verificarea ortografiei n "); printf ("2. Corectarea erorilor de ortografie n"); printf ("3. Afisarea erorilor de ortografie n "); do { printf ("n Introduceti optiunea dumneavoastra: "); ch=getche(); // Se citeste optiunea de la tastatura switch (ch) { case '1': verifica_ortografia(); break; case '2': corecteaza_erorile(); break; case '3': afiseaza_erorile(); break; } } while (ch != '1' && ch != '2' && ch != '3'); } După afişarea opţiunilor, programul va bucla până când se va selecta o opţiune validă. Exemplu: Adunarea elementelor a doi vectori: int a[10], b[10], c[10]; . . . . . . . . . . . . . . i = 0; 93
• 100. do { c[i] = a[i] + b[i]; i = i + 1; } while (i < 10); sau i = 0; do { c[i] = a[i] + b[i]; i++; } while (i < 10); 5.5.4. Bucle încuibate Când o buclă este introdusă în altă buclă, bucla interioară se spune a fi inclusă (nested, încuibată) în bucla exterioară. Exemplu: Programul următor afişează primele 4 puteri ale numerelor cuprinse între 1 şi 9: # include <stdio.h> void main (void) { int i, j, k, p; printf (" i i^2 i^3 i^4 n "); for (i = 1; i < 10; i++) { for (j = 1; i < 5; j++) { p = 1; for (k = 1; i < j; k++) p = p * i; printf (" %9d ", p); } printf (" n "); } } Când se execută acest program se obţin următoarele rezultate: i i^2 i^3 i^4 1 1 1 1 2 4 8 16 3 9 27 81 . . . . . . . . . . . 9 81 729 6561 Alinierea rezultatelor se datoreşte utilizării în printf() a unui format de afişare corespunzător (%9d) care precizează dimensiunea minimă a câmpului specificat. Un alt exemplu, puţin mai complex, este un program de înmulţire a două matrice. Evident, în acest caz vom avea 3 bucle for incluse una în cealaltă. // Program de inmultire a doua matrici # include <stdio.h> float a[100][100],b[100][100],c[100][100]; float elem, s; int la, ca, lb, cb, lc, cc, i, j, k; void main(void) { la=101; ca=101; lb=ca+1; cb=ca; 94
• 101. printf("Program de inmultire a doua matricinSe declara dimensiunile fiecarei matricinn"); /* Introducem pe rand dimensiunile fiecarei matrici. Verificam sa nu se depaseasca dimensiunile maxime si verificam posibilitatea inmultirii matricilor */ while (ca!=lb){ printf("Se verifica daca dimensiunile declarate sunt compatibile pentru inmultire!nn"); while ((la>=100)||(ca>=100)) { printf("Intoduceti dimensiunile primei matrice"); printf("nNr. linii matrice A = n"); scanf("%d",&la); printf("Nr. coloane matrice A = n"); scanf("%d",&ca); } while ((lb>=101)||(cb>=101)) { printf("Intoduceti dimens. celei de-a doua matrice"); printf("nNr. linii matrice B = n"); scanf("%d",&lb); printf("Nr. coloane matrice B = n"); scanf("%d",&cb); } if(ca!=lb) { la=101;ca=101; lb=ca+1;cb=ca;} } /* Se introduc matricile */ for(i=0; i<=la-1; i++) for(j=0; j<=ca-1; j++) { printf("a(%d,%d) = ", i, j); scanf("%f",&elem); a[i][j] = elem; } for(i=0;i<=lb-1;i++) for(j=0;j<=cb-1;j++) { printf("b(%d,%d) = ",i,j); scanf("%f",&elem); b[i][j]=elem; } // Se calculeaza fiecare element al matricei produs for(i=0;i<=la-1;i++) for(j=0;j<=cb-1;j++) { s=0; for(k=0;k<=ca-1;k++) s = s+a[i][k]*b[k][j]; c[i][j] = s; } // Se afisaza matricile printf("nnA = n"); for(i=0;i<=la-1;i++) { printf("n"); for(j=0;j<=ca-1;j++) printf("%6.3f ",a[i][j]); } printf("nnB = n"); 95
• 102. for(i=0;i<=lb-1;i++) { printf("n"); for(j=0;j<=cb-1;j++) printf("%6.3f ",b[i][j]); } printf("nnC = A*Bn"); for(i=0;i<=la-1;i++) { printf("n"); for(j=0;j<=cb-1;j++) printf("%6.3f ",c[i][j]); }} 5.5.5. Instrucţiunea break Instrucţiunea break are două utilizări. Prima utilizare constă în terminarea unui case în cadrul instrucţiunii switch. A doua utilizare constă în terminarea imediată a unui ciclu scurtcircuitând testul condiţional normal al buclei. Dacă într-o buclă se întâlneşte o instrucţiune break, calculatorul termină (părăseşte) imediat bucla şi controlul programului se transferă la instrucţiunea ce urmează instrucţiunii de buclare. De exemplu, programul: # include <stdio.h> void main (void) { int t; for (t = 0; t < 100; t++) { printf (" %3d ", t); if (t == 10) break; }} tipăreşte numerele până la 10 şi atunci se opreşte deoarece break determină ieşirea imediată din ciclu. În cazul buclelor incluse, este important de notat ca break determină ieşirea imediată numai din bucla interioară (din bucla în care este introdus). De exemplu, programul: # include <stdio.h> void main (void) { int t; for (t = 0; t < 100; ++t) { count = 1; for (;;) { printf (" %d ", count); count++; if (count == 10) break; } } va afişa pe ecran numerele de la 1 la 10 de 100 de ori. Instrucţiunea break se poate utiliza şi în cadrul ciclurilor programate cu while sau do-while, schema generală de utilizare fiind următoarea: while (expresie) { ................. 96
• 103. if (conditie) break; ................. } Dacă la una din iteraţii, condiţia din if este îndeplinită, atunci ciclul se termină automat, altfel el poate continua până când expresia din while are valoarea fals. Dacă instrucţiunea break se execută în cadrul unei instrucţiuni switch, care la rândul ei este inclusă într-un ciclu programat cu while, for sau do-while, atunci ea determină terminarea numai a instrucţiunii switch, nu şi ieşirea din ciclu. 5.5.6. Instrucţiunea continue Instrucţiunea continue, executată într-un ciclu, determină oprirea iteraţiei curente şi asigură trecerea imediată la iteraţia următoare. De exemplu, programul următor va afişa pe ecran numai numerele pare. # include <stdio.h> void main (void) { int x; for (x = 0; t < 100; x++) { if (x % 2) continue; printf (" %d ", x); } } Se observă că atunci când se generează un număr impar se execută instrucţiunea continue ce va determina trecerea la iteraţia următoare by-pasând instrucţiunea printf(). În cazul instrucţiunilor while şi do-while, o instrucţiune continue determină trecerea direct la testul condiţional şi prin urmare, continuarea procesului de buclare. În cazul unui for, se realizează mai întâi operaţia de incrementare a variabilei de control a ciclului, apoi testarea condiţiei de continuare a buclei. Capitolul VI TIPURI DE DATE STRUCTURATE În C există două categorii de tipuri de date structurate: tablourile şi structurile. Un tablou este o colecţie omogenă de valori de acelaşi tip identificate printr-un indice, iar o structură este o colecţie neomogenă de valori identificate prin nume simbolice, denumite selectori. 97
• 104. 6.1. Tablouri unidimensionale Un tablou este o colecţie de variabile de acelaşi tip care sunt referite printr-un nume comun. În C, un tablou constă din locaţii de memorie contigue. Adresa cea mai mică corespunde primului element, iar adresa cea mai mare corespunde ultimului element. Un tablou poate avea de la una la mai multe dimensiuni. Accesul la un element specific al tabloului se face utilizând un index. Cel mai utilizat tablou este tabloul de caractere. Şirurile de caractere pot fi definite prin conceptele: vector de caractere şi pointer-caracter. Declararea unui tablou cu o singură dimensiune are următoarea formă generală: tip var_nume[size]; Aici, tip, declară tipul de bază al tabloului. Tipul de bază determină tipul de dată al fiecărui element al tabloului. var_nume este numele tabloului, iar size este numărul elementelor pe care le va conţine tabloul. Exemple: int a[10]; // vectorul a contine 10 intregi float v[3]; // vectorul v contine 3 reali În C toate tablourile folosesc pe zero ca index al primului lor element. Elementele tabloului a[10] sunt a[0],...,a[9]. Exemplu: Programul următor încarcă un tablou de întregi cu numerele de la 0 la 9: void main (void) { int x[10]; // se rezerva 10 elemente intregi int t; for (t = 0; t < 10; t++) x[t] = t; } Pentru un tablou unidimensional, dimensiunea totală, în bytes, a acestuia va fi: Total bytes = sizeof (tip) * lungimea_tabloului Observaţie: Limbajul C nu realizează verificarea dimensiunilor unui tablou: astfel, nu există nimic care să ne oprească să nu trecem peste sfârşitul tabloului. Dacă se trece peste sfârşitul unui tablou într-o operaţie de atribuire, atunci se vor atribui valori unor alte variabile sau chiar se vor distruge părţi din program. Exemplu: Deşi următorul program este incorect, compilatorul C nu semnalează nici o eroare: void main (void) { 98
• 105. int crash[10], i; for (i = 0; i < 100; i++) crash[i] = i; } Se observă că bucla se iterează de 100 de ori, deşi vectorul crash conţine numai 10 elemente. Aceste verificări rămân în sarcina exclusivă a programatorului. Tablourile unidimensionale sunt, de fapt, liste de informaţii de acelaşi tip. De exemplu, prin rularea programului: char ch[7]; void main (void) { int i; for (i = 0; i < 7; i++) ch[i] = 'A' + i; } vectorul “ch“ arată astfel: ch(0) ch(1) ch(2) ch(3) ch(4) ch(5) ch(6) A B C D E F G 6.1.1. Constante şir În C o constantă şir este o secvenţă de caractere închisă între ghilimele. Exemplu: "acesta este un sir". Fiecare constantă şir conţine cu un caracter mai mult decât numărul de caractere din şir, deoarece aceasta se termină totdeauna cu caracterul NULL '0' care are valoarea 0. De exemplu, sizeof ("asaf") = 5. Tipul unui şir este "vector de un număr de caractere"; astfel "asaf" are tipul char[5]. §irul vid este descris prin " " şi are tipul char[1]. De notat că, pentru fiecare şir s, funcţia strlen(s) din fişierul antet "string.h" întoarce numărul caracterelor din şir fără terminatorul 0, adică: strlen(s) = sizeof(s) - 1. În interiorul unui şir se poate folosi convenţia de notaţie cu . Aceasta face posibilă reprezentarea caracterelor " şi în interiorul unui şir. Cel mai frecvent caracter folosit este caracterul 'n' = new line (NL). De exemplu, instrucţiunea: printf("beep at end of message 007 n "); determină scrierea unui mesaj, a caracterului BEL şi a caracterului NL. Nu este permisă continuarea şirurilor de caractere de pe o linie pe alta. Exemplu: "this is not a string but a syntax error". O secvenţă de forma n într-un şir nu determină introducerea unui caracter NL în şir, ci este o simplă notaţie. Este posibil să folosim 99
• 106. caracterul null într-un şir, dar majoritatea programelor nu testează dacă mai sunt caractere după el. 6.1.2. Iniţializarea vectorilor de caractere • Citirea unui şir de la tastatură utilizând funcţiile scanf() şi gets().  Utilizarea funcţiei scanf(). Exemplu: # include <stdio.h> void main (void) { char nume[21], adresa[41]; printf ("n Nume: "); scanf ("%s", nume); printf ("n Adresa: "); scanf ("%s", adresa); printf ("%sn%sn", nume, adresa); } S-au definit variabilele nume şi adresa ca tip şir de caractere de maximum 20 şi 40 de caractere. Şirul "%s" care apare în apelul funcţiei scanf() precizează că se vor citi în variabilele nume, respectiv adresa, valori de tip şir de caractere. În printf() descriptorul "%s" are rolul de a preciza cum trebuie convertite valorile datelor de afişat (în cazul de faţă, valorile variabilelor nume şi adresa). Funcţia scanf() citeşte un şir de caractere din bufferul de intrare până când întâlneşte un spaţiu, un caracter TAB, sau ajunge la sfârşitul acestuia. Astfel, dacă se tastează, "ENE ALEXANDRU", atunci în variabila nume se va memora doar valoarea "ENE". Pentru a obţine şirul în întregime este recomandat să se transmită numele sub forma: "ENE_ALEXANDRU".  Cea mai bună cale de a introduce un şir de la tastatură constă în utilizarea funcţiei gets() din fişierul antet "stdio.h". Forma generală a funcţiei gets() este: gets (nume_vector) Pentru a citi un şir se apelează gets() având ca argument numele vectorului, fără nici un index. Funcţia gets() returnează vectorul ce va păstra şirul de caractere introdus de la tastatură. gets() va continua să citească caractere până la introducerea caracterului CR. Exemplu: Programul următor afişează şirul de caractere introdus de la tastatură. # include <stdio.h> void main (void) { char sir[80]; gets (sir); /* citeste un sir de la tastatura */ printf ("%s", sir); } 100
• 107. Se observă că funcţia printf() poate primi ca argument un şir de caractere. Dacă se introduce un şir mai lung decât dimensiunea tabloului, vectorul va fi suprascris. • Iniţializarea vectorilor de caractere utilizând constantele şir Folosind constantele şir, vectorii de caractere se iniţializează sub forma: char nume_vector[size] = "sir_de_caractere" unde size = numărul caracterelor din şir plus 1. Exemplu: # include <stdio.h> void main (void) { char nume[14] = "ENE ALEXANDRU"; char adresa[24] = "Str. A. I. Cuza, nr.13"; puts (nume); puts (adresa); } Vectorul nume va ocupa începând de la adresa nume, 13 + 1 = 14 octeţi, iar cel de-al doilea vector va ocupa începând de la adresa adresa, 23 + 1 = 24 locaţii (bytes). Funcţia puts() scrie pe stdout şirul memorat în vectorul al cărui nume apare ca parametru al funcţiei puts(), precum şi caracterul "n". De multe ori, în C se realizează iniţializarea unor vectori de caractere a căror dimensiune nu este precizată. Dacă dimensiunea vectorului nu este precizată, compilatorul C va crea un vector suficient de lung încât să permită iniţializarea dorită. Exemplu: În loc să scriem : char e1[12] = "read errorn"; char e2[13] = "write errorn"; char e3[18] = "cannot open filen"; putem scrie: char e1[ ] = "read errorn"; char e2[ ] = "write errorn"; char e3[ ] = "cannot open filen"; Cu această ultimă iniţializare, instrucţiunea printf ("%s are lungimea %dn", e2, sizeof (e2)); va tipari: write error are lungimea 13  Iniţializarea unui vector (tablou unidimensional) se poate face şi cu o listă de iniţializatori scrişi între acolade. Dacă vectorul are o lungime necunoscută, numărul de iniţializatori determină mărimea tabloului, iar tipul devine complet. Dacă tabloul are lungime fixă, numărul de iniţializatori nu poate depăşi numărul de membri din 101
• 108. tablou. În cazul în care sunt mai puţini iniţializatori, membrii în plus sunt iniţializaţi cu zero. Exemple: - Instrucţiunea următoare iniţializează un vector de 10 întregi cu numerele de la 1 la 10: int i[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; Rezultă că: i[0] = 1, ... , i[9] = 10. - Instrucţiunea următoare declară şi iniţializează vectorul x ca un tablou unidimensional cu 3 membri: int x[] = {1, 2, 3}; - Instrucţiunea următoare: char sir[6] = { 'h', 'e', 'l', 'l', 'o', '0' }; este echivalentă cu: char sir[6] = "hello"; 6.1.3. Funcţii pentru prelucrarea şirurilor (fişierul antet string.h) • Funcţia strcpy() Apelul funcţiei strcpy() are următoarea formă generală: strcpy (nume_sir, constanta_sir); Funcţia strcpy() copiază conţinutul constantei_sir (inclusiv caracterul terminator 'n') în nume_sir. Exemplu: # include <string.h> void main(void) { char sir[80]; strcpy (sir, "hello"); printf("%s", sir); } Acest program va copia "hello" în şirul sir. • Funcţia strcat() Apelul funcţiei strcat() are forma: strcat (s1, s2); Funcţia strcat() concatenează şirul s2 la sfârşitul şirului s1 şi întoarce şirul s1. Şirul s2 nu se modifică. Ambele şiruri trebuie să aibă caracterul terminator NULL, iar rezultatul va avea de asemenea caracterul terminator NULL. Exemplu: # include <stdio.h> # include <string.h> void main(void) { char first[20], second[10]; strcpy (first, "hello"); 102
• 109. strcpy (second, "there"); strcat (first, second); printf ("%s", first); } Acest program va afişa "hellothere" pe ecran. • Funcţia strcmp() Se apelează sub forma: strcmp (s1, s2); Această funcţie compară şirurile s1 şi s2 şi returnează valori negative, dacă s1 < s2, 0, dacă s1 = s2 şi un număr pozitiv, dacă s1 > s2. Exemplu: Această funcţie poate fi folosită ca o subrutină de verificare a parolei: # include <stdio.h> # include <string.h> void main (void) { char s[80]; printf ("Introduceti parola: "); gets (s); if (strcmp (s, "pasword")) { printf (" Invalid pasword n "); return 0;} return 1; } • Funcţia strlen() Funcţia strlen() se apelează sub forma: strlen (s) unde s este un şir. Funcţia strlen() returnează lungimea şirului s. Exemplu: Programul următor returnează lungimea unui şir introdus de la tastatură. # incude <stdio.h> # incude <string.h> void main (void) { char sir[80]; printf ("Introduceti un sir: "); gets (sir); printf ("Sirul %s contine %d caractere ", sir, strlen(sir)); } Observaţie: Funcţia strlen() nu numără şi caracterul NULL. Exemplu: Programul următor afişează inversul unui şir de caractere introduse de la tastatură. # include <stdio.h> # include <string.h> void main (void) { char sir[80]; 103
• 110. int i; gets(sir); for(i=strlen(sir)-1;i>=0;i--) printf("%c",sir[i]); } Exemplu: Programul următor realizează introducerea unor şiruri, compararea lor, concatenarea lor şi afişarea rezultatului. # include <stdio.h> # include <string.h> void main (void) { char s1[80], s2[80]; gets(s1); gets(s2); printf("Lungimi: %d %d n",strlen(s1),strlen(s2)); if (!strcmp (s1, s2)) printf ("Sirurile sunt egalen"); strcat (s1, s2); printf ("%sn", s1); } Dacă se rulează acest program şi se introduc şirurile s1 = "AUTOMATICA" şi s2 = "AUTOMATICA", se va afişa: Lungimi 10 10 Sirurile sunt egale AUTOMATICAAUTOMATICA Dacă şirurile sunt egale, funcţia strcmp() returnează fals (0) şi din această cauză în instrucţiunea if s-a folosit !strcmp(). Observaţie: Caracterul NULL de terminare a vectorului de caractere poate fi utilizat în buclele for ca în exemplul următor, unde se converteşte un şir de caractere scris cu litere mici la litere mari. # include <stdio.h> # include <string.h> void main (void) { char sir[80]; int i; strcpy (sir, "acesta este un test"); for(i = 0; sir[i]; i++) sir[i] = toupper (sir[i]); printf("%s", sir); } Conversia caracterelor se face cu funcţia toupper() care returnează litera mare corespunzătoare argumentului (literei mici). Ciclul funcţionează până când sir[i] devine caracterul null. 6.2. Tablouri cu două dimensiuni (matrice) Tablourile bidimensionale (matricele) sunt reprezentate ca vectori de vectori. De exemplu, declaraţia: int v[2][5]; 104
• 111. declară un vector cu 2 elemente, fiecare element fiind un vector de tip int[5]. Se observă că fiecare dimensiune a tabloului este separată (închisă) între paranteze, iar dimensiunile nu sunt separate prin virgulă. Astfel, declaraţia: int v[2, 5]; conduce la eroare. 6.2.1. Iniţializarea matricelor Declaraţia : char v[2][5] = { 'a', 'b', 'c', 'd', 'e', '0', '1', '2', '3', '4' }; conduce la iniţializarea primului vector cu primele 5 litere, iar a celui de-al doilea cu primele 5 cifre. Exemplu: Programul: # include <stdio.h> void main (void) { char v[2][5] = { 'a', 'b', 'c', 'd', 'e', '0', '1', '2', '3', '4' }; int i, j; for (i = 0; i < 2; i++){ for(j = 0; j < 5; j++) printf ("v[%d][%d] = %c", i, j, v[i][j]); printf ("n"); } } va produce : v[0][0]=a v[0][1]=b v[0][2]=c v[0][3]=d v[0][4]=e v[1][0]=0 v[1][1]=1 v[1][2]=2 v[1][3]=3 v[1][4]=4. Exemplu: Secvenţa de instrucţiuni: # include <stdio.h> void main (void) { int l, c, num[3][4]; for (l = 0; l < 3; ++l) for (c = 0; c < 4; ++c) num[l][c] = (l * 4) + c + 1; } conduce la încărcarea tabloului num[3][4]cu numerele de la 1 la 12. Astfel, num[0][0] = 1, ..., num[2][3] = 12. Se observă că limbajul C memoreză tablourile bidimensionale într-o matrice linii-coloane, unde primul indice se referă la linie şi al doilea indice se referă la coloană. Cantitatea de memorie alocată permanent pentru un tablou, exprimată în bytes, este: nr_linii * nr_coloane * sizeof(tipul_datei) Declaraţia: float y[4][3] = { {1,3,5}, {2,4,6}, {3,5,7},}; este o iniţializare cu paranteze complete şi are următorul efect: 105
• 112. - numerele 1, 3, 5 iniţializează prima linie a tabloului: y[0][0], y[0][1], y[0][2] sau y[0]; - numerele 2, 4, 6 iniţializează pe y[1]; - numerele 3, 5, 7 iniţializează pe y[2]. Întrucât iniţializatorul se termină cu virgulă, elementele lui y[3] vor fi iniţializate cu 0. Acelaşi efect ar fi putut fi realizat de: float y[4][3]={1, 3, 5, 2, 4, 6, 3, 5, 7, }; Secvenţa: float y[4][3] = { {1}, {2}, {3}, {0}, }; iniţializează prima coloană a lui y, privit ca un tablou bidimensional, cu 1, 2, 3 şi 0, restul tabloului fiind iniţializat cu 0. 6.2.2. Tablouri bidimensionale de şiruri Pentru crearea unui tablou de şiruri se foloseşte un tablou de caractere, bidimensional, în care mărimea indicelui din stânga determină numărul de şiruri, iar indicele din drepta specifică lungimea maximă a fiecărui şir. De exemplu, declaraţia : char sir_tablou[30][80]; defineşte un tablou de 30 de şiruri, fiecare şir având maximum 80 de caractere. Accesul la un singur şir este foarte uşor: se specifică numai primul indice. De exemplu: gets (sir_tablou[2]) întoarce al treilea şir din tabloul sir_tablou. Funcţional, instrucţiunea anterioară este echivalentă cu: gets (&sir_tablou[2][0]); 6.3. Tablouri multidimensionale Forma generală a declaraţiei unui tablou multidimensional este: tip nume[size1][size2]...[sizeN]; De exemplu, declaraţia: int trei[4][10][3]; creează un tablou de 4*10*3 întregi. Forma generală de iniţializare a tablourilor este următoarea: specificator_tip nume_tablou[size1][size2]...[sizeN]={lista_valori}; unde lista_valori este o listă de constante separate prin virgulă, compatibile cu tipul de bază al tabloului. Observaţie: Limbajul C permite şi iniţializarea tablourilor multidimensionale fără dimensiune. Trebuie menţionat că pentru 106
• 114. Name 30 bytes Street 40 bytes City 20 bytes State 3 bytes Zip 4 bytes Când se defineşte o structură şablon, se pot declara una sau mai multe variabile-structuri, astfel : struct addr { char name[30]; char street[40]; char city[20]; char state[3]; unsigned int zip; } addr_info, binfo, cinfo; Secvenţa anterioară defineşte o structură şablon numită addr şi declară variabilele addr_info, binfo, cinfo de acelaşi tip. Pentru declararea unei singure structuri numite addr_info, nu mai este necesară includerea numelui addr al structurii, astfel: struct { char name[30]; char street[40]; char city[20]; char state[3]; unsigned int zip; } addr_info; În cazul de mai sus se defineşte variabila-structură addr_info cu şablonul definit, dar fără nume Forma generală de definire a unei structuri este : struc nume_tip_structura { tip nume_variabile; tip nume_variabile; tip nume_variabile; . . . . . . . . . . . . . . } variabile_structura; unde pot fi omise fie numele tipului structurii nume_tip_struct, fie variabile_structura, dar nu ambele. După cum se observă, nume_tip_structura după cuvântul cheie struct se referă la şablonul structurii (tipul său) iar variabile_structura se referă la lista de variabile de acest tip (cu această structură) 108
• 121. printf("%5sn","Zip"); for (t=0;t<SIZE;t++) { if (*addr_info[t].name!='0') { printf("%20s",addr_info[t].name); printf("%30s",addr_info[t].street); printf("%15s",addr_info[t].city); printf("%10s",addr_info[t].state); printf("%5d",addr_info[t].zip); getchar();}}} 6.4.2. Introducerea structurilor în funcţii a) Introducerea elementelor unei structuri în funcţii Când un element al unei variabile tip structură este transmis (pasat) unei funcţii, de fapt este transmisă valoarea acelui element. Exemplu: struct struct1{ char x; int y; float z; char s[10]; } struct2; Modalitatea de a introduce fiecare element într-o funcţie este următoarea: func (struct2.x); /* se paseaza valoarea caracterului x */ func2 (struct2.y); /* se paseaza valoarea intregului y */ func3 (struct2.z); /* se paseaza valoarea reala a lui z */ func4 (struct2.s); /* se utilizeaza adresa sirului s */ func (struct2.s[2]); //se utilizeaza valoarea caracterului lui s[2] unde func(), func2(), func3(), func4() sunt numele unor funcţii. Pentru a transmite funcţiei func() adresele elementelor din structura struct2, se scrie astfel: func (&struct2.x); /* se paseaza adresa caracterului x */ func (&struct2.s[2]); /* se paseaza adresa caracterului s[2] */ b) Transmiterea unei întregi structuri unei funcţii Când o structură este utilizată ca argument al unei funcţii, limbajul C transmite funcţiei întreaga structură utilizând metoda 115
• 122. standard a apelului prin valoare. Aceasta înseamnă că orice modificare asupra conţinutului structurii în interiorul funcţiei în care este apelată structura, nu afectează structura folosită ca argument. Ceea ce trebuie avut neapărat în vedere atunci când, ca parametru, se utilizează o structură este ca tipul argumentului să fie identic cu tipul parametrului. Exemplu: # include <stdio.h> void f1(); void main() { struct { int a,b; char ch; } arg; arg.a = 1000; f1(arg); /* se apeleaza functia f1() */ } void f1(param) /* se declara functia f1 */ struct { int x, y; char ch; } param; {printf ("%dn", param.x); } Acest program declară atât argumentul arg al lui f1, cât şi parametrul param ca având acelaşi tip de structură. Programul va afişa pe ecran valoarea 1000. Pentru a face programele mai compacte se procedează la definirea structurii ca variabilă globală şi apoi la utilizarea numelui acesteia pentru a declara diversele variabile structură. Exemplu: # include <stdio.h> void f1(); /* Se defineste un tip structura */ struct struct_tip { int a, b; char ch;}; void main() { struct struct_tip arg; /* se declara structura arg */ arg.a = 1000; f1(arg);} void f1(struct struct_tip param) /* se declara functia f1() */ {printf ("%dn",param.a); } 116
• 123. Pentru exemplificare, propunem următorul program: - se preia de la tastatura un prim şir de numere întregi - se preia de la tastatura un al doilea şir de numere întregi - se concatenează cele două şiruri - şirul rezultat se sortează în ordine crescătoare, având în vedere ca primele poziţii să fie ocupate de numerele pare din şir sortate crescător după care să urmeze numerele impare din şir sortate tot crescător. Pentru a realiza acest program, vom utiliza nu variabile de tip şir ci o structură care să conţină ca prim element şirul respectiv iar cel de-al doilea element să fie lungimea acestuia. Se vor construi funcţii care să realizeze citirea şirurilor de la tastatură, scrierea lor pe display, respectiv să le ordoneze şi să le sorteze după paritate. Toate aceste funcţii comunică prin intermediul unei variabile globale de tip structură şi a mai multor variabile locale de tip structură. Programul în C este prezentat în continuare: # include <stdio.h> // definim prototipurile functiilor utilizate struct sir_lung cit_sir(); struct sir_lung concat_sir(); struct sir_lung ord_sir(); struct sir_lung par_sir(); struct sir_lung impar_sir(); void tip_sir(); /* se defineste structura sir+lungime si variabila globala sir */ struct sir_lung { int sir[80]; int lung;} sir; // programul principal void main(){ struct sir_lung sir_init,sir_ord,sir_par,sir_impar; sir_init=cit_sir(); getchar(); sir_ord=cit_sir(); sir_init=concat_sir(sir_init,sir_ord); sir_par=par_sir(sir_init); sir_par=ord_sir(sir_par); sir_impar=impar_sir(sir_init); sir_impar=ord_sir(sir_impar); sir_ord=concat_sir(sir_par,sir_impar); tip_sir(sir_init); tip_sir(sir_ord);} // se definesc functiile 117
• 124. struct sir_lung cit_sir() {int result=1, i=0; sir.lung=0; while (result) { result=scanf("%d",&sir.sir[i]); i++;} sir.lung=--i; return sir;} void tip_sir(struct sir_lung sirf) { int i; for (i=0;i<sirf.lung;i++) printf("%d ",sirf.sir[i]);printf("n");} struct sir_lung concat_sir(struct sir_lung sir1, struct sir_lung sir2) { int i; struct sir_lung sir_concat; sir_concat=sir1; for (i=0;i<sir2.lung;i++) sir_concat.sir[sir1.lung+i]=sir2.sir[i]; sir_concat.lung=sir1.lung+sir2.lung; return sir=sir_concat;} struct sir_lung ord_sir(struct sir_lung sir1) {int i,j,temp; for (i=0;i<sir1.lung;i++) for (j=i+1;j<sir1.lung;j++) if (sir1.sir[i]>sir1.sir[j]) {temp=sir1.sir[i];sir1.sir[i]=sir1.sir[j]; sir1.sir[j]=temp;} return sir=sir1;} struct sir_lung par_sir(struct sir_lung sir1) { int i,j=0; struct sir_lung sir_par; for (i=0;i<sir1.lung;i++) if (!(sir1.sir[i]%2)) {sir_par.sir[j]=sir1.sir[i];j++;} sir_par.lung=j; return sir=sir_par;} struct sir_lung impar_sir(struct sir_lung sir1) { int i,j=0; struct sir_lung sir_impar; for (i=0;i<sir1.lung;i++) if (sir1.sir[i]%2) {sir_impar.sir[j]=sir1.sir[i];j++;} sir_impar.lung=j; return sir=sir_impar;} Din funcţia cit_sir() se poate ieşi prin tastarea oricarui caracter nenumeric. Se observă cum aceasta funcţie lucrează direct cu variabila 118
• 125. globală şir iar celelalte cu variabile locale proprii care apoi sunt asignate variabilei globale şir la returnare. Rulând programul vom obţine rezultatele: 1 2 17 -3 6 -4; -9 -2 31 2 -7; 1 2 17 -3 6 -4 -9 -2 31 2 -7 -4 -2 2 2 6 -9 -7 -3 1 17 31 6.4.3. Tablouri şi structuri în structuri Elementele unei structuri pot fi simple (int, char etc.) sau complexe (tablouri de elemente de diferite tipuri, structuri). Exemplu: Considerăm structura : struct x { int a[10][10]; /* tablou de 10x10 intregi */ float b;} y; De exemplu, referirea întregului a[3][7] se face prin: y.a[3][7] Când o structură este un element al altei structuri, structurile se numesc încuibate (nested, incluse). Exemplu: struct sal { struct addr adresa; float salariu; } muncitor; Se observă că elementele variabilei-structură "adresa" sunt incluse în structura "sal". Aici "addr" este structura definită anterior. Exemplul anterior defineşte structura "sal" cu 2 elemente: primul este o structură de tip "addr" care conţine adresa salariatului; al doilea element este salariul săptămânal al acestuia. Instrucţiunea următoare va atribui codul 90178 variabilei "zip" din adresa muncitorului muncitor.adresa.zip = 90178; Se observă că elementele fiecărei structuri sunt referite de la exterior către interior, respectiv de la stânga la dreapta. 6.5. Uniuni În C o uniune este o zonă de memorie utilizată de mai multe variabile ce pot fi diferite ca tip. Definitia uniunilor este similară celei a structurilor. Exemplu: 119
• 126. union tip_u { int i; char ch;}; O variabilă de acest tip poate fi declarată fie prin plasarea numelui său la sfîrşitul acestei definiţii, fie utilizând o instrucţiune de declarare separată. De exemplu, instrucţiunea : union tip_u exuniune; declară variabila-uniune "exuniune" în care atât întregul i, cât şi caracterul ch ocupă aceeaşi zonă de memorie (cu toate ca int ocupa 4 octeţi, iar char numai un octet). Când se declară o variabilă union compilatorul C rezervă o zonă de memorie suficient de lungă capabilă să preia variabila cu lungimea cea mai mare. Pentru a accesa elementele unei uniuni se utilizează aceeaşi sintaxă folosită la structuri (operatorii punct şi " -> "). Când un element al unei uniuni se adresează direct, se utilizează operatorul ".", iar când elementul se adresează printr-un pointer, se foloseşte operatorul "-> ". De exemplu, pentru a atribui elementul întreg i al uniunii anterioare "exuniune" valoarea 10, se va scrie: exuniune.i = 10; În exemplul următor, programul transferă un pointer la "exuniune" unei funcţii : func1(union tip_u *un) { un -> i = 10; } /* se atribuie valoarea 10 intregului i al uniunii exuniune */ In C, uniunile sunt des utilizate când sunt necesare conversii de tip. De exemplu, funcţia standard putw() ce realizează scrierea binară a unui întreg într-un fişier de pe disc, poate fi scrisă folosind o uniune. Pentru aceasta, mai întâi se crează o uniune ce cuprinde un întreg şi un vector de două caractere, astfel: union pw { int i; char ch[2];}; Atunci, structura lui putw(), utilizând aceasta uniune este : putw(word, fp) union pw word; /* se declara uniunea word */ FILE *fp { putc(word ->ch[0]); // se scrie primul caracter putc(word->ch[1]); // se scrie al doilea caracter } 120
• 127. 6.6. Enumerări O enumerare este o mulţime de constante întregi ce pot lua toate valorile unei variabile de un anumit tip. Enumerările se definesc în acelaşi mod ca şi structurile, utilizând cuvântul cheie enum ce semnalează începutul unui tip enumerare. Forma generală de definire a unei enumerări este: enum nume_tip_enum { lista_enumeratori} lista_variabile; unde atât nume_tip_enum, cât şi lista_variabile sunt opţionale. Exemplu: Următorul fragment defineşte o enumerare numită "bancnota" cu care apoi se declară o enumerare numită "bani" având acest tip: enum bancnota {suta,douasute,cincisute,mie,cincimii,zecemii}; enum bancnota bani; Dându-se această definiţie şi declaraţie, sunt valabile urmatoarele instructiuni: bani = mie; if (5*bani == cincimii) printf("Sunt 5000 lei.n"); Trebuie precizat că fiecare enumerator este caracterizat printr-o valoare întreaga. Fără nici o altă iniţializare, valoarea primului enumerator este 0, a celui de-al doilea este 1, s.a.m.d. De aceea, instrucţiunea: printf ("%d %d, suta, mie); va afisa pe ecran: 0 3 Se pot specifica valorile unuia sau mai multor simboluri folosind iniţializatori. De exemplu: enum bancnota {suta, douasute, cincisute, mie=1000, cincimii, zecemii}; face ca simbolurile din enumerarea bancnota să aibă valorile: suta = 0 douasute = 1 cincisute = 2 mie = 1000 cincimii = 1001 zecemii = 1002 Urmatorul fragment de program nu funcţionează, deoarece "bani" este un întreg şi nu un şir : bani = cincimii; printf ("%s", bani); Nici acest program nu functionează: gets (s); strcpy (bani, s); 121
• 128. Pentru a afişa tipurile bancnotelor conţinute în enumerarea "bani", se va scrie: switch (bani) { case suta: printf (" suta "); break; case douasute: printf (" douasute "); break; . . . . . . . . . . . . . . . . . . . . . . . . . . case zecemii: printf (" zecemii "); } Uneori pentru a translata valoarea unui enumerator în şirul corespunzător, se poate declara un tablou de şiruri şi utiliza valoarea enumeratorului ca index. De exemplu, următorul fragment va afişa şirul corespunzător: char name[ ][20] = { "suta", "douasute", "cincisute", "mie", "cincimii" "zecemii" }; . . . . . . printf ("%s", name[bani]); Fragmentul anterior va funcţiona numai dacă nu se realizează iniţializarea simbolurilor, deoarece indexarea şirurilor începe cu zero. Următorul program afişează numele bancnotelor: # include <stdio.h> enum bancnota { suta, douasute, cincisute,mie, cincimii,zecemii,cincizecimii,sutamii,cincisutemii}; char name[][20]= {"suta","douasute","cincisute","mie","douamii","cincimii" ,"zecemii","cincizecimii" "sutamii","cincisutemii"}; void main() { enum bancnota bani; for (bani = suta; bani <= cincisutemii; bani ++) printf ("%sn", name[bani]);} Dacă variabilei uniune y din exemplul următor i se aplică operatorul sizeof() vom găsi sizeof(y) = 8. # include <stdio.h> union { char ch; int i; double f; } y; void main() {printf("%dn",sizeof(y));} Deci compilatorul va reţine valoarea celei mai largi tipuri de date din uniune. 122
• 129. Capitolul VII POINTERI Un pointer este o variabilă care păstrează adresa unui obiect de tip corespunzător. Forma generală pentru declararea unei variabile pointer este: tip * nume_variabila; unde tip poate fi oricare din tipurile de bază admise în C, iar nume_variabila este numele variabilei pointer. Tipul de bază al pointerului defineşte tipul variabilelor spre care indică pointerul. Variabila pointer este o variabilă de un tip special, aparte de tipurile char, int, float. Cuvântul cheie tip din declaraţia unui pointer se referă la tipul de dată spre care indică pointerul, nu la formatul în care se stochează efectiv o variabilă pointer în memorie. Formatul în care se stochează o variabilă pointer în memorie depinde de tipul de compilator care se foloseşte, deci depinde în mare măsură de tipul procesorului pentru care a fost proiectat compilatorul. O indicaţie despre formatul în care se stochează o variabilă pointer în memorie poate fi obţinută prin tipărirea conţinutului unei variabile pointer (o adresă) utilizând printf() cu formatul %p. Exemplu: char *p; /* pointer la caracter */ int *temps, *start; /* pointeri la intregi */ char *const q; /* pointer constant la caracter */ 7.1. Operatori pointer Există doi operatori pointer speciali * şi &: • Operatorul & este un operator unar care oferă (returnează) adresa unei variabile (adresa operandului său). • Operatorul * este complementarul lui &. Este un operator unar care returnează valoarea variabilei plasată la adresa care urmează după acest operator. Exemplu: # include <stdio.h> void main (void) { int *count_addr, count, val; count = 100; /* int count are valoarea 100 */ 123
• 131. 7.1.1. Importanţa tipului de bază Considerăm declaraţia: val = *count_addr; Se pune întrebarea: care va fi numărul de bytes ce va fi transferat variabilei val de la adresa indicată prin *count_addr. Sau, mai general, de unde ştie compilatorul câţi bytes să transfere în cazul oricărei asignări care utilizează pointeri. Răspunsul la aceste întrebări este acela că, tipul de bază al pointerului determină tipul datei spre care indică pointerul. Exemplu: /* Acest program nu lucreaza corect */ # include <stdio.h> void main (void) { float x = 10.12, y; short int *p; /* pointer la intreg */ p = &x; /* p preia adresa lui x */ y = *p; /* y preia valoarea de la adresa p */ printf ("x = %f y = %f",x,y); } Acest program nu va atribui valoarea lui x lui y, deoarece în program se declară p ca fiind pointer la întreg scurt şi compilatorul va transfera în y numai 2 bytes (corespunzători reprezentării unui întreg scurt) şi nu 4 bytes, corespunzători unui număr real în virgulă mobilă. 7.1.2. Expresii în care intervin pointeri În general, expresiile în care intervin pointeri respectă aceleaşi reguli ca orice alte expresii din limbajul C. • Atribuirea pointerilor Ca orice variabilă, un pointer poate fi folosit în membrul drept al unei instrucţiuni de asignare (atribuire), pentru atribuirea valorii unui pointer unui alt pointer. Exemplu: # include <stdio.h> void main (void) { int x; int *p1,*p2; /* pointeri la intregi */ p1 = &x; /* p1 indica spre x */ p2 = p1 /* p2 indica tot spre x */ printf ("p1 = %p p2 = %p", p1, p2); } /* Se afiseaza valoarea hexa a adresei lui x, nu valoarea lui x */ Se observă că în funcţia printf() tipărirea se face cu formatul %p care specifică faptul că variabilele din listă vor fi afişate ca adrese pointer. 125
• 132. Din cele de mai sus se observă că operaţia fundamentală care se execută asupra unui pointer este indirectarea, ceea ce presupune referirea la un obiect indicat de acel pointer. Exemplu: char c1 = 'a'; char *p = &c1; /* p contine adresa lui c1 */ char c2 = *p; /*c2 preia valoarea de la adresa p*/ printf ("c1 = %c c2 = %c", c1, c2); Deci variabila indicată de p este c1, iar valoarea memorată în c1 este 'a', aşa încât valoarea lui *p atribuită lui c2 este 'a'. • Operaţii aritmetice efectuate asupra pointerilor  Utilizarea operatorilor de incrementare şi decrementare Fie secvenţa: int *p1; /* pointer la intreg */ p1++; De fiecare dată când se incrementează p1, acesta va indica spre următorul întreg. Astfel, dacă p1 = 2000, după efectuarea instrucţiunii p1++, acesta va fi p1 = 2004 (va indica spre următorul întreg).  După fiecare incrementare a unui pointer, acesta va indica spre următorul element al tipului său de bază.  După fiecare decrementare a unui pointer, acesta va indica spre elementul anterior. Valoarea pointerilor va fi crescută sau micşorată în concordanţă cu lungimea tipului datelor spre care aceştia indică, aşa cum se poate vedea în exemplul următor: char *ch = 3000; short int *i = 3000; ch 3000 i ch + 1 3001 ch + 2 3002 i+1 ch + 3 3003 ch + 4 3004 i+2 ch + 5 3005 ch + 6 3006 i+3 Memoria Cum valoarea indirectată de un pointer este o l-valoare, ea poate fi asignată şi incrementată ca orice altă variabilă. O l-valoare (left value) este un operand care poate fi plasat în stânga unei operaţii de atribuire. Verificaţi utilizarea pointerilor din programul următor: # include <stdio.h> 126
• 133. void main(void) { short *pi, *pj, t; long *pl; double *pd; short i, j; i=1; j=2; t=3; printf("i= %d, j= %dn", i, j); pi=&i; pj=&j; printf("pi= %p, pj= %pn", pi, pj); *pj /= *pi+1; printf("*pi= %d *pj= %dn", *pi, *pj); *pj /= *pi+2; printf("*pi= %d *pj= %dn", *pi, *pj); printf("++pj= %p, ++*pj= %dn",++pj,++*pj); } În urma rulării sale, pe calculatoarele mai moderne obţinem rezultatul i= 1,j= 2 pi= 0065FDE0,pj= 0065FDDC *pi= 1 *pj= 1 *pi= 1 *pj= 0 ++pj= 0065FDDE,++*pj= 1  Utilizarea operatorilor de adunare şi de scădere La sau dintr-un pointer, se pot aduna sau scădea valori de tip întreg. Rezultatul este un pointer de acelaşi tip cu cel iniţial, indicând spre un alt element din tablou. De exemplu, p1 = p1 + 9; face ca p1 să indice spre al 9-lea element având tipul lui p1, considerând că elementul curent este indicat de p1. Evident că valoarea pointerului se va modifica corespunzător lungimii tipului datei indicată prin pointer. Exemplu: int *p1; /* Pointer la intreg */ p1 = p1 + 9; Dacă valoarea p1 = 3000, atunci p1 + 9 va avea valoarea: (valoarea lui p1)+9*sizeof(int)=3000+9*4=3036 Aceleaşi considerente sunt valabile în cazul în care un întreg este scăzut dintr-un pointer. Dacă doi pointeri de acelaşi tip sunt scăzuţi, rezultatul este un număr întreg cu semn care reprezintă deplasamentul dintre cei doi pointeri (pointerii la obiecte vecine diferă cu 1). În cazul tablourilor, dacă pointerul rezultat indică în afara tabloului, rezultatul este nedefinit. 127
• 134. Dacă p indică spre ultimul membru dintr-un tablou, atunci (p+1) are valoare nedeterminată. Observaţii :  Nu se pot aduna sau scădea valori de tip float sau double la/sau dintr-un pointer.  Nu se pot efectua operaţii de înmulţire şi împărţire cu pointeri. Exemplu: Scăderea a doi pointeri este exemplificată în programul: # include <stdio.h> void main(){ int i=4, j; float x[] = {1,2,3,4,5,6,7,8,9}, *px; j = &x[i]-&x[i-2]; px = &x[4]+i; printf("%d %f %p %pn",j,*px,&x[4],px); }  Compararea pointerilor Doi pointeri de acelaşi tip se pot compara printr-o expresie relaţională, astfel: dacă p şi q sunt doi pointeri, atunci instrucţiunile: if (p < q) printf (“ p indica spre o adresa mai mica decit q n “); sunt corecte. Compararea pointerilor se utilizează când doi sau mai mulţi pointeri indică spre acelaşi obiect comun. Exemplu: Un exemplu interesant de utilizare a pointerilor constă în examinarea conţinutului locaţiilor de memorie ale calculatorului. /* Programul afiseaza continutul locatiilor de memorie de la o adresa specificata */ # include <stdio.h> # include <stdlib.h> dump (start); void main (void) { /* start = adresa de inceput */ unsigned long int start; printf (“Introduceti adresa de start: “); scanf (“ %lu “, &start); dump (start); /* Se apeleaza functia dump () */ } dump (start) /* Se defineste functia dump() */ unsigned long int start; {char far *p; int t; p = (char far *) start; /*Conversie la un pointer*/ for (t = 0; ; t++, p++) { if (!(t%16)) printf ("/n"); printf ("%2X ", *p); 128
• 135. /*Afiseaza in hexazecimal continutul locatiei de memorie adresata cu *p*/ if (kbhit()) return;} // Stop cand se apasa orice tasta } Programul introduce cuvântul cheie far care permite pointerului p să indice locaţii de memorie care nu sunt în acelaşi segment de memorie în care este plasat programul. Formatul %lu din scanf() înseamnă: "citeşte un unsigned long int". Formatul %X din printf() ordonă calculatorului să afişeze argumentul în hexazecimal cu litere mari (Formatul %x afişează în hexazecimal cu litere mici). Programul foloseşte explicit un şablon de tip pentru transferul valorii întregului fără semn, start, pe care îl introducem de la tastatură, într-un pointer. Funcţia kbhit() returnează ADEVARAT dacă se apasă o tastă, altfel returnează FALS.  Utilizarea pointerilor ca parametri formali ai funcţiilor În exemplele de până acum, s-au folosit funcţii C care atunci când erau apelate, parametrii acestor funcţii erau (întotdeauna) actualizaţi prin pasarea valorii fiecărui argument. Acest fapt ne îndreptăţeşte să numim C-ul ca un limbaj de apel prin valoare. Există totuşi o excepţie de la această regulă atunci când argumentul este un tablou. Această excepţie este explicată, pe scurt, prin faptul că valoarea unui nume al unui tablou (vector, matrice etc.) neindexate este adresa primului său element. Deci, în cazul unui argument de tip tablou, ceea ce se pasează unei funcţii este o anumită adresă. Folosind variabile pointer se pot pasa adrese pentru orice tip de date. Spre exemplu, funcţia scanf() acceptă un parametru de tip pointer (adresă): scanf(“%1f“,&x); Ceea ce este important de evidenţiat este cum anume se poate scrie o funcţie care să accepte ca parametri formali sau ca argumente pointeri ?. Funcţia care recepţionează o adresă ca argument va trebui să declare acest parametru ca o variabilă pointer. De exemplu, funcţia swap() care va interschimba valorile a doi întregi poate fi declarată astfel: # include <stdio.h> void swap();/*Prototipul functiei swap()*/ void main(void) { int i,j; i=1; j=2; printf("i= %d j= %dn", i, j); swap(&i,&j); /* Apelul functiei */ 129
• 136. printf("i= %d j= %dn", i, j); } void swap(int *pi, int *pj) { short t; t = *pi; *pi = *pj; *pj = t; } 7.2. Pointeri şi tablouri Între pointeri şi tablouri există o strânsă legătură în limbajul C. Există însă o mare deosebire între tablouri şi pointeri pe care trebuie să o avem mereu în vedere. Un tablou constă întotdeauna dintr-o mare cantitate de memorie, îndeajuns de mare pentru a reţine toţi octeţii corepunzători tuturor elementelor tabloului. Astfel, tabloul q declarat ca short q[100]; rezervă 2x100 octeţi de memorie iar int q[100] rezervă 4x100 octeţi de memorie. Pe de altă parte, pentru un pointer se alocă o zonă de memorie redusă la numai câţiva octeţi necesari pentru a reţine o adresă de memorie. De fapt, zona de memorie alocată unui pointer depinde de tipul pointerului (tipul datelor spre care punctează acesata) şi va fi de numai câţiva octeţi ( în exemplele anterioare - 4 octeţi). Considerăm secvenţa: char sir[80]; char *p1; p1 = sir; Acest fragment face ca p1 să adreseze primul element al tabloului sir. În C numele unui tablou fără indici este adresa de start a tabloului. De fapt, numele tabloului este un pointer la tablou. Ca o concluzie, un pointer declarat sau numele unui tablou fără indici reprezintă adrese, pe când numele unui tablou cu indici se referă la valorile stocate în acea poziţie în tablou. Deoarece indicele inferior al oricărui vector este zero, pentru a referi al 5-lea element al şirului sir, putem scrie sir[4] sau *(p1+4) sau p1[4]. Deci pointerul p1 indică spre primul element al lui sir. Pentru a avea acces la elementul unui tablou, în limbajul C, se folosesc 2 metode : 1. utilizarea indicilor tabloului; 2. utilizarea pointerilor . Deoarece a doua metodă este mai rapidă, în programarea în C, de regulă, se utilizează această metodă. 130
• 137. Pentru a vedea modul de utilizare a celor două metode, considerăm un program care tipăreşte cu litere mici un şir de caractere introdus de la tastatură cu litere mari: Exemplu: Versiunea cu indici void main (void) { char sir[80]; int i; printf ("Introduceti un sir de caractere scrise cu litere mari: n"); gets (sir); printf ("Acesta este sirul in litere mici: n"); for(i=0;sir[i];i++) printf("%c", tolower(str[i])); } Exemplu: Versiunea cu pointeri void main (void) { char sir[80] , *p; printf ("Introduceti un sir de caractere scrise cu litere mari: n"); gets (sir); printf (" Acesta este sirul in litere mici: n"); p = sir; /* p preia adresa de inceput a sirului */ while (*p) printf (" %c ", tolower(*p++)); } Pointerii sunt rapizi şi uşor de utilizat când se doreşte referirea elementelor unui tablou în ordine strict crescătoare sau strict descrescătoare. Dacă însă se doreşte referirea aleatoare a elementelor unui tablou, atunci indexarea tabloului este cel mai simplu şi sigur de utilizat. 7.2.1. Indexarea pointerilor În C, dacă p este un pointer, iar i este întreg, p[i] este identic cu *(p+i). Dacă avem declaraţiile: short q[100]; short *pq atunci sunt permise şi posibile următoarele declaraţii: Varianta cu Varianta cu Descriere tablou pointeri pq=&q[0] pq=&q[0] Pointerul pq indică adresa pq=q pq=q primului element al tabloului q q[n] pq[n] pq[n] înseamnă acelaşi lucru cu *(pq+n) *(pq+n) În C, dacă se pune un index unui pointer, ca în pq[n], atunci se consideră această utilizare echivalentă cu *(pq+n). Cu alte cuvinte, 131
• 138. orice referire la pq[n] înseamnă acelaşi lucru cu valoarea de la adresa (pq+n). Exemplu: Programul următor realizează tipărirea pe ecran a numerelor cuprinse între 1 şi 10. void main (void) { int v[10]={1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; int *p, i; p = v; /* p indica spre v */ for (i=0;i<10;i++) printf ("%d", p[i]); } Utilizarea constantelor şir în locul poinetrilor la caractere este posibilă dar nu este uzuală. Exemplu: # include <stdio.h> void main(){ char *sir = "To be or not to be", *altsir; printf("%sn", "That don't impress me much"+5); printf("%cn",*("12345"+3)); printf("%cn","12345"[1]); puts("stringn"); altsir = "American pie"; printf("sir = %snaltsir = %sn",sir,altsir); } Exemplu de utilizare a stivei. Stiva este o listă cu acces tip LIFO (last in - first out). Pentru a avea acces la elementele stivei, se utilizează doua rutine: push() şi pop(). Calculatorul păstrează stiva într-un tablou, iar rutinele push() şi pop() realizează accesul la elementele stivei utilizând pointeri. Pentru memorarea vârfului stivei, utilizăm variabila tos (top of stack). Considerăm că folosim numai numere de tip întreg. În programul main(), rutinele push() şi pop() sunt utilizate astfel: push() citeşte numerele introduse de la tastatură şi le memorează în stivă dacă acestea sunt nenule. Când se introduce un zero, pop() extrage valoarea din stivă. # include <stdlib.h> # include <stdio.h> void push(); /* Prototipul functiei push() */ int pop(); /* Prototipul functiei pop() */ // Se rezerva pentru stiva 50x4 = 200 locatii int stack[50]; int *p1, *tos; void main(void) { int value; p1 = stack; /* p1 preia adresa bazei stivei */ tos = p1; /* tos va contine varful stivei */ do { scanf ("%d", &value); 132
• 139. if (value != 0) push (value); else printf ("Nr. extras din stiva este %d n", pop()); } while (value != -1); } void push(int i) /* Functia push() */ { p1++; if (p1==(tos+50)) { printf("n Stiva este plina."); exit (0);} *p1 = i; printf("introdus in stivan"); } int pop() { if (p1 == tos) { printf ("n Stiva este complet goala."); exit (0); } p1 --; return *(p1+1); } 7.2.2. Pointeri şi şiruri Deoarece numele unui tablou fără indici este un pointer la primul element al tabloului, pentru implementarea unor funcţii care manipulează şiruri, se pot utiliza pointeri. §tim că funcţia strcmp(s1, s2) realizează compararea şirurilor s1 şi s2 şi întoarce 0 dacă s1 = s2, o valoare negativă, dacă s1 < s2 şi o valoare pozitivă, dacă s1 > s2. Exemplu: Prezentăm o variantă de scriere a funcţiei strcmp(s1,s2) char *s1, *s2; { while (*s1) if (*s1 - *s2) /* Daca valoarea punctata de s1 este diferita de valoarea punctata de s2, */ return *s1-*s2; /* Returneaza diferenta */ else {s1++; s2++;} return '0'; //Se returneaza 0 in caz de egalitate } Reamintim că un şir în C se termină cu caracterul NULL. De aceea, instructiunea while(*s1) rămâne adevărată până când se întâlneşte caracterul NULL, care este o valoare falsă. Dacă într-o expresie se utilizează un şir constant, calculatorul tratează constanta ca pointer la primul caracter al şirului. Exemplu: Programul următor afişează pe ecran mesajul " Acest program funcţionează ": # include <stdio.h> void main (void) { char *s; s = " Acest program functioneaza "; printf (s); } 133
• 140. 7.2.3. Preluarea adresei unui element al unui tablou Până acum s-a văzut că un pointer poate să adreseze primul element al unui tablou. Este posibil să se adreseze orice element al unui tablou aplicând operatorul & unui tablou indexat. De exemplu, p = &x[2]; plasează adresa celui de-al 3-lea element al vectorului x în pointerul p. Un domeniu în care această practică este esenţiala constă în găsirea unui subşir într-un şir dat. Exemplu: Programul următor afişează ultima parte a unui şir introdus de la tastatură, din punctul în care se întâlneşte primul spaţiu: # include <stdio.h> void main (void) { char s[80]; char *p; int i; printf (" Introduceti un sir : n "); gets (s); /* Gaseste primul spatiu sau sfarsitul sirului */ for (i = 0; s[i] && s[i] != ' '; i++) p = & s[i+1]; printf (p); } Dacă p indică spre un spaţiu, programul va afişa spaţiul şi apoi subşirul rămas. Dacă în şirul introdus nu este nici un spaţiu, p indică spre sfârşitul şirului şi atunci nu se va afişa nimic. De exemplu, dacă se introduce “my friend“, atunci printf() afişează mai întâi un spaţiu şi apoi “friend“. 7.2.4. Tablouri de pointeri Putem construi tablouri de pointeri în aceeaşi manieră în care se definesc alte tipuri de date. Exemplu: int *x[10]; // Vector de 10 pointeri la intregi char *p[20]; // Vector de 20 pointeri la caracter Pentru atribuirea unei variabile întregi, var, celui de al treilea element al tabloului de pointeri *x[10], se va scrie: x[2] = &var; Pentru găsirea valorii lui var, se va scrie: y = *x[2]; //Valoarea lui var este atribuita lui y Exemplu: Tablourile de pointeri pot fi utilizate în construirea mesajelor de eroare, astfel: 134
• 141. char *err[ ] = { " Cannot open file n ", " Read error n ", " Write error n " }; selmes (int num) /* Selecteaza un mesaj */ { scanf("%d", &num); /* Se introduce un numar de la tastatura */ printf("%s", err[num]); } Funcţia printf() este apelată din funcţia selmes(). Aceasta va afişa mesajul de eroare indexat prin numărul de eroare num, care este pasat ca argument funcţiei selmes(). De exemplu, dacă se introduce 2, atunci se va afişa mesajul: Write error Atentie !. Trebuie facută distincţia între: int *v[10]; // Tablou de 10 pointeri la intregi int (*v)[10]; // Pointer la un tablou de 10 intregi Pentru aceasta trebuie ţinut cont de faptul că * este un operator prefixat, iar [] şi () sunt operatori postfixaţi. Deoarece prioritatea operatorilor postfixaţi este mai mare decât cea a operatorilor prefixaţi, atunci când se doreşte schimbarea priorităţii, trebuie folosite paranteze. 7.2.5. Pointeri la pointeri Un tablou de pointeri este ceea ce numim pointeri la pointeri. Conceptul de tablou de pointeri este simplu, deoarece indexarea tabloului conduce la clarificarea semnificaţiei lui. Un pointer la un pointer este o formă de indirectare multiplă sau un lanţ de pointeri. În cazul unei indirectari simple, valoarea pointerului este adresa variabilei care conţine valoarea dorită: Pointer Variabilă Adresă ---------> Valoare În cazul unui pointer la pointer, primul pointer conţine adresa celui de-al doilea pointer, care indică spre variabila ce conţine valoarea dorită: Pointer Pointer Variabilă Adresă --------- Adresă --------- Valoare > > Declararea indirectărilor multiple se face sub forma: 135
• 142. /* cpp este un pointer la pointer la caracter */ char **cpp; /* newbalance este un pointer la pointer la float */ float **newbalance; Pentru a avea acces la o valoare indirectată printr-un pointer la pointer este necesară, de asemenea, utilizarea operatorului * de două ori, aşa cum se vede în exemplul următor: # include <stdio.h> void main (void) { int x, *p, **q; x = 10; p = &x; /* p preia adresa lui x */ q = &p; /* q preia adresa lui p */ printf(" %d ", **q);/*Se afiseaza valoarea lui x*/ } 7.2.6. Iniţializarea pointerilor Secvenţa: char *p; char s[] = "Hello world n "; p = s; /* p indica spre s */ este echivalentă cu: char *p = "Hello world n"; Într-un program, p din ultima declaraţie poate fi utilizat ca orice alt şir. Astfel, programul următor este corect: # include <stdio.h> char *p = " Hello world n "; void main (void) { register int t; /*Se tipareste sirul in ordine directa si inversa*/ printf (p); for(t = strlen(p)-1; t > -1; t--) printf("%c",p[t]); } Observaţie: Neiniţializarea pointerilor sau iniţializarea greşită a acestora, poate duce la erori care, în cazul programelor de dimensiuni mari, sunt foarte greu de depistat şi pot avea urmări catastrofale. Exemplu: Considerăm programul: # include <stdio.h> void main(void) { int x, *p; x = 10; *p = x; printf ("%dn", *p); } 136
• 143. Acest program atribuie valoarea 10 anumitor locaţii de memorie necunoscute. Programul nu va oferi niciodată o valoare pointerului p dar va tipări valoarea lui x. Exemplu: Considerăm acum următorul program: # include <stdio.h> void main (void) { int x, *p; x = 10; p = x; printf("%d",*p); } Funcţia printf() nu va afişa niciodată valoarea lui x (care este 10), dar va tipări o valoare necunoscută. Aceasta datorită atribuirii greşite: p = x; Instrucţiunea atribuie valoarea 10 pointerului p, care se presupune că reprezintă o adresă şi nu o valoare. Pentru a corecta programul, trebuie scris: p = &x; 7.2.7. Alocarea dinamică a memoriei Există două metode principale prin care un program C poate memora informaţii în memoria principală a calculatorului. • Prima metodă foloseşte variabilele globale şi locale. În cazul variabilelor globale, memoria ce li se alocă este fixă pe tot timpul execuţiei programului. Pentru variabilele locale, programul alocă memorie în spaţiul stivei, în timpul execuţiei programului. Deşi variabilele locale sunt eficient de implementat, în C, de multe ori, utilizarea acestora, necesită cunoaşterea în avans a cantităţii de memorie necesare în fiecare situaţie. H h ig Stiva Memorie liberá pentru alocare (heap) Variabile globale (statice) Low Program M m ria e o 137
• 144. • A doua metodă de alocare a memoriei, constă în utilizarea funcţiilor de alocare dinamică malloc() şi free(). Prin această metodă, un program alocă memorie pentru diversele informaţii în spaţiul memoriei libere numită heap, plasată între programul util şi memoria sa permanentă şi stivă. Se observă că stiva creşte în jos, iar dimensiunea acesteia depinde de program. Un program cu multe funcţii recursive va folosi mult mai intens stiva în comparaţie cu un program ce nu utilizeaza funcţii recursive, aceasta deoarece adresele de retur şi variabilele locale corespunzătoare acestor funcţii sunt salvate în stivă. Funcţiile malloc() şi free() Aceste funcţii formează sistemul de alocare dinamică a memoriei în C şi fac parte din fisierul antet <stdlib.h>. Acestea lucrează împreună şi utilizează zona de memorie liberă plasată între codul program şi memoria sa permanentă (fixă) şi vârful stivei, în scopul stabilirii şi menţinerii unei liste a variabilelor memorate. De fiecare dată când se face o cerere de memorie, funcţia malloc() alocă o parte din memoria rămasă liberă. De fiecare dată când se face un apel de eliberare a memoriei, funcţia free() eliberează memorie sistemului. Declararea funcţiei malloc() se face sub forma: void *malloc (int numar_de_bytes); Aceasta întoarce un pointer de tip void, ceea ce înseamnă că trebuie utilizat un şablon explicit de tip atunci când pointerul returnat de malloc() se atribuie unui pointer de un alt tip. Dacă apelul lui malloc() se execută cu succes, malloc() va returna un pointer la primul byte al zonei de memorie din heap ce a fost alocată. Dacă nu este suficientă memorie pentru a satisfce cererea malloc(), apare un eşec şi malloc() returnează NULL. Pentru determinarea exactă a numărului de bytes necesari fiecărui tip de date, se poate folosi operatorul sizeof(). Prin aceasta, programele pot deveni portabile pe o varietate de sisteme. Funcţia free() returnează sistemului memoria alocată anterior cu malloc(). După eliberarea memoriei cu free(), aceasta se poate reutiliza folosind un apel malloc(). Declararea funcţiei free() se realizează sub forma: free(void *p); 138
• 145. Funcţia free() eliberează spaţiul indicat de p şi nu face nimic dacă p este NULL. Parametrul actual p trebuie să fie un pointer la un spaţiu alocat anterior cu malloc(), calloc() sau realloc(). Exemplu: Următorul program va aloca memorie pentru 40 de întregi, va afişa valoarea acestora, după care eliberează zona, utilizând free(): # include <stdio.h> # include <stdlib.h> void main(void) { int t, *p; p = (int *) malloc(40*sizeof(int)); if (!p) printf("Out of memory n"); //Verificati neaparat daca p este un pointer corect else { for (t=0; t<40; ++t) *(p + t) = t; for (t=0; t < 40; ++t) printf("%d", *(p + t)); free(p); } } Funcţiile calloc() şi realloc() Funcţia calloc() alocă un bloc de n zone de memorie, fiecare de dim octeţi şi setează la 0 zonele de memorie; funcţia returnează un pointer la acel bloc (adresa primului octet din bloc). Declararea funcţiei se face cu: void *calloc(unsigned int n, unsigned int dim); Funcţia realloc() primeşte un pointer la un bloc de memorie alocat în prealabil (declarat pointer de tip void) şi redimensionează zona alocată la dim octeţi (dacă este nevoie, se copiază vechiul conţinut într-o altă zonă de memorie). Declararea funcţiei se face cu: void *realloc(void *ptr, unsigned int dim); 7.2.8. Pointeri la structuri Limbajul C recunoaşte pointerii la structuri în acelaşi mod în care se recunoaşte pointerii la orice alt tip de variabilă. Declararea unui pointer la structură se face plasând operatorul * în faţa numelui unei variabile structură. De exemplu, pentru structura addr definită mai înainte, următoarea instrucţiune declară pe addr_pointer ca pointer la o dată de acest tip : struct addr *addr_pointer; O utilizare importantă a pointerilor la structură constă în realizarea apelului prin adresă într-o funcţie. 139
• 146. Când unei funcţii i se transmite ca argument un pointer la o structură, calculatorul va salva şi va reface din stivă numai adresa structurii, conducând astfel la cresterea vitezei de executare a programului. Pentru a găsi adresa unei variabile structură, se plasează operatorul & înaintea numelui variabilei structură. De exemplu, dându-se următorul fragment : struct balanta{ float balance; char name[80]; } person; struct balanta *p; /* se declara un pointer la structura */ atunci: p = &person; plasează adresa lui person în pointerul p. Pentru a referi elementul balance, se va scrie: (*p).balance Deoarece operatorul punct are prioritate mai mare decât operatorul *, pentru o referire corectă a elementelor unei structuri utilizând pointerii sunt necesare paranteze. Actualmente, pentru referirea unui element al unei variabile structură dându-se un pointer la acea variabilă, există două metode: Prima metodă utilizează referirea explicită a pointerului nume- structură şi este considerată o metoda învechită (obsolete), iar a doua metodă, modernă, utilizează operatorul săgeată -> (minus urmat de mai mare). Exemplu: Pentru a vedea cum se utilizează un pointer-struct, examinăm următorul program care afişează ora, minutul şi secunda utilizând un timer software. # include <stdio.h> void actualizeaza(); void afiseaza(), delay(); struct tm { /* se defineste structura tm */ int ore; int minute; int secunde;}; void main() {struct tm time; // Structura time de tip tm time.ore = 0; time.minute = 0; time.secunde = 0; for (;;) { 140
• 147. actualizeaza (&time); afiseaza (&time); }} void actualizeaza(t) /*Versiunea 1- referirea explicita prin pointeri */ struct tm *t; { (*t).secunde ++; if ((*t).secunde == 60) { (*t).secunde = 0; (*t).minute ++; } if ((*t).minute == 60) { (*t).minute = 0; (*t).ore ++;} if ((*t).ore == 24) (*t).ore = 0; delay();} void afiseaza(t) // Se defineste functia afiseaza() struct tm *t; { printf ("%d : ", (*t).ore); printf ("%d : ", (*t).minute); printf ("%d ", (*t).secunde); printf ("n");} void delay() /* Se defineste functia delay() */ { long int t; for (t = 1;t<140000000;++t);} Pentru ajustarea duratei întârzierii se poate modifica valoarea contorului t al buclei. Se vede ca programul defineşte o structură globală numită tm, dar nu declară variabilele. In interiorul funcţiei main() se declară structura "time" şi se iniţializează cu 00:00:00. Programul transmite adresa lui time funcţiilor actualizeaza() şi afiseaza(). În ambele funcţii, argumentul acestora este declarat a fi un pointer-structură de tip "tm". Referirea fiecărui element se face prin intermediul acestui pointer. Elementele unei structuri pot fi referite utilizând în locul operatorului ".", operatorul "->". De exemplu : (*t).ore este echivalent cu t -> ore Atunci programul de mai sus se poate rescrie sub forma: # include <stdio.h> void actualizeaza(); void afiseaza(), delay(); struct tm { /* se defineste structura tm */ int ore; int minute; int secunde;}; 141
• 148. void main() {struct tm time; // Declara structura time de tip tm time.ore = 0; time.minute = 0; time.secunde = 0; for (;;) { actualizeaza (&time); afiseaza (&time); }} void actualizeaza(t) /*Versiunea 1- referirea explicita prin pointeri */ struct tm *t; { t->secunde ++; if (t->secunde == 60) { t->secunde = 0; t->minute ++; } if (t->minute == 60) { t->minute = 0; t->ore ++;} if (t->ore == 24) t->ore = 0; delay();} void afiseaza(t) // Se defineste functia afiseaza() struct tm *t; { printf ("%d : ", t->ore); printf ("%d : ", t->minute); printf ("%d ", t->secunde); printf ("n");} void delay() /* Se defineşte funcţia delay() */ { long int t; for (t = 1;t<140000000;++t);} 7.2.9. Structuri dinamice liniare de tip listă Structura a fost introdusă ca fiind o grupă (colecţie) ordonată de date care pot fi de tipuri diferite şi care au anumite legături logice între ele. Adesea, această grupă de date se repetă de mai multe ori. Se ajunge astfel la noţiunea de tablou, ale cărui elemente sunt fiecare câte o structură. Tabloul de date definit în acest fel este şi el de acest tip nou, pe care îl mai numim şi tip structurat. După cum s-a remarcat, în exemplele de până acum am folosit structuri de tip static. Static se referă la faptul că tablourile de structuri au dimensiuni predefinite. Termenul structuri de date statice exprimă ideea că putem modifica cu uşurinţă valorile componentelor dar este foarte dificil să mărim numărul lor peste limita maximă declarată 142
• 149. înainte de compilare. Mai mult, prin ştergerea unor elemente structură dintr-un tablou de structuri obţinem goluri în tablou, pe care le putem umple numai printr-o gestiune foarte precisă a poziţiilor din tablou. Folosind pointeri la tabloul de structuri, este foarte posibil să indicăm spre un element care a fost şters. Dacă dorim o reprezentare contiguă în memorie, va trebui să compactăm (sau să defragmentăm) tabloul la fiecare ştergere a unui element de tip structură. Mai mult, dacă dorim să schimbăm ordinea în care s-au stocat elementele din tablou sau să inserăm într-o poziţie intermediară un element nou, aceaste operaţii devin foarte anevoioase. Într-un exemplu anterior am folosit secvenţa # define SIZE 100 struct addr { char name[20]; char street[30]; char city[15]; char state[10]; unsigned int zip; } addr_info[SIZE]; Rezultă că am definit un tablou static cu 100 de elemente, cu numele addr_info, la care fiecare element este o structură cu şablonul addr. Dacă în această aplicaţie, chiar în timpul execuţiei programului, constatăm că avem nevoie de mai mult de 100 de rezervări de memorie, nu există nici o posibilitate de a mări tabloul fără a modifică şi apoi recompila sursa programului. Tabloul trebuie redeclarat cu o dimansiune mai mare (în cazul nostru prin #define SIZE 200, de exemplu), apoi se recompilează programul şi abia apoi se execută cu succes. Acest lucru prezentă două inconveniente (vezi [Mocanu, 2001]): 1- Execuţia şi obţinerea rezultatelor sunt amânate şi produc întârzieri pentru modificarea programului sursă, recompilare şi reintroducerea datelor care fuseseră deja introduse până în momentul în care s-a constatat necesitatea măririi tabloului. 2- Este posibil ca programul sursă să nu fie disponibil. Eliminarea neajunsurilor prezentate mai sus se face prin implementarea listelor cu ajutorul unor structuri de date dinamice. Când apare necesitatea introducerii unui element în listă, se va aloca dinamic spaţiu pentru respectivul element, se va crea elementul prin înscrierea informaţiilor corespunzătoare şi se va lega în listă. 143
• 150. Când un element este scos din listă spaţiul de memorie ocupat de acesta va fi eliberat şi se vor reface legăturile. Structurile dinamice se construiesc prin legarea componentelor structurii, numite variabile dinamice. O variabilă dinamică ce intră în componenţa unor structuri de date dinamice (nod) prezintă în structura sa două părţi: 1. Partea de informaţie (info) ce poate aparţine unui tip simplu (int, char, float, double, etc.) conform cu necesităţile problemei. 2. Partea de legătură (link) ce conţine cel puţin un pointer de legătură (next) la o altă variabilă dinamică, de obicei de acelaşi tip, ce realizează înlănţuirea variabilelor dinamice în structuri de date dinamice. Dintre structurile de date dinamice, cele mai simple şi mai utilizate sunt listele. Lista este o structură liniară, de tipul unui tablou unidimensional (vector), care are un prim element şi un ultim element. Celelalte elemente au un predecesor şi un succesor. Elementele unei liste se numesc noduri. La rândul lor, listele pot fi grupate în mai multe categorii, cele mai importante fiind listele simplu înlănţuite, listele circulare şi listele dublu legate. Principalele operaţii care se pot efectua asupra unei liste sunt: crearea listei, adăugare/ştergere/modificare au unui element (nod), accesul la un element şi ştergerea în totalitate a listei. Lista simplu înlănţuită este cel mai simplu tip de listă din punctul de vedere al legării elementelor: legătura între elemente este într-un singur sens, de la primul către ultimul. Există un nod pentru care pointerul spre nodul următor este NULL. Acesta este ultimul nod al listei simplu înlănţuite (sau simplu legate). De asemenea, există un nod spre care nu pointează nici un alt nod, acesta fiin primul nod al listei. O listă simplu înlănţuită poate fi identificată în mod unic prin primul element al listei. Determinarea ultimului element se poate face prin parcurgerea secvenţială a listei până la întâlnirea nodului cu pointerul spre nodul următor cu valoarea NULL. 144
• 151. info info info next next NULL Listă simplă înlănţuită Listele circulare sunt un alt tip de liste pentru care relaţia de precedenţă nu mai este liniară ci ultimul element pointează către primul. Procedurile necesare pentru crearea şi utilizarea unei liste circulare sunt extrem de asemănătoare cu cele de la liste liniare, cu singura deosebire că ultimul element nu are adresa de pointer vid (NULL) ci adresa primului element. info info info next next next Listă circulară Listele dublu legate sunt utile în rezolvarea unor probleme care necesită parcurgeri frecvente ale structurilor dinamice în ambele sensuri. Ele pot fi privite ca structuri dinamice ce combină două liste liniare simplu înlănţuite ce partajează acelaşi câmp comun info, una fiind imaginea în oglindă a celeilalte. info info info next next NULL NULL previous previous Listă dublu legată Pointerul next indică spre următorul nod, iar câmpul previous indică spre câmpul anterior. Vom prezenta în continuare modul în care putem proiecta funcţiile principale care acţionează asupra unei structuri dinamice. Pentru aceasta vom utiliza două variabile globale de tip pointer, una care pointează spre primul nod al listei iar cealaltă spre ultimul nod al listei. Vom denumi aceste variabile first respectiv last. Particularizările se vor face pe exemplul bazei de date construite anterior. Tipul unui nod se declară în felul următor: 145
• 152. General Particular struct tip_nod { struct addr { declaratii char name[20]; struct tip_nod *next; char street[30]; }; char city[15]; char state[10]; unsigned int zip; struct addr *next; }; Atât la crearea unei liste cât şi la inserarea unui nod se va apela funcţia malloc() pentru a rezerva spaţiu de memorie pentru un nod. Zona alocată prin intermediul funcţiei malloc() se poate elibera folosind funcţia free(). Propunem conceperea unui program asemănător cu programul de exploatare a unei baze de date conceput anterior dar care să folosească în locul unui tablou static o listă simplu înlănţuită. Programul poate fi extins ulterior pentru structuri dinamice mai complexe, care să folosească liste dublu înlănţuite sau circulare. Programul va avea următoarele facilităţi: 1. Crearea unei liste simplu înlănţuite în memorie (pentru prima oară). 2. Exploatarea unei liste simplu înlănţuite în memorie: 2.1. Inserarea unor înregistrări (noduri): a) Inserarea unui nod înaintea primului nod al listei b) Inserarea unui nod înainte/după un nod intern al listei c) Inserarea unui nod după ultimul nod al listei (adăugare la coada listei) 2.2. Ştergerea unor înregistrări a) Ştergerea primului nod al listei b) Ştergerea unui nod intern al listei c) Ştergerea ultimului nod al listei 3. Salvarea din memorie pe disc a unei liste simplu înlănţuite 4. Citirea de pe disc în memorie a bazei de date salvate anterior 5. Afişarea pe ecran, înregistrare cu înregistrare, a bazei de date conţinute în lista dinamică. 146
• 153. Programul este prevăzut cu o intefaţă simplă prin care utilizatorul poate alege dintre opţiunile pe care le are la dispoziţie. Interfaţa este sub forma unui meniu din care, prin tastarea iniţialelor comenzilor, acestea se lansează în execuţie. Vom descrie pe rând funcţiile care îndeplinesc sarcinile enumerate mai sus. Pentru o mai bună proiectare a programului, vom folosi atât modularizarea internă prin construirea mai multor funcţii cât şi o modularizare externă prin izolarea programului principal de restul funcţiilor care se folosesc. Bază de date cu listă simplu înlănţuită Afişare pe ecran a înregistrărilor Interfaţa cu din baza de date utilizatorul Comenzi pentru citire/scriere Comenzi pentru procesarea pe disc listei dinamice Citire de Salvarea pe pe disc în disc a listei Inserare Ştergere lista dinamice din dinamică memorie - prima înregistrare - ultima înregistrare - înregistrare intermediară Exemplu: Programul principal bd_main.c # include "local.h" void main() { char choice; for (; ;) { choice = menu(); switch (choice) { case 'c' : create_list(); break; case 'l' : loadf_list(); break; case 's' : savef_list(); break; 147
• 155. Prin cele două fişiere header cele două fişiere sursă bd_main.c şi bd_bib.c se pot compila împreună, rezultând un singur fişier executabil. Interfaţa cu utilizatorul Aceasta constă într-un meniu principal, care permite accesarea funcţiilor principale, şi din două submeniuri pentru operaţiile de ştergere şi inserare de înregistrări. /* Functia menu() principala */ char menu() { char s[5],ch; do { printf ("n(C)reate new listn"); printf ("(L)oad list from filen"); printf ("(S)ave to filen"); printf ("(D)isplay listn"); printf ("(I)nsert recordn"); printf ("(E)rasen"); printf ("(Q)uitn"); printf (" Alegeti optiunea: "); gets(s); ch=s[0]; } while (!strrchr("clsdieq",ch)); return tolower(ch); } //meniu functia de stergere char menu_del() { char s[5],ch; do { printf ("(E)ntire list deleten"); printf ("(F)irst record deleten"); printf ("(L)ast record deleten"); printf ("(I)ntermediate deleten"); printf ("(Q)uitn"); printf (" Option ? "); gets(s);ch=s[0]; } while (!strrchr("efliq",ch)); return tolower(ch); } //meniu functia de inserare char menu_insert() { char s[5],ch; do { printf ("(F)irst record insertn"); printf ("(L)ast record insertn"); printf ("(I)ntermediate insertn"); printf ("(Q)uitn"); printf (" Option ? "); 149
• 157. loadf_nod(), funcţia savef_list() nu face nici un apel la o altă funcţie definită de utilizator: /* Functia savef_list() */ int savef_list() { TNOD *p; if ((fp = fopen("maillist", "wb")) == NULL) { printf (" Cannot open filen ");return 0;} p=first; do { if(fwrite(p, sizeof(TNOD), 1,fp) !=1) {printf (" File write error n "); fclose (fp);return 0;} p=p->next;} while (p!=NULL); fclose (fp);return 1;} Crearea unei liste simple înlănţuite La început variabilele first şi last au valoarea NULL, lista fiind vidă. Crearea unei liste se face prin funcţia create_list. Ea returnează 0 pentru eroare şi 1 dacă operaţia reuşeşte. Prin această funcţie se iniţializează o listă dinamică în memorie, fără a face o citire a unei baze de date create anterior. Aceasta este prima funcţie care se apelează când construim o bază de date nouă. /* Functia create_list() new */ void create_list() { int n=sizeof(TNOD); char ch='c'; TNOD *p; first=last=NULL; while ((p=(TNOD *)malloc(n))!=NULL) {p=add_nod(p); if (first==NULL){ /* prima creare */ first=last=p; first->next=NULL;} else { last->next=p; last=p; last->next=NULL;} printf ("Exit? (x): "); if ((ch=getchar())=='x') break;} if (p==NULL){ printf("Memorie insuficienta pentru listan"); free(p);}} Afişarea listei dinamice din memorie 151
• 158. Prin această operaţie, realizată de funcţia display_list(), se afişează secvenţial toate înregistrările conţinute în nodurile listei pornind de la prima şi terminând cu ultima. // functie de afisare antet void disp_antet() { printf("n%20s","Name"); printf("%30s","Street"); printf("%15s","City"); printf("%10s","State"); printf("%5sn","Zip");} // afisare o singura inregistrare (nod) void disp_nod(TNOD *p) { printf("%20s",p->name); printf("%30s",p->street); printf("%15s",p->city); printf("%10s",p->state); printf("%5d",p->zip);} /* Functia display_list() */ void display_list() { TNOD *p; disp_antet(); p=first; if (p!=NULL) do { disp_nod(p); p=p->next; getchar();} while (p!=NULL); else printf("Lista vida !n");} Funcţia display_list() apelează la funcţia disp_nod() care afişeză o singură înregistrare. Dacă este nevoie de afişarea unui cap de tabel (antet) se apelează la funcţia disp_antet(). Inserarea unor noduri în lista dinamică Această operaţie presupune introducerea unor noduri (înregistrări) fie la începutul listei înaintea primului nod, fie la sfârşitul său (adăugare) după ultimul nod, fie între două noduri vecine nesituate la extremităţi. Funcţiile listate mai jos realizează aceste sarcini. // functia de inserare void insert() { char choice; for (; ;) { choice = menu_insert(); switch (choice) { case 'f' : ins_first(); break; case 'l' : ins_last(); break; 152
• 159. case 'i' : ins_int(); break; case 'q' : break;} break;}} /* Functia insert_last() */ void ins_last() { int n=sizeof(TNOD); char ch='c',s[2]; TNOD *p; /* ne pozitionam pe ultimul nod */ p=first; while (p->next!=NULL) p=p->next; // se creaza lista in memorie while (ch!='x') { p=(TNOD *)malloc(n); last->next=p; p=add_nod(p); p->next=NULL; last=p; printf("Exit? (x): "); gets(s);ch=s[0];}} /* Functia insert_first() */ void ins_first() { int n=sizeof(TNOD); char ch='c',s[2]; TNOD *p; while (ch!='x') { p=(TNOD *)malloc(n); p->next=first; p=add_nod(p); first=p; printf("Exit? (x): "); gets(s);ch=s[0];}} // inserare dupa inregistrarea afisata void ins_int() { TNOD *p, *pi; char ch='n', s[2]; disp_antet(); p=first; while ((p!=NULL)&&(ch!='y')) { disp_nod(p); printf("Here ? [y]: "); gets(s);ch=s[0]; if (ch!='y') p=p->next;} pi=(TNOD *)malloc(sizeof(TNOD)); pi=add_nod(pi); pi->next=p->next; p->next=pi;} La inserarea unui nod, este nevoie ca acesta să fie legat de cele între care se introduce cu ajutorul pointerilor next. Dacă se doreşte ca 153
• 160. nodul inserat să fie primul sau ultimul din listă, este nevoie să modificăm pointerii globali first şi last. Ştergerea unor noduri din lista dinamică Nodurile pe care dorim să le ştergem pot fi interioare sau se pot afla la extremităţi. În acest caz, este nevoie să modificăm pointerii first şi last . În toate cazurile este nevoie să refacem legăturile distruse după dispariţia prin ştergere a unor noduri. §tergerea nu este efectivă, ci numai se refac legăturile şi se eliberează cu funcţia free() zona de memorie alocată în prealabil (prin inserare sau creare) cu funcţia malloc(). Mai mult, avem opţiunea de a şterge întreaga listă din memorie în scopul înlocuirii în totalitate a datelor conţinute în înregistrări. // funcţia de ştergere void erase() { char choice; for (; ;) { choice = menu_del(); switch (choice) { case 'e' : del_list(); break; case 'f' : del_first(); break; case 'l' : del_last(); break; case 'i' : del_int(); break; case 'q' : break;} break;}} // se sterge intreaga lista si se elibereaza memoria void del_list() { TNOD *p,*pu; p=first;pu=p->next; while (pu!=NULL) {free(p); p=pu;pu=pu->next;} first=NULL;last=NULL;} // sterge prima inregistrare void del_first() { int n=sizeof(TNOD); char ch='c',s[2]; TNOD *p,*pu; while (ch!='x') { p=first;pu=p->next; free(p); first=pu; printf("Exit? (x): "); gets(s);ch=s[0];}} // stergere ultima inregistrare void del_last() { int n=sizeof(TNOD); 154
• 161. char ch='c',s[2]; TNOD *p; /* ne pozitionam pe penultimul nod */ while (ch!='x') { p=first; while (p->next!=last) p=p->next; free(p->next); p->next=NULL; last=p; printf("Deleted. Exit? (x): "); gets(s);ch=s[0];}} // stergere inregistrare intermediara void del_int() { TNOD *p, *pa, *pu; char ch='n', s[2]; disp_antet(); pa=first;p=pa->next;pu=p->next; while ((p!=last)&&(ch!='y')) { disp_nod(p); printf("Delete ? [y]: "); gets(s);ch=s[0]; if (ch='y') {pa->next=pu; free(p);} else {pa=p;p=pu;pu=pu->next;}}} Capitolul VIII FUNCŢII 8.1. Forma generală a unei funcţii Principalul mijloc prin care se pot modulariza programele C este oferit de conceptul de funcţie (unele funcţii standard au fost deja folosite pentru diverse operaţii). În C orice funcţie "întoarce" (returnează), după apel, o valoare al cărui tip trebuie cunoscut. În practică, însă, de multe ori valorile returnate de funcţii sunt ignorate. Standardul limbajului C permite chiar declararea explicită a funcţiilor care nu returnează valori ca fiind de tip void. În C o funcţie poate fi definită, declarată şi apelată. Definirea unei funcţii C se realizează după următorul format general: tip nume_funcţie (lista_parametri) declaraţii_parametri { 155
• 162. declaraţii _locale instrucţiuni } sau, o formă mai nouă adoptată de ANSI-C în 1989: tip nume_funcţie (declaraţii _parametri) { declaraţii _locale instrucţiuni } Tipul unei funcţii corespunde tipului valorii pe care funcţia o va returna utilizând instrucţiunea return. Acesta poate fi un tip de bază (char, int, float, double etc.) sau un tip derivat (pointer, structură etc.). Dacă pentru o funcţie nu se specifică nici un tip, atunci, implicit se consideră că funcţia întoarce o valoare întreagă. Lista parametrilor, lista_parametri, este o listă de nume de variabile separate prin virgulă care vor primi valorile argumentelor în momentul apelării acesteia. Tipul acestor parametri este descris fie în paragraful declaraţii_parametri, fie direct în lista parametrilor. Lista parametrilor este închisă între paranteze. Chiar dacă o funcţie nu are parametri, parantezele nu trebuie să lipsească. De exemplu, funcţia max(a, b), care returnează cel mai mare dintre numerele întregi a şi b, se poate defini sub forma: int max(a, b) sau int max(int a, int b) int a, b; { { if (a > b) if (a > b) return (a); return (a); else else return (b); return (b); } } În cazul în care tipul parametrilor formali ai unei funcţii nu se declară, atunci ei sunt consideraţi implicit de tip int. În cazul compilatoarelor moderne, programul următor va genera două avertismente. Exemplu: # include <stdio.h> float max(); // Prototipul functiei max() float x; void main() { x = max(3, 4); printf("max= %d",x); 156
• 163. } float max(a, b) float a, b; { if (a > b) return (a); else return (b); } În urma compilării va rezulta: Compiling... test.c C:cpp_examplestest.c(11): warning C4244: 'return': conversion from 'int' to 'float', possible loss of data C:cpp_examplestest.c(13): warning C4244: 'return': conversion from 'int' to 'float', possible loss of data test.obj - 0 error(s), 2 warning(s) Aceste avertismente sunt generate deoarece, la primul pas al compilării, la parcurgerea liniei de declarare a funcţiei: float max(a,b) parametrii formali a şi b sunt consideraţi de tip întreg. Programul, modificat ca mai jos, va duce la o compilare fără probleme: Exemplu: # include <stdio.h> float max(); float x; void main() { x = max(3.2, 4.1); printf("max= %dn",x); } float max(float a, float b) { if (a > b) return (a); else return (b); } Tot corect va rula şi programul: # include <stdio.h> int max(); int x; void main() { x = max(-3,4); printf("max= %dn",x); } int max(a,b) { if (a > b) return (a); 157
• 164. else return (b); } Se observă că nu mai este nevoie de declararea explicită a parametrilor formali de tip întreg a şi b. 8.2. Reîntoarcerea dintr-o funcţie Mai intâi precizăm că instrucţiunea return are două utilizări importante: ♦ return determină ieşirea imediată din funcţia în care se află instrucţiunea şi reîntoarcerea în programul apelant; ♦ return poate fi folosită pentru a întoarce o valoare. Reîntoarcerea dintr-o funcţie în programul apelant (funcţia apelantă) se poate face în două moduri: a) După parcurgerea codului corespunzător funcţiei se revine în programul apelant la instrucţiunea imediat următoare. Exemplu: Aceasta funcţie tipăreşte un şir în ordine inversă: # include <string.h> void afis_invers(char s[]); void main() { char s[10]; printf("Introduceti un sir de caracrere de la tastatura (max 10)n"); scanf("%s",s); afis_invers(s); } void afis_invers(char s[]) { register int t; for (t = strlen(s)-1; t >= 0; t--) printf("%c", s[t]); printf("n"); } b) Al doilea mod de întoarcere dintr-o funcţie se realizează utilizând funcţia return. Funcţia return poate fi folosită fără nici o valoare asociată. Exemplu: Funcţia următoare afişează rezultatele ridicării unui număr întreg la o putere întreagă pozitivă: power (baza, exp){ int baza, exp, i; scanf("%d %d", &baza, &exp); if (exp < 0) return; /* Functia se termina daca exp e negativ */ i = 1; for (; exp; exp--) i = baza * i; 158
• 165. printf (" Rezultatul este %d n", i); } Dacă exponentul exp este negativ, instrucţiunea return determină terminarea funcţiei înainte ca sistemul să întâlnească }, dar nu returnează nici o valoare. O funcţie poate conţine mai multe instrucţiuni return, care pot simplifica anumite algoritme. 8.3. Valori returnate Toate funcţiile, cu excepţia celor daclarate a fi de tip void, returnează o valoare. Această valoare este fie explicit specificată prin return, fie este zero dacă nu se utilizează instrucţiunea return. Dacă o funcţie este declară ta ca fiind de tip void, aceasta poate fi folosită în orice expresie C. O funcţie nu poate fi membrul stâng într-o expresie de atribuire. De exemplu, instrucţiunea: swap(x,y) = 100; este greşită. Funcţiile care nu sunt de tip void se pot împărţi în trei categorii: 1) Funcţii "pure" sunt funcţiile care efectuează operaţii asupra argumentelor şi returnează o valoare de bază pe acea operaţie. Exemplu: sqrt() şi sin() returnează respectiv radăcina pătrată şi sinusul argumentului. 2) A doua categorie de funcţii sunt cele care manipulează informaţii şi întorc o valoare care arată reuşita sau eşecul acestei manipulări. Un exemplu este fwrite() folosită pentru a scrie informaţii pe disk. Dacă scrierea se face cu succes, fwrite() întoarce numărul de octeţi înscrişi (ceruţi să se înscrie); orice altă valoare indică apariţia unei erori. 3) A treia categorie de funcţii sunt cele care nu trebuie să întoarcă o valoare explicită. De exemplu, funcţia printf() întoarce numărul de caractere tipărite, număr care, de obicei, nu are o utilizare ulterioară. Dacă pentru o funcţie care returnează o valoare nu se specifică o operaţie de atribuire, calculatorul va ignora valoarea returnată. Exemplu: Considerăm următorul program care utilizează funcţia mul(): # include <stdio.h> mul(); void main (void){ int x, y, z; x = 10; y = 20; z = mul(x, y); //- primul apel al lui mul() 159
• 166. printf("%dn", mul(x,y)); /- al doilea apel al lui mul() mul(x,y); //- al treilea apel al lui mul() } mul(a,b) // Se defineste functia mul() { return a*b; } Linia a atribuie valoarea returnată de mul() lui z. În linia b, valoarea returnată nu este atribuită, dar aceasta este utilizată de printf(). In linia c valoarea returnată este pierdută, deoarece nu se atribuie nici unei variabile ce va fi utilizată în altă parte a programului. 8.4. Domeniul unei funcţii În C, fiecare funcţie este un bloc de instrucţiuni. Codul unei funcţii este propriu acelei funcţii şi nu poate fi accesat (utilizat) prin nici o instrucţiune din orice altă funcţie, cu excepţia instrucţiunii de apel al acelei funcţii. (De exemplu, nu putem utiliza goto pentru a realiza saltul dintr-o funcţie în mijlocul unei alte funcţii). Blocul de instrucţiuni care descrie corpul unei funcţii este separat de restul programului şi dacă acesta nu utilizează variabile globale sau date, el nici nu poate afecta, nici nu va fi afectat de alte părţi ale programului. Codul şi datele care sunt definite în interiorul unei funcţii nu pot interacţiona cu codul şi datele definite în altă funcţie, deoarece cele două funcţii au scopuri diferite. În cadrul unei funcţii se deosebesc trei tipuri de variabile, astfel: variabile locale, parametri formali şi variabile globale. Domeniul unei funcţii determină modul în care alte părţi ale programului pot avea acces la aceste trei tipuri de variabile stabilind şi durata de viaţă a acestora. 8.4.1. Variabile locale Variabilele declarate în interiorul unei funcţii se numesc variabile locale. Variabilele locale pot fi referite numai prin instrucţiuni interioare blocului în care au fost daclarate aceste variabile. Variabilele locale nu sunt cunoscute în afara blocului în care au fost daclarate, domeniul lor limitându-se numai la acest bloc. Mai exact, variabilele locale există numai pe durata execuţiei blocului de cod în care acestea au fost daclarate; deci o variabilă locală este creată la intrarea în blocul său şi distrusă la ieşire. De obicei, blocurile de program în care se declară variabilele locale sunt funcţiile. Implicit, o variabilă locală este auto, deci se stochează în memoria stivă. Ea 160
• 167. poate fi declarată şi register, caz în care se stochează în regiştrii interni ai microprocesorului sau poate fi declarată static, caz în care se stochează în memoria de date sau statică, valoarea sa păstrându-se şi la ieşirea din funcţie. Exemplu: func1() { int x; x = 10; } func2() { int x; x = -199; } Aici variabila întreagă x este declarată de două ori, o dată în func1() şi o dată în func2(). x din func1() nu are nici o legatură cu x din func2(), deoarece fiecare x este cunoscut numai în blocul în interiorul căruia a fost declarat. Limbajul C conţine cuvântul cheie auto, care poate fi folosit pentru declararea de variabile locale. Cu toate acestea, întrucât C presupune că toate variabilele neglobale sunt prin definiţie (implicit) variabile locale, deci au atributul auto, acest cuvânt cheie nu se utilizează. De obicei, variabilele locale utilizate în interiorul unei funcţii se declară la începutul blocului de cod al acestei funcţii. Acest lucru nu este neapărat necesar, deoarece o variabilă locală poate fi declarată oriunde în interiorul blocului în care se utilizează, dar înainte de a fi folosită. Exemplu: Considerăm următoarea funcţie: func (){ char ch; printf (" Continuam (y / n) ? : "); ch = getche(); //Se preia optiunea de la tastatura /* Daca raspunsul este yes */ if (ch == 'y') { char s[80]; /* s se creeaza numai dupa intrarea in acest bloc */ printf (" Introduceti numerele: n "); gets (s); prelucreaza_nr (s); /* Se prelucreaza numerele */ } } Aici, func() creează variabila locală s la intrarea în blocul de cod a lui if şi o distruge la ieşirea din acesta. Mai mult, s este cunoscută numai în interiorul blocului if şi nu poate fi referită din altă parte, chiar din altă parte a funcţiei func() care o conţine. 161
• 168. Deoarece calculatorul creează şi distruge variabilele locale la fiecare intrare şi ieşire din blocul în care acestea sunt daclarate, conţinutul lor este pierdut o dată ce calculatorul părăseste blocul. Astfel, variabilele locale nu pot reţine valorile lor după încheierea apelului funcţiei. 8.4.2. Parametri formali Dacă o funcţie va folosi argumente, atunci aceasta trebuie să declare variabilele care vor accepta (primi) valorile argumentelor. Aceste variabile se numesc parametri formali ai funcţiei. Parametrii formali ai funcţiei se comportă ca orice altă variabilă locală din interiorul funcţiei. Declararea parametrilor formali se face după numele funcţiei şi înaintea corpului propriu-zis al funcţiei. Exemplu: /* Funcţia următoare întoarce 1 dacă caracterul c aparţine şirului s altfel întoarce 0 */ # include <stdio.h> int func (char s[10],char c) { while (*s) if (*s == c) return 1; else s++; return 0; } void main() { char s[10], c; scanf("%c %s", &c, &s); if (func(s, c)) printf("Caracterul se afla in sirn"); else printf("Caracterul NU se afla in sirn"); } Funcţia func() are doi parametri: s şi c. Aceasta funcţie întoarce 1 dacă caracterul c aparţine şirului şi 0 dacă c nu aparţine şirului. Precizăm că argumentele cu care se va apela funcţia trebuie să aibă acelaşi tip cu parametrii formali declaraţi în funcţie. Aceşti parametri formali pot fi utilizaţi ca orice altă variabilă locală. Un al doilea mod (recomandat de ANSI-C în 1989) de a declara parametrii unei funcţii constă în declararea completă a fiecărui parametru în interiorul parantezelor asociate funcţiei. De exemplu, declararea parametrilor funcţiei func() de mai sus se poate face şi sub forma: func (char *s, char c) { . . . . . . . . . . . } 162
• 169. 8.4.3. Variabile globale Spre deosebire de variabilele locale, variabilele globale sunt cunoscute întregului modul program şi pot fi utilizate de orice parte a programului. De asemenea, variabilele globale vor păstra valorile lor pe durata execuţiei complete a programului, deci se stochează în memoria statică. Variabilele globale se creează prin declararea lor în afara oricărei funcţii (inclusiv main()). Variabilele globale pot fi plasate în orice parte a programului, evident în afara oricărei funcţii, şi înainte de prima lor utilizare. De obicei, variabilele globale se plasează la începutul unui program, mai exact înaintea funcţiei main(). Exemplu: int count; /* count este global */ void main (void) { count = 100; func1(); } func1() /* Se defineste functia func1() */ { int temp; /* temp preia variabila globala count */ temp = count; func2(); printf ("count is %d",count); // Se va afisa 100 } func2() /* se defineste functia func2() */ { int count; /* count este local */ for (count = 1; count < 10; count ++) printf ("%2dn" , count); } Se observă că, deşi nici funcţia main() şi nici funcţia func1() nu au declarat variabila count, ambele o folosesc. Funcţia func2() a declarat o variabilă locală count. Când se referă la count, func2() se va referi numai la variabila locală count şi nu la variabila globală count declarată la începutul programului. Reamintim că, dacă o variabilă globală şi o variabilă locală au acelaşi nume, toate referirile la numele variabilei în interiorul funcţiei în care este declarată variabila locală se vor efectua numai asupra variabilei locale şi nu vor avea nici un efect asupra variabilei globale. Deci, o variabilă locală ascunde o variabilă globală. Variabilele globale sunt memorate într-o zonă fixă de memorie destinată special acestui scop (memoria statică), rămânând în această zonă pe parcursul întregii execuţii a programului. 163
• 170. Variabilele declarate explicit extern sunt tot variabile globale, dar accesibile nu numai modulului program în care au fost declarate, ci şi tuturor modulelor în care au fost declarate de tip extern. Un alt exemplu util şi pentru înţelegerea lucrului cu pointeri este următorul: se declară o variabilă globală x, şi apoi două variabile locale cu acelaşi nume, x, care se vor “ascunde“ una pe cealaltă. Pentru a vedea şi modul în care se stochează în memorie aceste variabile, vom afişa şi locaţiile de memorie pentru fiecare tip de variabilă x, precum şi pentru pointerul p corespunzător. Reţinem că, în general, dacă: p = &x => p = &x = &(*p) = (&*)p = p *p = *(&x) = (*&)x = x Faptul că * respectiv & sunt operaţiuni complementare (inverse una celeilalte) se observă din relaţiile de mai sus, din care deducem că: &* = *& = identitate &(*z)= z; // z este pointer *(&z) = z; // z este o variabila de un anume tip (z este de alt tip în fiecare din egalităţile de mai sus, pointer sau variabilă). Adresa Memoria .. .. .. .. .. .. .. .. . &x=a re ax d s x .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. &p d s p =a re a p =&x .. .. .. .. .. .. .. .. &q d s q =a re a q=&p # include <stdio.h> int x = 34; /* x este global */ void main(void) { int *p = &x, *r; /* p este o variabila pointer catre un intreg */ void **q; printf("x=%d &x=%p *p=%d p=%p &p=%pn",x,&x, *p, p, &p); { int x; x = 1; /* Acest prim x este o variabila locala ce o ascunde pe cea globala */ 164
• 171. p = &x; printf("x=%d &x=%p *p=%d p=%p &p=%pn",x,&x, *p, p, &p);} { int x; /* Acest al doilea x ascunde prima variabila locala x */ x = 2; // Se atribuie valoarea 2 acestui x p = &x; /* Pointerul p retine adresa variabilei x */ printf("x=%d &x=%p *p=%d p=%p &p=%pn",x, &x, *p, p, &p); q = &p; // q retine adresa pointerului p r = *q; // r retine valoarea de la adresa q /*Cum q = &p => r = *(&p) = p => *r = *p = x */ printf("q=%p *q=%p **q=%d &q=%pn", q, *q, *r, &q); } } În urma execuţiei programului, obţinem următorul rezultat: x=34 &x=00426A54 *p=34 p=00426A54 &p=0065FDF4 x=1 &x=0065FDE8 *p=1 p=0065FDE8 &p=0065FDF4 x=2 &x=0065FDE4 *p=2 p=0065FDE4 &p=0065FDF4 q=0065FDF4 *q=0065FDE4 **q=2 &q=0065FDEC Prin declaraţia int *p = &x; variabila p este declarată variabilă pointer către o dată de tip întreg şi este iniţializată cu adresa variabilei spre punctează, anume x. Pointerul q este declarat ca un pointer la un alt pointer. Denum. Tipul Caracteristici ale Caracteristici ale variabilei Variab. variabilei variabilei pointer ataşate Adresa &x Valoare Adresa &p Valoarea p *p x int x globală 00426A54 34 0065FDF4 00426A54 34 int x locală 0065FDF0 1 0065FDF4 0065FDF0 1 int x locală 0065FDEC 2 0065FDF4 0065FDEC 2 p pointer p = &x *p = x local q pointer q = &p *q = p &q= q= **q local 0065FDEC 0065FDF4 =2 Acelaşi lucru se face pentru celelalte două variabile locale. Din interpretarea rezultatelor de mai sus putem trage următoarele concluzii. Spre exemplu, printf(“%d”,x); este echivalentă cu printf(“%d”,*p); scanf(“%d”,&x); este echivalentă cu scanf(“%d”,p); dacă în prealabil s-a făcut atribuirea p = &x; Se mai observă cum pointerul p, care iniţial indica spre variabila globală x, este încărcat cu adresa de memorie 4336176 165
• 172. (00426A54 H), pe când în cazurile când indica variabilele locale x se alocau adresele de memorie 6684140 (0065FDF0 H) şi 6684144 (0065FDEC H), adrese adiacente, la un interval de 4 octeţi, atât cât sunt alocaţi pentru variabila de tip întreg. Se observă că variabila globală se află într-o altă zonă de memorie decât variabilele locale. Modul de lucru cu pointerii este scos în evidenţă prin instrucţiunile: q=&p; // q retine adresa pointerului p r=*q; // r retine valoarea de la adresa q // q=&p => r = *(&p) = p => *r = *p = x printf("q=%p *q=%p **q=%d &q=%pn",q,*q,*r,&q); prin care se iniţializează pointerul q cu adresa pointerului p, apoi pointerul r va primi valoarea *q, adică valoarea p. Una din principalele caracteristici a limbajelor structurate o constituie compartimentarea codului şi a datelor. În C, compartimentarea se realizează folosind variabile şi funcţii. De exemplu, pentru scrierea unei funcţii mul() care determină produsul a doi întregi se pot utiliza două metode, una generală şi una specifică, astfel: General : Specific : mul (x, y) int x, y; int x, y mul () { return (x * y);} { return (x * y);} Când se doreşte realizarea produsului a oricăror doi întregi x şi y se utilizează varianta generală a funcţiei, iar când se doreşte produsul numai al variabilelor globale x şi y se utilizează varianta specifică. Exemplu: # include <stdio.h> # include <string.h> int count; // count este global intregului program play(); // Prototipul pentru functia play() void main(void) { char sir[80]; // sir este variabila locala a functiei main() printf("Introduceti un sir : n"); gets(sir); play(sir); } play(char *p) // Se declara functia play() { // p este local functiei play() if (!strcmp(p, "add")) { 166
• 173. int a,b; /* a si b sunt locale blocului if din interiorul functiei play()*/ scanf ("%d %d", &a, &b); printf ("%d n", a+b); } // int a, b nu sunt cunoscute sau evidente aici else if(!strcmp(p,"beep")) printf("%c",7); } 8.5. Apelul funcţiilor Apelul unei funcţii înseamnă referirea funcţiei, împreună cu valorile actuale ale parametrilor formali, precum şi preluarea valorii returnate, dacă este necesar. La apelul funcţiei, tipul argumentelor trebuie să fie acelaşi cu cel al tipului parametrilor formali ai funcţiei. Dacă apar nepotriviri de tip (de exemplu, parametrul formal al funcţiei este de tip int, iar apelul funcţiei foloseşte un argument de tip float) de obicei, compilatorul C nu semnalizează eroare, dar rezultatul poate fi incorect. În C transmiterea argumentelor de la funcţia apelantă spre funcţia apelată se face prin valori sau prin adrese. a) În cazul transmiterii argumentului prin valoare, se realizează copierea (atribuirea) valorilor fiecărui argument în (la) câte un parametru formal al funcţiei apelate. Exemplu: Se apelează o funcţie ce calculeaza pătratul unui număr întreg. # include <stdio.h> square(); // Prototipul functiei sqrt() void main(void) { int t = 10; printf("%d %dn", t, square(t)); } square(x) // Se declara functia sqrt() int x; { x = x*x; return(x); } Se observă că prin această metodă, schimbările survenite asupra parametrului formal x nu afectează variabila utilizată pentru apelul funcţiei (schimbările lui x nu modifică în nici un fel pe t). b) Dacă transmiterea argumentului se realizează prin adrese, atunci la apelul funcţiei în loc de valori se folosesc adrese, iar în definiţie, parametrii formali se declară ca pointeri. Exemplu: O funcţie swap() care schimbă valorile a două variabile reale se poate defini astfel: void swap(float *x, float *y){ 167
• 174. float temp; temp = x; /* temp preia valoarea de la adresa x */ *x = *y; /* valoarea de la adresa y este copiata la adresa x */ y = temp; /* la adresa y se copiaza valoarea lui temp */ } Se observă că parametrii formali ai funcţiei swap() sunt pointeri la float. Programul următor arată modul de apel al acestei funcţii. # include <stdio.h> void swap(float *x,float *y); void main(void) { float x, y; // x si y sunt de tip float scanf("%f,%f",&x,&y);/*Se introduc de la tastatura doua numere reale separate prin virgula*/ printf ("x = %f, y = %f n ",x,y); swap(&x,&y); /*Se apeleaza functia swap() avand ca argumente adresele lui x si y */ printf("x = %f, y = %f n ",x,y); } Prin &x şi &y, programul transferă adresele lui x şi y funcţiei swap() şi nu valorile lui x şi y. Un apel combinat, valoare-referinţă este prezentat în exemplul următor: # include <stdio.h> void f(); void main (void) { int x = 1, y = 1; printf("x = %d, y = %d n", x, y); f(x,&y);} void f(int val, int *ref) { val++; (*ref)++; printf("x = %d, y = %d n",val,*ref); } 8.6. Apelul funcţiilor având ca argumente tablouri Când se apelează o funcţie având ca argument un tablou, acesteia i se va transmite un pointer la primul element al tabloului. Reamintim că în C numele unui tablou fără nici un indice este un pointer la primul element al tabloului. Deci, un argument de tipul T[ ] (vector de tipul T) va fi convertit la T * (pointer de tipul T). Rezultă că vectorii, ca şi tablourile multidimensionale, nu pot fi transmise prin valoare. Aceasta înseamnă că declararea parametrului formal trebuie 168
• 175. să fie compatibilă tipului pointer. Există trei moduri de a declara un parametru care va primi un pointer la un tablou (vector). a) Parametrul formal poate fi declarat ca un tablou, astfel: # include <stdio.h> display(); // Prototipul functiei display() void main(void) { int v[10], i; for (i = 0; i < 10; ++i) v[i] = i; display(v);} display(num) // Se defineste functia display() int num[10]; { int i; for (i = 0; i < 10; i++) printf ("%d", num[i]); } Chiar dacă acest program declară parametrul num ca pe un vector de 10 întregi, compilatorul C va converti automat pe num la un pointer la întreg, deoarece parametrul nu poate primi întregul tablou (vector). b) O a doua cale de a declara un parametru vector (tablou), constă în a specifica parametrul ca pe un vector fără dimensiune: display(int num[]) { int i; for(i = 0; i < 10; i++) printf ("%d",num[i]); } Aceasta funcţie declară pe num ca fiind un vector de întregi cu dimensiune necunoscută. Deoarece limbajul C nu verifică dimensiunea vectorilor, dimensiunea actuală a vectorului este irelevantă ca parametru al funcţiei. §i de aceasta dată, num va fi convertit la un pointer la întreg. c) Ultima metodă prin care se poate declara un parametru tablou este ca pointer, astfel: display(int *num) { int i; for (i = 0; i < 10; i++) printf ("%d", num[i]); } Limbajul C permite acest tip de declaraţie deoarece putem indexa orice pointer utilizând []. Toate cele trei metode de declarare a unui tablou ca parametru produc acelaşi rezultat: un pointer. Cu toate acestea, un element al unui tablou folosit ca argument al unei funcţii va fi tratat ca orice altă variabilă. Astfel, programul de mai sus poate fi rescris sub forma: # include <stdio.h> void main (void) { int v[10], i; for (i = 0; i < 10; i++) v[i] = i; for (i = 0; i < 10; i++) display (v[i]); } 169
• 176. display(int num) { printf ("%d" , num); } De data aceasta, parametrul din display() este de tip int, deoarece programul utilizează numai valoarea elementului tabloului. Exemplu: Vom prezenta un program pentru afişarea tuturor numerelor prime cuprinse între două limite întregi. Programul principal apelează două funcţii: nr_prim() returnează 1 dacă argumentul său întreg este prim şi 0 dacă nu este prim; numerele prime sunt grupate într-un vector, care se afişează ulterior cu funcţia display(). # include <stdio.h> int nr_prim(); // Se declara prototipul void display(); void main (void) { int a,b,i,j,v[80]; printf("Introduceti limitele: "); scanf("%d %d", &a, &b); j = 0; for (i=a; i<=b; ++i) if (nr_prim(i)) {v[j]=i; ++j;} display(v,j);} int nr_prim(int i) // Decide daca i este prim { int j; for (j=2; j<=i/2; j++) if (i%j==0) return 0; return 1; } void display(int *p, int j) /* Tipareste un vector de intregi */ { int i; for (i=0; i<j; ++i) printf("%d ", p[i]); } Din cele de mai sus, trebuie reţinut că atunci când un tablou se utilizează ca argument al unei funcţii, calculatorul transmite funcţiei adresa de început a tabloului. Acest lucru constituie o excepţie a limbajului C în convenţia de transmitere a parametrilor prin valoare. Astfel, codul funcţiei poate acţiona asupra conţinutului tabloului şi îl poate altera. Exemplu: Programul următor va modifica conţinutul vectorului sir din funcţia main() după apelul funcţiei afis_litmari(). # include <stdio.h> # include <ctype.h> afis_litmari(); void main (void) { char sir[80]; gets(sir); afis_litmari(sir); printf("n%sn",sir);} 170
• 177. // Se defineste functia afis_litmari() afis_litmari(char *s) { register int t; for (t = 0; s[t]; ++t) { // Se modifica continutul sirului sir s[t] = toupper(s[t]); printf("%c",s[t]);}} Rezultatul rulării programului va fi: abcdefghijklmnoprstuvxyzw ABCDEFGHIJKLMNOPRSTUVXYZW ABCDEFGHIJKLMNOPRSTUVXYZW Exemplu: Dacă nu dorim să se întâmple acest lucru, programul de mai sus se poate rescrie sub forma: # include <stdio.h> # include <ctype.h> afis_litmari(); void main (void) { char sir[80]; gets(sir); afis_litmari(sir); printf("n%sn",sir);} afis_litmari(char *s) /* Se defineste functia afis_litmari() */ { register int t; for (t = 0; s[t]; ++t) printf("%c",toupper(s[t])); } // Nu se modifica continutul sirului sir Rezultatul rulării va fi de această dată: abcbdefghijklmnoprstuvxyzw ABCBDEFGHIJKLMNOPRSTUVXYZW abcbdefghijklmnoprstuvxyzw În aceasta variantă conţinutul tabloului ramâne nemodificat, deoarece programul nu-i schimbă valoarea. Un exemplu clasic de transmitere a tablourilor într-o funcţie îl constituie funcţia gets() din biblioteca C standard. Prezentăm o variantă simplificată a acestei funcţii numită xgets(). xgets(s) char *s; { char ch; int t; for (t = 0; t < 80; ++t) { ch = getchar(); switch (ch) { case 'n' : s[t] = '0'; /* terminare sir */ return; 171
• 178. case 'b': if (t > 0) t--; break; default: s[t] = ch; } } s[80] ='0'; } Funcţia xgets() trebuie apelată având ca argument un tablou de caractere, care, prin definiţie, este un pointer la caracter. Numărul caracterelor introduse de la tastatură, prin funcţia for este de 80. Dacă se introduc mai mult de 80 de caractere, funcţia se încheie cu return. Dacă se introduce un spaţiu, contorul t este redus cu 1. Când se apasă CR, xgets() introduce terminatorul de şir. 8.7. Argumentele argc şi argv ale funcţiei main() Singurele argumente pe care le poate avea funcţia main() sunt argv şi argc. Parametrul argc conţine numărul argumentelor din linia de comandă şi este un întreg. Întotdeauna acesta va fi cel puţin 1, deoarece numele programului este codificat ca primul argument. Parametrul argv este un pointer la un tablou de pointeri la caractere. Fiecare element din acest tablou indică spre un argument linie_comanda. Toate argumentele linie_comanda sunt şiruri. Exemplu: Următorul program arată modul de utilizare al argumentelor linie_comanda şi va afişa Hello urmat de numele dumneavoastră, dacă vă introduceţi numele, imediat după numele programului: # include <stdio.h> void main (argc, argv) // Numele programului int argc; char *argv[]; {if (argc != 2) { printf (" Ati uitat sa va introduceti numele n"); return; } printf ("Hello %s !", argv[1]); } Dacă acest program se numeşte ARG_LC.C şi numele dumneavoastră este DAN, atunci, pentru a executa programul, în linia de comandă, veţi tipări ARG_LC DAN. Ieşirea programului va fi Hello DAN !. Argumentele linie_comanda trebuie separate prin spaţiu sau TAB şi nu prin virgulă, sau;. 172
• 179. Parametrul argv[] se declară, de obicei, sub forma char *argv[]; şi reprezintă un tablou de lungime nedeterminată, mai precis reprezintă un tablou de pointeri. Accesul la elementele lui argv[] se realizează prin indexarea acestuia, astfel: argv[0] va indica spre primul şir, care este întotdeauna numele programului; argv[1] va indica spre primul argument etc. Evitaţi folosirea sa fără paranteze, adică char *argv. Următorul program numit "nrinvers" numără invers de la o valoare specificată prin linia de comandă şi transmite un beep când ajunge la zero. Precizăm că programul converteşte primul argument, care conţine numărul la un întreg folosind funcţia standard atoi(). Dacă şirul "display" apare ca al doilea argument_comanda, programul va afişa, de asemenea, numărul introdus pe ecran. # include <stdio.h> # include <string.h> # include <stdlib.h> void main(int argc, char *argv[]) /* nrinvers */ { int disp, count; if (argc < 2) { printf ("Trebuie introdusa lungimea numarului in linia de comandan"); return; } if (argc==3 && !strcmp(argv[2], "display")) disp = 1; else disp = 0; for (count = atoi(argv[1]); count; --count) if (disp) printf("%d ",count); printf("%c",7); /* Se emite un beep */ } Observaţie: Dacă în linia de comandă nu se specifică nici un argument, programul va afişa un mesaj de eroare. 8.8. Funcţii care returnează valori neîntregi Dacă nu se declară explicit tipul funcţiei, compilatorul C o va declara implicit de tip int. Pentru ca funcţia să întoarcă un tip diferit de int trebuie, pe de o parte, să se precizeze un specificator de tip al funcţiei şi apoi să se identifice tipul funcţiei înaintea apelului acesteia. O funcţie C poate returna orice tip de dată din C. Declararea tipului este similară celei de la declararea tipului variabilei: specificatorul de tip ce precede funcţia indică tipul datei întoarse de funcţie. Pentru a nu se genera incertitudini datorate dimensiunii de reprezentare, înainte de utilizarea unei funcţii ce întoarce tipuri 173
• 180. neîntregi, tipul acestei funcţii trebuie făcut cunoscut programului. Acest lucru este necesar deoarece compilatorul nu cunoaşte tipul datei întoarse de funcţie şi acesta va genera un cod greşit pentru apelul funcţiei. Pentru a preveni această greşeală, la începutul programului se plasează o formă specială de declaraţie care să precizeze compilatorului ce tip de valoare va returna acea funcţie. Această declaraţie se numeşte prototipul funcţiei. Exemplu: # include <stdio.h> float sum();//Prototipul functiei (fara parametri) void main(void) { float first = 123.23, second = 99.09; printf("%fn", sum(first, second)); } float sum(float a, float b) // Definitie sum() //Se returnează o valoare de tip float { return a+b; } Instructiunea de declarare a tipului funcţiei are forma generală: specificator_de_tip nume_funcţie(); Chiar dacă funcţia are argumente, în declaraţia de tip acestea nu se precizează (cu excepţia compilatoarelor mai vechi de 1989, care nu sunt adaptate la cerinţele ANSI-C). Dacă o funcţie ce a fost declarată int întoarce un caracter, calculatorul converteşte valoarea caracter într-un întreg. Deoarece conversiile caracter --> întreg-caracter sunt fără probleme, o funcţie ce întoarce un caracter poate fi definită ca o funcţie care întoarce un întreg. 8.9. Returnarea pointerilor Deşi funcţiile care întorc pointeri se manipulează în acelaşi mod ca şi celelalte tipuri de funcţii, trebuie discutate câteva concepte importante. Pointerii la variabile nu sunt nici întregi, nici întregi fără semn. Pointerii sunt adrese de memorie a anumitor tipuri de date: int, char, float, double, struct etc. Motivul acestei distincţii este legat de faptul că atunci când se prelucrează un pointer aritmetic, această prelucrare este dependentă de tipul datei indirectate: de exemplu, dacă este increment un pointer la 174
• 181. int, noua valoare (a adresei) va fi cu 4 mai mare faţă de valoarea anterioară. În general, când un pointer este incrementat sau decrementat, acesta va indica către elementul următor, respectiv anterior, din tabloul pe care îl indirectează. De exemplu, dacă funcţia int f() returnează un întreg, atunci funcţia int *f(), returnează un pointer la o dată de tip int. Deoarece fiecare tip de date poate avea lungimi diferite, compilatorul trebuie "să ştie" ce tip de dată este indirectată de pointer, pentru a-l face să indice corect spre următorul element. Exemplu: Programul următor conţine o funcţie care întoarce un pointer într-un şir în locul în care calculatorul găseşte o coincidenţă de caractere. char *match (char c, char *s) {int count; count = 0; while (c!=s[count] && s[count] != '0') count ++; return (&s[count]); } Funcţia match() va încerca să întoarcă un pointer la locul (elementul) din şir unde calculatorul găseşte prima coincidenţă cu caracterul c. Dacă nu se găseste nici o coincidenţă, funcţia va întoarce un pointer la terminatorul de şir (NULL). Un scurt program ce ar utiliza funcţia match() este următorul : # include <stdio.h> # include <conio.h> char *match(); // Prototipul functiei void main (void) { char s[80], *p, ch; gets (s); /* Se introduce un sir */ ch = getche(); /* Se introduce un caracter */ p = match (ch, s); /* Apelul functiei */ /* p preia valoarea functiei match() */ if (p) { printf("n Adresa caracterului ce coincide cu cel dat este: %p", p); printf("n Subsirul de la adresa caracterului ce coincide cu cel dat este:n %sn",p);} else printf("Nu exista nici o coincidenta"); } Acest program citeşte mai întâi un şir şi apoi un caracter. În cazul în care caracterul este în şir, atunci se tipăreste şirul din punctul unde se află caracterul, altfel se tipăreşte "Nu există nici o coincidenţă". 175
• 182. Un caz interesant este oferit de funcţiile care returnează pointeri către şiruri de caractere. O astfel de funcţie se declară sub forma: char *f() Exemplu: Programul următor arată modul în care se defineşte, se declară şi se apelează o astfel de funcţie. # include <stdio.h> void main(void) { int i; char *NumeLuna(); scanf("%d", &i); printf("%s n ", NumeLuna(i)); } char *NumeLuna(nr) int nr; { char *luna[]= {"Eroare", "Ianuarie", "Februarie", "Martie", "Aprilie", "Mai", "Iunie","Iulie", "August", "Septembrie", "Octombrie", "Noiembrie", "Decembrie"}; return ((nr>=1) && (nr <= 12)?luna[nr]:luna[0]); } Un alt exemplu va fi reprezentat de o variantă a funcţiei strcpy() din string.h , deci o funcţie care copiază caracterele din şirul s2 în şirul s1. Rezultatul se găseşte în s1. /* Vom incepe cu definirea functiei strcpy2() si apoi vom declara programul principal main(). In acest fel nu mai este necesara declararea prototipului functiei strcpy2() */ # include <stdio.h> char *strcpy2(register char s1[],register char s2[]) { char *s0 = s1; // Echivalent: char *s0;s0 = s1; while ((*s1++ = *s2++) != '0'); return s0; } void main() { char *sir1,*sir2; puts(“Introduceti un sir de la tatstatura n”); gets(sir2); puts(strcpy2(sir1,sir2));} Se observă cum se iniţializează s0 cu s1. Bucla while atribuie valorile (caracterele) (*s2) în locaţiile indicate de pointerul s1, incrementând ambii pointeri simultan. Bucla se termină la întâlnirea caracterului null, care se copiază şi el. Valoarea returnată, s0, reţine adresa de început a şirului s1. Un ultim exemplu îl constituie un program de manipulare a unor matrici. Acest program realizează citirea unei matrici, 176
• 183. transpunerea sa şi respectiv afişarea rezultatului apelând la funcţiile cit_mat(), trans_mat() şi tip_mat(). # include <stdio.h> # define DIM_MAX 10 void cit_mat(); void tip_mat(); int *trans_mat(); void main() { int a[DIM_MAX][DIM_MAX], dim_lin, dim_col, *p; printf("Introduceti dimensiunea matricei [dim_lin dim_col]: "); scanf("%d %d", &dim_lin, &dim_col); cit_mat(a, dim_lin, dim_col); tip_mat(a, dim_lin, dim_col); p = trans_mat(a, dim_lin, dim_col); tip_mat(a, dim_col, dim_lin); } void cit_mat(int p[][DIM_MAX], int lin, int col) { int i, j; for (i=0; i<lin; i++) for (j=0; j<col; j++) { printf("x[%d][%d] = ", i, j); scanf("%d", &p[i][j]); } } void tip_mat(int p[][DIM_MAX], int lin, int col) { int i, j; for (i=0; i<lin; i++) { for (j=0; j<col; j++) printf("%d ",p[i][j]); printf("n"); } printf("n"); } int *trans_mat(int p[][DIM_MAX], int lin, int col) { int t, i, j; for (i=0; i<lin; i++) for (j=i; j<col; j++) {t = p[i][j], p[i][j] = p[j][i], p[j][i] = t;} return p; } 8.10. Funcţii de tip void Din punct de vedere sintactic, tipul void se comportă ca un tip fundamental (de bază). Nu există obiecte de tip void. 177
• 184. Tipul void este utilizat pentru declararea implicită a acelor funcţii care nu întorc o valoare. void se utilizează şi ca tip de bază pentru pointeri la un obiect de tip necunoscut. Exemplu: void f(void) /* functia f nu intoarce o valoare */ void *pv /* pointer la un obiect necunoscut */ Utilizând void se impiedică folosirea funcţiilor ce nu întorc o valoare în orice expresie, prevenind astfel o întrebuinţare greşită a acestora. De exemplu, funcţia afis_vertical() afişează pe ecran argumentul său şir, vertical, şi întrucât nu întoarce nici o valoare, este declarată de tip void. void afis_vertical (sir) char *sir; { while (*sir) printf ("%c n", *sir ++); } Înaintea utilizării acestei funcţii sau oricărei alte funcţii de tip void, aceasta trebuie declarată. Dacă nu se declară, compilatorul C consideră că aceasta întoarce o valoare întreagă. Astfel, modul de utilizare al funcţiei afis_vertical() este următorul: # include <stdio.h> void afis_vertical(); // Se declara prototipul void main (void) { afis_vertical ("Hello "); } void afis_vertical (sir) char *sir; { while (*sir) printf ("%c n", *sir ++); } 8.11. Funcţii prototip După cum se ştie, înaintea folosirii unei funcţii care întoarce o altă valoare decât int, aceasta trebuie definită. Funcţiile prototip au fost adăugate de comitetul ANSI-C standard. Declararea unei funcţii prototip se face conform următorului format: tip nume_funcţie (tip_arg1, tip_arg2,...) unde: tip = tipul valorii întoarse de funcţie; tip_arg1, tip_arg2,... = tipurile argumentelor funcţiei. Exemplu: Programul următor va determina compilatorul să emită un mesaj de eroare sau de avertisment deoarece acesta încearcă să 178
• 185. apeleze funcţia func() având al doilea argument de tip int, în loc de float, cum a fost declarat în funcţia func(): #include <stdio.h> void func(int, float);//Prototipul functiei func() void main (void) { int x, y; x = 10; y = 10; func (x, y); } /* Se afiseaza o nepotrivire */ void func (x, y) /* Parametrii functiei sunt: */ int x; /* x - intreg */ float y; /* y - real */ { printf ("%f", y/(float) x); } Funcţiile prototip se folosesc pentru a ajuta compilatorul în prima fază în care funcţiile utilizate sunt definite după programul principal. Acesta trebuie înştiinţat asupra tipul datei returnat de o funcţie pentru a aloca corect memoria. Dacă funcţiile sunt declarate înaintea liniei de program main(), funcţiile prototip nu mai sunt necesare, deoarece compilatorul extrage informaţia despre funcţii în momentul în care parcurge corpul definiţiei lor. Spre exemplu, programul de mai sus se poate scrie şi sub forma următoare, în care nu vom mai avea o declaraţie de funcţie prototip: #include <stdio.h> void func (x, y) /* Parametrii functiei sunt: */ int x; /* x - intreg */ float y; /* y - real */ { printf ("%f", y/(float) x); } void main (void) { int x, y; x = 10; y = 10; func (x, y); } /* Nu se afiseaza nepotrivire */ Utilizând recomandările ANSI-C din 1989, programul de mai sus se poate scrie mai compact: #include <stdio.h> void func (int x, float y) /* Parametrii formali includ tipul */ { printf ("%f", y/(float) x); } void main (void) { int x, y; x = 10; y = 10; func (x, y); }//afisare avertisment de conversie sau, folosind funcţia prototip: #include <stdio.h> void func(); /* Declarare prototip fara parametri formali ! */ 179
• 186. void main (void) { int x, y; x = 10; y = 10; func (x, y); } void func (int x, float y) /* Parametrii formali includ tipul */ { printf ("%f", y/(float) x); } În ultimul program am evidenţiat o recomandare care simplifică efortul de programare în sensul că în linia de declarare a prototipurilor funcţiilor folosite este necesar să definim tipul funcţiei nu şi tipul parametrilor formali. Compilatorul se informează despre tipul parametrilor formali la parcurgerea corpului definiţiei funcţiei. Din cele de mai sus se observă ca folosirea funcţiilor prototip ne ajută la verificarea corectitudinii programelor, deoarece nu este permisă apelarea unei funcţii cu alte tipuri de argumente, decât tipul celor declarate. 8.12. Funcţii recursive Funcţiile C pot fi recursive, adică se pot autoapela direct sau indirect. O funcţie este recursivă dacă o instrucţiune din corpul funcţiei este o instrucţiune de apel al aceleiaşi funcţii. Uneori o funcţie recursivă se numeşte şi funcţie circulară. Un exemplu de o astfel de funcţie este funcţia factorial() care determină factorialul unui număr. Această funcţie se poate organiza recursiv, ştiind că: n! = n(n-1)!. Având în vedere 0!=1, această funcţie se poate organiza astfel: long factorial (int n) { if (n == 0) return (1); else return (n * factorial(n-1)); } Programul de apel al acestei funcţii se scrie sub forma: # include <stdio.h> void main (void) { int n; printf("Introduceti un numar intreg : n"); scanf ("%d, &n); printf ("(%d) ! = %ld",n,factorial(n)); } long factorial (int n) { if (n == 0) return (1); else return (n * factorial(n-1)); } 180
• 187. Observaţie: Atunci când o funcţie se autoapelează recursiv, la fiecare apel al funcţiei se memorează pe stivă atât valorile parametrilor actuali, cât şi întregul set de variabile dinamice definite în cadrul funcţiei. Din aceasta cauză stiva trebuie dimensionată corespunzător. O variantă echivalentă a funcţiei factorial() definită mai sus ar fi următoarea: long factorial(int n) { if (!n) return (1); else return (n * factorial (n-1)); } Un alt exemplu interesant este dat de şirul lui Fibonacci, în care termenul general an este dat de relaţia de recurenţă: an = an-1+ an-2 , unde a0 = 0 şi a1=1. Codul funcţiei poate fi scris sub forma: long fib(int n) { if (n == 0) return (0); else if (n == 1) return (1); else return (fib(n-1)+fib(n-2)); } Utilizarea recursivităţii poate să nu conducă la o reducere a memoriei necesare, atât timp cât stiva este folosită intens pentru fiecare apel recursiv. De asemenea şi execuţia programului poate să nu fie mai rapidă. Dar codul recursiv este mai compact şi de multe ori mai uşor de scris şi înţeles decât echivalentul său recursiv. Recursivitatea este convenabilă în mod deosebit pentru operaţii pe structuri de date definite recursiv, cum sunt listele, arborii etc. 8.13. Clase de memorare (specificatori sau atribute) Din punct de vedere al execuţiei programelor C, memoria computerului este organizată în trei zone, cunoscute în mod tradiţional ca segment de memorie text, segment de memorie statică (sau de date) şi segment de memorie dinamică (sau stivă). Segment de memorie text Conţine instrucţiunile programului, deci (memorie program) programul executabil Segment de memorie Conţine variabilele a caror locaţie rămâne fixă statică 181
• 188. Segment de memorie Conţine variabilele de tip automatic, dinamică (de tip stivă) parametrii funcţiilor şi apelurile şi retururile de/din funcţii În tabelul următor se prezintă caracteristicile claselor de memorie. Specificator Domeniul de Durata de viaţă Plasament de memorie vizibilitate al variabilei a variabilei Auto Local fiecărei funcţii Temporară, numai În memoria (automatic) sau fiecărui bloc în care când se execută dinamică (de a fost declarată funcţia în care este tip stivă) declarată Register Local fiecărei funcţii Temporară, numai În regiştrii (registru) când se execută microproceso funcţia în care este rului declarată Extern Global, de către toate Permanentă, pe În memoria funcţiile dintr-un fişier parcursul rulării statică sursă sau din mai multe programului fişiere sursă executabil Static Local sau global Permanentă, cât In memoria timp este in statică memorie programul executabil Vizibilitatea precizează domeniul sau locul în care o variabilă este vizibilă. Domeniul de vizibilitate este în general determinant şi în stabilirea duratei de viaţă a variabilei. Din punctul de vedere al duratei de viaţă a variabilei, aceasta poate fi temporară (există numai pe perioada în care funcţia care o declară este activată) sau permanentă (există pe toată durata de execuţie a programului). Dacă tipul se declară explicit în declaratorul variabilei, clasa de memorie se determină prin specificatorul de clasă de memorie şi prin locul unde se face declaraţia (în interiorul unei funcţii sau înaintea oricărei funcţii). Variabilele cele mai folosite sunt cele care sunt declarate în blocurile aparţinând unei funcţii. Aceste variabile sunt de două feluri: - auto, aşa cum sunt marea majoritate a variabilelor declarate numai prin tip. Acesta este un specificator implicit, deci nu 182
• 189. este nevoie să îl invocăm la declararea variabilelor. Variabilele auto sunt plasate în memoria stivă, iar domeniul de vizibilitate este local, numai pentru funcţia în care variabila a fost declarată, iar din punctul de vedere al duratei de viaţă sunt volatile, adică dispar din memoria stivă după reîntoarcerea din funcţie. - static, declarate explicit. Variabilele static sunt plasate în memoria statică, iar domeniul de vizibilitate este local, numai pentru funcţia în care variabila a fost declarată, iar din punctul de vedere al duratei de viaţă sunt permanente, adică nu dispar din memoria statică după reîntoarcerea din funcţie. - register, declarate explicit. Variabilele register sunt identice cu cele auto cu excepţia faptului că stocarea nu are loc în memoria stivă ci în regiştrii interni ai microprocesorului în scopul sporirii vitezei de execuţie a programelor. - extern, declarate explicit. Din punct de vedere al modulării unor programe, este preferabil să divizăm un program complex în mai multe module program care se leagă în faza de link-editare. O variabilă declarată extern într-un modul program semnalează compilatorului faptul că această variabilă a fost declarată într-un alt modul. Aceste variabile sunt globale, adică sunt văzute de orice modul de program şi de orice funcţie componentă a unui modul program. Stocarea are loc în memoria statică iar durata de viaţă este permanentă, pe toată perioada execuţiei programului. Iniţializarea unei variabile static diferă de cea a unei variabile auto prin aceea că iniţializarea este făcută o singură dată, la încărcarea programului în memorie şi lansarea sa în execuţie. După prima iniţializare, o variabilă static nu mai poate fi reiniţializată (de exemplu, la un nou apel al funcţiei în care este iniţializată). Iată ilustrat acest lucru prin două exemple simple. Se tipăreşte, cu ajutorul funcţiei receip(), un număr care este mai întâi iniţializat cu valoarea 1 şi returnat incrementat cu o unitate. În cazul folosirii variabilelor implicite locale auto se rulează programul: # include <stdio.h> short receip(); void main(){ printf("First = %dn",receip()); printf("Second = %dn",receip());} 183
• 190. short receip() { short number = 1; return number++;} şi se obţine rezultatul: First = 1 Second = 1 Dacă se modifică în funcţia receip() variabila number din auto în static, vom avea # include <stdio.h> short receip(); void main(){ printf("First = %dn",receip()); printf("Second = %dn",receip());} short receip() { static short number = 1; return number++;} şi obţinem rezultatul First = 1 Second = 2 Limbajul C suportă patru specificatori ai claselor de memorare: auto, extern, static, register. Aceştia precizează modul de memorare al variabilelor care îi urmează. Specificatorii de memorare preced restul declaraţiei unei variabile care capătă forma generală: specificator_de_memorare specificator_de_tip lista_de_variabile; Specificatorul auto Se foloseşte pentru a declară varibilele locale (obiectele dintr-un bloc). Totuşi, utilizarea acestuia este foarte rară, deoarece, implicit, variabilele locale au clasa de memorare automată (auto). Specificatorul extern Se utilizează pentru a face cunoscute anumite variabile globale declarare într-un modul de program (fişier) altor module de programe (fişiere) cu care se va lega primul pentru a alcătui programul complet. Exemplu: Modulul 1 Modulul 2 int x, y; extern int x, y; char ch; extern char ch; main() func22() { { . . . . . . x = y / 10; . . . . . . } } func23() func1() { { x = 123; } y = 10; } 184
• 191. Dacă o variabilă globală este utilizată într-una sau mai multe funcţii din modulul în care acestea au fost declarate nu este necesară utilizarea opţiunii extern. Dacă compilatorul găseşte o variabilă ce n-a fost declarată, atunci acesta o va căuta automat printre variabilele globale. Exemplu: int first, last; /* variabile globale */ main( ) { extern int first;}//folosire optionala declaratie extern Variabile statice Obiectele statice pot fi locale unui bloc sau externe tuturor blocurilor, dar în ambele situaţii ele îşi păstrează valoarea la ieşirea şi intrarea, din sau în funcţii. Variabile locale statice Când cuvântul cheie static se aplică unei variabile locale, compilatorul C crează pentru aceasta o memorie permanentă în acelaşi mod ca şi pentru o variabilă globală. Diferenţa dintre o variabilă locală statică şi o variabilă globală este că variabila locală statică este cunoscută numai în interiorul blocului în care a fost declarată. Un exemplu de funcţie care necesită o astfel de variabilă este un generator de numere care produce un nou număr pe baza celui anterior. serie() {static int numar_serie; numar_serie = numar_serie + 23; return (numar_serie); } Se observă că variabila numar_serie continuă să existe între două apeluri ale funcţiei serie() fără ca aceasta să fi fost declarată ca variabilă globală. Se observă de asemenea că funcţia nu atribuie nici o valoare iniţială variabilei numar_serie, ceea ce înseamnă că valoarea inţială a acesteia este 0. Variabile globale statice O variabilă globală cu atributul static este o variabilă globală cunoscută numai în modulul în care a fost declarată. Deci o variabilă globală statică nu poate fi cunoscută şi nici modificată din alte module de program (alte fişiere). Exemplu: static int numar_serie; //var. globala este cunoscuta numai in acest fisier 185
• 192. serie() { numar_serie = numar_serie + 23; return (numar_serie); } /* initializarea variabilei numar_serie */ serie_start(val_init) int val_init;{ numar_serie = val_init; } Apelul funcţiei serie_start() cu o valoare intreagă iniţializează seria generatoare de numere, după care apelul funcţiei serie() va genera următorul număr din serie. Specificatorul register Acest modificator se aplică numai variabilei de tip int şi char. Acest specificator precizează faptul ca variabilele declarate cu acest modificator sunt des utilizate şi se pastrează de obicei în registrele CPU. Specificatorul register nu se aplica variabilelor globale. Exemplu: Aceasta funcţie calculeaza me pentru întregi : int_putere (m, e) int m; register int e; { register int temp; temp = 1; for (; e; e--) temp * = m; return temp; } În acest exemplu au fost declarate ca variabile registru atât e cât şi temp. De obicei utilizarea unei variabile registru conduce la micşorarea timpului de execuţie al unui program. Exemplu : unsigned int i; unsigned int delay; main() { register unsigned int j; long t; t = time ('0'); for (delay = 0; delay < 10; delay++) for (i = 0; i < 64000; i++); printf("Timpul pentru bucla non-registru: %ldn" ,time(' 0')-t); t = time ('0'); for (delay = 0; delay < 10; delay++) for (j = 0; j < 64000; j++); printf ("Timpul bucla registru: %ld",time ('0')-t);} 186
• 193. Dacă se execută acest program se va găsi că timpul de execuţie al buclei registru este aproximativ jumătate din timpul de execuţie al variabilei non-registru. 8.14. Pointeri la funcţii Într-un fel, un pointer funcţie este un nou tip de dată. Chiar dacă o funcţie nu este o variabilă, aceasta are o locaţie fizică în memorie care poate fi atribuită unui pointer. Adresa atribuită pointerului este punctul de intrare al funcţiei. Acest pointer poate fi utilizat în locul numelui funcţiei. Pointerul permite de asemenea funcţiilor să fie pasate (trecute) ca argumente în alte funcţii. Adresa unei funcţii se obţine utilizând numele funcţiei fără nici o paranteză sau argumente (ca în cazul tablourilor). Exemplu: # include <stdio.h> # include <ctype.h> void check(); int strcmp(); /* prototip functie */ void main() { char s1[80], s2[80]; void *p; /* p preia adresa de intrare a functiei */ p = strcmp; gets(s1); gets(s2); check(s1,s2,p); } void check (char *a, char *b, int (*cmp) ()) /* cu int (*cmp) () se declara un pointer functie */ { printf (" Test de egalitate n "); if (!(*cmp) (a,b)) printf ("Egaln"); else printf ("Neegaln"); } Declararea lui strcmp() în main() s-a facut din două motive: 1) programul trebuie să ştie ce tip de valoare returnează strcmp(); 2) numele trebuie cunoscut de compilator ca şi funcţie. Deoarece în C nu există o modalitate de a declara direct un pointer funcţie, acesta se declară indirect folosind un pointer void care poate primi orice fel de pointer. Apelul funcţiei check() se face având ca parametri doi pointeri la caracter şi un pointer funcţie. Instrucţiunea : (*cmp)(a, b) 187
• 194. realizează apelul funcţiei, în acest caz strcmp() iar a şi b sunt argumentele acestuia. Exemplu: # include <stdio.h> # include <ctype.h> int strcmp(); /* prototip functie */ void main() { char s1[80], s2[80]; int (*p)(); /* p este pointer la functie */ p = strcmp; gets (s1); gets (s2); printf (" Test de egalitate n "); if (!(*p) (s1,s2)) printf ("Egaln"); else printf("Neegaln"); } Observaţie: Funcţia check() poate utiliza direct funcţia strcmp() sub forma: check (s1, s2, strcmp); Exemplu: # include <stdio.h> # include <ctype.h> void check (); int strcmp(); /* prototip functie */ void main() { char s1[80], s2[80]; gets (s1); gets (s2); check (s1, s2, strcmp); } void check (char *a, char *b, int (*cmp) ()) // se defineste functia check() /* cu int (*cmp) () se declara un pointer functie */ { printf (" Test de egalitate n "); if (!(*cmp) (a,b)) printf ("Egaln"); else printf ("Neegaln"); } Capitolul IX 188
• 195. PREPROCESAREA Un preprocesor C realizează substituirea macrodefiniţiilor, o serie de calcule adiţionale şi incluziunea fişierelor. Liniile programului sursă care încep cu "#", precedat eventual de spaţiu comunică cu preprocesorul. Sintaxa acestor linii este independentă de restul limbajului; pot apare oriunde în program şi pot avea efect care se menţine (indiferent de domeniul în care apare) până la sfârşitul unitatii de translatare. Preprocesorul C conţine următoarele directive: #if #include #ifdef #define #ifndef #undef #else #line #elif #error #pragma 9.1. Directive uzuale Directiva #define se utilizează pentru a defini un identificator şi un şir (o secvenţă) pe care compilatorul îl va atribui identificatorului de fiecare dată când îl întâlneşte în textul sursă. Forma generală a directivei #define este : #define identificator şir Se observă că directiva #define nu conţine "; ". În secvenţa de atomi lexicali "şir" nu trebuie să apară spaţiu. Linia se termina cu CR. Exemplu: # define TRUE 1 # define FALSE 0 Când în program se întâlnesc numele TRUE şi FALSE, acestea se vor înlocui cu 1, respectiv 0. Instrucţiunea: printf ("%d %d %d", FALSE, TRUE, TRUE + 5); va afişa pe ecran 0 1 6. După definirea unui macro_name, acesta poate fi folosit pentru definirea altui macro_name. 189
• 196. Exemplu: # define ONE 1 /* Se defineşte macro_name ONE */ # define TWO ONE + ONE /* Se utilizează macro_name ONE */ # define THREE ONE + TWO Deci această macrodefiniţie realizează simpla înlocuire a unui identificator cu şirul asociat. Dacă, de exemplu, se doreşte definirea unui mesaj standard de eroare, se poate scrie: # define E_MS "standard error on input n" . . . . . . . . . . printf (E_MS); Ultima linie este echivalentă cu : printf ("standard error on inputn"); atunci când în program se întâlneşte identificatorul E_MS. Exemplu: Programul următor nu va afişa "this is a test", deoarece argumentul lui printf() nu este închis între ghilimele. # define XYZ this is a test . . . . . . . . . . . . . . . . . printf ("XYZ"); Se va afişa XYZ şi nu "this is a test". Dacă şirul este prea lung şi nu încape pe o linie, acesta se scrie sub forma: # define LONG_STRING " this is a very long string that is used as an example " Observaţie: De obicei macro_names sunt definite cu litere mari. Directiva #define poate fi folosită şi pentru precizarea dimensiunii unui tablou, astfel: # define MAX_SIZE 100 float balance [ MAX_SIZE ]; Macro_nameul dintr-o directiva #define poate avea şi argumente. Exemplu : # define MIN (a ,b) a < b ? a : b void main() { int x, y; x = 10; y = 20; printf("Numarul mai mic este: %d ", MIN (x,y)); } După substituirea lui MIN(a, b) în care a = x şi b = y, instrucţiunea printf() va arata astfel : printf("Numarul mai mic este: %d",(x<y)?x:y); Directiva #error 190
• 197. Directiva #error forţează compilatorul să stopeze operaţia de compilare când această este intilnita în program. Este utilizata în primul rind pentru depanarea programelor. Forma generală a directivei este: #error mesaj_de_eroare Aceasta linie determină procesorul să scrie mesajul de eroare şi să termine compilarea. Directiva # include Directiva # include comandă compilatorului să includă în fişierul ce conţine directiva #include un alt fişier sursă al cărui nume este specificat în directivă. Formele directivei sunt : # include <nume_fisier> # include "nume_fisier" Prima formă se referă la fişiere header (cu extensia .h) care se găsesc în subdirectorul include din fiecare mediu de programare C, iar cea de-a doua la fişiere header create în directorul de lucru al utilizatorului (directorul curent). Directivele # include pot fi folosite şi una în interiorul celeilalte. 9.2. Directive pentru compilare condiţionată Limbajul C conţine câteva directive care ne permit să compilăm selectiv anumite porţiuni de program. Directivele #if, #else, #elif şi #endif Forma generală a lui #if este: #if expresie_constanta secventa de instructiuni #endif Dacă expresie_constanta este adevărată, compilatorul va compila fragmentul de cod cuprins între #if şi #endif, iar dacă expresie_constanta este falsă, compilatorul va sări peste acest bloc. Exemplu: #define MAX 100 void main() { #if MAX > 99 printf("Se compileaza pentru tablouri > 99n"); #endif } Observaţie: Expresie_constanta se evaluează în timpul compilării. De aceea, aceasta trebuie să conţină numai variabile constante definite 191
• 198. anterior utilizării lor. Expresie_constanta nu trebuie să conţină operatorul sizeof. Directiva #else lucrează similar cu instrucţiunea else determinând o alternativă de compilare. Exemplu : # define MAX 10 void main() { #if MAX > 99 printf("Se compileaza pentru tablouri > 99n"); #else printf("Se compileaza pentru tablouri < 99n"); #endif } Deoarece MAX = 10, compilatorul va compila numai codul cuprins între #else şi #endif, deci va tipări mesajul : Se compilează pentru tablouri < 99 Directiva #elif inlocuieşte "else if" şi este utilizată pentru realizarea opţiunilor multiple de tip if / else / if utilizate la compilare. Forma generală a directivelor #if , #elif, #endif este: #if expresie Secventa_de_instructiuni #elif expresie_1 Secventa_de_instructiuni_1 #elif expresie_2 Secventa_de_instructiuni_2 . . . . . . . . . . . . . . #elif expresie_N Secventa_de_instructiuni_N #endif Dacă "expresie" este adevărată se compilează "Secventa_de_instructiuni" şi nu se mai tastează nici o altă expresie #elif. Dacă "expresie" este falsă, compilatorul verifică următoarele expresii în serie, compilându-se "Secventa_de_instructiuni_i", corespunzatoare primei "expresie_i" adevărată, i = 1, 2, . . . , N. Directivele #if şi #elif se pot include unele pe altele. Exemplu: #if MAX > 100 #if VERSIUNE_SERIALA int port = 198; #elif int port = 200; #endif #else char out_buffer[100]; #endif Directivele #ifdef şi #ifndef 192
• 199. O altă metodă de compilare condiţionată utilizează directivele #ifdef şi #ifndef, care înseamnă "if defined" şi "if not defined". Forma generală a lui #ifdef este : #ifdef macro_name Secventa_de_instructiuni #endif Dacă anterior apariţiei secvenţei de mai sus s-a definit un macro_name printr-o directivă #define, compilatorul va compila "Secventa_de_instructiuni" dintre #ifdef şi #endif. Forma generală a lui #ifndef este: #ifndef macro_name Secventa_de_instructiuni #endif Dacă macro_name nu este definit prîntr-o directivă #define, atunci se va compila blocul dintre #ifndef şi #endif. Atât #ifdef, cât şi #ifndef pot utiliza un #else, dar nu #elif. Exemplu: # define TOM 10 void main() { #ifdef TOM printf("Hello TOM !n"); #else printf("Hello anyone !n"); #endif #ifndef JERY printf ("Jery not defined n"); #endif } Programul va afişa: Hello TOM ! şi JERY not defined. Dacă nu s-a definit TOM, atunci programul va afişa : Hello anyone !. Directiva #undef Se utilizează pentru a anula definiţia unui macro_name definit printr-o directivă #define. Exemplu: #define LENGTH 100 #define WIDTH 100 char array[LENGTH][WIDTH]; #undef LENGTH #undef WIDTH Acest program defineşte atât LENGTH, cât şi WIDTH până se întâlneşte directiva #undef. Principala utilizare a lui #undef este de a permite localizarea unui macro_name numai în anumite secţiuni ale programului. 193
• 200. Directiva #line O linie cu una din formele: #line numar "nume_fiaier" #line numar determină compilatorul să considere, din motive de diagnosticare a erorilor, că numărul de linie al urmatoarei linii din programul sursă este dat de "număr", iar numele fişierului în care se află programul sursă este dat de "nume_fişier". Dacă lipseste "nume_fişier", programul sursă se află în fişierul curent. Exemplu: Următoarea secvenţă face ca numărul de linie să înceapă cu 100. # line 100 void main() /* linia 100 */ { /* linia 101 */ printf ("%dn" , __LINE__); /* linia 102 */ } Instructiunea printf() va afişa valoarea 102 deoarece această reprezintă a treia linie în program, după instrucţiunea #line 100. Directiva #pragma O linie de control de forma: #pragma nume determină compilatorul să realizeze o acţiune care depinde de modul de implementare al directivei #pragma. "nume" este numele acţiunii #pragma dorite. Limbajul C defineşte două instrucţiuni #pragma: warn şi inline. Directiva warn determină compilatorul să emită un mesaj de avertisment. Forma generală a lui warn este : #pragma warn mesaj unde "mesaj" este unul din mesajele de avertisment definite în C. Forma generală a directivei inline este : #pragma inline şi avertizează compilatorul că programul sursă conţine şi cod în limbajul de asamblare. Directiva vidă O linie de forma: # nu are nici un efect. Macro_names (macrosimboluri) predefinite 194
• 201. Limbajul C conţine câţiva identificatori predefiniţi, care la compilare se expandează pentru a produce informaţii speciale. Aceştia sunt: __LINE__ o constanta zecimală care conţine numele liniei sursă curente. __FILE__ un şir care conţine numele fişierului care se compilează. __DATA__ un şir care conţine data compilării sub forma luna/zi/an. __TIME__ un şir care conţine ora compilării sub form: hh:mm:ss __STDC__ constanta 1. Acest identificator este 1 numai în implementarile standard; dacă constanta este orice alt număr, atunci implementarea este diferită de cea standard. Aceste macrosimboluri, împreună cu simbolurile definite cu #define nu pot fi redefinite. 9.3. Modularizarea programelor De obicei (vezi [Mocanu, 2001] programele C constau din fişiere sursă unice, cu excepţia fişierelor header. Un singur fişier sursă este în general suficient în cazul programelor mici. Modularizarea internă este un principiu de bază al programării în C şi constă în utilizarea pe scară largă a funcţiilor definite de utilizator. Scrierea programului principal (main) se concentrează mai ales pe apelul acestor funcţii. În cazul în care corpul de definiţie al funcţiilor utilizator se află după corpul de definiţie main, este necesar ca să declarăm prototipul funcţiilor utilizate de main() pentru a informa corect compilatorul despre tipul variabilelor returnate de funcţii. O altă modalitate este aceea de a defini funcţiile utilizator înaintea funcţiei principale main(), caz în care nu mai sunt necesare prototipurile. Programul este modularizat cu ajutorul funcţiilor prin divizarea sa în nuclee funcţionale. Acestea pot fi comparate cu nişte mici piese de lego cu ajutorul cărora se pot construi ulterior structuri (programe) foarte complexe. Pe scurt, modularizarea internă constă în descompunerea sarcinii globale a unui program în funcţii de prelucrare distincte. O funcţie de uz general este o funcţie care poate fi folosită într- o varietate de situaţii şi, probabil, de către mai mulţi utilizatori. Este 195
• 202. de preferat ca aceste funcţii de uz general să nu primească informaţii prin intermediul unor variabile globale ci prin intermediul parametrilor. Sporeşte astfel foarte mult flexibilitatea în folosirea acestor funcţii. Modularizarea externă constă în divizarea unui program foarte complex în mai multe subprograme. Astfel, un fişier sursă mai mare se poate diviza în două sau mai multe fişiere sursă mai mici. Evident, aceste fişiere sunt strâns legate între ele pentru a forma în final un tot unitar echivalent cu programul complex iniţial (dinainte de divizare). În figura de mai sus se prezintă un ecran al Microsoft Visual C+ + din MSDN 6.0 Noţiunea cea mai cuprinzătoare este aceea de Workspace (spaţiu de lucru) care cuprinde în esenţă o colecţie de proiecte corelate şi prelucrabile împreună. Un workspace cuprinde unul sau mai multe Projects (proiecte) dintre care numai unul este principal şi restul sunt subordonate (subprojects). Fiecare proiect este compus la rândul său din mai multe fişiere, de acelaşi tip sau de tipuri diferite. Prezentarea exhaustivă a organizării acestui mediu de dezvoltare a aplicaţiilor C/C++ este un demers în afara prezentei lucrări. Ceea ce 196
• 203. merită să subliniem este faptul că, în cadrul cel mai întâlnit, anume un workspace care include un singur project, acest proiect conţine mai ales fişiere sursă şi fişiere de tip header. Aceste fişiere se numesc module. Modulul principal este fişierul care conţine funcţia principală main(). Celelalte fişiere sursă, dacă există, se numesc module secundare. De obicei, cu fiecare modul secundar se asociază un fişier header propriu separat. Acest fişier header trebuie să conţină toate directivele şi declaraţiile de variabile necesare pentru o corectă compilare separată a modulului cu care se asociază. Pentru a exemplifica cele de mai sus, vom modulariza un exemplu anterior, anume al unei baze de date simple. Workspace-ul va conţine un singur project, care va conţine următoarele 4 fişiere: bd_main.c local.h bd_bib.c local1.h bd_main.c (bd - bază de date) este modulul principal, cel care conţine funcţia main(). El are asociat fişierul header local.h. În mod asemănător, local1.h este fişierul header asociat cu modulul secundar bd_bib.c (bib - bibliotecă) care conţine toate definiţiile funcţiilor utilizator. Conţinutul lor este prezentat în continuare. Modulul bd_main.c este: # include "local.h" void main() { char choice; init_list(); for (; ;) { choice = menu(); switch (choice) { case 'e' : enter(); break; case 'd' : display(); break; case 's' : save(); break; case 'l' : load(); break; case 'q' : exit(); }}} Fişierul local.h conţine: # include <stdio.h> # include <ctype.h> # include <string.h> # define SIZE 100 struct addr { char name[20]; char street[30]; char city[15]; char state[10]; unsigned int zip; 197
• 206. char state[10]; unsigned int zip; } addr_info[SIZE]; extern FILE *fp; Se poate verifica cum fiecare modul în parte este compilabil fără erori, iar la link-editare nu se semnalează, de asemenea, erori. Capitolul X INTRĂRI/IEŞIRI 10.1. Funcţii de intrare şi ieşire - stdio.h Limbajul C nu dispune de instrucţiuni de intrare/ieşire. Aceste operaţii se realizează prin intermediul unor funcţii din biblioteca standard a limbajului C. Aceste funcţii pot fi aplicate în mod eficient la o gamă largă de aplicaţii datorită multiplelor facilităţi pe care le oferă. De asemenea, ele asigură o bună portabilitate a programelor, fiind implementate într-o formă compatibilă pe toate sistemele de operare. O altă caracteristică a limbajului C constă în faptul că nu există un sistem de gestionare a fişierelor care să permită organizări de date, aşa cum în alte limbaje există fişiere cu organizare relativă sau indexată. În limbajul C toate fişierele sunt tratate ca o înşiruire de octeţi, neexistând structuri de date specifice care să se aplice acestor fişiere. Programatorul poate să interpreteze datele după cum doreşte Prin urmare, prin scrierea/citirea datelor se scriu/citesc un număr de octeţi fără o interpretare specifică. Funcţiile de intrare/ieşire, tipurile şi macrodefiniţiile din "stdio.h" reprezintă aproape o treime din bibliotecă. În C, intrarea standard respectiv ieşirea standard sunt în mod implicit reprezentate de terminalul de la care s-a lansat programul. Prin fişier înţelegem o mulţime ordonată de elemente păstrate pe diferite suporturi. Aceste elemente se numesc înregistrări. Suporturile cele mai des utilizate sunt cele magnetice (floppy sau harddiscuri). Ele se mai numesc suporturi reutilizabile deoarece zona utilizată pentru păstrarea înregistrărilor unui fişier poate fi ulterior reutilizată ulterior pentru păstrarea înregistrărilor unui alt fişier.În C 200
• 207. un fişier reprezintă o sursă sau o destinaţie de date, care poate fi asociată cu un disc sau cu alte periferice. Biblioteca acceptă fişiere de tip text şi binar, deşi în anumite sisteme, de exemplu UNIX, acestea sunt identice. Un fişier de tip text este o succesiune de linii, fiecare linie având zero sau mai multe caractere terminate cu ' n '. Într-o altă reprezentare, anumite caractere pot fi convertite într-o succesiune de caractere, adică să nu existe o relaţie unu la unu între caracterele scrise (citite) şi acţiunea perifericului. De exemplu, caracterul NL (new line), ' n ', corespunde grupului CR (carriage return) şi LF (line feed). În aceeaşi idee, se consideră că datele introduse de la un terminal formează un fişier de intrare. Înregistrarea se consideră că este formată de datele unui rând tastate de la terminal (tastatură, keyboard), deci caracterul de rând nou NL se consideră ca fiind terminator de înregistrare. În mod analog, datele care se afişează pe terminal (monitor, display) formează un fişier de ieşire. Şi în acest caz înregistrarea este formată din caracterele unui rând. Ceea ce este important de subliniat este că fişierele text pot fi accesate la nivel de octet sau de caracter, ele putând fi interpretate drept o colecţie de caractere, motiv pentru care se şi numesc fişiere text. Toate funcţiile de intrare/ ieşire folosite până acum se pot utiliza şi pentru fişierele text. Un fişier de tip binar este o succesiune de octeţi neprelucraţi care conţin date interne, cu proprietatea că dacă sunt scrise şi citite pe acelaşi sistem, datele sunt egale. Aceste fişiere sunt organizate ca date binare, adică octeţii nu sunt consideraţi ca fiind coduri de caractere. La fişierele binare înregistrarea se consideră că este o colecţie de date structurate numite articole. Structurile de date sunt pretabile pentru stocarea în astfel de fişiere Tratarea fişierelor se poate face la două nivele, inferior şi superior. Nivelul inferior de prelucrare a fişierelor oferă o tratare a fişierelor fără zone tampon (buffere), făcând apel direct la sistemul de operare. Rezervarea de zone tampon este lăsată pe seama utilizatorului. Fişierele de tip text se pretează la o astfel de tratare. Nivelul superior de prelucrare a fişierelor se bazează pe utilizarea unor proceduri specializate în prelucrarea fişierelor care printre altele pot rezerva şi gestiona automat zonele tampon necesare. 201
• 208. Fişierele binare se pot manipula cu facilitate la acest nivel. Funcţiile specializate de nivel superior au denumiri asemănătoare cu cele de nivel inferior, doar prima literă a numelui este f. În practică operaţiile de intrare/ieşire (I/O) cu memoria externă (hard-disk sau floppy-disk) sunt mult mai lente decât cele cu memoria internă. Din această cauză, pentru a spori viteza de lucru, se încearcă să se reducă numărul de operaţii de acces la disc. În acest scop se folosesc bufferele. Un buffer este o zonă de memorie în care sistemul memorează o cantitate de informaţie (număr de octeţi), în general mai mare decât cantitatea solicitată de o operaţie de I/O. Dacă un program efectuează o operaţie de citire a 2 octeţi dintr-un fişier, atunci sistemul citeşte într-un buffer întreg sectorul de pe disc (512 octeţi) în care se găsesc şi cei 2 octeţi solicitaţi, eventual chiar mai mult, în funcţie de dimensiunea bufferului (zonei tampon). Dacă în continuare se vor solicita încâ 2 octeţi, aceştia vor fi preluaţi din bufferul din memorie, fără a mai fi nevoie să mai accesăm discul pe care se află fişierul din care se face citirea. Operaţiile de citire continuă în acest mod până la citirea tuturor octeţilor din buffer, moment în care se va face o nouă umplere a bufferului cu noi date prin citirea următorului sector de pe disc. Invers, dacă un program efectuează o operaţie de scriere a unui număr de octeţi pe disc, aceştia se vor înscrie de fapt secvenţial în buffer şi nu direct pe disc. Scrierea va continua astfel până la umplerea bufferului, moment în care sistemul de operare efectuează o operaţie de scriere a unui secto de pe disc cu cei 512 octeţi din buffer (se goleşte bufferul prin scriere). În acest fel, reducând numărul de operaţii de acces la disc (pentru citire sau scriere) creşte viteza de execuţie a programelor şi fiabilitatea dispozitivelor de I/O. Bufferele au o mărime implicită, dar ea poate fi modificată prin program. Dimensiunea trebuie aleasă în funcţie de aplicaţie ţinând cont de faptul că prin mărirea bufferului creşte viteza de execuţie dar scade dimensiunea memoriei disponibile codului programului şi invers, prin micşorarea sa creşte memoria cod disponibilă dar scade viteza de lucru. Bufferul de tastatură are, spre exemplu, dimensiunea de 256 octeţi, din care 254 sunt puşi la dispoziţie. Orice fişier are o înregistrare care marchează sfârşitul de fişier. În cazul fişierelor de intrare ale căror date se introduc de la terminal, sfârşitul de fişier se generează în funcţie de sistemul de operare 202
• 209. considerat. Pentru sistemele de operare MS-DOS sau MIX şi RSX11 se tastează CTRL/Z iar pentru UNIX se tastează CTRL/U. Un fişier stocat pe suport magnetic se mai numeşte şi fişier extern. Când se prelucrează un astfel de fişier se crează o imagine a acestuia în memoria internă (RAM) a calculatorului. Această imagine se mai numeşte şi fişier intern. Un fişier intern este conectat la un fişier extern sau dispozitiv prin deschidere; conexiunea este întreruptă prin închidere. Deschiderea unui fişier întoarce un pointer la un obiect de tip FILE, care conţine toate datele necesare pentru controlul fişierului. Operaţiile de deschidere şi închidere a fişierelor se poate realiza în C prin funcţii specializate din biblioteca standard I/O a limbajului. Alte operaţii care sunt executate frecvent în prelucrarea fişierelor sunt: • Crearea unui fişier (acest fişier nu există în format extern) • Actualizarea unui fişier (deja existent) • Adăugarea de înregistrări unui fişier deja existent • Consultarea unui fişier • Poziţionarea într-un fişier • Redenumirea unui fişier • Ştergerea unui fişier Ca şi operaţiile de deschidere şi închidere de fişiere, operaţiile indicate mai sus pot fi realizate printr-un set de funcţii aflate în biblioteca standard I/O a limbajului. Aceste funcţii realizează acţiuni similare sub diferite sisteme de operare, dar multe dintre ele pot depinde de implementare. În cele ce urmează se prezintă funcţiile care au o utilizare comună pe diferite medii de programare şi sunt cele mai frecvent utilizate. 10.2. Operaţii cu fişiere În acest subcapitol vom detalia principalele operaţii efectuate asupra unor fişiere. În timpul lucrului cu fişierele, sistemul de operare păstrează un indicator de fişier care indică poziţia curentă în fişier, poziţie la care se va face următoarea operaţie de scriere sau citire. De exemplu, la deschiderea unui fişier pentru citire indicatorul de fişier va indica la începutul fişierului. Dacă se va face o operaţie de citire a 2 octeţi se 203
• 210. vor citi octeţii cu numărul de ordine 0 şi 1 iar indicatorul va indica spre următorul octet, adică cel cu numărul de ordine 3. Pentru o mai corectă înţelegere a acestor funcţii le vom structura după nivelul la care se utilizează: inferior sau superior. În momentul începerii execuţiei unui program, interfeţele standard (cu ecranul, tastatura şi porturile seriale şi paralele) sunt deschise în mod text. Principalele funcţii sunt grupate în tabelul de mai jos: Descriere Nume funcţie de Nume funcţie de nivel inferior nivel superior Deschidere _open fopen Creare _creat fcreate Citire _read fread Scriere _write fwrite Închidere _close fclose Poziţionare _lseek fseek Ştergere _unlink remove Redenumire _rename rename În afara acestor funcţii principale mai există anumite funcţii specializate, cum ar fi: - Funcţii pentru prelucrarea pe caractere a unui fişier: putc (scriere caracter) şi getc (citire caracter). - Funcţii pentru Intrări/Ieşiri cu format: fscanf şi fprintf. - Funcţii pentru Intrări/Ieşiri de şiruri de caractere: fgets şi fputs. Pentru ca sistemul de operare să poată opera asupra fişierelor ca fluxuri (stream) de intrare/ieşire trebuie să cunoască anumite informaţii despre ele. Acest lucru se realizează prin operaţia de deschidere a fluxurilor (stream-urilor). Pointerul fişier În urma operaţiei de deschidere se crează în memorie o variabilă de tip structură FILE care este o structură predefinită. În această variabilă, care se numeşte bloc de control al fişierului, FCB (File Control Block) sistemul păstrează informaţii despre fişierul deschis, precum: • Nume 204
• 211. • Dimensiune • Atribute fişier • Descriptorul fişierului Un pointer-fişier este un pointer la informaţiile care definesc diferitele aspecte ale unui fişier: nume, stare, poziţie curentă. Un pointer-fişier este o variabilă pointer de tip FILE, definită în "stdio.h". Tipul FILE este un tip structurat care depinde de sistemul de operare. Dacă facem abstracţie de cazurile speciale de calculatoare tip VAX sau U3B, pe majoritatea implementărilor tipul FILE se defineşte prin următoarea structură: typedef struct { unsigned char *_ptr; int _cnt; unsigned char *_base; char _flag; char _file; } FILE; Variabila de tip FILE este creată şi gestionată de către suportul pentru exploatarea fişierelor în limbajul C. În urma deschiderii unui fişier, programul primeşte un pointer la variabila creată, deci un pointer la o structură de tip FILE. Se spune că s-a deschis un stream (flux de date). Toate operaţiile care se fac pe acest stream se referă la fişierul asociat stream-ului. În limbajul C există 5 stream-uri standard, definite în <stdio.h>: FILE *stdin; care se referă la dispozitivul standard de intrare (tastatura). Orice operaţie de citire de la stream-ul stdin înseamnă citire de la tastatură. Bufferul folosit are o dimensiune de 254 de caractere şi bufferul se goleşte la tastarea NL (‘n’). Se mai spune că stdin este cu buffer la nivel de linie. FILE *stdout; care se referă la dispozitivul standard de ieşire (ecranul). Orice operaţie de scriere la stream-ul stdout înseamnă scriere pe ecran. Spre deosebire de stdin, stdout este ne-bufferizat deoarece orice scriere pe ecran se face direct la scrierea unui caracter în fişierul stdout. FILE *stderr; care se referă la dispozitivul standard pentru afişarea mesajelor de eroare (ecranul). Este ne-bufferizat. FILE *stdprn; 205
• 212. care se referă la primul port paralel PRN la care se conectează de obicei imprimanta (LPT). Este bufferizat la nivel linie. FILE *stdaux; care se referă la primul port serial COM1. Este ne-bufferizat. 10.3. Nivelul inferior de prelucrare a fişierelor La acest nivel operaţiile de prelucrare a fişierelor se execută fără o gestiune automată a zonelor tampon, făcându-se apel direct la sistemul de operare. Programatorul are în gestiune o zonă declarată drept buffer şi trebuie să ţină cont de faptul că această bufferizare este la nivel linie. Numele funcţiilor de nivel inferior, orientate pe text (transfer de octeţi) încep de obicei cu _ (underline). Dacă un fişier se deschide în modul text, atunci, în cazul citirii dintr-un fişier, secvenţa de octeţi CR-LF (0DH, 0AH) este translatată (înlocuită) cu un singur caracter LF, iar în cazul scrierii în fişier caracterul LF este expandat la secvenţa CR-LF. De asemenea, în cazul MS-DOS sau Windows CTRL/Z este interpretat în cazul citirii drept caracter de sfârşit de fişier (EOF). 10.3.1. Deschiderea unui fişier Orice fişier înainte de a fi prelucrat trebuie deschis, motiv pentru care operaţia de deschidere a unui fişier este de mare importanţă. Deschiderea unui fişier existent se realizează prin intermediul funcţiei _open. La revenirea din ea se returnează un aşa numit descriptor de fişier. Acesta este un număr întreg. El identifică în continuare fişierul respectiv în toate operaţiile realizate asupra lui. În forma cea mai simplă funcţia _open se apelează printr-o expresie de atribuire de forma: df = _open(spf,mod) unde: df – este un număr întreg care reprezintă descriptorul de fişier spf – este specificatorul fişierului care se deschide mod – defineşte modul de prelucrare a fişierului Specificatorul de fişier este fie un şir de caractere, fie un pointer spre un astfel de şir de caractere. Conţinutul şirului de caractere depinde de sistemul de operare folosit. În cea mai simplă formă el este un nume sau mai general o cale care indică plasamentul pe disc al fişierului care se operează. Fişierele deschise la acest nivel pot fi 206
• 213. prelucrate în citire (consultare), scriere (adăugare de înregistrări) sau citire/scriere (actualizare sau punere la zi). Calea spre fişier trebuie să respecte convenţiile sistemului de operare MS-DOS în general. În cea mai simplă formă ea este un şir de caractere care defineşte numele fişierului, urmat de extensia fişierului. Aceasta presupune că fişierul se găseşte în directorul curent. Dacă fişierul nu se află în fişierul curent, atunci numele este precedat de o construcţie de forma: litera:nume_1nume_2…nume_k unde: litera – defineşte discul (în general A, B pentru floppy-disk şi C, D,.. pentru hard-disk) nume_i – este un nume de subdirector. Deoarece calea se include între ghilimele, caracterul ‘’ se dublează. Spre exemplu, putem folosi o comandă de deschidere de forma: int d; d=_open(“A:JOCBIO.C“,O_RDWR); caz în care fişierul BIO.C din directorul JOC de pe dscheta A se deschide în citire/scriere. În funcţie de operaţia dorită, mod poate avea valorile: 0 - pentru citire 1 - pentru scriere 2 - pentru citire/scriere Deschiderea unui fişier nu reuşeşte dacă unul dintre parametri este eronat. În acest caz funcţia _open returnează valoarea (-1). int _open( const char *filename, int oflag [, int pmode] ); este definiţia generală a funcţiei _open. Modul de acces mod se poate furniza în mod explicit printr-o variabilă de tip întreg (oflag) care poate avea valorile: Variabila mod Modul de deschidere a fişierului _O_RDONLY Fişierul se deschide numai în citire (read-only) Nu se poate specifica împreună cu _O_RDWR sau _O_WRONLY _O_WRONLY Fişierul se deschide numai în scriere (write-only) Nu se poate specifica împreună cu _O_RDWR sau _O_RDONLY _O_RDWR Fişierul se deschide în citire/scriere (read/write) 207
• 214. _O_APPEND Fişierul se deschide pentru adăugarea de înregistrări la sfârşitul său. _O_CREAT Crează şi deschide un nou fişier pentru scriere. Nu are nici un efect dacă fişierul este deja existent. _O_BINARY Fişierul se prelucrează în mod binar _O_TEXT Fişierul este de tip text, adică se prelucrează pe caractere sau octeţi (implicit) Menţionăm că în MSDN aceste variabile se mai numesc şi oflag (open-flag) şi sunt definite în fişierul header FCNTL.H. În cazul în care oflag este _O_CREAT, atunci este necesară specificarea constantelor opţionale pmode, care se găsesc definite în SYSSTAT.H. Acestea sunt: _S_IREAD - este permisă numai citirea fişierului _S_IWRITE - este permisă şi citirea (permite efectiv citirea/scrierea fişierului) _S_IREAD | _S_IWRITE - este permisă şi scrierea şi citirea fişierului. Argumentul pmode este cerut numai când se specifică _O_CREAT. Dacă fişierul există deja, pmode este ignorat. Altcumva, pmode specifică setările de permisiune asupra fişerului care sunt activate când fişierul este închis pentru prima oară. _open aplică masca curentă de permisiune la fişier înainte de setarea accesului la fişier. Pentru a crea un fişier nou se va utiliza funcţia _creat pentru a-l deschide. De fapt se deschide prin creare un fişier inexistent. Funcţia este definită astfel: int _creat( const char *filename, int pmode ); în care parametrii au fost descrişi mai sus. Protecţia unui fişier este dependentă de sistemul de operare. Spre exemplu, în UNIX protecţia se defineşte prin 9 biţi ataşaţi oricărui fişier, grupaţi în 3 grupe de câte 3 biţi. Fiecare bit controlează o operaţie de citire, scriere, execuţie. Protecţia operaţiilor se exprimă faţă de proprietar, grup sau oricine altcineva. Numărul octal 0751 permite proprietarului toate cele 3 operaţii indicate mai sus (7 = 1112), grupul la care aparţine proprietarul poate citi şi executa fişierul (5 = 1012) iar alţi utilizatori pot numai executa fişierul (1 = 0012). Funcţia _creat poate fi apelată şi în cazul în care se deschide un fişier deja 208
• 215. existent, caz în care se pierde conţinutul vechi al fişierului respectiv şi se crează în locul lui unul nou cu acelaşi nume. Fiecare din funcţiile _open sau _creat returnează un specificator de fişier (handle) pentru fişierul deschis. Acest specificator este o valoare întreagă pozitivă. Implicit, stdin are specificatorul 0, stdout şi stderr au specificatorii 1 respectiv 2 iar fişierele disc care sunt deschise primesc pe rând valorile 3, 4,..etc. până la numărul maxim admis de fişiere deschise. Valoarea returnată -1 indică o eroare de deschidere, în care caz variabila errno este setată la una din valorile: EACCES – (valoare 13) s-a încercat deschiderea pentru scriere a unui fişier read-only sau modul de partajare a fişierului nu permite operaţia specificată sau calea nu specifică un nume de fişier ci de director. EEXIST – (valoare 17) flagurile _O_CREAT şi _O_EXCL sunt specificate, dar numele de fişier este al unui fişier deja existent. EINVAL – (valoare 22) unul dintre argumentele oflag sau pmode sunt invalide. EMFILE – (valoare 24) nu mai sunt disponibile specificatoare de fişier (prea multe fişiere deschise). ENOENT – (valoare 2) fişierul sau calea nu au fost găsite. Variabila globală errno păstrează codurile de eroare folosite de funcţiile perror (print error) sau strerror (string error) pentru tratarea erorilor. Constantele manifest pentru aceste variabile sunt declarate în STDLIB.H după cum urmează: extern int _doserrno; extern int errno; errno este setată de o eroare într-un apel de funcţie la nivel de sistem (la nivelul inferior). Deoarece errno păstrează valoarea setată de ultimul apel, această valoare se poate modifica la fiecare apel de funcţie sistem. Din această cauză errno trebuie verificată imediat înainte şi după un apel care poate s-o modifice. Toate valorile errno, definite drept constante manifest în ERRNO.H, sunt compatibile UNIX. Valorile valide pentru aplicaţiile Windows pe 32 de biţi sunt un subset al acestor valori UNIX. Valorile specificate mai sus sunt valabile pentru aplicaţii Windows pe 32 de biţi. La o eroare, errno nu este setată în mod necesar la aceeaşi valoare cu codul erorii de sistem. Numai pentru operaţii de I/O se 209
• 216. foloseşte _doserrno pentru a accesa codul erorilor sistemului de operare echivalent cu codurile semnalate de errno. Exemplu: Acest program foloseste _open pentru a deschide un fisier numit OPEN.C pentru citire si un fisier numit OPEN.OUT scriere. Apoi fisierele sunt inchise #include <fcntl.h> #include <sys/types.h> #include <sys/stat.h> #include <io.h> #include <stdio.h> void main( void ) { int fh1, fh2; fh1 = _open( "OPEN.C", _O_RDONLY ); if( fh1 == -1 ) perror( "open failed on input file" ); else { printf( "open succeeded on input filen" ); _close( fh1 );} fh2=_open("OPEN.OUT",_O_WRONLY|_O_CREAT,_S_IREAD|_S_IWRITE); if( fh2 == -1 ) perror( "Open failed on output file" ); else {printf( "Open succeeded on output filen" ); _close( fh2 );}} Prin execuţia acestui program se vor obţine următoarele mesaje pe display: open failed on input file: No such file or directory Open succeeded on output file Press any key to continue 10.3.2. Scrierea într-un fişier Scrierea într-un fişier se realizează folosind funcţia _write. Se presupune că fişierul respectiv a fost în prealabil deschis prin funcţiile _open sau _creat. Ea este asemănătoare cu funcţia _read, doar că se realizează transferul de date în sens invers şi anume din memorie pe suportul fiierului. Funcţia _write, ca şi _read, se apelează printr-o atribuire de forma: nr = _read(df,zt,n) unde: nr – este o variabilă de tip întreg căreia i se atribuie numărul de octeţi scrişi în fişier. 210
• 217. df – este descriptorul de fişier returnat de funcţia _open la deschiderea sau _creat la crearea fişierului. zt - este un pointer spre zona tampon definită de utilizator, zonă din care se face scrierea. n – este dimensiunea zonei tampon sau numărul de octeţi care se doreşte să se scrie. Definiţia funcţiei este: int _write( int handle, const void *buffer, unsigned int count ); Funcţia _write scrie count octeţi din buffer în fişierul asociat cu descriptorul handle. Operaţia de scriere începe la poziţia curentă a pointerului de fişier asociat cu fişierul dat. Dacă fişierul este deschis cu indicatorul _O_APPEND, operaţia de scriere începe la sfârşitul fişierului. După o operaţie de scriere pointerul de fişier este incrementat cu numărul de biţi scrişi efectiv. Dacă fişierul a fost deschis în mod text (implicit), atunci _write tratează CTRL/Z drept un caracter ce indică sfârşitul logic al fişierului. Când se scrie într-un dispozitiv, _write tratează CTRL/Z din buffer drept terminator al operaţiei de ieşire. În general trebuie ca la revenirea din funcţia _write să avem nr=n, ceea ce semnifică faptul că s-au scris pe disc exact numărul de biţi din buffer. În caz contrar scrierea este eronată: aceasta semnifică faptul că pe disc a rămas mai puţin spaţiu (în octeţi) decât numărul de octeţi ai bufferului. Dacă valoarea returnată este -1, se semnalizează eşecul operaţiei de scriere. În acest caz variabila globală errno poate avea una din valorile EBADF (care semnifică un descriptor de fişier invalid sau că fişierul nu a fost deschis pentru scriere) sau ENOSPC (care semnifică lipsa spaţiului pe disc pentru operaţia de scriere). Funcţia _write poate fi utilizată pentru a scrie pe ieşirile standard (display). Astfel, pentru a scrie pe ieşirea standard identificată prin stdout se foloseşte descriptorul 1, iar pentru a scrie pe ieşirea standard pentru erori, stderr, se foloseşte descriptorul de fişier 2. De asemenea, în acest caz nu este nevoie să apelăm funcţia _open sau _creat deoarece fişierele respective se deschid automat la lansarea programului. Exemplu: /*Acest program deschide un fisier pentru scriere si foloseste _write pentru a scrie octeti in fisier*/ #include <io.h> #include <stdio.h> #include <stdlib.h> #include <fcntl.h> 211
• 218. #include <sys/types.h> #include <sys/stat.h> char buffer[]="This is a test of '_write' function"; void main( void ) { int fh; unsigned byteswritten; if((fh=_open("write.o",_O_RDWR|_O_CREAT, _S_IREAD|_S_IWRITE))!=-1) { if((byteswritten = write(fh,buffer,sizeof(buffer)))== -1) perror( "Write failed" ); else printf( "Wrote %u bytes to filen", byteswritten ); _close( fh );}} În urma execuţiei programului, se va afişa mesajul: Wrote 36 bytes to file Press any key to continue 10.3.3. Citirea dintr-un fişier Citirea dintr-un fişier deschis în prealabil cu funcţia _open se realizează cu ajutorul funcţiei _read. Ea returnează numărul efectiv al octeţilor citiţi din fişier. Funcţia _read se poate apela folosind o expresie de atribuire de forma: nr = _read(df,zt,n) cu definiţia generală: int _read( int handle, void *buffer, unsigned int count ); unde: nr – este o variabilă de tip întreg căreia i se atribuie numărul de octeţi citiţi din fişier. df – este descriptorul de fişier returnat de funcţia open la deschiderea sau creat la crearea fişierului. zt - este un pointer spre zona tampon definită de utilizator, zonă în care se face citirea. n – reprezintă numărul de biţi care se citesc Funcţia _read citeşte maximum count octeţi în buffer din fişierul asociat cu descriptorul handle. Operaţia de citire începe de pe poziţia curentă îndicată de pointerul de fişier asociat cu fişierul dat. După operaţia de citire, pointerul de fişier indică spre următorul octet necitit din fişier. Dacă fişierul a fost deschis în mod text, citirea se termină la întâlnirea caracterului CTRL/Z, care este interpretat drept indicator de sfârşit de fişier. 212
• 219. _read returnează numărul de biţi citiţi din fişier, care poate fi mai mic decât count dacă sunt mai puţini decât count octeţi rămaşi în fişier sau dacă fişierul este deschis în mod text. În acest caz fiecare pereche CR-LF (carriage return–linefeed) (CR-LF) este înlocuită cu un singur caracter LF. Numai acest caracter LF se consideră în valoarea returnată. Înlocuirea nu afectează pointerul de fişier. Dacă funcţia încearcă să citească după sfârşitul fişierului, se returnează valoarea 0. Dacă descriptorul de fişier (handle) este invalid sau dacă fişierul nu este deschis pentru citire sau dacă este blocat, funcţia returnează valoarea negativă -1 şi setează variabila errno la EBADF. Tipul erorii şi depistarea ei este dependentă de sistemul de operare utilizat. Dacă n = 1, se citeşte un singur octet. De obicei, nu este eficient să se citească câte un octet dintr-un fişier, deoarece apelurile multiple ale funcţiei _read pot conduce la un consum de timp apreciabil. Dimensiunea maximă a lui n este dependentă de sistemul de operare. O valoare utilizată frecvent este 512, valoare optimă pentru MS-DOS sau pentru UNIX. Funcţia _read citeşte maximum count biţi în zona buffer din fişierul cu descriptorul handle. Operaţia de citire începe de la poziţia curentă a pointerului de fişier asociat cu fişierul respectiv. După o operaţie de citire, pointerul fişier indică spre următorul caracter (octet) necitit din fişier. Dacă fişierul a fost deschis în mod text, _read se termină când se întâlnete indicatorul de fişier CTRL/Z. Funcţia _read poate fi utilizată pentru a citi de la intrarea standard (tastatură). În acest caz descriptorul de fişier are valoarea 0. De asemenea, în acest caz nu este nevoie să apelăm funcţia _open deoarece fişierul se deschide automat la lansarea programului. Exemplu: /* Acest program deschide fisierul WRITE.O creat anterior si incearca sa citeasca 60000 octeti din fisier folosind _read. Apoi va afisa numarul de octeti cititi */ #include <fcntl.h> /* Necesara numai pentru definirea _O_RDWR */ #include <io.h> #include <stdlib.h> #include <stdio.h> char buffer[60000]; void main( void ) { int fh; 213
• 220. unsigned int nbytes = 60000, bytesread; /* Deschide fisierul in citire: */ if( (fh = _open( "write.o", _O_RDONLY )) == -1 ) { perror( "open failed on input file" ); exit( 1 ); } /* Read in input: */ if((bytesread = _read(fh,buffer,nbytes)) <= 0) perror( "Problem reading file" ); else printf( "Read %u bytes from filen", bytesread ); _close( fh );} La execuţia programului se va afişa următorul mesaj: Read 36 bytes from file Press any key to continue 10.3.4. Închiderea unui fişier După terminarea prelucrării unui fişier el trebuie închis. Acest lucru se realizează automat dacă programul se termină prin apelul funcţiei exit. Programatorul poate închide un fişier folosind funcţia _close. Se recomandă ca programatorul să închidă orice fişier de îndată ce s-a terminat prelucrarea lui, deoarece numărul fişierelor ce pot fi deschise simultan este limitat între 15 şi 25, în funcţie de sistemul de operare. Menţionăm că fişierele corespunzătoare intrărilor şi ieşirilor standard nu trebuie închise de programator. Definiţia funcţiei este: int _close( int handle ); Funcţia _close închide fişierul asociat cu descriptorul handle. Funcţia _close returnează valoarea 0 la o închidere reuşită şi -1 în caz de incident. Apelul ei se realizează printr-o expresie de atribuire de forma: v =_ close(df) unde: v – este variabila de tip întreg ce preia valoarea returnată de funcţie df – este descriptorul de fişier (handle) al fişierului pe care dorim să-l închidem. 10.3.5. Poziţionarea într-un fişier Operaţiile de citire/scriere într-un fişier se execută secvenţial, astfel încât: - fiecare apel al funcţiei _read citeşte înregistrarea din poziţia următoare din fişier 214
• 221. - fiecare apel al funcţiei _write scrie înregistrarea în poziţia următoare din fişier. Acest mod de acces la fişier se numeşte secvenţial şi el este util când dorim să prelucrăm fiecare înregistrare a fişierului. În practică apar însă şi situaţii în care noi dorim să scriem şi să citim înregistrări într-o ordine oarecare. În acest caz se spune că accesul la fişier este aleator. Pentru a realiza un acces aleator este nevoie să ne putem poziţiona oriunde în fişierul respectiv O astfel de poziţionare este posibilă pe hard-uri şi floppy-uri prin funcţia _lseek. Definiţia funcţiei este: long _lseek( int handle, long offset, int origin ); Funcţia _lseek mută pointerul de fişier asociat cu descriptorul handle (df) pe o nouă locaţie care este situată la offset octeţi de origin. Următoarea operaţie de citire/scriere se va efectua de la noua locaţie. Argumentul origin trebuie să fie una dintre următoarele constante, definite în STDIO.H: SEEK_SET – începutul fişierului (valoare 0) SEEK_CUR – poziţia curentă a pointerului de fişier (valoare 1) SEEK_END – sfârşitul fişierului (valoare implicită 2) Funcţia _lseek returnează valoarea 0 la poziţionare corectă şi -1 la incident. Ea poate fi apelată prin: v = _lseek(df, deplasament, origine) unde: v – este o variabilă de tip întreg căreia i se atribuie valoarea returnată de către funcţie (0 sau -1) df – este descriptorul de fişier (handle) a cărui valoare a fost definită la deschiderea sau crearea fişierului. deplasament – este o variabilă de tip long şi conţine numărul de octeţi peste care se va deplasa capul de citire/scriere al discului. origine – are una din valorile 0 - deplasamentul se socoteşte de la începutul fişierului; 1 - deplasamentul se socoteşte din poziţia curentă a capului de citire/ scriere; 2 - deplasamentul se socoteşte de la sfârşitul fişierului. Menţionăm că prin apelul lui _lseek nu se realizează nici un fel de transfer de informaţie ci numai poziţionarea în fişier. Operaţia următoare realizată prin apelul funcţiei _read sau _write se va realiza din această poziţie. Spre exemplu, apelul: v = _lseek(df, 0l, 2) 215
• 222. permite să se facă poziţionarea la sfârşitul fişierului. În continuare se pot adăuga articole folosind funcţia _write. Poziţionarea la începutul fişierului se face prin apelul: v = _lseek(df, 0l, 0) Exemplu: #include <io.h> #include <fcntl.h> #include <stdlib.h> #include <stdio.h> void main( void ) { int fh; long pos; /* Pozitia pointerului fisier */ char buffer[10]; fh = _open( "write.o", _O_RDONLY ); /* Pozitionare la inceputul fisierului: */ pos = _lseek( fh, 0L, SEEK_SET ); if( pos == -1L ) perror( "_lseek inceput nu a reusit!" ); else printf("Pozitia pentru inceputul fisierului = %ldn", pos ); /* Muta pointerul fisier cu 10 octeti */ _read( fh, buffer, 10 ); /* Gaseste pozitia curenta: */ pos = _lseek( fh, 0L, SEEK_CUR ); if( pos == -1L ) perror( "_lseek pozitia curenta nu a reusit!" ); else printf( "Pozitia curenta = %ldn", pos ); /* Pozitionare pe ultima pozitie: */ pos = _lseek( fh, 0L, SEEK_END ); if( pos == -1L ) perror( "_lseek sfarsit nu a reusit!" ); else printf( "Pozitia ultima este = %ldn", pos ); _close( fh );} În urma execuţiei programului se va afişa: Pozitia pentru inceputul fisierului = 0 Pozitia curenta = 10 Pozitia ultima este = 36 Press any key to continue 10.3.6 Ştergerea unui fişier Un fişier poate fi şters apelând funcţia _unlink astfel: 216
• 223. v = _unlink(spf) unde v este o variabilă de tip întreg căreia i se atribuie valoarea 0 pentru ştergere reuşită şi (-1) pentru ştergere nereuşită. spf este specificatorul de fişier folosit la deschidere a fişierului. Definiţia funcţiei este: int _unlink( const char *filename ); Funcţia _unlink şterge de pe disc fişierul specificat prin filename. Exemplu: /* Acest program sterge fisierul WRITE.O creat si prelucrat anterior. */ #include <stdio.h> void main( void ) { if( _unlink( "write.o" ) == -1 ) perror( "Nu se poate sterge 'WRITE.O'" ); else printf( "S-a sters 'WRITE.O'n" );} În urma execuţiei programului se afişează: S-a sters 'WRITE.O' Press any key to continue 10.3.7. Exemple de utilizare a funcţiilor de intrare/ieşire de nivel inferior 1. Să se scrie un program care copiază intrarea standard la ieşirea standard. Această problemă se poate rezolva uşor prin folosirea funcţiilor getchar şi putchar. Acum o vom rezolva folosind funcţiile _read şi _write. # include <stdio.h> # include <io.h> void main() /* copiaza intrarea standard la iesirea standard */ { char c[1]; while (_read(0,c,1)>0) _write(1,c,1);} Menţionăm că cel de-al doilea parametru al funcţiei _read sau _write trebuie să fie pointer spre caractere. Lucrul la nivelul inferior nu este chiar atât de simplu pe cât pare. Vom ilustra în continuare responsabilitatea pe care o are programatorul în gestionarea zonelor tampon. Să considerăm exemplul anterior în care zona tampon o mărim la 3 caractere, deci programul arată astfel: # include <stdio.h> 217
• 224. # include <io.h> void main() { char c[3]; while (_read(0,c,3)>0) _write(1,c,3);} Citirea nu se va opri după 3 caractere, ci funcţia _read va continua să funcţioneze până la tastarea ENTER (CR+LF). Imediat funcţia _read va tipări grupele de 3 caractere introduse, inclusiv grupul final CR+LF. Zona tampon definită este supraînscrisă de fiecare dată când se introduc noi caractere. Dacă de la tastatură vom introduce 123456<CR><LF> atunci se va tipări primul grup (prima înscriere a zonei tampon) 123, apoi a doua grupă 456 şi grupul <CR> şi <LF> va supraînscrie primele două caractere ale bufferului, anume codurile ASCII ale lui 4 şi 5 şi se va tipări <CR><LF>6. 123456 123456 6 Primul grup 123456 este scris prin ecou de la tastatură, iar următoru se înscrie de către program. Dacă în continuare vom introduce 1<ENTER> atunci se va tipări 1 urmat de două rânduri noi deoarece fiecare CR sau LF sunt expandate de stdout în perechi <CR><LF>. Dacă mărim la 5 dimensiunea bufferului şi de la tastatură introducem 12<ENTER>, atunci se va tipări 12 12 ¦ deoarece cel de-al 5-lea octet al bufferului nu a fost alocat prin citire, având o valoare nedefinită. Problemele de mai sus legate de gestiunea bufferului în/din care se face citirea/scrierea pot fi depăşite cu o modificare simplă, prezentată mai jos. Prin scriere nu se vor trimite spre stdout decât numărul de caractere citit de la stdin. # include <stdio.h> # include <io.h> # define LZT 10 // lungime zona tampon void main() /* copiaza intrarea standard la iesirea standard */ 218
• 225. { char zt[LZT]; int n; while ((n=_read(0,zt,LZT))>0) _write(1,zt,n);} Programatorul trebuie să ţină cont însă şi de alte amănunte cum ar fi dimensiunea implicită a bufferului stdin, care este de 254 de caractere. 2. Să se scrie un program care citeşte un şir de numere flotante de la intrarea standard şi crează 2 fişiere fis1.dat şi fis2.dat, primul conţinând numerele de ordin impar citite de la intrarea standard (primul, al 3-lea, al 5-lea, etc.) iar cel de-al doilea pe cele de ordin par citite de la aceeaşi intrare. Apoi să se listeze, la ieşirea standard, cele două fişiere în ordinea fis1.dat, fis2.dat câte un număr pe un rând în formatul număr de ordine: număr Vom scrie programul folosindu-ne de funcţii definite de utilizator care să facă apel la funcţiile de nivel inferior. Programul arată astfel: # include <stdio.h> # include <io.h> # include <sys/types.h> # include <sys/stat.h> # include <fcntl.h> #include <stdlib.h> char nume1[]="fis1.dat"; char nume2[]="fis2.dat"; union unr { float nr; char tnr[sizeof(float)];}; union unr nrcit; int creare_fis(const char *nume) { int df; if ((df=_creat(nume,_S_IWRITE|_S_IREAD))==-1) { printf("%s: ",nume); printf("Nu se poate deschide fisierul in crearen"); exit(1);} return df;} void scrie_fis(int df,char *nume) {if(_write(df,nrcit.tnr,sizeof(float))!=sizeof(float)) { printf("%s: ",nume); printf("Eroare la scrierea fisieruluin");exit(1);}} 219
• 226. void date_fis(int df1,char *nume1,int df2,char *nume2) { int j=1,i; while ((i=scanf("%f",&nrcit.nr))==1) { if(j%2) scrie_fis(df1,nume1); else scrie_fis(df2,nume2); j++;}} void inchid_fis(int df, char *nume) { if (_close(df)<0) { printf("%s: ",nume); printf("eroare la inchiderea fisieruluin"); exit(1);}} int deschid_fis_cit(char *nume) { int df; if ((df=_open(nume,_O_RDONLY))==-1) { printf("%s: ",nume); printf("Nu se poate deschide fisierul in citiren"); exit(1);} return df;} void list_fis(int df,char *nume,int ord) { int j,i; if (ord%2) j=1; else j=2; while ((i=_read(df,nrcit.tnr,sizeof(float)))>0) { printf("%6d: %gn",j,nrcit.nr);j+=2;} if (i<0) { printf("%s: ",nume); printf("Eroare la citire din fisieruln"); exit(1);} _close(df);} void main() { int df1,df2; df1=creare_fis(nume1); df2=creare_fis(nume2); date_fis(df1,nume1,df2,nume2); inchid_fis(df1,nume1);inchid_fis(df2,nume2); df1=deschid_fis_cit(nume1); df2=deschid_fis_cit(nume2); list_fis(df1,nume1,1);list_fis(df2,nume2,2);} 3. Să se realizeze programul de mai sus folosind un singur fişier fis.dat. Programul va diferi faţă de cel anterior prin faptul că înregistrările se stochează într-un singur fişier, deci funcţia de listare se va modifica pentru citirea din 2 în 2 a înregistrărilor. După fiecare citire din fişier, se va face un salt cu o înregistrare pentru a poziţiona capul de citire/scriere peste înregistrarea următoare. # include <stdio.h> 220
• 227. # include <io.h> # include <sys/types.h> # include <sys/stat.h> # include <fcntl.h> #include <stdlib.h> char nume[]="fis.dat"; union unr { float nr; char tnr[sizeof(float)];}; union unr nrcit; int creare_fis(const char *nume) { int df; if ((df=_creat(nume,_S_IWRITE|_S_IREAD))==-1) { printf("%s: ",nume); printf("Nu se poate deschide fisierul in crearen"); exit(1);} return df;} void scrie_fis(int df,char *nume) { if (_write(df,nrcit.tnr,sizeof(float))!=sizeof(float)) { printf("%s: ",nume); printf("Eroare la scrierea fisieruluin");exit(1);}} void date_fis(int df,char *nume) { while (scanf("%f",&nrcit.nr)==1) { scrie_fis(df,nume);}} void inchid_fis(int df, char *nume) { if (_close(df)<0) { printf("%s: ",nume); printf ("eroare la inchiderea fisieruluin"); exit(1);}} int deschid_fis_cit(char *nume) { int df; if ((df=_open(nume,_O_RDONLY))==-1) { printf("%s: ",nume); printf("Nu se poate deschide fisierul in citiren"); exit(1);} return df;} void list_fis(int df,char *nume) { int j,i; j=1; while ((i=_read(df,nrcit.tnr,sizeof(float)))>0) { printf("%6d: %gn",j,nrcit.nr); // avans peste o inregistrare if(_lseek(df,(long)sizeof(float),1)==-1l) break; j+=2;} if (i<0) { printf("%s: ",nume); printf("Eroare la citire din fisieruln"); 221
• 228. exit(1);} j=2; // pozitionare pe prima inregistrare _lseek(df,0l,0); // avans la inregistrarea a doua _lseek(df,(long)sizeof(float),1); while((i=_read(df,nrcit.tnr,sizeof(float)))>0) {printf("%6d: %gn",j,nrcit.nr); // avans peste o inregistrare if(_lseek(df,(long)sizeof(float),1)==-1l) break; j+=2;} if (i<0) { printf("%s: ",nume); printf("Eroare la citire din fisieruln"); exit(1);} _close(df);} void main() { int df; df=creare_fis(nume); date_fis(df,nume); inchid_fis(df,nume); df=deschid_fis_cit(nume); list_fis(df,nume);} Atragem atenţia asupra modului în care lucrează funcţiile de intrare/ieşire pentru stdin şi stdout faţă de cele pentru disc. Dacă intrările şi ieşirile pentru perifericele standard le putem executa în formatul dorit cu ajutorul funcţiilor specializate scanf şi printf, pentru lucrul cu discul variabila float este tratată sub forma unui grup de 4 octeţi care se scriu sau se citesc pe disc aşa cum este reprezentarea lor internă. Există funcţii specializate pentru scrierea/citirea pe disc cu format, dar care sunt de nivel superior. Ceea ce merită să subliniem este faptul că echivalentele de nivel superior pentru fişiere ale funcţiilor printf() şi scanf() sunt fprintf() şi fscanf(). Echipamentele periferice pot fi considerate fişiere externe şi deci funcţiile specializate pentru I/O cu fişiere pot fi folosite şi pentru operaţii de I/O cu echipamentele periferice. Funcţiile printf şi scanf sunt proiectate pentru a lucra implicit cu fişierele stdout respectiv stdin, deci cu monitorul şi tastatura. 10.4. Nivelul superior de prelucrare a fişierelor Nivelul superior de prelucrare a fişierelor se referă la aşa numitul format binar de reprezentare a informaţiei în fişiere care la rândul său face apel la informaţia structurată. Bufferul este alocat automat şi gestionat de funcţii C specializate. 222
• 229. 10.4.1. Funcţia fopen() Funcţia fopen se apelează printr-o expresie de atribuire de forma: pf = fopen(spf,mod) unde: pf - este un pointer spre tipul FILE spf – este specificatorul fişierului care se deschide mod – este un şir de caractere care defineşte modul în care se deschide fişierul. Forma generală de declarare a funcţiei fopen() este: FILE *fopen(char *filename, char *mode); Funcţia deschide fişierul al cărui nume este specificat prin "filename" (de obicei un fişier disc) şi întoarce un pointer la FILE pentru operaţie reuşită şi NULL pentru operaţie nereuşită. Varibilele permise pentru modul "mode" sunt: a _O_WRONLY | _O_APPEND (usual _O_WRONLY | _O_CREAT | _O_APPEND) a+ _O_RDWR | _O_APPEND (usual _O_RDWR | _O_APPEND | _O_CREAT ) r _O_RDONLY r+ _O_RDWR w _O_WRONLY(usual _O_WRONLY | _O_CREAT | _O_TRUNC) w+ _O_RDWR (usual _O_RDWR | _O_CREAT | _O_TRUNC) b _O_BINARY t _O_TEXT c Nimic n Nimic Modul "a" nu şterge markerul de sfârşit d fişier EOF înainte de a adăuga la sfârşitul fişierului. După ce s-a făcut o adăugare, comanda MS-DOS TYPE tipăreşte datele până la markerul original EOF şi nu până la ultima dată adăugată. Modul "a+" şterge identificatorul de sfârşit de fişier EOF înainte de adăugarea de înregistrări la sfârşitul fişierului. După adăugare comanda MS-DOS TYPE va tipări toate 223
• 230. datele conţinute în fiier. Modul "a+" este cerut pentru adăugarea la sfârşitul unui fişier care are marker terminator CTRL/Z = EOF. Dacă modul "mode" include "b" după litera iniţială, ca în "rb" sau "w+b" se indică un fişier binar. Numele fişierului conţine cel mult FILENAME_MAX caractere. La un moment dat pot fi deschise cel mult FOPEN_MAX fişiere. Menţionăm că stdin, stdout şi stderr sunt pointeri spre tipul FILE şi permit ca funcţiile de nivel superior de prelucrare a fişierelor să poată trata intrarea standard, ieşirea standard şi ieşirea standard pentru erori, la fel ca şi restul fişierelor. Singura deosebire constă în faptul că în acest caz programatorul nu trebuie să deschidă sau să închidă fişierele respective. Exemplu: FILE *fp, *fopen(); /* se declara pointerii de tip file *fp si *fopen() */ fp = fopen("test","w"); /* se deschide fisierul " test " pentru screiere */ Pentru detectarea unei erori la deschiderea unui fişier se utilizează secvenţa: if ((fp = fopen("test", "w")) == NULL) { puts ("Cannot open filen"); exit(1); } Dacă pentru operaţia de citire se încearcă deschiderea unui fişier inexistent, fopen() va returna o eroare. 10.4.2. Funcţia fclose() Forma generală de declarare a funcţiei fclose() este: int fclose(FILE *fp); unde "fp" este pointerul la fişier returnat după apelul funcţiei fopen(). Funcţia fclose() se utilizează pentru a închide un fişier deschis cu fopen(). Funcţia fclose() scrie mai întâi în fişier datele rămase în fişierul buffer apoi închide fişierul. fclose() întoarce zero (0) pentru închidere reuşită şi EOF (-1) dacă apare o eroare. La execuţia funcţiei exit se închid automat toate fişierele deschise. Cu toate acestea, se recomandă ca programatorul să închidă un fişier de îndată ce s-a terminat prelucrarea lui, altfel se poate ajunge în situaţia de a se depăşi numărul limită admis pentru fişierele care pot fi simultan deschise într-un program. Exemplu: /* Acest program deschide fisierele numite "test1.c" si "test2.c".Apoi foloseste fclose pentru a inchide "test1.c" si _fcloseall pentru a inchide restul fisierelor deschise */ #include <stdio.h> 224
• 231. FILE *stream1, *stream2; void main( void ){ int numclosed; /* Deschidere in citire (esec daca fisierul "test1.c" nu exista) */ if( (stream1 = fopen( "test1.c", "r" )) == NULL ) printf( "Fisierul 'test1.c' nu a fost deschisn" ); else printf( "Fisierul 'test1.c' a fost deschisn" ); /* Deschidere pentru scriere */ if( (stream2 = fopen( "test2.c", "w+" )) == NULL ) printf( "Fisierul 'test2.c' nu a fost deschisn" ); else printf( "Fisierul 'test2.c' a fost deschisn" ); /* Inchide fisierul cu pointerul stream1 */ if( fclose( stream1 ) ) printf( "Fisierul 'test1.c' nu a fost inchisn" ); /* Toate celelalte fisiere se inchid: */ numclosed = _fcloseall( ); printf( "Numarul fisierelor inchise cu _fcloseall: %un", numclosed );} În urma execuţiei programului se obţine: Fisierul 'test1.c' a fost deschis Fisierul 'test2.c' a fost deschis Numarul fisierelor inchise cu _fcloseall: 1 Press any key to continue 10.4.3. Funcţiile rename() şi remove() Funcţia rename() schimbă numele vechi al fişierului, "oldname", cu numele nou, "newname". Întoarce o valoare diferită de zero dacă incercarea nu reuseste. int rename (char *oldname, char *newname); Funcţia remove() int remove(char *filename); Funcţia remove() elimină fişierul cu numele specificat, astfel încât o incercare ulterioară de deschidere a fişierului va eşua. Întoarce o valoare diferită de zero dacă încercarea reuşeşte. 10.4.4. Funcţii de tratare a erorilor a) Funcţia ferror() Aceasta funcţie determină dacă în urma unei operaţii cu fişiere s- a produs sau nu o eroare. Forma generală de declarare este: int ferror(FILE *fp) 225
• 232. unde "fp" este un pointer la fişier. Funcţia ferror() întoarce o valoare diferită de zero dacă s-a detectat o eroare şi o valoare 0 dacă nu s-a detectat nici o eroare. b) Funcţia feof() int feof(FILE *fp) Funcţia feof() întoarce o valoare diferită de zero dacă indicatorul de sfârşit de fişier este valid şi o valoare zero dacă indicatorul de sfârşit de fişier nu este valid. c) Funcţia perror() void perror(const char *s) Funcţia perror() scrie s şi un mesaj de eroare care depinde de implementare, corespunzator cu intregul din errno.h, astfel: fprintf(stderr,"%s %sn, s, "error message") 10.4.5. Funcţii cu acces direct a) Funcţia fread() Permite citirea unui bloc de date. Forma generală de declarare: int fread(void *buffer,int num_bytes,int count,FILE *fp) Funcţia fread() citeşte din fişierul specificat prin "fp" cel mult "count" obiecte, fiecare obiect având lungimea egală cu "num_bytes" şi îi trimite în zona de memorie indirectată prin "buffer" . *fp este un pointer fişier la fişierul deschis anterior cu fopen(). Funcţia întoarce numărul de obiecte citite, acesta putând fi mai mic decât cele cerute. Pentru a determina starea funcţiei se pot utiliza funcţiile feof(), ferror(). b) Funcţia fwrite() Permite scrierea unui bloc de date. Forma generală de declarare: int fwrite(void *buffer,int num_bytes,int count, FILE *fp) Funcţia fwrite() scrie din zona (tabloul) "buffer" în fişierul indirectat prin "fp", "count" obiect de lungime "nr_bytes". Funcţia întoarce numărul de obiecte scrise, care, în caz de eroare este mai mic decât "count". Exemplu: Programul următor scrise un număr real pe disc # include "stdio.h" void main() { FILE *fp; float f = 12.23; if ((fp = fopen ("test", "wb")) == NULL){ printf (" Cannot open filen "); 226
• 233. return; } fwrite (&f, sizeof (float), 1, fp); fclose (fp); } Aşa cum se vede din acest program, "buffer" poate fi o simplă variabilă. Exemplu: Programul următor copiază un tablou de numere reale "balance", în fişierul "balance": # include "stdio.h" void main() { FILE *fp; float balance[100]; /* tabloul balance */ if ((fp = fopen("balance", "w+")) == NULL) { printf ("Cannot open filen"); return;} . . . . . . . . . . . . . . . . . fwrite (balance, sizeof (balance), 1, fp); . . . . . . . . . . . . . . . . . fclose (fp); } Exemplu: Programul următor deschide fişierul FREAD.OUT şi scrie în el 25 de caractere şi apoi îl redeschide şi citeşte din nou caracterele din fişier după care afişează numărul caracterelor citite şi conţinutul. #include <stdio.h> void main( void ) { FILE *stream; char list[30]; int i, numread, numwritten; /* Deschide fisierul in mod text: */ if( (stream = fopen( "fread.out", "w+t" )) != NULL ) { for ( i = 0; i < 25; i++ ) list[i] = (char)('z' - i); /* Scrie 25 caractere in fisier */ numwritten = fwrite(list,sizeof(char),25,stream ); printf( "S-au scris %d caracteren", numwritten ); fclose( stream );} else printf( "Probleme cu deschiderea fisieruluin" ); if( (stream = fopen( "fread.out", "r+t" )) != NULL ) { /* Incearca sa citeasca 25 caractere */ numread = fread( list, sizeof( char ), 25, stream ); printf("Nr. caracterelor citite = %dn", numread); printf( "Continutul bufferului = %.25sn", list ); fclose( stream );} else printf( "Fisierul nu a putut fi deschisn" );} În urma execuţie programului se obţine: 227
• 234. S-au scris 25 caractere Nr. caracterelor citite = 25 Continutul bufferului = zyxwvutsrqponmlkjihgfedcb Press any key to continue 10.4.6. Funcţii pentru poziţionare a) Funcţia fseek() Determină poziţionarea fişierului la citire sau scriere, începând cu poziţia selectată. Forma funcţiei: int fseek(FILE *fp, long offset, int origin) unde "fp" este un pointer-fişier returnat prin apelul funcţiei fopen(), "offset" este deplasamentul (număr octeţi) noii poziţii faţă de "origin", iar "origin" este una din următoarele macrodefiniţii: SEEK_SET - început de fişier; SEEK_CUR - poziţie curentă; SEEK_END - sfârşit de fişier. Funcţia returnează 0 dacă se execută cu succes şi o valoare nenulă în caz de eroare. Dacă nu s-a efectuat nici o operaţie de I/O de la deschiderea fişierului în mod APPEND (adăugare), atunci pointerul indică începutul fişierului. Nu se recomanda utilizarea funcţiei fseek() pentru fişiere text; se sugerează utilizarea acesteia numai pentru fişiere binare. Translaţiile CR-LF efectuate în mod text pot cauza funcţionarea defectoasă a funcţiei fseek. Funcţia fopen şi toate celelalte funcţii vor căuta să înlăture caracterul CTRL/Z terminator de fişier (EOF). Singurele operaţii garantate să funcţioneze corect când se utilizează fseek asupra fişierelor deschise în mod text este poziţionarea cu offset 0 relativă la orice poziţie din fişier şi poziţionarea faţă de începutul fişierului cu un offset returnat de funcţia ftell(). Funcţia ftell() este definită astfel: long ftell( FILE *stream ); Funcţia returnează valoarea curentă a pointerului fişier. Poziţia este exprimată prin offsetul faţă de începutul fiierului. În cazul fişierelor deschise în mod text, acest offset nu reflectă întotdeauna exact numărul de octeţi datorită translaţiei CR-LF. Este preferată folosirea simultană a funcţiilor fseek şi ftell pentru a opera asupra fişierelor text, dar se recomandă folosirea lor în special asupra fişierelor binare. 228
• 235. Exemplu: Pentru a citi cel de-al 235 byte din fişierul numit "test" se poate folosi următorul program: func1() /* se declara funcţia func1() */ { FILE *fp; if ((fp = fopen("test", "rb")) == NULL) { printf ("Cannot open filen"); exit (1); } fseek(fp, 235L, 0); return getc(fp);} /* se citeste un caracter de la pozitia 235 */ Observaţie: L modifică constanta 235 la tipul long int. Exemplu: /* Acest program deschide fisierul FSEEK.OUT si muta pointerul in diverse locuri din fisier */ #include <stdio.h> void main( void ) { FILE *stream; char line[81]; int result; stream = fopen( "fseek.out", "w+" ); if( stream == NULL ) printf( "”Fisierul fseek.out nu s-a deschisn" ); else {fprintf( stream, "Fseek incepe aici: " "Acesta este fisierul 'fseek.out'.n" ); result = fseek( stream, 19L, SEEK_SET); if( result ) perror( "Fseek esec" ); else { printf( "Pointerul fisier este plasat la mijlocul primei linii.n" ); fgets( line, 80, stream ); printf( "%s", line );} fclose( stream );}} În urma execuţie programului se obţine: Pointerul fisier este plasat la mijlocul primei linii. Acesta este fisierul 'fseek.out'. Press any key to continue 10.4.7. Ieşiri cu format Funcţiile de tip printf() asigură conversiile de ieşire cu format. a) Funcţia fprintf() Forma acestei funcţii este: int fprintf(FILE *fp, "format", lista_argumente) Funcţia fprintf() realizează conversia şi scrierea la ieşire în fişierul indirectat cu "fp" sub controlul formatului, "format". Valoarea 229
• 236. întoarsă de funcţie este numărul de caractere scrise, sau orice valoare negativă, dacă apare o eroare. Şirul "format" conţine două tipuri de obiecte: caractere obişnuite care sunt copiate în fişierul de ieşire şi descriptori de conversie, fiecare determinând conversia şi tipărirea argumentelor din lista de argumente. Fiecare descriptor începe cu caracterul % şi se încheie cu un caracter de conversie. Între % şi caracterul de conversie pot exista: 1) Indicatori (în orice ordine): "-" - determină alinierea la stânga a argumentului convertit în câmpul de reprezentare; "+" - precizează că numărul va fi reprezentat cu semn; " " - dacă primul caracter nu este un semn se va scrie un blanc la început; "0" - se utilizează în conversiile numerice şi indică umplerea cu zerouri la începutul câmpului; "#" - indică o formă de ieşire alternativă : pentru "0", prima cifra va fi zero; pentru "x" sau "X", la începutul fiecărui număr nenul se va scrie "0x" sau "0X"; pentru "e", "E", "g", "G", "f" ieşirea va avea întotdeauna un punct zecimal; pentru "g" şi "G" nu se vor elimina zerourile de la sfârşit. 2) Un număr care indică lungimea minimă a câmpului de reprezentare. Argumentul convertit va fi tipărit într-un câmp cu o lungime cel puţin egală cu cea specificată, dacă va fi nevoie şi mai mare. Dacă argumentul specificat are mai puţine caractere, atunci câmpul va fi umplut la stânga sau la dreapta, funcţie de aliniere. Caracterul de umplere este de obicei spatiul, dar este 0 dacă s-a ales această optiune (exemplu: %05d). 3) Un punct ce separă lungimea câmpului de precizie. 4) Un număr, precizia, care indică numărul maxim de caracetre care se vor tipări după virgulă pentru "e", "E", sau "f", sau numărul de cifre semnificative pentru conversiile "g" sau "G", sau numărul maxim de caractere ce se vor tipări dintr-un şir. Lungimea, sau precizia, sau amândoua se pot specifica şi prin "*". De exemplu: %10.4f - va afişa un număr de cel puţin 10 caractere cu 4 caractere după virgulă; %5.7s - va afişa un şir de cel puţin 5 caractere dar nu mai lung de 7 caractere; 230
• 237. %-10.2f - va alinia la stânga un număr real cu 2 zecimale într- un câmp de reprezentare de cel puţin 10 caractere. Descriptorii de conversie utilizaţi de C sunt: %c - un singur caracter. %d, %i - notaţie zecimala cu semn. %x, %X - notaţie hexazecimală fără semn (fără 0x sau 0X). %u - notaţie zecimală fără semn. %s - caracterele din şir sunt tipărite până se întâlneşte ' 0' sau cât timp numărul de caractere tipărit precizia. %f - notaţie zecimală de forma [-]mmm.ddd, unde numărul d-urilor este indicat de precizie; precizia implicită este 6, iar o precizie egală cu zero elimina punctul zecimal. %e, %E - notaţie zecimală de forma: [-]m.dddddde+/-xx sau [-]m.ddddddE+/-XX unde numărul de d-uri este indicat de precizie (precizia implicită este 6, iar o precizie egală cu 0 va elimina punctul zecimal). %g, %G - se utilizează %e sau %E dacă exponentul este mai mic decât -4, sau precizie, în caz contrar se utilizează %f. %p - afiseaza un pointer. %o - notaţie octalăa fără semn (fără 0 la început). %% - nu se face conversie, se tipăreşte "%". %n - nu se realizează conversie de argument; numărul de caractere scrise până în acel moment este scris în argument. Există doi modificatori de format care permit funcţiei fprintf() să afişeze întregii long şi short. Aceşti modificatori se pot aplică caracterelor de conversie d, i, o, u şi x, precedându-i pe aceştia (exemplu: %ld, %li, %lu). Modificatorul l poate prefixa şi caracterele de conversie e, f şi g şi indică faptul că numerele tiparite sunt de tip double. Modificatorul h comandă funcţiei fprintf() să afişeze short int. Atunci %hu va preciza că data este de tip short unsigned int.] b) Funcţia printf() Forma funcţiei : int printf("format", lista-argumente) Funcţia printf() este echivalentă cu : fprintf(stdout, "format", lista_argumente) 231
• 238. Exemplu: printf() ieşire ("%-5.2f", 123.456) 123.45 ("%5.2f", 3.4565) 3.45 ("%10s", "hello") hello ("%-10s", "hello") hello (%5.7s", "123456789") 1234567 Exemplu de utilizare a functiei fprintf. /* Acest program foloseste fprintf pentru scrierea datelor cu diferite formate intr-un fisier si apoi tipareste fisierul folosind functia sistem system ce apeleaza comanda TYPE a sistemului de operare */ #include <stdio.h> #include <process.h> FILE *stream; void main( void ) { int i = 10; double fp = 1.5; char s[] = "this is a string"; char c = 'n'; stream = fopen( "fprintf.out", "w" ); fprintf( stream, "%s%c", s, c ); fprintf( stream, "%dn", i ); fprintf( stream, "%fn", fp ); fclose( stream ); system( "type fprintf.out" );} 10.4.8. Intrări cu format Funcţiile de tip scanf() realizează conversiile de intrare cu format a) Funcţia fscanf() Forma acestei funcţii este: int fscanf(FILE *fp, "format", lista_argumente) Funcţia fscanf() citeşte din fişierul indirectat prin "fp" sub controlul formatului "format" şi atribuie valorile citite argumentelor următoare, fiecare argument trebuind să fie un pointer. Funcţia întoarce EOF dacă se detectează sfârşitul de fişier sau apare o altă eroare înainte de orice conversie. În caz contrar, funcţia întoarce numărul de elemente care au primit valori. Şirul "format" poate conţine: - specificatori de conversie, constând dintr-un caracter %, un caracter opţional de suprimare a atribuirii; un număr opţional care indică lungimea câmpului, un caracter opţional h, l sau L, care indică lungimea argumentului şi un caracter de conversie; 232
• 239. - spaţii sau caractere HT sau VT care se ignoră; - caractere obişnuite (diferite de %) care indică următorul caracter diferit de caracterele albe cu care începe fişierul de intrare. Un câmp de intrare se defineşte ca un şir de caractere diferite de cele albe şi se întinde până la următorul caracter alb (spaţiu, tab-uri, CR, LF, VT, FF). Rezultatul conversiei unui câmp de intrare este plasat în variabilă indicată de argumentul corespunzător din lista de argumente. Dacă se indică suprimarea atributului prin "*" ca în %*s, câmpul de intrare este ignorat, fără a se face nici o atribuire. Descriptorii de conversie utilizaţi în C pentru citire sunt: %c - citeşte un singur caracter; caracterele următoare sunt plasate în tablourile indicate, respectându-se numărul de caractere indicat de lungimea câmpului; implicit este 1. Nu se adaugă '0'. %d - citeşte un număr întreg zecimal. %u - citeşte un număr întreg zecimal fără semn. %i - citeşte un număr întreg (intregul poate fi octal, cu 0 la început, sau hexazecimal, cu 0x sau 0X la început). %o - întreg octal (cu sau fără zero la început). %x - întreg hexazecimal (cu sau fără 0x sau 0X la început). %s - şir de caractere diferite de caracterele albe, indicând spre un tablou de caractere destul de mare pentru a memora şirul şi caracterele terminator '0' care se va adauga. %e, %f, %g - numere zecimale în virgulă mobilă. %p - citeşte valoarea pointerului. %n - se scrie în argument numerele de caractere citite până în acel moment. Nu se citeşte nimic din intrare. %h - citeşte un întreg scurt. Un caracter obişnuit în şirul "format" determină ca funcţia fscanf() să citească un caracter ce coincide cu cele din "format". De exemplu, "%d, %d" face că fscanf() să citească un întreg, apoi caracterul "," şi apoi un alt întreg. Dacă calculatorul nu găseşte caracterul specificat, fscanf() va fi încheiată. Toate variabilele menite să primească valori prin fscanf() trebuie să fie transmise prin adresele lor. Aceasta înseamnă că toate argumentele trebuie să fie pointeri la variabilele utilizate ca argumente. b) Funcţia scanf() 233
• 240. Forma funcţiei: int scanf("format", lista-argumente) Funcţia scanf() este echivalenta cu: fscanf(stdin, "format", lista-argumente) Exemple: scanf ("%d", &count); /* se citeşte un întreg în variabilă count */ scanf ("%s", address); /* se citeşte un şir de caractere în vectorul address */ scanf ("%d %d", &r, &c); /* se citesc doua numere separate prin spaţiu, tab sau linie noua */ Un * plasat între % şi caracterul de conversie, va suspenda atribuirea datei citite. Astfel, instrucţiunea : scanf("%d%*c%d", &x, &y); face ca, dacă de la tastatură se introduce 10/20, 10 să fie atribuit lui x, %*c este ignorat (nu este atribuit), iar 20 se atribuie lui y. Instrucţiunea : scanf("%20s", sir); citeşte nu mai mult de 20 caractere în variabilă şir. Dacă se introduce un şir de mai mult de 20 caractere, vor fi reţinute numai primele 20, iar restul se pierd. Pentru caracterele rămase se poate apela din nou funcţia scanf() sub forma : scanf("%s", sir); care va plasa restul caracterelor tot în "şir". Dacă de la tastatura se introduce 10#20, instrucţiunea : scanf("%s#%s", &x, &y); va plasa 10 în x şi 20 în y. Instrucţiunea : scanf("%s ", name); nu se încheie decât dacă după ultimul caracter se introduce un spaţiu. Exemplu de utilizare a funcţiilor fscanf şi fprintf. /* Acest program scrie date cu format cu printf intr-un fisier. Apoi foloseste fscanf pentru a citi datele din fisier */ #include <stdio.h> FILE *stream; void main( void ) { long l; float fp; char s[81]; char c; stream = fopen( "fscanf.out", "w+" ); if( stream == NULL ) 234
• 241. printf( "Fisierul fscanf.out nu a fost deschisn" ); else { fprintf( stream, "%s %ld %f%c", "a-string", 65000, 3.14159, 'x' ); /* Seteaza pointerul la inceputul fisierului: */ fseek( stream, 0L, SEEK_SET ); /* Citeste datele inapoi din fisierul disc: */ fscanf( stream, "%s", s ); fscanf( stream, "%ld", &l ); fscanf( stream, "%f", &fp ); fscanf( stream, "%c", &c ); /* Tipareste datele citite din fisier: */ printf( "%sn", s ); printf( "%ldn", l ); printf( "%fn", fp ); printf( "%cn", c ); fclose( stream ); }} 10.4.9. Funcţii de citire şi scriere a caracterelor a) Funcţia fgetc() int fgetc(FILE *fp) Funcţia fgetc() întoarce următorul caracter al fişierului indirectat cu "fp", caracter de tip unsigned char (convertit la int) sau EOF dacă s-a detectat sfârşitul de fişier sau a apărut o eroare. Exemplu de utilizare a funcţiei fgetc(). /* Acest program foloseste getc pentru a citi 80 de caractere dintr-un fisier si apoi le plaseaza dintr-un buffer la intrarea standard */ #include <stdio.h> #include <stdlib.h> void main( void ) { FILE *stream; char buffer[81]; int i, ch; /* Deschide fisierul pentru a citi o inregistrare */ if( (stream = fopen( "fgetc.c", "r" )) == NULL ) exit( 0 ); /* Citeste primele 80 de caractere si le plaseaza in "buffer": */ ch = fgetc(stream); for(i=0;(i<80) && (feof(stream)==0); i++ ) { buffer[i] = (char)ch; ch = fgetc( stream ); } /* Adauga null la sfarsitul fisierului */ buffer[i] = '0'; printf( "%sn", buffer ); fclose( stream );} 235
• 242. b) Funcţia getc() int getc (FILE *fp) Această funcţie este identică cu fgetc() cu deosebirea că este o macrodefiniţie, putând să evalueze fişierul mai mult decât o dată. Observaţie: "fp" este un pointer-fişier returnat de funcţia fopen(). Exemplu: Pentru a citi un fişier text până la întâlnirea indicatorului de sfârşit de fişier se scrie: ch = getch (fp); while (ch != EOF) { ch = getc (fp); } c) Funcţia getchar() int getchar(void) Funcţia getchar() este echivalentă cu getc (stdin) . Dezavantajul funcţiei getchar() este că această poate păstra în bufferul de intrare nu unul, ci mai multe caractere, primul caracter fiind preluat după apasarea tastei CR. d) Funcţiile getche() şi getch() int getche(void) int getch(void) Funcţiile introduc un caracter de la tastatură. Funcţiile asteaptă până se apasă o tastă şi apoi întorc valoarea acesteia. Funcţia getche() preia un caracter cu "ecou" iar getch() preia caracterul fără ecou. e) Funcţia gets() char *gets(char *s) Funcţia gets() citeşte un şir de caractere introduse de la tastatură şi îl plasează în vectorul indirectat prin s. §irul se termină cu 'n' ce va fi înlocuit cu '0'. Funcţia întoarce vectorul s sau EOF, în caz de eroare. f) Funcţia fgets() char *fgets(char *s, int n, FILE *fp) Funcţia fgets() citeşte în tabloul s cel mult n-1 caractere, oprindu-se dacă a detectat NL (New Line) care este inclus în tablou, înlocuit prin'0'. Funcţia întoarce tabloul s, sau NULL, dacă apare o eroare sau sfârşit de fişier. Exemplu de folosire a funcţiei fgets. /* Acest program utilizeaza fgets pentru afisarea unei linii dintr-un fisier la display */ #include <stdio.h> void main( void ) { FILE *stream; char line[100]; 236
• 243. if( (stream = fopen( "fgets.c", "r" )) != NULL ) { if( fgets( line, 100, stream ) == NULL) printf( "fgets errorn" ); else printf( "%s", line); fclose( stream ); }} a') Funcţia fputc() int fputc(int ch, FILE *fp) Funcţia fputc() scrie caracterul "ch" convertit la unsigned char, în fişierul indirectat prin "fp". Întoarce valoarea caracterului scris, sau EOF, dacă apare vreo eroare. Exemplu de utilizare a funcţie fputc. /* Acest program foloseste fputc si _fputchar pentru a trimite un sir de caractere la stdout. */ #include <stdio.h> void main( void ) { char strptr1[] = "Test pentru fputc !!n"; char strptr2[] = "Test pentru _fputchar!!n"; char *p; /* Tipareste linia folosind fputc. */ p = strptr1; while((*p != '0') && fputc(*(p++), stdout)!=EOF); /* Identic cu _fputchar. (Aceasta functie nu este compatibila ANSI */ p = strptr2; while((*p != '0') && _fputchar(*(p++))!=EOF);} În general, funcţiile care încep cu _ (subliniere) dar şi cu f (de la file) sunt destinate lucrului cu interfeţele standard stdin şi stdout. b') Funcţia putc() int putc(int ch, FILE *fp) Funcţia putc() este echivalenta cu fputc() cu excepţia că este o macrodefinitie, putând evalua fişierul mai mult decât o dată. c') Funcţia putchar() int putchar(int ch) Funcţia putchar(ch) este echivalenta cu putc (ch, stdout). d') Funcţia de la punctul d) nu are un corespondent pentru ieşire. e') Funcţia puts() int puts(const char *s) Funcţia puts() scrie şirul "s" şi NL în "stdout". Întoarce EOF, dacă apare o eroare, sau o valoare nenegativă în caz contrar. f') Funcţia fputs() int fputs(const char *s,FILE *fp) 237
• 244. Funcţia fputs() scrie şirul "s" (care nu este neapărat necesar să conţină 'n') în fişierul "fp". Întoarce EOF, în caz de eroare, o valoare nenegativă, în caz contrar. Spre exemplu, /* Acest program foloseste fputs pentru a scrie o linie la terminalul standard */ #include <stdio.h> void main( void ) { fputs( "Hello world from fputs.n", stdout );} Funcţia ungetc() int ungetc(int ch,FILE *fp) Funcţia ungetc() pune argumentul "ch" inpoi în fişier, de unde va fi preluat la următoarea citire. Se poate pune inapoi în fişier doar un singur caracter. Marcajul EOF nu poate fi pus înapoi. Funcţia întoarce caracterul ce trebuie pus, sau EOF, în caz de eroare. Funcţiile getw() şi putw() Aceste funcţii se folosesc pentru a citi, respectiv a scrie întregi dintr-un sau într-un fişier disc. Aceste funcţii lucrează exact că funcţiile getc() şi putc(). Exemplu de utilizare a functiilor fprintf() şi fscanf() : Programul permite actualizarea unei agende telefonice. # include "stdio.h" void ad_num(void); /*prototipul functiilor */ void cauta(void); char menu(void); void main() { char choice; do { choice = menu(); switch (choice) { case 'a' : ad_num(); break; case 'c' : cauta(); break; } } while (choice != 'q'); } char menu() {/* Afiseaza meniul si preia optiunea */ char ch; do { printf ("A(dauga), C(auta), Q(uit) : "); ch = tolower (getche()); printf ("n"); } while (ch != 'q' && ch != 'a' && ch != 'c'); return ch; } void ad_num() /* Adauga un nou numar */ {FILE *fp; char name[80]; int a_code, schimb, numar; 238
• 245. if ((fp = fopen ("telefon", "a")) == NULL) { printf ("Cannot open file n"); exit (1);} printf("Introduceti numele si numarul: "); fscanf(stdin,"%s%d%d%d",nume,&a_code,&schimb, &numar); fscanf(stdin,"%*c"); /* inlatura CR din sirul de intrare */ /* Se scrie in fisier */ printf("%d",fprintf(fp,"%s %d %d %dn", nume, a_cod, schimb, numar)); fclose (fp); } void cauta() /* Cauta un numar dandu-se un nume */ { FILE *fp; char nume[80], nume2[80]; int a_code, schimb, numar; /* Se deschide fisierul pentru citire */ if ((fp = fopen ("telefon", "r")) == NULL) { printf("Cannot open filen "); exit (1); } printf ("Nume ?"); gets (nume); /* Se cauta numarul */ while (!feof (fp)) { fscanf(fp,"%s%d%d%d", nume2, &a_cod, &schimb, &numar); if (!strcmp(nume, nume2)) { printf("%s : (%d) %d - %dn", nume, a_code, schimb, numar); break;} } fclose (fp);} Capitolul XI UTILIZAREA ECRANULUI ÎN MOD TEXT Biblioteca standard a limbajului C şi C++ conţine funcţii pentru gestiunea ecranului. Acesta poate fi gestionat în două moduri: în mod text sau în mod grafic. Prezentăm funcţiile standard mai importante utilizate la gestiunea ecranului în mod text. Toate funcţiile standard de gestiune a ecranului în mod text au prototipurile în fişierul antet <conio.h>. Modul text presupune că ecranul este compus dintr-un număr de linii şi un număr de coloane. În mod curent se utilizează 25 de linii a 80 de coloane fiecare, deci ecranul are o capacitate de 25*80=2000 de caractere. 239
• 246. Poziţia pe ecran a unui caracter se defineşte printr-un sistem de două coordonate întregi (x,y) unde: x este numărul coloanei în care este situat caracterul y este numărul liniei în care este situat caracterul Colţul din stânga sus al ecranului are coordonatele (1,1), iar colţul din dreapta jos are coordonatele (80,25). În mod implicit, funcţiile de gestiune a ecranului în mod text au acces la tot ecranul. Accesul poate fi însă limitat la o parte din ecran utilizând aşa numitele ferestre. Fereastra este o parte a ecranului în formă de dreptunghi şi care poate fi gestionată separat de restul ecranului. Atributele unui caracter de pe ecran sunt următoarele: - coordonatele x şi y; - culoarea caracterului; - culoarea fondului pe care este afişat caracterul; - clipirea caracterului. 11.1. Setarea ecranului în mod text Se realizează cu ajutorul funcţiei textmode care are următorul prototip: void textmode (int modtext) unde modtext poate fi exprimat simbolic sau numeric în modul următor: Modul text Constantă simbolică Valoare Caractere albe pe fond negru; BW40 0 40 de coloane Color 40 de coloane C40 1 Caractere albe pe fond negru; BW80 2 80 de coloane Color 80 de coloane C80 3 Monocrome 80 de coloane MONO 7 Color cu 50 de linii pentru C4350 64 placa VGA Modul precedent LASTMODE -1 Modul MONO se poate seta pe un adaptor monocolor, în timp ce celelalte moduri se pot seta pe adaptoare color. 240
• 247. 11.2. Definirea unei ferestre După setarea ecranului în mod text, este activ tot ecranul şi acesta are caracteristicile indicate în paragraful precedent. De multe ori însă se doreşte partajarea ecranului în zone care să poată fi gestionate în mod independent. Acest lucru poate fi realizat cu ajutorul ferestrelor. O fereastră se defineşte cu ajutorul funcţiei window care are următorul prototip: void window (int x1, int y1, int x2, int y2); unde: (x1,y1) – reprezintă coordonatele colţului stânga sus ; (x2,y2) – reprezintă coordonatele colţului dreapta jos. La un moment dat este activă o singură fereastră şi anume acea definită la ultimul apel al funcţiei window. Dacă parametri de la apelul funcţiei window sunt eronaţi, aceasta nu are nici un efect. 11.3. Ştergerea unei ferestre O fereastră activă se şterge utilizând funcţia clrscr care are următorul prototip: void clrscr(void) După apelul acestei funcţii, fereastra activă (sau tot ecranul dacă nu s-a definit în prealabil o fereastră cu funcţia window) devine vidă. Fondul ei are culoarea definită prin culoarea de fond curentă. Funcţia clrscr poziţionează cursorul pe caracterul din stânga sus al ferestrei active, adică în poziţia de coordonate (1,1). 11.4. Deplasarea cursorului Cursorul poate fi plasat pe un caracter al ferestrei active folosind funcţia gotoxy ce are următorul prototip: void gotoxy (int x, int y); unde (x, y) reprezintă coordonatele caracterului pe care se plasează cursorul. Dacă la apelul funcţiei coordonatele sunt definite în afara ferestrei active, funcţia este ignorată. Poziţia cursorului din fereastra activă poate fi determinată cu ajutorul funcţiilor wherex şi wherey care returnează numărul coloanei, respectiv liniei, din fereastra activă pe care se află cursorul, şi care au următoarele prototipuri: int wherex(void); 241
• 248. int wherey(void); În cazul în care se doreşte ascunderea cursorului (facerea invizibilă a cursorului) se utilizează o secvenţă ce utilizează funcţia geninterrupt: { _AH=1; _CH=0x20; geninterrupt(0x10); } Cursorul poate fi rafiaşat utilizând următoarea secvenţă: { _AH=1; _CH=6; _CL=7; geninterrupt(0x10);} 11.5. Setarea culorilor Culoarea fondului se setează cu ajutorul funcţiei textbackground ce are următorul prototip: void textbackground(int culoare); unde culoare este un întreg în intervalul [0, 7] şi are semnificaţia din tabelul de mai sus. Pot fi setate ambele culori precum şi clipirea caracterului folosind funcţia textattr ce are următorul prototip: void textattr (int atribut) unde atribut se defineşte cu ajutorul relaţiei: atribut=16*culoare_fond+culoare_caracter+clipire; Culoarea caracterelor se setează cu ajutorul funcţiei textcolor ce are următorul prototip: void textcolor(int culoare); unde culoare este un întreg în intervalul [0,15] a cărui semnificaţie este explicată de tabelul următor: Culoare Constantă simbolică Valoare Negru BLACK 0 Albastru BLUE 1 Verde GREEN 2 Turcoaz CYAN 3 Roşu RED 4 Purpuriu MANGETA 5 Maro BROWN 6 Gri deschis LIGHTGRAY 7 Gri închis DARKGRAY 8 Albastru deschis LIGHTBLUE 9 242
• 249. Verde deschis LIGHTGREEN 10 Turcoaz deschis LIGHTCYAN 11 Roşu deschis LIGHTRED 12 Purpuriu deschis LIGHTMANGETA 13 Galben YELLOW 14 Alb WHITE 15 Clipire BLINK 128 11.6. Funcţii pentru gestiunea textelor Pentru afişarea caracterelor se pot folosi funcţiile: - int putch (int c); – afişează un singur caracter; - int cputs (const char *str); – afişează un şir de caractere în mod similar funcţiei puts; - int cprintf (const char *format); – afişează date sub controlul formatelor în mod similar funcţiei printf. - void clreol (void); - şterge sfârşitul liniei începând cu poziţia cursorului; - void delline (void); - şterge toată linia pe care este poziţionat cursorul; - int gettext (int left, int top, int right, int bottom, void *destination); - copiază textul cuprins în dreptunghiul definit de coordonatele (left, top) – stânga sus şi (right, bottom) – dreapta jos la adresa de memorie indicată de pointerul destination; - int puttext( int left, int top, int right, int bottom, void *source ); - citeşte textul cuprins în dreptunghiul definit de coordonatele (left, top) – stânga sus şi (right, bottom) – dreapta jos de la adresa de memorie indicată de pointerul source; - int movetext( int left, int top, int right, int bottom, int destleft, int desttop ); - mută textul cuprins în dreptunghiul definit de coordonatele (left, top) – stânga sus şi (right, bottom) – dreapta jos în dreptunghiul cu coordonatele colţului din stânga sus (destleft, desttop); - void insline (void); - inserează o linie vidă în fereastra activă; - int getch (void); - citeşte un caracter fără ecou de la tastatură, adică după ce este citit caracterul nu mai este afişat pe ecran; funcţia returnează codul ASCII al caracterului citit de la tastatură. 243
• 250. - int getche (void); - citeşte un caracter cu ecou de la tastatură, adică după ce este citit caracterul este afişat automat pe ecran; funcţia returnează codul ASCII al caracterului citit de la tastatură. - int kbhit (void); - controlează dacă s-a tastat ceva la tastatură. Dacă a fost apăsată o tastă se returnează o valoare diferită de zero, altfel se returnează valoarea 0. Exemplu: Următorul program desenează o fereastră şi scrie un număr în aceasta. #include <stdio.h> #include <stdlib.h> #include <conio.h> #include <alloc.h> #include <dos.h> #define MAX 100 #define SIMPLU 1 #define DUBLU 2 typedef struct{ int x,y,u,v; void *zonfer; }ELEM; ELEM *stiva[MAX]; int istiva; void orizontal(int,int); void vertical(int,int,int,int); void fereastra(int st,int sus,int dr,int jos,int fond,int culoare, int chenar,int n) //Afiseaza o fereastra limitata de un chenar { extern ELEM *stiva[]; extern int istiva; //memoreaza partea din ecran pe care se va afisa fereastra if(istiva==MAX){ printf("nPrea multe ferestre!"); exit(1); } if ((stiva[istiva]=(ELEM *)farmalloc(sizeof(ELEM)))==0){ printf("nMemorie insuficientan"); exit(1); } stiva[istiva]->x=st; stiva[istiva]->y=sus; stiva[istiva]->u=dr; stiva[istiva]->v=jos; if((gettext(st,sus,dr,jos,stiva[istiva]->zonfer))==0){ printf("nEroare la memorarea ecranului!"); 244
• 251. exit(1); } istiva++; //Activeaza fereastra si o afiseaza pe ecran window(st,sus,dr,jos); textattr(16*fond+culoare); clrscr(); //Trasare chenar if (chenar){ textcolor(WHITE); highvideo(); switch(chenar){ case SIMPLU: putch(218); break; case DUBLU: putch(201); break; } orizontal(dr-st-2,chenar); switch(chenar){ case SIMPLU: putch(191); break; case DUBLU: putch(187); break; } vertical(jos-sus,1,2,chenar); gotoxy(1,jos-sus+1); switch(chenar){ case SIMPLU: putch(192); break; case DUBLU: putch(200); break; } orizontal(dr-st-2,chenar); vertical(jos-sus-1,dr-st,2,chenar); gotoxy(dr-st,jos-sus+1); switch(chenar){ case SIMPLU: putch(217); break; case DUBLU: putch(188); break; } normvideo(); 245
• 252. textattr(16*fond+culoare); } gotoxy(3,3); cprintf("%d",n); //Ascunde cursorul _AH=1; _CH=0x20; geninterrupt(0x10); } void orizontal(int a,int chenar) { while(a--) switch(chenar){ case SIMPLU: putch(196); break; case DUBLU: putch(205); break; } } void vertical(int a,int col,int lin,int chenar) { while(a--) { gotoxy(col,lin++); switch(chenar){ case SIMPLU: putch(179); break; case DUBLU: putch(186); break; } } } void main(void) { clrscr(); fereastra(4,4,60,20,3,10,2,6); getch(); } Capitolul XII UTILIZAREA ECRANULUI ÎN MOD GRAFIC Pentru aplicaţiile grafice limbajul C pune la dispoziţie peste 60 de funcţii standard ce au prototipul în fişierul graphics.h. În 246
• 253. continuare sunt prezentate cele mai importante funcţii ce permit gestiunea ecranului în mod grafic. 12.1. Iniţializarea modului grafic Pentru a se putea lucra în mod grafic trebuie realizată o iniţializare utilizând funcţia initgraph. Aceasta poate fi folosită singură sau împreună cu o altă funcţie numită detectgraph care determină parametrii adaptorului grafic. Prototipul ei este: void far detectgraph(int far *gd, int far *gm); unde: Pointerul gd păstrează adresa uneia din valorile din tabelul următor (în funcţie de adaptorul grafic utilizat): Constantă simbolică Valoare CGA 1 MCGA 2 EGA 3 EGA64 4 EGAMONO 5 IBM8514 6 HERCMONO 7 ATT400 8 VGA 9 PC3270 10 Valorile spre care pointează gd definesc nişte funcţii standard corespunzătoare adaptorului grafic. Aceste funcţii se numesc drivere. Ele se află în subdirectorului BGI. Funcţia detectgraph detectează adaptorul grafic prezent la calculator şi păstrează valoarea corespunzătoare acestuia în zona spre care pointează gd. Zona spre care pointează gm memorează una din valorile: Adaptor Constantă simbolică - Rezoluţie Valoare 247
• 254. CGA CGAC0 – 0 320*200 CGAC1 – 1 320*200 CGAC2 – 2 320*200 CGAC3 – 3 320*200 CGAHI – 4 640*200 EGA EGALO – 0 640*200 EGAHI – 1 640*350 VGA VGALO – 0 640*200 VGAMED – 1 640*350 VGAHI – 2 640*480 Modul grafic se defineşte în aşa fel încât el să fie cel mai performant pentru adaptorul grafic curent. Cele mai utilizate adaptoare sunt cele de tip EGA şi VGA. Apelul funcţiei detectgraph trebuie să fie urmat de apelul funcţiei initgraph. Aceasta setează modul grafic în conformitate cu parametri stabiliţi de apelul prealabil al funcţiei detectgraph şi are următorul prototip: void far initgraph(int far *gd,int far *gm, int far *cale); unde: gd şi gm sunt pointeri ce au aceeaşi semnificaţie ca şi în cazul funcţiei detectgraph; cale este pointer spre şirul de caractere care defineşte calea subdirectorului BGI care conţine driverele. De exemplu dacă BGI este subdirector al directorului BORLANDC, atunci se utilizează şirul de caractere: ”C:BORLANDCBGI” Exemplu: Pentru setarea în mod implicit a modului grafic se poate utiliza următoarea secvenţă de instrucţiuni: …………………………… int gd,gm; detectgraph(&gd,&gm); initgraph(&gd,&gm, ”C:BORLANDCBGI”); …………………………… Doar după apelul funcţiei initgraph pot fi utilizate şi alte funcţii de gestionare a ecranului în mod grafic. Utilizatorul poate defini el însuşi parametri pentru iniţializarea modului grafic. De exemplu, secvenţa următoare: …………………………… 248
• 255. int gd=1,gm=0; initgraph(&gd,&gm, ”C:BORLANDCBGI”); …………………………… setează modul grafic corespunzător unui adaptor grafic CGA cu rezoluţia 320*200 de puncte. În afara acestor funcţii mai pot fi utilizate şi următoarele funcţii: void far setgraphmode (int mode) – utilizată pentru setarea modului grafic unde mode are valorile 0 – 4 pentru VGA, 0-1 pentru EGA, 0 – 2 pentru VGA; void far retorecrtmode(void) – ce permite revenirea la modul precedent; void far graphdefaults(void) – repune parametri grafici la valorile implicite; int far getgraphmode(void) – returnează codul modului grafic; char *far getmodename(int mod) – returnează pointerul spre numele modului grafic definit de codul numeric mod; char *far getdrivername(void) – returnează pointerul spre numele drieverului corespunzător adaptorului grafic curent; void far getmoderange(int grafdriv,int far *min, int far *max) – defineşte valorile minimale şi maximale ale modului grafic utilizat. void far closegraph(void) – se utilizează pentru a ieşi din modul grafic. 12.2. Gestiunea culorilor Adaptoarele grafice sunt prevăzute cu o zonă de memorie în care se păstrează date specifice gestiunii ecranului. Această zonă de memorie poartă denumirea de memorie video. În mod grafic, ecranul se consideră format din puncte luminoase numite pixeli. Poziţia pe ecran a unui pixel se defineşte prin două valori întregi: (x,y) unde: x – defineşte coloana în care este afişat pixelul; y – defineşte linia în care este afişat pixelul. Fiecărui pixel îi corespunde o culoare ce este pătrată în memoria video. Numărul maxim de culori care pot fi afişate cu ajutorul unui adaptor EGA este 64.. Culorile se codifică prin numere întregi din intervalul [0, 63] şi prin constante simbolice. Cele 64 de culori nu pot fi afişate simultan. În cazul adaptorului EGA pe ecran se 249
• 256. pot afişa cel mult 16 culori ce formează o paletă. Paleta implicită este dată de tabelul următor: Denumire simbolică Valoare BLACK 0 BLUE 1 GREEN 2 CYAN 3 RED 4 MANGETA 5 BROWN 6 LLIGHTGRAY 7 DARKGRAY 8 LIGHTBLUE 9 LIGHTGREEN 10 LIGHTCYAN 11 LIGHTRED 12 LIGHTMANGETA 13 YELLOW 14 WHITE 15 În mod implicit, culoarea fondului este întotdeauna cea corespunzătoare indicelui zero, iar culoarea pentru desenare este cea corespunzătoare indicelui 15. Pentru controlul culorilor pot fi utilizate următoarele funcţii: void far setbkcolor(int culoare) – modifică culoarea fundalului; int far getbkcolor(void) – returnează indexul din tabloul care defineşte paleta pentru culoarea fundalului; void far setcolor(int culoare) – setează culoarea utilizată pentru desenare; int far getcolor(void) – returnează indexul din tabloul care defineşte paleta pentru culoarea de desenare; void far setpalette(int index,int cod) – setează o nouă culoare în paleta ce este utilizată la colorare (index ia valori între [0, 15] iar cod între [0, 63]); void far setallpalette(struct palettetype far* paleta) – modifică mai multe culori din paletă. Palettetype este o structură definită ca mai jos: struct palettetype { unsigned char size; 250
• 257. signed char colors[MAXCOLORS+1]; }; unde size – este dimensiunea paletei; colors – este un tablou ale cărui elemente au ca valori codurile culorilor componente ale paletei care se defineşte. Modificarea paletei curente cu ajutorul funcţiei setpalette sau setallpalette conduce la schimbarea corespunzătoare a culorilor afişate pe ecran în momentul apelului funcţiilor respective. void far getpalette(struct palettetype far* paleta) – determină codurile culorilor componente ale paletei curente; int far getmaxcolor(void) – returnează numărul maxim de culori diminuat cu 1; int far getpalettesize(void) – returnează numărul culorilor componente ale paletei. 12.3. Setarea ecranului În mod grafic, ecranul se compune din n*m puncte luminoase (pixeli), adică pe ecran se pot afişa m linii a n pixeli fiecare. Poziţia unui pixel este dată de două numere întragi (x,y) numite coordonatele pixelului. Pixelul aflat în stânga sus are coordonatele (0,0). Coloanele se numerotează de la stânga la dreapta, iar liniile de sus în jos. Informaţii referitoare la ecran pot fi obţinute cu ajutorul următoarelor funcţii: int far getmaxx(void) – returnează coordonta maximă pe orizontală; int far getmaxy(void) – returnează coordonta maximă pe verticală; int far getx(void) – returnează poziţia pe orizontală a pixelului curent; int far gety(void) – returnează poziţia pe verticală a pixelului curent. 12.4. Utilizarea textelor în mod grafic Afişarea textelor în modul grafic presupune definirea unor parametri care pot fi controlaţi prin intermediul funcţiilor descrise în continuare: a) void far settextstyle(int font,int direcţie,int charsize) 251
• 258. unde: font – defineşte setul de caractere şi poate lua următoarele valori: Constantă simbolică Valoare DEFAULT_FONT 0 TRIPLEX_FONT 1 SMALL_FONT 2 SANS_SERIF_FONT 3 GOTHIC_FONT 4 direcţie – defineşte direcţia de scris a textului, astfel: - de la stânga la dreapta: HORIZ_DIR; - de jos în sus: VERT_DIR. charsize – defineşte dimensiunea caracterului în pixeli, astfel: Valoarea Matricea utilizată pentru parametrului afişarea caracterului (în pixeli) 1 8*8 2 16*16 3 24*24 …. …….. 10 80*80 b) void far settextjustify(int oriz, int vert) – defineşte cadrajul textului; oriz – defineşte încadrarea pe orizontală, astfel: - în stânga: LEFT_TEXT; - în centru: CENTER_TEXT; - în dreapta: RIGHT_TEXT. vert – defineşte încadrarea pe verticală, astfel: - marginea inferioară: BOTTOM_TEXT; - în centru: CENTER_TEXT; - marginea superioară: TOP_TEXT. După setarea acestor parametri pot fi afişate texte folosind funcţiile outtext şi outtextxy care au următoarele prototipuri: void far outtext(char far* şir) , unde şir este un pointer spre zona de memorie în care se păstrează caracterele de afişat, afişează caracterele începând cu poziţia curentă de pe ecran; void far outtextxy(int x,int y,char far* şir) , unde şir este un pointer spre zona de memorie în care se păstrează caracterele de afişat, x,y defineşte poziţia de pe ecran unde se face afişarea. 252
• 259. Dimensiunile în pixeli ale unui şir de caractere se pot determina utilizând funcţiile textheight şi textwidth: void far textheight(char far* şir) – returnează înălţimea în pixeli a şirului păstrat în zona spre care pointează şir, void far textwidth(char far* şir) – returnează lălţimea în pixeli a şirului păstrat în zona spre care pointează şir. 12.5. Gestiunea imaginilor În modul grafic, ecranul poate fi partajat în mai multe părţi ce pot fi gestionate independent. Aceste părţi se numesc ferestre grafice. Următoarele funcţii sunt utilizate pentru prelucrarea ferestrelor grafice: void far setviewport(int st, int sus, int dr, int jos, int d) – defineşte o fereastră grafică, unde: - (st,sus) – coordonatele colţului stânga sus al ferestrei; - (dr,jos) – coordonatele colţului dreapta jos al ferestrei; - d – indicator cu privire la decuparea desenului. Dacă d are valoarea 1, atunci funcţiile de afişare a textelor şi de desenare nu pot scrie sau desena în afara limitelor ferestrei. void far clearviewport(void) – şterge fereastra activă; după apelul acestei funcţii, toţi pixelii ferestrei au aceeaşi culoare, şi anume culoarea de fond, iar poziţia curentă a cursorului este punctul de coordonate relative (0,0); void far cleardevice(void) – şterge tot ecranul iar poziţia curentă a cursorului este colţul din stânga sus al ecranului; void far getviewsettings(struct viewporttype far* fereastra) – returnează parametri ferestrei active. Imaginea ecranului se păstrează în memoria video a adaptorului grafic şi formează o pagină. Funcţiile următoare sunt utilizate pentru gestionarea paginilor void far setactivepage(int nrpag) – activează o pagină al cărei număr este specificat de parametrul nrpag; void far setvisualpage(int nrpag) – cu toate că în mod normal este vizualizată pe ecran pagina activă, utilizatorul are posibilitatea de a vizualiza altă pagină decât cea activă utilizând această funcţie (această funcţie poate fi utilă pentru animaţie); 253
• 260. void far getimage(int st, int sus, int dr, int jos,void far* zt) – salvează o zonă dreptunghiulară de pe ecran, unde: - (st,sus) – coordonatele colţului stânga sus a zonei de pe ecran ce se salvează; - (dr,jos) – coordonatele colţului dreapta jos a zonei de pe ecran ce se salvează; - zt – pointer spre zona de memorie în care se salvează imaginea de pe ecran. unsigned far imagesize(int st, int sus, int dr, int jos) – determină dimensiunea unei zone dreptunghiulare de pe ecran, unde: - (st,sus) – coordonatele colţului stânga sus a zonei de pe ecran; - (dr,jos) – coordonatele colţului dreapta jos a zonei de pe ecran. void far putimage(int st, int sus, int jos,void far* zt, int op) – afişează oriunde pe ecran o zonă dreptunghiulară salvată cu funcţia getimage, unde: - (st,sus) – coordonatele colţului stânga sus a zonei de pe ecran ce se salvează; - zt – pointer spre zona de memorie în care se păstrează imaginea ce se va afişa pe ecran; - op – defineşte operaţia între datele aflate în zona spre care pointează zt şi cele existente pe ecran în zona dreptunghiulară definită de parametri st, sus. Parametrul op se defineşte astfel: Constantă Valoare Acţiune simbolică COPY_PUT 0 copiază imaginea din memorie pe ecran XOR_PUT 1 „sau exclusiv” între datele de pe ecran şi cele din memorie OR_PUT 2 „sau” între datele de pe ecran şi cele din memorie AND_PUT 3 „şi” între datele de pe ecran şi cele din memorie NOT_PUT 4 copiază imaginea din memorie pe ecran completând datele aflate în memorie 12.6. Desenarea şi colorarea figurilor geometrice Biblioteca standard pune la dispoziţia utilizatorului o serie de funcţii care permit desenarea şi colorarea unor figuri geometrice: 254
• 261. void far putpixel(int x, int y, int culoare) – afişează un pixel pe ecran în punctul de coordonate (x,y) (relativ la fereastra activă) şi având culoarea culoare; unsigned far getpixel(int x, int y) – determină culoarea unui pixel aflat pe ecran în poziţia (x,y); void far moveto(int x, int y) – mută cursorul în dreptul pixelului de coordonate (x,y); void far moverel(int dx, int dy) – mută cursorul în dreptul pixelului de coordonate (x+dx,y+dy), unde (x,y) reprezintă coordonatele pixelului curent; void far line(int xi, int yi, int xf, int yf) – trasează un segment de dreaptă între punctele de coordonate (xi,yi) şi (xf,yf); void far lineto(int x, int y) – trasează un segment de dreaptă între punctul curent şi punctul de coordonate (x,y); void far linerel(int dx, int dy) – trasează un segment de dreaptă între punctul curent şi punctul de coordonate (x+dx,y+dy), unde (x,y) sunt coordonatele punctului curent; void far arc(int xcentru, int ycentru, int unghistart, int unghifin,int raza) – trasează un arc de cerc, unghiurile fiind exprimate în grade sexagesimale; void far circle(int xcentru, int ycentru, int raza) – trasează un cerc, cu (xcentru,ycentru) coordonatele centrului şi raza raza acestuia; void far ellipse(int xcentru, int ycentru, int unghistart, int unghifin,int semiaxamare, int semiaxamică) – trasează un arc de elipsă cu centrul în punctul de coordonate (xcentru,ycentru), semiaxa mare definită de parametrul semiaxamare iar semiaxa mică definită de parametrul semiaxamică; void far rectangle(int st, int sus, int dr, int jos) – trasează un dreptunghi definit de colţurile diagonal opuse; void far drawpoly(int nr, int far* tabpct) – trasează o linie polignală, parametrul nr specificând numărul de laturi iar tabpct este un pointer spre un tablou de întregi ce definesc vârfurile liniei poligonale păstrate sub forma: abscisa_i,ordonata_i unde i are valorile 1,2,…., nr+1; void far setlinestyle(int stil, unsigned şablon, int grosime) – defineşte stilul utilizat pentru trasarea liniilor, unde: stil – este un întreg din intervalul [0,4] care defineşte stilul liniei conform următorului tabel: 255
• 262. Constantă simbolică Valoare Stil SOLID_LINE 0 Linie continuă DOTTED_LINE 1 Linie punctată CENTER_LINE 2 Linie întreruptă formată din liniuţe de două dimensiuni DASHED_LINE 3 Linie întreruptă formată din liniuţe de aceeaşi dimensiune USERBIT_LINE 4 Stil definit de utilizator prin şablon şablon – defineşte stilul liniei şi are sens doar când parametrul stil are valoarea 4; grosime – defineşte lăţimea liniei în pixeli, astfel: NORM_WIDTH – valoarea 1 pixel şi THICK_WIDTH – valoarea 3 pixeli. void far getlinesettingstype(struct linesettingstype far* linieinfo) – este utilizată pentru a determina stilul curent; void far bar(int st, int sus, int dr, int jos) – are aceeaşi semnificaţie cu funcţia rectangle însă dreptunghiul este colorat; void far bar3d(int st, int sus, int dr, int jos, int profunzime, int ind) – funcţia desenează o prismă colorată pentru ind diferit de zero; pentru ind=0, nu se trasează partea de sus a prismei; void far pieslice(int xcentru, int ycentru, int unghistart, int unghifin,int raza) – desenează un sector de cerc colorat; void far fillpoly(int nr, int far* tabpct) – desenează un poligon colorat; void far fillellipse(int xcentru, int ycentru, int semiaxamare, int semiaxamică) – desenează o elipsă colorată; void far setfillstyle(int haşura, int culoare) – defineşte modul de colorare al figurilor, astfel: culoare – defineşte culoarea utilizată pentru haşurare; haşura – defineşte haşura utilizată pentru colorare conform tabelului: Constantă simbolică Valoare EMPTY_FILL 0 SOLID_FILL 1 LINE_FILL 2 LTSLASH_FILL 3 SLASH_FILL 4 BKSLASH_FILL 5 LTBKSLASH_FILL 6 HATCH_FILL 7 256
• 263. XHATCH_FILL 8 INTERLEAVE_FILL 9 WIDE_DOT_FILL 10 CLOSE_DOT_FILL 11 USER_FILL 12 void far setfillpattern(char far *h_utilizator,int culoare) – este utilizată pentru a defini o haşură a utilizatorului, astfel: culoare – defineşte culoarea de haşurare; h_utilizator – este un pointer spre o zonă de memorie care defineşte haşura utilizatorului; void far getfillsettings(struct fillsettingstype far* stilculoare) – este utilizată pentru determinarea stilului curent de colorare; void far floodfill(int x, int y, int culoare) – este o funcţie utilizată pentru colorarea unui domeniu închis, astfel: (x,y) – reprezintă coordonatele unui punct din interiorul domeniului închis; culoare – defineşte culoarea utilizată la trasarea conturului figurii (interiorul este colorat în conformitate cu setările efectuate de funcţia setfillstyle). Exemplu: Prezentăm în acest exemplu un model de utilizare a modului grafic pentru trasarea graficelor unor funcţii matematice elementare. #include <stdio.h> #include <math.h> #include <graphics.h> #include <conio.h> int x,y; float a,b; void desen(void) //functia care deseneaza axele si //coloreaza ecranul { cleardevice(); setbkcolor(14); setcolor(12); line(0,y,2*x,y); line(x,0,x,2*y); line(2*x-4,y-4,2*x,y); line(2*x-4,y+4,2*x,y); line(x,0,x-4,4); line(x,0,x+4,4); } void interval(int l1, int l2) //functia care verifica // daca intervalele pe care sunt definite functiile 257
• 264. // trigonometrice, sunt respectate { while ((a<l1)||(b>l2)) { clrscr(); cleardevice(); setbkcolor(0); printf("reintroduce-ti intervalul astfel incat sa fie cuprins intre -1 si 1:n "); printf("a="); scanf("%f",&a); printf("n"); printf("b="); scanf("%f",&b); printf("n"); } desen();} void grafic(float (*trig)(float))//functia care traseaza // graficul functiilor trigonometrice { float ymax,i,i1,h,y0,y1,lx,ly; h=0.001*(b-a); if (abs(a)>abs(b)) lx=(x-25)/(abs(a)+1); else lx=(x-25)/(abs(b)+1); ymax=0; for(i=a;i<=b;i+=h) if (ymax<abs(trig(i))) ymax=abs(trig(i)); if (ymax>y/2) ymax=y-25; ly=(y-25)/(ymax+1); if (ly>lx) ly=lx; for(i=a;i<=b;i+=h) { y0=trig(i); i1=i*lx ; y1=y0*ly; putpixel(x+i1,y-y1,4);} } float sinx (float x) { return sin(x);} float cosx(float x) { return cos(x);} float tanx(float x) { return tan(x);} float ctanx(float x) { return 1/tan(x);} float acosx(float x) { return acos(x);} float asinx(float x) { return asin(x);} float atanx(float x) { return atan(x);} float actanx(float x) 258
• 265. { return atan(1/x);} void main() { int p,l,t,dr=DETECT, modgr; initgraph(&dr,&modgr,"c:borlandcbgi"); setbkcolor(1); x=getmaxx()/2,y=getmaxy()/2; p=0; while(p==0) { setbkcolor(1); p=1; printf("1 : sin(x)n"); printf("2 : cos(x)n"); printf("3 : tg(x)n"); printf("4 : ctg(x)n"); printf("5 : arcsin(x)n"); printf("6 : arccos(x)n"); printf("7 : arctg(x)n"); printf("8 : arcctg(x)n"); printf("Alegeti nr. corespunzator functiei dorite: "); scanf("%d",&t); while(t<1 || t>8 ) { printf("Reintroducet t-ul cuprins intre 1 si 8n "); printf("t="); scanf("%d",&t);} printf("Dati intervalul: n"); do{ printf("a="); scanf("%f",&a); printf("n"); printf("b="); scanf("%f",&b); printf("n"); if (a>b) printf("Reintroduce-ti intervalul astfel incat a sa fie mai mic ca b:n "); } while(a>b); desen(); switch(t) { case 1:grafic(sinx); break; case 2:grafic(cosx); break; case 3:grafic(tanx); break; case 4:grafic(ctanx); break; case 5:interval(-1,1); grafic(asinx); break; case 6:interval(-1,1); grafic(acosx); break; case 7:grafic(atanx); break; case 8:grafic(actanx); break; 259
• 266. defalut: p=0 ; } getch(); clrscr(); cleardevice(); setbkcolor(0); printf("Doriti graficul altei functii? 1-DA 0-NU :"); scanf("%d", &l); if (l==1){clrscr();cleardevice(); p=0;}} closegraph(); } Capitolul XIII FUNCŢII MATEMATICE Limbajul C conţine mai multe funcţii matematice care utilizează argumente de tip double şi întorc valori de tip double. Aceste funcţii se împart în următoarele categorii: - funcţii trigonometrice; - funcţii hiperbolice; - funcţii exponenţiale şi logaritmice; - alte tipuri. Toate funcţiile matematice sunt incluse în fişierul antet "math.h". Acesta mai conţine o serie de macrodefiniţii cum ar fi EDOM, ERANGE şi HUGE_VAL. Macrodefiniţiile EDOM şi ERANGE se găsesc în fişierul "errno.h" şi sunt constante întregi diferite de zero, utilizate pentru a semnala erorile de domeniu şi de plajă ale funcţiei. HUGE_VAL (aflată tot în "errno.h") este o valoare pozitivă de tip double. Dacă un argument al unei funcţii matematice nu este în domeniul pentru care a fost definită funcţia, atunci funcţia întoarce 0 şi în domeniul de eroare, "errno" este modificat la EDOM. Dacă o funcţie produce un rezultat prea mare pentru a fi reprezentat printr-un double, apare o depăşire, funcţia returnând HUGE_VAL cu semnul adecvat iar "errno" este modificat la ERANGE. Dacă se produce subdepăşire, funcţia întoarce zero, iar "errno" este modificat la ERANGE în funcţie de implementare. 260
• 267. 13.1 Funcţii trigonometrice - sin(x) , x în radiani - sinusul lui x; - cos(x) , x în radiani - cosinusul lui x. - tan(x) , x în radiani - tangenta lui x; Exemplu: Programul următor afişează valorile sinusului, cosinusului şi tangentei unghiului a[-1,+1] radiani, din 0.1 în 0.1. # include <math.h> void main() { double val = -1.0; do { printf("sinusul lui %f este %fn", val, sin(val)); printf("cosinusul lui %f este %fn",val, cos(val)); printf("tangenta lui %f este %fn", val, tan(val)); val += 0.1;} while (val <= 1.0); } 13.2 Funcţii trigonometrice inverse - asin(x) , cu x [-1,1] - arcsinusul lui x; - acos(x) , cu x [-1,1] - arccosinusul lui x; - atan(x) , x R - arctangenta lui x; - atan(y,x) , - returneaza arctg (y/x). Exemplu: Programul următor afişează valorile arcsinusului, arccosinusului şi arctangentei unghiului a[-1,+1], din 0.1 în 0.1. # include <math.h> void main() { double val = -1.0; do { printf("arcsin lui %f este %fn", val, asin(val)); printf("arccos lui %f este %fn", val, asin(val)); printf("arctg lui %f este %fn", val, asin(val)); val += 0.1; } while (val <= 1.0); } 13.3 Funcţii hiperbolice 261
• 268. - sinh(x) , x R - sinus hipebolic de x; - cosh(x) , x R - cosinus hipebolic de x; - tanh(x) , x R - tangenta hipebolica de x. 13.4 Funcţii exponenţiale şi logaritmice - exp(x) , x R - exponentiala lui x. - log(x) , x > 0 - logaritmul natural al lui x; - log10(x) , x>0 - logaritmul zecimal al lui x. Exemplu: printf ("Valoarea lui e este: %f", exp(1.0)); Exemplu: Programul următor afişează valorile logaritmului natural şi logaritmului zecimal din 1 în 1 al numerelor de la 1 la 10. # include <math.h> void main() { double val =1.0; do{printf("%f %f %fn",val,log(val),log10(val)); val ++; } while (val < 11.0);} - pow(x,y); funcţia calculeaza xy. O eroare de domeniu apare dacă x = 0 şi y = 0 sau dacă x < 0 şi y nu este întreg. Exemplu: Programul următor afişeaza primele 11 puteri ale lui 10. # include <math.h> void main() { double x =10.0, y = 0.0; do { printf ("%fn", pow(x,y)); y ++;} while (val < 11.0); } 13.5 Generarea de numere aleatoare În multe aplicaţii este necesară generarea de numere aleatoare. Pentru asemenea cazuri limbajul C dispune de două funcţii, rand şi random, care returnează numere întregi aleatore. Funcţia rand are următorul prototip: int rand(void) şi returnează un număr întreg, aleator, cuprins în intervalul de la 0 la RAND_MAX (valoare definită în fişierul antet stdlib.h). Funcţia random are prototipul: int random(int val_maxima) 262
• 269. şi returnează un număr întreg, aleator, cuprins în intervalul [0, val_maxima]. Pentru generarea de numere aleatoare în virgulă mobilă se împarte rezultatul funcţiei random la o valoare întreagă. Următorul program exemplifică utilizarea acestor funcţii: Exemplu: #include <stdio.h> #include <stdlib.h> void main(void) { int k; printf(”Valorile furnizate de functia randn”); for(k=0;k<100;k++) printf(”%d ”,rand()); printf(”Valorile furnizate de functia random(100)n”); for(k=0;k<100;k++) printf(”%d ”,random(100)); printf(”Valori reale intre 0 si 1n”); for(k=0;k<10;k++) printf(”%f ”,random(10)/10.0); printf(”Valori intregi intre -10 si 10n”); for(k=0;k<10;k++) printf(”%d ”,10-random(20));} 13.6 Alte tipuri de funcţii matematice Nume funcţie Caracterizarea funcţiei sqrt(x) - - radicalul lui x, sqrt(x)= x - ceil(x) - - cel mai mic întreg, mai mare că x, convertit la double (ex. ceil(1.05) va returna valoarea 2). - floor(x) - - cel mai mare întreg, mai mic sau egal cu x, convertit la double (exemplu: floor(1.02) va returna valoarea 1.0, floor(-1.02) va returna valoarea -2.0). - fabs(x) - - modulul numărului x; - ldexp(x, n) - - calculeaza x*2n , unde n este de tip int. - fmod(x, y) - - restul în virgulă mobila a lui x/y, cu acelaşi semn ca x. - modf(x, double *ip) - - împarte pe x în parte întreagă şi parte fracţionară, fiecare cu acelaşi semn că x; memorează partea întreagă în "*ip" şi întoarce partea fracţionară. - modf(x, double *ip) - - întoarce x într-o funcţie normalizată în intervalul [1/2, 1] şi o putere a lui 2, care se memoreaza în "*exp"; dacă x este 0, ambele părţi ale rezultatului 263
• 270. sunt 0. Modul de utilizare al acestor funcţii este similar cu al celorlalte funcţii matematice descrise în acest capitol. Capitolul XIV ELEMENTE DE PROGRAMARE AVANSATĂ 14.1 Gestionarea memoriei Un calculator poate avea trei tipuri de memorie: convenţională, extinsă şi expandată. În programare memoria constituie un factor important ce influenţează viteza de lucru a programelor. Fiecare tip de memorie are diferite viteze de acces, ceea ce afectează performanţa programelor. Volumul şi tipul de memorie instalată poate fi determinat utilizând comanda DOS: C:>MEM /CLASSIFY (pentru versiuni ale sistemului de operare DOS mai mari de varianta 5). Sistemul de operare DOS dispune de capacităţi de gestionare a memoriei ce pot maximiza performanţele calculatorului. 14.1.1 Memoria convenţională 264
• 271. Primul PC compatibil IBM utiliza de obicei între 64Kb şi 256Kb memorie RAM (Read Only Memory). Pe atunci această memorie era mai mult decât suficientă. Astăzi, memoria convenţională a unui PC este formată din primul 1Mb de RAM. Programele DOS rulează, în mod obişnuit, cu primii 640Kb de memorie convenţională. PC-ul utilizează restul de 384Kb de memorie (numită memorie rezervată sau memorie superioară) pentru memoria video a calculatorului, driverele de dispozitive, alte dispozitive HARD mapate în memorie şi BIOS (Basic Input-Output Services – servicii intrare- ieşire de bază). Sistemul de operare Windows utilizează modelul de memorie virtuală pentru a gestiona memoria, ceea ce înseamnă că eliberarea memoriei convenţionale nu are semnificaţie sub acest sistem de operare. Însă, memoria convenţională este importantă când se rulează programe în cadrul unei ferestre DOS sub Windows. Structura memoriei convenţionale a unui calculator personal este următoarea: BIOS ROM Memorie rezervată Memorie video COMMAND.COM Memorie pentru programe Intrări CONFIG.SYS Nucleul DOS Zona de comunicaţii BIOS Vectori de întrerupere BIOS PC-ul împarte memoria în blocuri de 64Kb numite segmente. În mod obişnuit, programul utilizează un segment de cod (ce conţine instrucţiunile programului) şi un al doilea segment de memorie pentru date. Dacă un program este foarte mare compilatorul va trebui să dispună de mai multe segmente de cod sau de date, sau de ambele. Modelul de memorie defineşte numărul de segmente pe care le poate folosi pentru fiecare. Modele sunt foarte importante deoarece, dacă se utilizează un model de memorie necorespunzător, programul poate să 265
• 272. nu deţină suficientă memorie pentru execuţie. Compilatorul va alege un model de memorie suficient de mare pentru a rula programul, însă cu cât memoria utilizată este mai mare cu atât viteza de execuţie a programului scade. Din această cauză trebuie ales modelul cel mai mic pentru necesităţile programului. Majoritatea compilatoarelor acceptă următoarele modele de memorie: a) tiny – combină datele şi codul programului într-un singur segment de 64Kb (este cel mai mic şi mai rapid model de memorie); b) small – utilizează un segment de memorie pentru cod şi un segment pentru date (este cel mai obişnuit model de memorie); c) medium – utilizează un segment de 64Kb pentru date şi două sau mai multe segmente pentru codul programului. În acest caz datele sunt accesate rapid prin utilizarea de adrese near, în schimb însă, apelurile de funcţii se fac utilizând adrese far; d) compact – alocă un segment de 64Kb pentru codul programului şi două sau mai multe segmente pentru date (este un model utilizat pentru programe mici ce manipulează un număr mare de date); e) large – alocă mai multe segmente atât pentru date cât şi pentru cod şi este cel mai lent model de memorie din cele prezentate până acum. El trebuie utilizat doar ca ultimă resursă; f) huge – este un model utilizat doar în cazul utilizării unor matrici mai mari de 64Kb. Pentru a stoca o astfel de matrice programul trebuie să utilizeze cuvântul cheie huge pentru a crea un pointer, astfel: int huge *matrice_uriaşă după care programul trebuie să utilizeze funcţia halloc pentru alocarea memoriei şi funcţia hfree pentru eliberarea acesteia. Exemplul următor alocă o matrice de 400000 octeţi: Exemplu: #include <stdio.h> #include <malloc.h> void main(void) { long int k; int huge *matrice_uriasa; 266
• 273. if ((matrice_uriasa=(int huge*) halloc(100000L,sizeof (long int)))==NULL) printf(”Eroare la alocarea matricii”); else{ printf(”Completeaza matricean”); for(k=0;k<100000L;k++) matrice_uriasa[k]=k%32768; for(k=0;k<100000L;k++) printf(”%d ”,matrice_uriasa[k]); hfree(matrice_uriasa); } } Pentru selectarea unui anumit model de memorie se include, de regulă, o opţiune în cadrul liniei de comandă a compilatorului. Majoritatea compilatoarelor predefinesc o constantă specifică pentru a determina modelul curent de memorie. În tabelul următor sunt prezentate aceste constante definite de compilatoarele Microsoft C şi Borland C: Model de memorie Microsoft C Borland C Small M_I86SM _SMALL_ Medium M_I86MM _MEDIUM_ Compact M_I86CM _COMPACT_ Large M_I86LM _LARGE_ Programul poate verifica modelul de memorie utilizat folosind următoarea secvenţă de instrucţiuni: #ifndef _MEDIUM_ printf(”Programul cere modelul de memorie mediumn”); exit(1); #endif Atunci când un program trebuie să aloce memorie în mod dinamic se utilizează fie funcţia malloc pentru a aloca memorie din zona near (din segmentul curent), fie funcţia fmalloc pentru a aloca memorie far. 14.1.2 Memoria expandată În cazul programelor mari o memorie de numai 1Mb este insuficientă. Pentru a permite accesul la mai mult de 1Mb de memorie, companiile Lotus, Intel şi Microsoft au creat o specificaţie pentru memoria expandată, care combină software şi o platformă specială de memorie expandată pentru a „păcăli” PC-ul în scopul accesării unor volume mari de memorie. Mai întâi în zona de memorie superioară se alocă un bloc de 64Kb după care acest bloc de memorie este împărţit 267
• 274. în patru secţiuni de 16Kb, numite pagini în care se încarcă paginile logice ale programului. De exemplu un program de 128Kb este împărţit în opt pagini de 16Kb fiecare care sunt încărcate în funcţie de necesităţile programului în zona rezervată de 64Kb. 14.1.3 Memoria extinsă Calculatoarele cu procesoare peste 386 utilizează adresarea pe 32 de biţi ceea ce le dă posibilitatea de accesare directă de până la 4Gb de memorie. Programatorii au numit memoria de peste 1Mb memorie extinsă. Pentru a accesa memoria extinsă, trebuie încărcat un driver de dispozitiv pentru memoria extinsă, care în DOS este de obicei himem.sys. Pentru a utiliza însă memoria extinsă este necesară trecerea la modul protejat de lucru al procesorului, mod de lucru în care datele unui program nu pot fi scrise peste datele altui program ce rulează simultan cu acesta. 14.1.4 Stiva Stiva este o regiune de memorie în cadrul căreia programele păstrează temporar datele pe durata execuţiei. De exemplu, atunci când programele transmit parametri către o funcţie, C plasează aceşti parametri în stivă. Când funcţia îşi încheie execuţia aceştia sunt scoşi din stivă. Stiva este numită astfel deoarece ultimele valori depuse sunt primele extrase. În funcţie de modelul de memorie utilizat, spaţiul de memorie ocupat de stivă diferă. Valoarea minimă a stivei este 4Kb. În cazul modelelor compact sau large, C alocă pentru stivă un întreg segment de 64Kb. Dacă un program plasează în stivă mai multe informaţii decât poate reţine aceasta, va apărea o eroare de depăşire a stivei (stack-overflow). Dacă programul a dezactivat testarea stivei, datele depuse în stivă pot fi suprapuse peste datele programului. Exemplul următor prezintă modul de determinare a dimensiunii stivei utilizând funcţia _stklen. Exemplu: #include <stdio.h> #include <dos.h> void main(void) { printf(”Dimensiunea stivei este de %d octeti”,_stklen); } 268
• 275. 14.2 Servicii DOS şi BIOS Aşa cum am menţionat în paragraful anterior, BIOS-ul reprezintă serviciile de intrare-ieşire de bază. Pe scurt, BIOS este un cip din cadrul calculatorului ce conţine instrucţiunile pe care calculatorul le utilizează pentru a scrie pe ecran sau la imprimantă, pentru a citi caractere de la tastatură sau pentru a citi sau scrie pe disc. Programatorii au au proiectat rutinele BIOS pentru a fi utilizate de programe în limbaj de asamblare, totuşi, majoritatea compilatoarelor de C dispun de funcţiide bibliotecă ce permit utilizarea acestor servicii fără a avea nevoie de limbaje de asamblare. DOS este un sistem de operare pentru calculatoarele compatibile IBM PC. Sistemul DOS permite rularea programelor şi păstrează informaţia pe disc. În plus, sistemul DOS pune la dispoziţie servicii ce permit programelor să aloce memorie, să acceseze dispozitive, cum ar fi imprimanta, şi să gestioneze alte resurse ale sistemului. Biblioteca limbajului C oferă o interfaţă la multe servicii DOS, prin intermediul funcţiilor. Mulţi programatori confundă serviciile DOS cu serviciile BIOS. Tabelul următor prezintă relaţia dintre componenta HARD a calculatorului, serviciile BIOS, DOS şi componenta SOFT. Programe Nivel înalt DOS | BIOS | HARDWARE Nivelul cel mai jos Aşa cum se observă, BIOS este situat imediat deasupra componentei hardware, serviciiile DOS deasupra serviciilor BIOS, iar programele deasupra sistemului DOS. Uneori însă, programele pot evita serviciile DOS şi BIOS şi pot accesa direct o componentă hardware (cum este cazul memoriei video). Se recomandă ca ori de câte ori poate fi utilizată o funcţie de bibliotecă C în locul unui serviciu DOS sau BIOS, aceasta să fie utilizată pentru a mări portabilitatea programelor şi la calculatoarele ce utilizează alte sisteme de operare (WINDOWS, UNIX, etc.). În acest caz, programul nu va mai trebui modificat pentru a putea fi rulat sub WINDOWS sau UNIX. 269
• 276. Toate versiunile de WINDOWS vor apela propriile lor servicii de sistem. Însă, serviciile de sistem WINDOWS apelează până la urmă serviciile BIOS penttru a accesa componentele hardware ale cal;culatorului. 14.2.1 Serviciile BIOS Prezentăm în continuare o serie de servicii BIOS ce pot fi accesate utilizând funcţii de bibliotecă ale limbajului C. 1) accesul la imprimantă Înainte ca un program să scrie ieşirea la imprimantă utilizând indicatorul de fişier stdprn se poate face o verificare dacă imprimanta este conectată şi dacă are hârtie utilizând funcţia biosprint din fişierul antet bios.h: int biosprint(int comanda,int octet,int nr_port) unde comanda specifică una din următoarele operaţii: 0 – tipăreşte octetul specificat; 1 – iniţializează portul imprimantei; 2 – citeşte starea imprimantei. Parametrul octet specifică valoarea ASCII a caracterului ce se doreşte a fi scris la imprimantă iar nr_port specifică portul imprimantei care poate fi 0 pentru LPT1, 1 pentru LPT2, ş.a.m.d. Funcţia biosprint returnează o valoarea înteagă pe un octet ai cărui biţi au următoarea semnificaţie: 0 – dispozitiv în pauză; 3 – eroare I/O; 4 – imprimantă selectată; 5 – lipsă hârtie; 6 – confirmare dispozitiv; 7 – dispozitivul nu este ocupat. 2) operaţii intrare/ieşire Operaţiile intrare/ieşire de nivel jos pot fi realizate utilizând funcţia biodisk ce are următoarea sintaxă: int biodisk(int operatie, int unitate, int head, int track, int sector, int nr_sector, void *buffer) unde parametrul unitate precizează numărul unităţii, care este 0 pentru A, 1 pentru B, şi aşa mai departe. Parametrii head, track, sector şi nr_sector precizează sectoarele fizice ale disculuice trebie scris sau citit. Parametru buffer este un pointer la bufferul din care sunt citite sau în care sunt scrise datele. Parametru operatie specifică funcţia dorită astfel: 270
• 277. 0 Iniţializează sistemul de disc 1 Returnează starea ultimei operaţii pe disc 2 Citeşte numărul precizat de sectoare 3 Scrie numărul precizat de sectoare 4 Verifică numărul precizat de sectoare 5 Formatează pista specificată 6 Formatează pista specificată şi marchează sectoarele defecte 7 Formatează unitatea începând cu pista specificată 8 Returnează parametrii unităţii de disc 9 Iniţializează unitatea de disc 10 Execută o citire lungă – 512 octeţi de sector plus patru suplimentari 11 Execută o scriere lungă – 512 octeţi de sector plus patru suplimentari 12 Execută o poziţionare pe disc 13 Iniţializarea alternativă a discului 14 Citeşte bufferul sectorului 15 Scrie bufferul sectorului 16 Testează dacă unitatea este pregătită 17 Recalibrează unitatea 18 Execută diagnosticarea unităţii de RAM 19 Execută diagnosticarea unităţii 20 Execută diagnosticarea internă a controlerului Dacă se execută cu succes, funcţia returnează valoarea 0. Dacă apare o eroare, valoarea returnată precizează eroarea. 3) servicii de tastatură din BIOS Pentru accesul la serviciile de tastatură din BIOS, C-ul pune la dispoziţie funcţia _bios_keybrd ce are următoarea sintaxă: unsigned _bios_keybrd(unsigned comanda) unde parametrul comanda specifică operaţia dorită şi poate avea una din următoarele valori: _KEYBRD_READ Indică funcţiei să citească un caracter de la tastatură _KEYBRD_READY Determină dacă este prezent un caracter la bufferul tastaturii. Dacă funcţia returnează 0, înseamnă că nici o intrare de la tastatură nu este prezentă. Dacă valoarea returnată este 0xFFFF, utilizatorul a apăsat CTRL C _KEYBRD_SHIFTSTATUS Returnează starea tastelor de control: Bit 7 – INS este activat Bit 6 – CAPSLOCK este activat Bit 5 – NUMLOCK este activat 271
• 278. Bit 4 – SCRLLOCK este activat Bit 3 – ALT este apăsată Bit 2 – CTRL este apăsată Bit 1 – SHIFT stânga este apăsată Bit 0 – SHIFT dreapta este apăsată _NKEYBRD_READ Indică funcţiei să citească un caracter de la tastatură, inclusiv tastele speciale, cum ar fi tastele cu săgeţi _NKEYBRD_READY Determină dacă este prezent un caracter la bufferul tastaturii. Dacă funcţia returnează 0, înseamnă că nici o intrare de la tastatură nu este prezentă. Dacă valoarea returnată este 0xFFFF, utilizatorul a apăsat CTRL C Funcţia acceptă inclusiv tastele speciale, cum ar fi tastele cu săgeţi _NKEYBRD_SHIFTSTATUS Returnează starea tastelor de control, inclusiv a tastelor speciale: Bit 15 – SYSREQ este activat Bit 14 – CAPSLOCK este activat Bit 13 – NUMLOCK este activat Bit 12 – SCRLLOCK este activat Bit 11 – ALT dreapta este apăsată Bit 10 – CTRL dreapta este apăsată Bit 9 – ALT stânga este apăsată Bit 8 – CTRL stânga este apăsată 4) obţinerea listei cu echipamente din BIOS Unele programe necesită determinarea caracteristicilor hardware ale calculatorului. Pentru aceasta se utilizează funcţia _bios_equiplist care are următoarea sintaxă: unsigned _bios_equiplist(void); Funcţia returnează o valoare pe 16 biţi a căror valoare are următoarea semnificaţie: 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 15:14 – numărul de imprimante paralele instalate (de la 0 la 3); 13 – imprimanta serială; 12 – adaptorul de jocuri; 11:10:9 – numărul de porturi seriale COM (de la 0 la 7); 8 – prezenţa DMA (Direct Memory Acces); bitul are valoarea 0 dacă există DMA şi 1 dacă nu există; 7:6 – numărul drieverelor de disc; 272
• 279. 5:4 – modul video: 00-neutilizat, 01-mod video 40x25 mono, 10-mod video 80x25 color, 11-mod video 80x25 mono; 3:2 – dimensiunea memorie RAM: 00-16Kb, 01-32Kb, 10-48Kb, 11-64Kb; 1 – prezenţa coprocesorului matematic; 0 – prezenţa unităţii de disc flexibile. 5) controlul intrărilor şi ieşirilor pentru portul serial Pentru a executa operaţii intrare/ieşire utilizând portul serial se utilizează funcţia bioscom ce are următoarea sintaxă: unsigned bioscom(int comanda,int port,char octet); Parametrul comanda specifică operaţia dorită şi poate avea una din următoarele valori: _COM_INIT Stabileşte valorile pentru comunicare ale portului _COM_RECEIVE Primeşte un octet de la port _COM_SEND Trimite un octet la port _COM_STATUS Returnează valorile portului Parametrul port specifică portul serial ce se doreşte a fi utilizat, unde 0 corespunde lui COM1, 1 lui COM2 şi aşa mai departe. Parametrul octet specifică fie octetul pentru ieşire, fie valorile de comunicare dorite. 6) determinarea volumului de memorie convenţională BIOS Pentru a determina memoria convenţională ce poate fi utilizată de către un proggram se utilizează funcţia biosmemory ce are următoarea sintaxă: int biosmemory(void); Valoarea returnată de această funcţie nu cuprinde memoria extinsă, expandată sau superioară. 7) citirea cronometrului BIOS BIOS are incorporat un ceas intern ce bate de 18.2 ori pe secundă. Acest cronometru este util pentru a genera punctul iniţial al unui generator de numere aleatoare. Multe compilatoare de C pun la dispoziţie două funcţii pentru accesul la cronometrul BIOS: biostime şi _bios_timeofday. Sintaxa acestor funcţii este următoarea: long biostime(int operatie,long timp_nou); Parametrul operaţie poate lua două valori: 0 – dacă se doreşte ca funcţia să citească valoarea curentă a cronometrului; 1 – pentru a fixa valoarea cronometrului la valoarea timp_nou. long _bios_timeofday(int operatie,long *batai); 273
• 280. Această funcţie poate fi, de asemenea, utilizată pentru a citi sau a fixa cronometrul BIOS. 14.2.2 Serviciile DOS În acest paragraf prezentăm o serie de servicii DOS ce pot fi accesate utilizând funcţii de bibliotecă ale limbajului C. 1) suspendarea temporară a unui program Execuţia unui program poate fi suspendată temporar utilizând funcţia sleep.h din fişierul antet dos.h: void sleep(unsigned secunde); parametrul secunde specificând numărul de secunde pe care este suspendat programul. 2) utilizarea sunetelor Generarea de sunete ce utilizează difuzorul calculatorului se realizează utilizând funcţiile sound şi nosound: void sound(unsigned frecventa) generează un sunet cu frecvenţa frecventa; void sound(unsigned frecventa) deconectează difuzorul. Programul următor generează un sunet de sirenă dezactivat la apăsarea unei taste: Exemplu: #include <dos.h> #include <conio.h> void main() { unsigned frecventa; do{ for (frecventa=500;frecventa<=1000;frecventa+=50) { sound(frecventa); delay(50); } for (frecventa=1000;frecventa>=500;frecventa-=50) { sound(frecventa); delay(50); } } while(!kbhit()); nosound(); } 3) obţinerea de informaţii despre erori în DOS În cazul în care un serviciu al sistemului DOS eşuează, programele pot cere informaţii suplimentare despre acea eroare folosind funcţia dosexterr: int dosexterr(struct DOSERROR *info_eroare); unde structura DOSERROR are următoarele câmpuri: struct DOSERROR{ int de_exterror; //eroare int de_class; //clasa erorii int de_action;//actiune recomandata 274
• 281. int de_locus;//sursa erorii }; Dacă funcţia returnează valoarea 0, apelul serviciului DOS nu a avut nici o eroare. Clasa erorii descrie categotia erorii, astfel: 01H Resurse depăşite 02H Eroare temporară 03H Eroare de autorizare 04H Eroare de sistem 05H Eroare hardware 06H Eroare de sistem nedatorată programului curent 07H Eroare de aplicaţie 08H Articol neîntâlnit 09H Format nevalid 0AH Articol blocat 0BH Eroare de suport 0CH Articolul există 0DH Eroare necunoscută Parametrul de_action indică programului cum să răspundă erorii, astfel: 01H Mai întâi încearcă din nou, apoi cere intervenţia utilizatorului 02H Încearcă din nou, cu o întârziere, apoi cere intervenţia utilizatorului 03H Cere intervenţia utilizatorului pentru soluţie 04H Renunţă şi elimină 05H Renunţă, dar nu elimina 06H Ignoră eroarea 07H Încearcă din nou după intervenţia utilizatorului Parametrul de_locus specifică sursa erorii, astfel: 01H Sursă necunoscută 02H Eroare de dispozitiv bloc 03H Eroare de reţea 04H Eroare de dispozitiv serial 05H Eroare de memorie 4) citirea valorilor registrului segment Codul programului, datele şi stiva sunt controlate de compilator utilizând patru registre de segment: CS, DS, ES, SS. În unele cazuri este necesar să se cunoască valoarea acestor registre. Pentru astfel de cazuri se utillizează funcţia segread: 275
• 282. void segread(struct SREGS *segs); Structura SREGS are următoarele câmpuri: struct SREGS { unsigned int es; unsigned int cs; unsigned int ss; unsigned int ds; } 5) accesul la valorile de port Pentrul controlul hardware de nivel inferior, compilatoarele de C pun la dispoziţie următoarele funcţii: - int inport (int adresa_port); - citeşte un cuvânt de la portul specificat de parametrul adresa_port; - int inportb (int adresa_port); - citeşte un octet de la portul specificat de parametrul adresa_port; - int outport (int adresa_port); - scrie un cuvânt de la portul specificat de parametrul adresa_port; - int outportb (int adresa_port); - scrie un octet de la portul specificat de parametrul adresa_port; 6) suspendarea unui program Pentru suspendarea unui program pe un anumit interval de timp se poate utiliza funcţia delay, similară funcţiei sleep. Funcţia delay are însă ca parametru o constantă exprimată în milisecunde: void delay(unsigned milisecunde); 7) apelarea unei comenzi interne DOS Pentru apelarea unei comenzi DOS sau a unui fişier pentru comenzi se utilizează funcţia system: int system(const char *comanda); Parametrul comanda este un şir de caracter care conţine numele comenzii DOS sau a fişierului de comenzi. Dacă funcţia reuşeşte să execute comanda, se returnează valoarea 0, altfel returnează -1. Programul următor prezintă utilizarea funcţiei system. Exemplu: #include <stdlib.h> #include <stdio.h> void main(void) { if(system("DIR")) printf("EROARE!n"); } 8) lucrul cu vectori de întrerupere Un vector de întrerupere este o adresă de segment şi de deplasament a codului care tratează o anumită întrerupere. 276
• 283. Determinarea vectorului de întrerupere se realizează utilizând funcţia _dos_getvect în modul următor: void interrupt(* _dos_getvect(unsigned nr_intr))(); Parametrul nr_intr specifică numărul întreruperii dorite ce poate avea valori de la 0 la 255. Programul următor va afişa vectorii pentru toate întreruperile calculatorului: Exemplu: #include <stdio.h> #include <dos.h> void main(void) { int k; for(k=0;k<=255;k++) printf(”Intrerupere: %x Vector %lxn”,k, _dos_getvect(k)); } Dacă se doreşte crearea unui program de tratare a unei întreruperi, vectorul de întrerupere trebuie atribuit acestui program. Această atribuire se realizează cu ajutorul funcţiei _dos_setvect: void _dos_setvect(unsigned nr_intr, void interrupt(* handler)()); Parametrul nr_intr specifică întreruperea al cărui vector trebuie modificat. Pentru activarea şi dezactivarea întreruperilor se utilizează funcţiile: void _disable(void); void _enable(void); Dacă se doreşte reactivarea întreruperii originare se utilizează funcţia _chain_interrupt: void chain_interrupt(void(interrupt far *handler)()); Generarea unei întreruperi se realizează folosind funcţia geninterrupt: void geninterrupt(int intrerupere); unde parametrul intrerupere specifică întreruperea generată. 14.3 Bibliotecile C Dacă se examinează fişierele ce însoţesc un compilator C, se remarcă multe fişiere cu extensia LIB. Aceste fişiere conţin biblioteci obiect. Atunci când este compilat şi link-editat un program, editorul de legături examinează fişierele LIB pentru a rezolva referinţele la funcţii. Când sunt create funcţii utile ce sunt necesare şi în alte 277
• 284. programe, se pot construi biblioteci în care aceste funcţii să fie păstrate. 14.3.1 Reutilizarea unui cod obiect În cazul creării unei funcţii utile care se doreşte reutilizată, se poate compila fişierul ce conţine funcţia respectivă pentru a crea codul obiect (de exemplu din fişierul funcţie.c prin compilare se obţine fişierul obiect funcţie.obj). Funcţia definită în acest fişier obiect poate fi reutilizată în alt program utilizând următoarea instrucţiune: C:>bc fisier_nou.c funcţie.obj Totuşi, acest mod de a reutiliza codul unor funcţii este destul de dificil de utilizat în cazul în care se doreşte reutilizarea unui număr mare de funcţii aflate în fişiere obiect separate. 14.3.2 Lucrul cu fişiere bibliotecă Operaţiile acceptate de fişierele bibliotecă sunt următoarele: - crearea unei biblioteci; - adăugarea unuia sau mai multor fişiere obiect la bibliotecă; - înlocuirea unui fişier obiect cu altul; - ştergerea unuia sau mai multor fişiere obiect din bibliotecă; - listarea rutinelor pe care le conţine biblioteca. În funcţie de compilator, numele programului de bibliotecă şi opţiunile liniei de comandă pe care programul le acceptă vor diferi. În continuare prezentăm operaţiile ce pot fi realizate cu funcţiile bibliotecă utilizând programul TLIB al compilatorului Borland C. Presupunem că în urma compilării am creat fişierul obiect funcţie.obj ce conţine o serie de funcţii pe care dorim să le păstrăm într-o bibliotecă. Crearea unei bilioteci biblioteca.lib care să conţină acest fişier obiect se realizează cu următoarea linie de comandă: C:>tlib biblioteca.lib + functie.obj După ce fişierul bibliotecă a fost creat, funcţiile acestuia sunt disponibile pentru compilarea şi legarea noilor programe. Funcţia de biliotecă TLIB a compilatorului Borland C are următoarea sintaxă: tlib cale comandă, fişier unde: - cale – este un şir de caractere care specifică calea până la bilioteca asupra căreia se efectuează operaţia; - comandă – este formată dintr-un simbol şi numele unui fişier obiect. Simbol poate fi unul din caracterele: + (adaugă un 278
• 285. modul la bibliotecă), - (elimină un modul din bibliotecă), * (extrage un modul din bibliotecă într-un fişier cu acelaşi nume, fără al elimina), -+ (înlocuieşte un modul din bibliotecă), -* (extrage şi elimină un modul din bibliotecă); - fisier – reprezintă numele fişierul în care se scrie ieşirea operaţiei efectuate asupra bibliotecii. 14.3 Fişierele antet Fiecare program foloseşte una sau mai multe instrucţiuni #include pentru a cere compilatorului de C să folosească instrucţiunile incluse într-un fişier antet. Când compilatorul întâlneşte o instrucţiune #include în program, el compilează codul din fişierul antet ca şi cum ar fi scris în fişierul sursă. Fişierele antet conţin definiţii frecvent utilizate şi furnizează compilatorului informaţii referitoare la funcţiile sale. Dacă la compilarea programului se afişează un mesaj de eroare, avertizând că nu se poate deschide un anumit fişier antet, trebuie verificat subdirectorul care conţine fişierele antet, pentru a vedea dacă acel fişier există sau nu. Dacă se găseşte fişierul respectiv, în linia de comandă din sistemul de operare DOS trebuie scrisă următoarea instrucţiune: C:>SET INCLUDE=C:BORLANDCINCLUDE BIBLIOGRAFIE 1. Plum T., Learning to program in C, Prentice Hall, 1983 2. Auslander D.,Tham C., Real-time software for control: program examples in C, Prentice Hall, 1990. 279
• 286. 3. Schild H., Using Turbo C, Borland, Osborne / McGraw Hill, 1988. 4. Holzner S., Borland C++ Programming, Brady Books, New York, 1992. 5. Somnea D., Turturea D., Introducere în C++, Programarea orientatã pe obiecte, Ed. Tehnicã, Bucureşti, 1993. 6. Marian Gh., Bãdicã C., Pãdeanu L., Limbajul PASCAL, Indrumar de laborator, Reprografia Universitãţii din Craiova, 1993. 7. Negrescu L., Introducere în limbajul C, Editura MicroInformatica, Cluj Napoca, 1993. 8. Petrovici V., Goicea F., Programarea în limbajul C, Editura Tehnicã, Bucureşti, 1993. 9. Marian Gh., Muşatescu C., Laşcu M., Iordache Şt., Limbajul C, Editura ROM TPT, Craiova, 1999. 10. Mocanu M., Ghid de programare în limbajele C/C++, Editura SITECH, Craiova, 2001. 11. Zaharia, M.D., Structuri de date şi algoritmi. Exemple în limbajele C şi C++, Ed. Albastră, Cluj Napoca, 2002. 12. Kernighan, B.W., Ritchie, D.M., The C programming languages, Englewood. Cliffs, N.J. Prentice-Hall, 1978. 13. Bulac, C., Iniţiere în Turbo C++ şi Borland C, Editura Teora, Bucureşti, 1995. 280