The C Programming Language Brian W. Kernighan Dennis M. Ritchie C O N T I N U T Prefata....................................................... 3 Capitolul 0. Introducere...................................... 4 Capitolul 1. Initiere......................................... 7 1.1 Pornirea............................................... 7 1.2 Variabile si aritmetica................................ 8 1.3 Instructiunea For...................................... 10 1.4 Constante simbolice.................................... 11 1.5 O colectie de programe utile........................... 11 1.6 Tablouri............................................... 15 1.7 Functii................................................ 16 1.8 Argumente - apelul prin valoare........................ 17 1.9 Tablouri de caractere.................................. 18 1.10 Domenii. Variabile externe............................. 19 1.11 Rezumat................................................ 21 Capitolul 2. Tipuri de date, operatori si expresii............ 22 2.1 Nume de variabile...................................... 22 2.2 Tipurile si marimea datelor............................ 22 2.3 Constante.............................................. 22 2.4 Declaratii............................................. 23 2.5 Operatori aritmetici................................... 24 2.6 Operatori relationali si logici........................ 24 2.7 Conversii de tip....................................... 25 2.8 Operatori de incrementare si decrementare.............. 27 2.9 Operatori logici pe biti............................... 28 2.10 Operatorul si expresii de asignare..................... 29 2.11 Expresii conditionale.................................. 30 2.12 Pondere si ordine de evaluare.......................... 30 Capitolul 3. Controlul executiei............................. 32 3.1 Instructii si blocuri.................................. 32 3.2 If-else................................................ 32 3.3 Else-if................................................ 33 3.4 Switch................................................. 33 3.5 Bucle - while si for................................... 34 3.6 Bucle do-while......................................... 36 3.7 Break.................................................. 37 3.8 Continue............................................... 38 3.9 Goto-uri si etichete................................... 38 Capitolul 4. Structura programelor si functiilor............. 39 4.1 Notiuni de baza........................................ 39 4.2 Functii ce returneaza ne-intregi....................... 41 4.3 Argumentele functiilor................................. 42 4.4 Variabile externe...................................... 42 4.5 Reguli despre domenii.................................. 45 4.6 Variabile statice...................................... 47 4.7 Variabile registru..................................... 48 4.8 Structura de bloc...................................... 48 4.9 Initializare........................................... 49 4.10 Recursivitate.......................................... 50 4.11 Preprocesorul C........................................ 51 Capitolul 5. Pointeri si tablouri............................ 53 5.1 Pointeri si adrese..................................... 53 5.2 Pointeri si argumente de functii....................... 54 5.3 Pointeri si tablouri................................... 55 5.4 Aritmetica adreselor................................... 57 5.5 Pointeri la functii pe caractere....................... 58 5.6 Pointerii nu sint intregi.............................. 60 5.7 Matrici multi-dimensionale............................. 61 5.8 Matrici de pointeri; pointeri la pointeri.............. 62 5.9 Initializarea matricilor de pointeri................... 63 5.10 Comparatie Pointeri - Matrici multi-dimensionale....... 64 5.11 Argumente ale liniei de comanda........................ 64 5.12 Pointeri la functii.................................... 66 Capitolul 6. Structuri....................................... 69 6.1 Notiuni de baza........................................ 69 6.2 Structuri si functii................................... 70 6.3 Matrici de structuri................................... 71 6.4 Pointeri la structuri.................................. 73 6.5 Structuri auto-referite................................ 75 6.6 Cautarea tabelara...................................... 77 6.7 Cimpuri ("fields")..................................... 78 6.8 Uniuni................................................. 79 6.9 Definire de tip ("typedef")............................ 80 Capitolul 7. Intrari si iesiri............................... 82 7.1 Accesul la biblioteca standard......................... 82 7.2 Intrari si iesiri standard - getchar si putchar........ 82 7.3 Iesire cu format - printf.............................. 83 7.4 Intrar cu format - scanf............................... 84 7.5 Conversie de format in memorie......................... 85 7.6 Acces la fisiere....................................... 86 7.7 Trararea erorilor - stderr si exit..................... 87 7.8 Linia in intrare si iesire............................. 88 7.9 Diverse alte functii................................... 89 Capitolul 8. Interfata cu sistemul UNIX...................... 8.1 Descriptori de fisier.................................. 8.2 Intrari/iesiri de nivel inferior - read si write....... 8.3 Open, Creat, Close, Unlink............................. 8.4 Acces direct - seek si lseek........................... 8.5 Exemplu - fopen si getc implementat.................... 8.6 Exemplu - listarea directorului........................ 8.7 Exemplu - alocator de memorie.......................... Appendix A. Manual de referinta al limbajului C ............. 90 1. Introducere.............................................. 90 2. Conventii lexicale....................................... 90 3. Notatii de sintaxa....................................... 91 4. Ce este intr-un nume ? .................................. 91 5. Obiecte si "Lvalori"..................................... 92 6. Conversii................................................ 92 7. Expresii................................................. 93 8. Declaratii............................................... 98 9. Instructiuni.............................................104 10. Definitii externe........................................106 11. Reguli referitoare la vizibilitate.......................107 12. Linii de control al compilatorului.......................108 13. Declaratii implicite.....................................109 14. Rezumat despre tipuri....................................109 15. Expresii constante.......................................111 16. Consideratii despre portabilitate........................111 17. Anacronisme..............................................111 18. Rezumatul sintaxei.......................................112 Index......................................................... P R E F A T A C este un limbaj de programare cu scop general ale carui caracteristici sint economia de expresie, structuri moderne de control al fluxului si de date, precum si un set bogat de operatori. C nu este un limbaj de nivel "foarte inalt", nici "mare", si nu este specializat vreunei arii particulare de aplicatii. Dar absenta in restrictii si generalitatea sa il fac mai convenabil si mai de efect pentru mai multe scopuri decit limbaje presupuse mai puternice. C a fost la inceput proiectat si implementat pe sistemul de operare UNIX pe DEC PDP11 de catre Dennis Ritchie. Sistemul de operare, compilatorul C si in mod esential, toate programele de aplicatii ale lui UNIX (inclusiv software-ul folosit pentru a pregati cartea aceasta) sint scrise in C. Compilatoare de C exista deasemenea si pe mai multe alte calculatoare, intre care IBM System/370 Honeywell 6000 si Interdata 8/32. C nu este legat de nici un hardware sau calculator anumit si e simplu de scris programe care se pot executa fara nici o modificare pe diferite calculatoare care au limbajul C implementat. Aceasta carte are drept scop sa-l ajute pe cititor sa invete sa programeze in C. Ea contine o initiere, pentru ca noii utilizatori sa poata incepe cit mai repede posibil, capitole separate pentru fiecare caracteristica majora, si un manual de referinta. Marea parte a textului nu se bazeaza atit pe expunerea de reguli si propozitii cit pe citirea, scrierea si revizuirea de exemple. In cea mai mare parte exemplele sint programe reale si sint complete si nu fragmente izolate. Toate exemplele au fost testate direct din text, care este intr-o forma citibila pe calculator. Pe linga faptul ca am aratat cum se utilizeaza efectiv limbajul, am incercat in plus, acolo unde era posibil, sa-l ilustram cu algoritmi utili si cu principii de bun stil in programare si proiectare sanatoasa. Aceasta carte nu este un manual introductiv de programare. Ea presupune anumite familiaritati cu conceptele de baza din programare, ca variabile, instructiuni de asignare, bucle, functii. Cu toate acestea, un programator novice va fi in stare sa citeasca cartea si sa-si insuseasca limbajul, chiar daca ajutorul unui coleg cu experienta mai mare i-ar usura munca foarte mult. In experienta noastra, C s-a dovedit un limbaj placut, expresiv si adaptabil pentru o mare varietate de programe. Este usor de invatat si "se poarta bine" pe masura ce experienta in programare cu el creste. Speram ca aceasta carte va va ajuta sa-l folositi bine. Brian W. Kernighan Dennis M. Ritchie CAPITOLUL 0. I N T R O D U C E R E C este un limbaj de programare cu scop general. El este puternic legat de sistemul UNIX, deoarece a fost dezvoltat pe acest sistem si deoarece UNIX-ul si software-ul sau sint scrise in C. Cu toate acestea, limbajul nu este legat de un anume sistem de operare sau calculator; si, desi a fost numit "limbaj de programare sistem", deoarece este util in scrierea sistemelor de operare, el a fost folosit la fel de bine in scrierea de programe importante ce trateaza probleme numerice, prelucrari de texte sau baze de date. C este un limbaj relativ "de nivel inferior". Aceasta caracterizare nu este peiorativa; ea inseamna pur si simplu ca C opereaza cu aceeasi clasa de obiecte cu care lucreaza majoritatea calculatoarelor, si anume caractere, numere si adrese. Acestea pot fi combinate si prelucrate cu operatori aritmetici si logici implementati pe actualele calculatoare. C nu poseda operatii pentru a prelucra direct obiecte compuse, cum ar fi siruri de caractere, multimi, liste sau tablouri considerate ca un intreg. Nu exista nici o analogie, de exemplu, cu operatiile din PL/1 care manipuleaza un intreg tablou sau sir. Limbajul nu defineste nici o alta facilitate de alocare de memorie in afara de definitiile statice si de lucrul cu stiva folosite de variabilele locale ale functiilor; nu exista colectii reziduale sau de gramezi ca in Algol 68. In sfirsit, limbajul C in sine nu are facilitati de intrare-iesire: nu exista instructiuni READ sau WRITE si nici metode de acces la fisiere, "cablate" in limbaj. Toate aceste mecanisme de nivel inalt trebuiesc facute prin apeluri explicite de functii. In mod similar, C ofera numai constructii directe, liniare de control al fluxului: teste, bucle, grupari, si subprograme, insa nu multiprogramare, operatii paralele, sincronizari sau corutine. Cu toate ca absenta acestor caracteristici ar parea o grava deficienta ("Vrei sa spui ca trebuie sa apelez o functie pentru a compara doua siruri de caractere ?"), pastrarea limbajului la o dimensiune modesta a adus beneficii reale. Deoarece C este relativ mic, el poate fi descris intr-un spatiu redus si invatat repede. Un compilator pentru C poate fi simplu si compact. Compilatoarele sint deasemenea usor de scris; folosind tehnologia curenta, ne putem astepta la un timp de citeva luni pentru scrierea unui compilator nou si sa avem surpriza ca 80% din codul noului compilator este comun cu cele existente. Aceasta dovedeste marele grad de mobilitate a limbajului. Deoarece tipurile de date si structurile de control posedate de C sint suportate de majoritatea calculatoarelor existente, biblioteca necesara executiei [run - time] necesara pentru a implementa programele independente este minuscula. Pe PDP-11, de exemplu, ea contine numai rutinele pentru inmultirea si impartirea pe 32 de biti si subrutinele ce realizeaza secventele de inceput si de sfirsit. Desigur, fiecare implementare poseda o biblioteca cuprinzatoare si compatibila de functii pentru a indeplini functiile de I/O, a trata sirurile si operatiile de alocare de memorie dar, deoarece se apeleaza numai explicit, poate fi evitata daca e nevoie; ea poate fi scrisa portabil chiar in C. Din nou, deoarece limbajul reflecta capacitatile calculatoarelor curente, programele C tind sa fie suficient de eficiente astfel ca nu exista nici o constringere pentru a le scrie in limbajul de asamblare. Cel mai evident exemplu in acest sens este chiar sistemul de operare UNIX, care este scris aproape in intregime in C. Din cele 13000 de linii de cod ale sistemului, numai aproximativ 800 de linii de la nivelul cel mai de jos sint scrise in limbajul de asamblare. In plus, software-ul pentru toate aplicatiile UNIX esentiale este scris in C; marea majoritate a utilizatorilor UNIX (inclusiv unul din autorii acestei carti) nici macar nu cunosc limbajul de asamblare al lui PDP-11. Cu toate ca C se potriveste cu caracteristicile multor calculatoare, el este independent de arhitectura oricarui calculator particular si astfel, cu putina grija, este usor a scrie programe "portabile", adica programe care pot fi rulate fara modificari pe varietate de calculatoare. In mediul nostru este deja un fapt obisnuit ca programele dezvoltate pe UNIX sa fie transportate pe sistemele Honeywell, IBM si Interdata. In realitate, compilatoarele de C si suportul de executie pentru aceste patru tipuri de calculatoare sint mai compatibile decit versiunile presupuse standard ANSI pentru FORTRAN. Sistemul de operare UNIX insusi se executa acum atit pe PDP-11 cit si pe Interdata 8/32. In afara programelor care, in mod necesar, sint intrucitva dependente de tipul de calculator, ca: asamblorul, compilatorul, depanatorul - software-ul scris in C este identic pe ambele calculatoare. Chiar in cadrul sistemului de operare, 7000 de linii de cod, in afara suportului pentru limbajul de asamblare si handlerelor dispozitivelor de I/O este identic in proportie de 95%. Pentru programatorii familiari cu alte limbaje, se poate dovedi util sa mentionam citeva aspecte istorice, tehnice si filosofice legate de C, pentru contrast si comparatie. Multe din cele mai importante idei din C isi au radacina in limbajul -de acum suficient de batrin, dar inca viabil - BCPL, dezvoltat de Martin Richards. Influenta lui BCPL asupra lui C apare indirect prin limbajul B, care a fost scris de Ken Thompson in 1970 pentru primul sistem UNIX pe un PDP-7. Cu toate impartaseste multe caracteristici esentiale cu BCPL, limbajul C nu este in nici un sens un dialect al acestuia. Limbajele BCPL si B sint limbaje "fara tipuri"[typeless]: singurul tip de data este cuvintul masina, si accesul la alte tipuri de obiecte se face cu operatori speciali sau apeluri de functii. In C obiectele (datele) fundamentale sint caracterele, intregii de diferite dimensiuni si numerele flotante. In plus, exista o ierarhie de tipuri de date derivate create cu pointeri, tablouri, structuri, uniuni si functii. Limbajul C poseda constructiile fundamentale pentru controlul fluxului necesare pentru programele bine structurate: grupare de instructiuni; luare de decizii ("if"); buclare cu test de terminare la inceput ("while", "for") sau la sfirsit ("do"), selectare a unui caz dintr-o multime de cazuri posibile ("switch"). (Toate acestea erau valide si in BCPL, chiar daca cu o sintaxa diferita; acest limbaj anticipa voga pentru "programare structurata" cu mai multi ani inainte). Limbajul C foloseste pointeri si are abilitatea de a face aritmetica cu adrese. Argumentele functiilor sint pasate copiind valoarea argumentului si este imposibil pentru functia apelata sa modifice argumentul real din apelant. Cind se doreste sa se obtina un "apel prin referinta", se trimite explicit un pointer, iar functia poate modifica obiectul la care puncteaza pointerul. Numele de tablouri sint trimise ca locatie a originii tabloului, asa ca argumentele tablouri sint efectiv apeluri prin referinta. Orice functie poate fi apelata recursiv si variabilele sale sint tipic "automate" sau create nou cu fiecare invocare. Definitiile de functii nu pot fi imbricate dar variabilele pot fi declarate in maniera de bloc structurat. Functiile unui program C pot fi compilate separat. Variabilele pot fi interne unei functii, externe dar cunoscute numai intr-un singur fisier sursa, sau complet globale. Variabilele interne pot fi automate sau statice. Variabilele automate pot fi in registre pentru eficienta marita, dar declaratia de registru este numai interna compilatorului si nu se refera la vreun registru specific al calculatorului. Limbajul C nu este un limbaj puternic tipizat in sensul lui PASCAL sau Algol - 68. El este relativ liberal in conversia de date, cu toate ca nu converteste automat tipurile de date cum ar fi PL/1. Compilatoarele existente nu poseda verificare la executie a indicilor elementelor de tablouri, tipurilor argumentelor, etc. Pentru acele situatii in care se cere o puternica verificare a tipului, se foloseste o versiune separata a compilatorului. Acest program se numeste "lint" deoarece triaza bitii dubiosi of fluff dintr-un program. El nu genereaza cod, verifica numai foarte strict multe aspecte ale programelor asa cum pot fi verificate la compilare si la incarcare. El detecteaza nepotrivirile de tip, folosirea inconsistenta a argumentelor, variabilele nefolosite sau aparent neinitializate, dificultatile potentiale de portabilitate si alte asemenea aspecte. Programele care trec cu bine aceasta verificare, cu citeva exceptii, se elibereaza de erorile de tip la fel de complet ca si, de exemplu, programele scrise in Algol 68. Vom mentiona si alte capacitati ale lui "lint" atunci cind ni se va ivi ocazia. In fine, limbajul C, ca si alte limbaje, are defectele sale. Unii din operatori au o pondere gresita; o parte sau parti ale sintaxei ar fi putut fi mai bune; exista mai multe versiuni ale limbajului diferind foarte putin una de alta. Cu toate acestea, limbajul C s-a dovedit a fi un limbaj extrem de eficace si expresiv pentru o larga varietate de aplicatii de programare. Restul cartii este organizat dupa cum urmeaza. Capitolul 1 este o initiere in partea centrala a limbajului C. Scopul lui este sa faca cititorul sa inceapa sa programeze in C cit mai repede posibil, deoarece noi credem puternic ca singurul mod de a invata un limbaj nou este de a se scrie programe in el. "Initierea" presupune o cunoastere a elementelor de baza ale programarii; nu se dau explicatii despre calculatoare, compilatoare si nici despre semnificatia unor expresii ca, de exemplu, n = n + 1. Cu toate ca am incercat pe cit posibil sa aratam tehnici de programare utile, cartea aceasta nu se vrea de referinta in structuri de date si algoritmi; cind am fost fortati sa alegem, ne-am concentrat asupra limbajului. Capitolele 2-6 discuta diferite aspecte ale limbajului C mai detaliat si mai formal decit o face Capitolul 1, cu toate ca accentul cade tot pe exemple de programe utile complete si nu pe fragmente izolate. Capitolul 2 se ocupa cu tipurile de date fundamentale, operatorii si expresiile. Capitolul 3 trateaza controlul fluxului: if-else, while, for, etc. Capitolul 4 acopera functiile si structura programului -variabile externe, reguli de domeniu, si asa mai departe. Capitolul 5 discuta pointerii si aritmetica adreselor. Capitolul 6 contine detalii despre structuri si uniuni. Capitolul 7 descrie biblioteca C standard de I/O care asigura o interfata obisnuita cu sistemul de operare. Aceasta biblioteca de I/O este tratata pe toate calculatoarele care suporta limbajul C, asa ca programele care o folosesc pentru intrari, iesiri si alte functii sistem pot fi mutate de pe un sistem pe altul in principal fara modificari. Capitolul 8 descrie interfata intre programele C si sistemul de operare UNIX, concentrindu-se asupra operatiilor de intrare/iesire, sistemului de fisiere si portabilitatii. Cu toate ca acest capitol este oarecum specific pentru UNIX, programatorii care nu folosesc acest sistem pot gasi si aici un material util, inclusiv o privire asupra modului in care este implementata o versiune a bibliotecii standard, precum si sugestii pentru a obtine un cod portabil. Anexa A contine manualul de referinta al limbajului C. Acesta este declaratia "oficiala" a sintaxei si semanticii lui C si (exceptind compilatoarele proprii) arbitrul final al oricarui ambiguitati sau omisiuni din capitolele precedente. Deoarece C este un limbaj in dezvoltare care exista pe o varietate de calculatoare, anumite parti din aceasta carte pot sa nu mai corespunda cu stadiul curent de dezvoltare pentru un sistem particular. Am incercat sa evitam aceste probleme si sa atentionam asupra potentialelor dificultati. Cind am fost in dubiu, am ales in general descrierea situatiei PDP-11 UNIX, care este mediul de lucru al majoritatii programatorilor in C. Anexa A contine deasemenea diferentele de implementare pentru C pe sistemele majore pe care el poate fi gasit. CAPITOLUL 1. I N I T I E R E Sa incepem cu o introducere rapida in C. Scopul nostru este sa preezentam elementele esentiale ale limbajului in programe reale, fara insa a ne impotmoli in detalii, reguli formale si exceptii. In acest punct al expunerii nu incercam sa fim completi si nici macar foarte precisi (mentionam totusi ca exemplele vor sa fie corecte). Dorim sa va aducem cit mai repede posibil in punctul in care veti fi capabili sa scrieti programe utile si, pentru aceasta, ne-am concentrat asupra fundamentelor: variabile si constante, aritmetica, controlul fluxului, functii si rudi- mente de operatii de I/O. Am lasat deoparte intentionat din acest capitol acele caracteristici ale limbajului C care sint de importanta vitala in scrierea programelor mai mari. Acestea includ pointerii, structurile, majoritatea din bogatul set de operatori ai lui C, anumite instructiuni de control al fluxului si o multime de detalii. Acest mod de abordare are neajunsurile lui, desigur. Cel mai notabil este acela ca povestea completa a caracteristici- lor oricarui limbaj de programare nu este gasita intr-un singur loc, iar o initiere in el, fiind scurta, poate induce in eroare. Deoarece exemplele nu pot folosi intreaga putere a lui C, ele nu sint atit de concise si de elegante pe cit ar putea fi. Am incercat sa minimalizam aceste efecte, dar fiti atenti! Un alt neajuns este acela ca in capitolele urmatoare vom repeta in mod necesar cite ceva din acest capitol. Speram ca aceasta repetitie va va ajuta mai mult decit va va plictisi. In orice caz, programatorii experimentati vor fi capabili sa extrapoleze din materialul din acest capitol propriile lor nevoi de programare. Incepatorii vor putea scrie mici programe, similare celor prezentate de noi. Ambele grupe pot folosi acest capitol drept cadru pentru descrierile riguroase care incep cu Capitolul 2. 1.1 Sa incepem Singurul mod de a invata un nou limbaj de programare este de a scrie programe in el. Primul program pe care-l vom scrie este acelasi pentru toate limbajele: Tipariti cuvintele hello, world Acesta este primul obstacol; pentru a sari peste el, trebuie sa fiti in stare sa creati undeva textul program, sa-l compilati cu succes, sa-l incarcati, sa-l executati si sa aflati textul tiparit acolo unde este iesirea calculatorului dumneavoastra. In C, programul pentru a tipari "hello, world" este: main () { printf("hello, world\n"); } Cum ruleaza acest program, depinde de sistemul pe care-l folositi, Drept exemplu specific, pe sistemul de operare RSX, trebuie sa creati acest program sursa intr-un fisier al carui nume se termina in ".C", de exemplu "hello.C" apoi sa-l compilati cu comenzile: >cc hello >as hello Daca n-ati gresit nimic, de exemplu sa fi uitat un caracter sau sa fi inversat doua caractere, compilarea se va desfasura silentios si va produce un fisier obiect numit "hello.obj". Lansindu-l in executie dupa linkeditare cu comenzile >tkb hello=hello,lb:[1,1]clib/lb >run hello va produce hello, world ca iesire a sa. Pe alte sisteme, regulile vor fi diferite; verificati-le cu expertul local. Exercitiul 1.1. Executati acest program pe sistemul dumneavoas- tra. Incercati sa vedeti ce mesaje de eroare obtineti, lasind la o parte parti din program. Si acum citeva explicatii despre programul insusi. Un program C, oricare i-ar fi marimea, consta din una sau mai multe "functii" care specifica operatiile efective de calculat care trebuiesc facute. Functiile din C sint similare cu functiile si subrutinele dintr-un program Fortran sau cu procedurile din PL/1, Pascal,etc. In exemplul nostru, "main" este o astfel de functie. In mod normal aveti libertatea de a da functiilor ce nume doriti, dar "main" este un nume special - programul dumneavoastra se va executa de la inceputul lui "main". Aceasta inseamna ca fiecare program trebuie sa aibe un "main" undeva, ca "main" va invoca in mod obisnuit alte functii pentru a-si realiza scopul, unele venind din acelasi program iar altele din biblioteci ce contin functii scrise anterior. O metoda de a comunica date intre functii este prin argumen- tele functiilor. Parantezele care urmeaza dupa numele functiei includ lista de argumente. In cazul nostru, "main" este o functie fara argumente ceea ce se indica prin "()". Acoladele "{ }" includ instructiunile care alcatuiesc functia. Ele sint ana- loage lui "DO-END" din PL/1 sau lui "begin-end" din ALGOL, PASCAL, etc. O functie este apelata prin nume, urmate de o lista de argumente in paranteze. Nu exista instructiunea CALL ca in FORTRAN sau PL/1. Parante zele trebuie sa fie prezente chiar daca nu exista argumente. Linia care spune: printf("hello, world\n"); este un apel de functie, care cheama o functie numita "printf" cu argumentul ("hello, world\n"). "printf" este o functie din biblioteca care tipareste pe terminal (daca nu este specifica- ta o alta destinatie). In acest caz, ea tipareste sirul de caractere care alcatuiesc argumentul. O secventa alcatuita din orice numar de caractere cuprinse intre doua ghilimele "..." se numeste sir de caractere sau constanta sir. Pentru moment, singura folosire a sirurilor de caractere va fi ca argumentele pentru "printf" si alte functii. Secventa "\n" din sir este notatia din C pentru caracterul "linie noua", care, cind este tiparit, avanseaza cursorul terminalu- lui la marginea din stinga a urmatoarei linii. Daca uitati "\n" (un experiment care merita facut), veti observa ca iesirea dumneavoastra nu se termina cu o linie noua. Singurul mod de a avea caracterul "linie noua" in "printf" este "\n" ca argument. Daca incercati ceva de tipul printf("hello, world "); compilatorul C va va tipari un diagnostic neprietenos despre ghilimele absente. "printf" nu furnizeaza o linie noua in mod automat, asa ca apelurile multiple pot fi folosite pentru a tipari o linie pe etape. Primul nostru program poate fi scris la fel de bine si astfel: main() { printf("hello, "); printf("world"); printf("\n"); } pentru a produce o iesire identica. Sa notam ca "\n" reprezinta un singur caracter. O "secventa escape" ca de exemplu "\n" este in general un mecanism extensibil pentru reprezentarea caracterelor greu de obtinut sau invizibile. Printre alte secvente escape, limbajul C poseda: \t pentru tab, \b pentru backspace, \" pentru apostrof dublu si \\ pentru backspace. Exercitiul 1.2. Experimentati sa vedeti ce se intimpla cind sirul argument din "printf" contine "\x" unde x este un caracter oarecare care nu a fost listat mai sus. 1.2. Variabile si aritmetica Urmatorul program tipareste tabela de temperaturi Fahrenheit si echivalentele lor in centigrade sau grade Celsius, folosind formu- la: C = (5 / 9) * (F - 32). 0 -17.8 20 -6.7 40 4.4 60 15.6 ... 260 126.7 280 137.8 300 148.9 Iata acum si programul: /* Print Fahrenheit-Celsius table for f = 0, 20, ..., 300 */ main() { int lower, upper, step; float fahr, celsius; lower = 0; /* lower limit of temperature table */ upper = 300; /* upper limit */ step = 20; /* step size */ fahr = lower; while (fahr <= upper) { celsius = (5.0 / 9.0) * (fahr - 32.0); printf("%4.0f %6.1f\n", fahr, celsius); fahr = fahr + step; } } Primele doua linii : /* Print Fahrenheit-Celsius table for f = 0, 20, ..., 300 */ sint un comentariu, care in acest caz explica pe scurt ce face programul. Orice caractere cuprinse intre "/*" si "*/" sint ignorate de compilator; ele pot fi folosite liber pentru a face programul mai usor de inteles. Comentariile pot apare oriunde poate aparea un spatiu sau o linie noua. In limbajul C, toate variabilele trebuie declarate inainte de a fi folosite, deobicei la inceputul liniei, inaintea oricarei instruc- tiuni executabile. Daca veti uita o declaratie, veti primi un diagnostic de la compilator. O declaratie consta dintr-un "tip" si o lista de variabile care au acel tip, ca in: int lower, upper, step; float fahr, celsius; Tipul "int" implica faptul ca variabilele listate sint intregi; "float" denota virgula mobila, adica numere care pot avea parte fractionara. Precizia atit pentru "int" cit si pentru "float" depinde de calculatorul pe care-l folositi. Pentru PDP-11, de exemplu, un "int" este un numar cu semn de 16 biti, adica un numar cuprins intre -32768 si +32767 . Un numar "float" este o cantitate ce se reprezinta pe 32 de biti ceea ce revine la aproximativ sapte cifre semnificative, cu magnitudinea cuprinsa aproximativ intre 10 ^ -38 si 10 ^ +38. Capitolul 2 da olista a acestor marimi pentru alte calculatoare. Pe linga tipurile "int" si "float", limbajul C poseda si alte tipuri de date fundamentale : "char" caracter - un singur octet "short" intreg scurt "long" intreg lung "double" numar flotant dubla precizie Marimea acestor obiecte este deasemenea dependenta de calculator; detalii se dau in Capitolul 2. Exista deasemenea "tablouri", "structuri" si "uniuni" de asemenea tipuri de baza, "pointeri" la ele si "functii" care le returneaza si cu toate ne vom intilni in aceasta carte. Calculul efectiv in programul de conversie temperatura incepe cu asignarile: lower = 0; upper = 300; step = 20; fahr = lower; care seteaza variabilele pe valorile lor de start. Instructiu- nile individuale se termina cu punct si virgula. Fiecare linie a tabelei este calculata in acelasi fel, asa ca vom folosi o bucla care se repeta odata pe linie; acesta este scopul instructiunii "while": while (fahr <= upper) { ... } Este testata conditia din paranteza. Daca ea este adevarata (fahr este mai mic sau egal cu upper), este executat corpul buclei (toate instructiunile incluse intre parantezele { si }). Apoi conditia este retestata si, daca este adevarata, corpul este executat din nou . Cind testul devine fals (fahr este mai mare decit upper), bucla se termina si executia conti- nua cu instructiunea care urmeaza buclei. Deoarece in acest program nu mai exista alte instructiuni care sa succeada bucla, executia lui se termina. Corpul unei bucle while poate fi alcatuit din mai multe instructiuni incluse intre acolade, ca in programul de mai sus sau dintr-o singura instructiune, fara paranteze, ca in exemplul de mai jos: while(i,backspace,- care se va tipari ca "->" s fiecare backspace prin secventa similara "<-". Aceasta face taburile si backspace-urile vizibile. Contorizarea de cuvinte Al patrulea program din seria de programe utile va contoriza linii, cuvinte si caractere, un singur cuvint fiind definit ca orice secventa de caractere care nu contine blanc, tab sau linie noua (acesta este de fapt un schelet al programului utilitar "wc" din UNIX). #define YES 1 #define NO 0 main() /*contorizare linii, cuvinte si caractere la intrare*/ { int c, nl, nw, nc, inword; inword = NO; nl = nw = nc = 0; while ((c = getchar()) != EOF) { ++nc; if(c == '\n') ++nl; if(c == ' ' || c == '\n' || c == '\t') inword = NO; else if (inword == NO) { inword = YES; ++nw; } } printf("%d %d %d'\n", nl, nw, nc); } De fiecare data cind programul intilneste primul caracter al unui cuvint, il contorizeaza. Variabila "inword" inregistreaza de cite ori programul este intr-un cuvint sau nu ; initial el "nu este intr-un cuvint " si variabilei i s-a asignat valoarea NO. Preferam constantele simbolice YES si NO valorilor literale 1 si 0 deoarece ele fac programul mai usor citibil. Desigur ca intr- un program mic ca acesta diferenta este mica, dar intr-un program mai mare cresterea in claritate merita micul efort supli- menar de a-l scrie in acest mod de la inceput. Veti vedea deasemenea ca este mai usor sa efectuati modificari masive in programe in care numerele apar numai ca si constante simbolice. Linia nl = nw = nc = 0; seteaza toate cele trei variabile pe zero. Acesta nu este un caz special ci doar o consecinta a faptului ca o asignare asociaza de la dreapta spre stinga. Este ca si cind am fi scris; nc = (nl = (nw = 0)); Operatorul || inseamna SAU, asa ca linia if(c == ' ' || c == '\n' || c == '\t'); spune ca "daca c este un blanc sau c este o linie noua sau c este un tab...". (Secventa escape \t este reprezentarea vizibila a caracterului tab).Exista un operator corespunza- tor && pentru SI. Expresiile conectate prin && sau || sint evaluate de la stinga la dreapta si evaluarea se opreste atunci cind se cunoaste adevarul sau falsul expresiei. Astfel daca c contine un blanc, nu mai este nevoie sa testam daca el contine o line noua sau un tab, asa ca testele acestea nu se mai fac. In particular, aceasta nu este important aici, dar este foarte semni- ficativ in multe situatii complicate, asa cum vom vedea in curind. Exemplul nostru foloseste deasemenea instructiunea "else", care specifica o actiune alternativa ce trebuie executata daca partea de conditie unei instructiuni "if" este falsa. Forma generala este: if (expresie) instructiune1 else instructiune2 Una si numai una din instructiunile asociate cu if-else se executa. Daca "expresia" este adevarata, se executa "instruc- tiunea-1"; daca nu, se executa "instructiunea-2". Fiecare "in- structiune" poate fi, de fapt, mult mai complicata. In exemplul nostru instructiuea de dupa "else" este un "if" care controleaza doua instructiuni in paranteze. Exercitiul 1.9. Cum veti testa programul de contorizare cuvinte? Care sint unele dintre limitele lui ? Exercitiul 1.10. Scrieti un program care sa tipareasca cuvin- tele introduse,cite unul pe linie. Exercitiul 1.11. Revizuiti programul de contorizare cuvinte pentru a folosi o mai buna definitie a "cuvintului", de exemplu o secventa de litere, cifre si apostrofuri care incepe cu o litera. 1.6. Tablouri Vom scrie acum un program care va contoriza aparitiile fiecarei cifre, a fiecarui caracter de spatiere (blanc, tab, linie noua) si a tuturor celorlalte caractere. Desigur, este un program artificial, dar ne va permite sa ilustram mai multe aspec- te ale lui C intr-un singur program. Exista 12 categorii de intrari, asa ca ne este mai convenabil sa folosim un tablou pentru a tine numarul de aparitii a fiecarei cifre, decit sa folosim 10 variabile individuale. Iata acum o versiune a acestui program: main() /* contorizeaza cifre, spatii albe, alte caractere */ { int c, i, nwhite, nother; int, ndigit[10]; nwhite = nother = 0; for (i = 0; i < 10; ++i) ndigit[i] = 0; while ((c = getchar()) != EOF) if (c >= '0' && c <= '9') ++ndigit[c-'0']; else if (c == ' ' || c == '\n' || c == '\t') ++nwhite; else ++nother; printf("digits ="); for (i = 0; i < 10; ++i) printf(" %d", ndigit[i]); printf("\nwhite space = %d, other = %d\n", nwhite, nother); } Declaratia int ndigit[10]; spune ca ndigit este un tablou de 10 intregi. Indicii de tablou intodeauna incep de la zero in C (spre deosebire de FORTRAN sau PL/1 unde incepe de la unu), asa ca elementele tabloului sint ndigit[0], ndigit[1] , ...,ndigit[9]. Acestea se reflecta in buclele "for", care initializeaza si tiparesc tabloul. Un indice poate sa fie orice expresie intreaga, inclusiv desi- gur variabilele intregi ca "i", sau constantele intregi. Acest program particular se bazeaza mult pe proprietatile reprezentarii drept caractere a cifrelor. De exemplu, testul if (c >= '0' && c <= '9') ... determina daca un caracter din c este cifra. Daca el este cifra, valoare numerica a acelei cifre este c - '0' Acest algoritm functioneaza bine numai daca '0', '1', etc sint pozitive si in ordine crescatoare, si intre '0' si '9' nu se gaseste altceva decit cifre. Din fericire, aceasta este ade- varul pentru toate seturile de caractere conventionale. Prin definitie, calculele aritmetice care implica tipuri "char" si "int", convertesc totul in tipul "int" inainte de prelucrare, asa ca variabilele si constantele de tip "char" sint esential identice cu tipul "int" in contexte aritmetice. Acest fapt este aproape natural si convenabil; de exemplu: c- '0' este o expresie intreaga cu o valoare intre 0 si 9 corespunza- toare caracteruluidintre '0' si '9' depus in c, si deci este un indice valid pentru tabloul ndigit. Decizia se ia asupra caracterului (daca el este o cifra, un caracter de spatiere sau altceva) in secventa: if (c >= '0' && c <= '9') ++ndigit[c-'0']; else if (c == ' ' || c == '\n' || c == '\t') ++nwhite; else ++nother; Constructia de tipul if (conditie) instructiune else if (conditie) instructiune else instructiune apare frecvent in programe ca o modalitate de a exprima deci- ziile multiple. Codul se citeste simplu de sus in jos pina cind o conditie este indeplinita; in acest punct, se executa partea corespunzatoare de "instructiune" si intreaga constructie este terminata. (Desigur ca "instructiune" pot fi mai multe in- structiuni incluse intre paranteze). Daca nici una din con- ditii nu este indeplinita, instructiunea care urmeaza dupa ultimul "else" este executata daca este prezenta. Daca "else" final si "instructiune" lipsesc (ca in programul nostru), nu are loc nici o actiune. Pot exista un numar arbitrar de constructii de tipul else if (conditie) instructiune grupate intre "if"-ul initial si "else"-ul final. Ca o ches- tiune de stil, va sfatuim sa formati aceste constructii asa cum le-am facut si noi, astfel incit deciziile lungi sa nu ajunga pe marginea din dreapta a paginii. Instructiunea "switch", care va fi prezentata in Capitolul 3, reprezinta un alt mod de a scrie o decizie multipla si este potrivita, in particular, cind conditia care se testeaza este simpla sau cind o expresie de caractere sau de intregi se potri- veste cu o constanta dintr-un sir dat. Prin contrast, vom prezenta o versiune "switch" a acestui program in Capitolul 3. Exercitiul 1-12. Scrieti un program pentru a tipari histogra- ma lungimilor cuvintelor care apar la intrare. Este cel mai usor sa desenati histograma orizontal; o orientare verticala este mai laborioasa. 1.7. Functii In C o functie este echivalenta cu o subrutina sau cu o functie din FORTRAN sau cu o procedura din PL/1 sau PASCAL, etc. O functie reprezinta un mod convenabil de a incapsula anumite calcule intr-o cutie neagra care poate fi apoi folosita fara sa ne mai pese de ce se afla inauntru. Functiile sint singura modali- tate de a face fata la complexitatea potentiala a programe- lor mari. Cu functii scrise asa cum trebuie, este posibil sa ignoram "cum" este facuta o anumita treaba; ne este suficient sa stim "ce" anumita treaba este facuta. Limbajul C este proiectat pentru a face folosirea lor usoara, convenabila si eficienta; veti observa adesea o functie lunga numai de citeva rinduri, apelata o singura data, numai fiindca ea clarifica anumite porti- uni de cod. Pina acum am folosit numai functii ca printf, getchar si putchar, care au fost scrise de altii pentru noi; este momentul sa ne scriem si noi propriile noastre functii. Deoarece limbajul C nu poseda un operator de exponentiere ca ** din FORTRAN sau PL/1, vom ilustra mecanismul de definire de functii scriind o functie putere "power(m,n)" care va ridica un intreg la o putere intrea- ga pozitiva in n. Adica, valoarea lui power(2,5) este 32. Desi- gur ca aceasta functie nu realizeaza toata treaba lui ** deoarece minuieste numai puteri pozitive ale intregilor mici, dar cel mai bine,este bine sa aprofundam un lucru la un moment dat. Iata acum functia "power" si un program principal care o foloseste, asa ca puteti vedea deodata intreaga structura. main() /* testeaza functia power */ { int i; for (i = 0; i < 10; ++i) printf ('%d %d %d\n', i, power(2,i), power(-3,i)); } power(x,n) /* ridica pe x la puterea a n-a ; n > 0 */ int x, n; { int i, p; p = 1; for (i = 1, i <= n; ++i) p = p * x; return(p); } Orice functie are o aceeasi forma: nume (lista de argumente, daca exista) declaratii de argumente, daca exista { declaratii instructiuni } Functiile pot apare in orice ordine si intr-un fisier sursa sau in doua. Bineinteles, daca sursa apare in doua fi- siere, veti avea mai multe de spus la compilare si incarcare decit daca e un singur fisier dar asta este o problema a sistemului de operare si nu un atribut al limbajului. Pentru moment vom presupune ca ambele functii se gasesc intr-un acelasi fisier, asa ca ceea ce ati invatat despre executia programelor C nu se modifica.Functia "power" este apelata de doua ori in linia printf("%d %d %d\n", i, power(2,i), power(-3,i)); Fiecare apel trimite doua argumente lui power, care de fiecare data returneaza un intreg care trebuie formatat si tiparit. Intr- o expresie power(2,i) este un intreg, la fel ca si 2 si i. (Nu toate functiile produc o valoare intreaga; vom vedea aceasta in Capitolul 4). In "power" argumentele trebuie sa fie declarate corespunzator cu tipul lor cunoscut. Aceasta este facuta de linia int x,n; care urmeaza liniei cu numele functiei. Declaratiile de argumente urmeaza sa se situeze intre lista de argumente si acolada stinga deschisa {; fiecare declaratie se termina cu punct si virgula. Numele folosite de power pentru argumentele sale sint pur locale lui power si nu sint accesibile nici unei alte functii: alte rutine pot folosi aceleasi nume fara nici un conflict. Aceasta este adevarat si pentru variabilele i si p: i din power (nu este legat prin nimic) nu are nici o legatura cu i din main. Valoarea pe care power o calculeaza este returnata in main prin instructiunea "return", care este la fel ca in PL/1 . Orice expresie poate apare intre paranteze. O functie nu trebuie sa returneze o valoare; o instructiune "return" fara nici o expresie cauzeaza transferul controlului, dar nu o valoare utila, spre apelant, asa cum face "iesirea dupa sfirsit" a unei functii prin atingerea parantezei drepte terminatoare. Exercitiul 1.13. Scrieti un program care sa converteasca intrarea in litere mici, folosind o functie lower(c) care returneaza pe c, daca c nu este o litera, si valoarea "litera mica a lui c", daca c este o litera. 1.8. Argumente - apel prin valoare Un aspect al functiilor din limbajul C s-ar putea sa fie nefami- liar programatorilor obisnuiti cu alte limbaje, in particular cu FORTRAN sau PL/1. In C, toate argumentele functiei sint transmise "prin va- loare". Aceasta inseamna ca functiei apelate i se transmit valorile argumentelor in variabile temporare (de fapt intr-o stiva ) si nu i se transmit adresele lor. Aceasta duce la citeva proprietati diferite fata de limbajele cu "apel prin referinta" de tipul FORTRAN si PL/1, in care rutina apelata minuieste adresele argumentelor si nu valorile lor. Principala distinctie este aceea ca in limbajul C, functia apelata nu poate altera o variabila in functia apelata; ea poate altera numai copia ei temporara si privata. Apelul prin valoare este, cu toate acestea un avantaj si nu o obligatie. Uzual, el conduce la programe mai compacte cu mai putine variabile inutile, deoarece argumentele pot fi tratate ca variabile locale initializate convenabil in rutina apelata. Drept exemplu, dam in continuare o versiune a functiei power care face uz de acest fapt. power(x,n) /*ridica pe x la puterea a n-a; n > 0;versiunea 2*/ int x, n; { int p; for (p = 1; n > 0; --n) p = p * x; return(p); } Argumentul n este folosit ca o variabila temporara, si este decrementat pina cind devine zero; nu mai este nevoie de varia- bila i. Ceea ce se face cu n in interiorul lui power nu are nici un efect asupra argumentului cu care a fost apelata power initial. Cind este necesar, este posibil sa aranjam ca o functie sa modi- fice o variabila in rutina apelanta. Apelandul trebuie sa dea adresa variabilei de setat (in mod tehnic, sa creeze un pointer la variabila),iar functia apelata trebuie sa declare argumentul ca fiind un pointer si sa refere variabila reala in mod indirect prin el. Vom discuta in detaliu aceste probleme in Capitolul 5. Cind numele unui tablou este folosit ca si argument, valoarea transmisa functiei este locatia sau adresa de inceput a tablo- ului. (Nu se face nici o copiere de elemente de tablou). Indiciind aceasta valoare, functia poate avea acces si altera orice element al tabloului. Acesta este subiectul urmatoarei sectiuni. 1.9. Tablouri de caractere In mod probabil, cel mai comun tip de tablouri in limbajul C este tabloul de caractere. Pentru a ilustra folosirea tablourilor de caractere si a functiilor care le manipuleaza, vom scrie un pro- gram care citeste un set de linii si o tipareste pe cea mai lunga. Schita lui este destul de simpla: while (mai exista o alta linie) if (este mai lunga decit linia anterioara) salveaza-o pe ea si lungimea ei tipareste linia cea mai lunga Aceasta schita ne arata clar ca programul se imparte in bucati. O bucata citeste o linie noua, o alta bucata o testea- za, o alta o salveaza iar restul controleaza procesul. Deoarece lucrurile se impart asa de frumos, ar fi mai bine sa le scriem la fel. Pentru aceeasta, vom scrie la inceput o functie getline care va citi urmatoarea linie de la in- trare; ea este generalizare a functiei getchar. Pentru a face functia utila si in alte contexte, vom incerca sa o scriem cit mai flexibil. In mod minim, getline va trebui sa returneze un semnal despre posibilul sfirsit de fisier; proiectind-o mai general,ea va trebui sa returneze lungimea liniei sau zero daca se intilneste sfirsitul de fisier. Zero nu este niciodata o lungime valida de linie, deoarece orice linie are cel putin un caracter, chiar si o linie ce contine numai caracterul "linie noua" are lungimea 1. Cind gasim o linie care este mai lunga decit linia cea mai lunga gasita anterior, trebuie sa o salvam undeva. Aceasta suge- reaza o a doua functie, copy, pentru a salva noua linie intr-un loc sigur. In final, avem nevoie de un program principal care sa contro- leze functiile getline si copy. Iata rezulatul: #define MAXLINE 1000 /* lungimea maxima a liniei */ main() /* gaseste linia cea mai lunga */ { int len; /* lungimea liniei curente */ int max; /* lungimea maxima gasita pina acum */ char line[MAXLINE]; /* linia curenta introdusa */ char save[MAXLINE]; /* cea mai lunga linie salvata */ max = 0; while ((len = getline(line, MAXLINE)) > 0) if (len > max) { max = len; copy(line, save); } if (max > 0) /* s-a citit cel putin o linie */ printf("%s", save); } getline (s, lim) /* citeste linia in s, returneaza lungimea */ char s[]; int lim; { int c, i; for(i = 0; i < lim - 1 && (c=getchar())!=EOF && c!='\n';++i) s[i] = c; if (c == '\n') { s[i] = c; ++i; } s[i] = '\0'; return(i); } copy(s1, s2) /* copiaza pe s1 in s2; s2 suficient de mare */ char s1[], s2[]; { int i; i = 0; while ((s2[i] = s1[i]) != '\0') ++i; } main si getline comunica intre ele printr-o pereche de argumente si o valoare returnata. In getline, argumumentele sint declarate prin liniile: char s[]; int lim; care spun ca primul argument este un tablou iar al doilea un intreg. Lungimea tabloului s nu este specificata in getline deoarece ea este determinata in main. "getline" foloseste in- structiunea return pentru a trimite o valoare inapoi apelan- tului, la fel cum facea si functia power. Unele functii returneaza o valoare utila; altele, de exemplu copy, sint folosite numai pentru efectul lor si nu returneaza nici o valoare. getline pune caracterul \0 (caracterul nul, a carui valoare este zero) la sfirsitul tabloului pe care il creaza, pentru a marca sfirsitul sirului de caractere. Aceasta conventie este folosita de asemenea si de catre compilatorul C; cind o constanta sir de tipul "hello\n" este scrisa intr-un program C, compilatorul isi creaza un tablou de caractere continind carcterele sirului si terminat cu \0, astfel incit o functie, de exemplu printf, poate sa-i determine sfirsitul. ------------------------------- | h | e | l | l | o | \n | \0 | ------------------------------- Specificatorul de format %s din printf se asteapta la un sir reprezentat tocmai in aceasta forma. Daca examinati functia copy, veti descoperi ca si ea se bizuie de fapt pe terminarea argumentu- lui sau de intrare s1 cu un \0 si ea copiaza acest caracter in argumentul de iesire s2. (Toate acestea presupun ca \0 nu este parte a unui text normal). Este demn de mentionat in trecere ca, un program, chiar si atit de mic ca acesta, prezinta unele probleme delicate de proiec- tare. De exemplu, ce ar face main daca ar intilni o linie mai mare decit limita sa? getline lucreaza bine, adica se va opri atunci cind tabloul este plin chiar daca nu a intilnit nici un caracter "linie noua". Testind lungimea si ultimul caracter returnat, main poate determina cind a fost linia prea lunga si apoi sa actioneze cum vrea. Pentru a scurta programul, am ignorat acest aspect. Nu exista vre-o cale pentru utilizatorul lui getchar de a sti inainte cit va fi de lunga o linie de intrare, asa ca getline verifica daca nu s-a produs o depasire. Pe de alta parte, utilizatorul lui copy stie intodeauna (sau poate descoperi) cit este de mare sirul, asa ca nu trebuie sa adaugam la functie o verificare de erori. Exercitiul 1.14. Revizuiti rutina main din programul precedent astfel incit ea sa tipareasca corect lungimea unei linii de intrare de o lungime arbitrara, si atita text cit este posibil de tiparit. Exercitiul 1.15. Scrieti un program care sa tipareasca toate liniile mai lungi de 80 de caractere. Exercitiul 1.16. Scrieti un program care sa elimine blancurile nesemnificative (cele de dupa un caracter diferit de blanc sau tab) din fiecare linie de intrare si care sa stearga liniile care contin numai blancuri. Exercitiul 1.17 Scrieti o functie reverse(s) care sa inverseze un sir de caractere s. Folositi-o pentru a scrie un program care isi inverseaza linie cu linie intrarea. 1.10 Domeniu; variabile externe Variabile din main (line, save, etc) sint private sau locale lui main; deoarece ele sint declarate in main, nici o alta functie nu poate avea acces direct la ele. La fel se intimpla si cu variabilele din alte functii de exemplu variabila i din getline nu are nici o legatura cu varibila i din copy. Fiecare variabila locala dintr-o rutina se naste numai atunci cind functia este apelata si"dispare" cind functia isi termina activitatea. Aceasta este ratiunea pentru care astfel de variabile sint cunoscute uzual sint numele de variabile automate, urmind terminologia din alte limbaje. Vom folosi termenul de "automat" de aici inainte pentru a ne referi la aceste variabile dinamice locale. (Capitolul 4 discu- ta clasa de memorie "statica" in care variabilele locale isi pastreaza valoarea intre apelurile la functii). Deoarece variabilele automate vin si pleaca odata cu apeluri- le de functii, ele nu-si pastreaza valoarea de la un apel la altul si trebuie initializate explicit inainte de fiecare intrare. Daca nu sint setate, rezultatele vor fi imprevizibile. Ca o alternativa la variabilele automate, este posibil sa definim variabile care sa fie "externe" tuturor functiilor, adica, variabilele globale care sa fie accesate prin nume de orice functie care doreste sa o faca. (Acest mecanism este foarte asemanator cu COMMON din FORTRAN sau EXTERNAL din PL/1). Deoarece variabilele externe sint accesibile global, ele pot fi folosite in locul listelor de argumente pentru a comunica date intre functii. Mai mult, deoarece variabilele externe exista permanent, si nu apar si dispar dupa cum functia este apelata sau s-a terminat, ele isi pastreaza valorile chiar si dupa ce s-a terminat functia care le-a setat. O variabila externa trebuie sa fie definita in afara oricarei functii; acest lucru face sa se aloce memorie reala pentru ea. Variabila trebuie deasemenea sa fie declarata in fiecare functie care vrea sa o foloseasca; aceasta se poate face fie printr-o declaratie explicita "extern", fie implicit prin context. Pentru a face discutia corecta, vom rescrie programul precedent, in care line, save, max vor fi declarate variabile externe. Aceasta va cere modificari in apeluri, in declaratii si in corpurile celor trei functii. #define MAXLINE 1000 /* marimea maxima a liniei de intrare */ char line[MAXLINE]; /* linia de intrare */ char save[MAXLINE]; /* cea mai lunga linie este salvata aici*/ int max; /* lungimea liniei celei mai mari */ main() /* gaseste linia cea mai lunga ;versiune specializata*/ { int len; extern int, max; extern char save[]; max = 0; while ((len = getline() > 0) if (len > max) { max = len; copy(); } if (max > 0) /* a fost cel putin o linie */ printf("%s", save); } getline() /* versiune specializata */ { int c, i; extern char line[]; for (i = 0; i < MAXLINE-1 && (c = getchar()) != EOF && c != '\n'; ++i) line[i] = c; if (c == '\n') { line[i] = c; ++i; } line[i] = '\0'; return(i); } copy() /* versiune specializata */ { int i; extern char line[], save[]; i = 0; while ((save[i] = line[i]) != '\0') ++i; } Variabilele externe din main, getline si copy sint defi- nite de primele linii din exemplul de mai sus care declara tipul lor si provoaca o alocare de memorie pentru ele. Din punct de vedere sintactic, definitiile externe sint asemanatoare cu decla- ratiile pe care le-am folosit anterior dar deoarece ele apar in afara functiilor, variabilele sint externe. Inainte ca o functie sa poata folosi o variabila externa, numele variabilei trebuie sa fie facut cunoscut functiei O modalitate pentru a face aceasta este scriind o declaratie "extern"; declaratia este identica cu cea de dinainte, avind insa in plus cuvintul cheie extern. In anumite circumstante, declaratia "extern" poate fi omisa: daca definitia externa a variabilei apare in fisierul sursa inainte de folosirea ei intr-o functie particulara, atunci nu este necesara o declaratie "extern" in functie. Deci, declaratiile "extern" din main, getline si cpoy sint redundante. De fapt prac- tica uzuala consta in plasarea definitiilor tuturor variabilelor externe la inceputul fisierului sursa si apoi omiterea tuturor declaratiilor "extern". Daca programul consta din mai multe fisiere sursa si o variabila este definita in fisierul 1 si folosita in fisie- rul 2 atunci e nevoie de o declaratie "extern" in fisierul 2 pentru a conecta cele 2 aparitii ale variabilei. Acest subiect este discutat pe larg in capitolul 4. Puteti nota ca am folosit cu grija cuvintele "declaratie" si "definitie" cind ne-am referit la variabile externe in aceasta sectiune. "Definitiile" se refera la locul in care variabila este efectiv creata si i se asigneaza memorie; "declaratie" se refera la locul unde natura variabilei este declarata dar nu i se aloca memorie. Fiindca veni vorba, exista o tendinta de a face totul cu ajutorul variabilelor externe deoarece ele par a simplifica toate comunicatiile - listele de argumente sint scurte si variabilele sint intodeauna acolo cind aveti nevoie de ele. Dar variabilele externe sint intodeauna acolo chiar si cind nu aveti nevoie de ele. Acest stil de a codifica este plin de pericole deoarece el conduce la programe ale caror conexiuni de date nu sint evidente clar - variabilele pot fi modificate in moduri neastep- tate si chiar inadvertente iar programul devine greu de modi- ficat daca acest lucru este necesar. A doua versiune a progra- mului care cauta linia cea mai luunga este inferioara primei, partial din aceste motive, si partial deoarece ea distruge genera- litatea a doua functii atit de utile introducind in ele numele unor variabile pe care le vor folosi. Exercitiul 1.18 Testul din instructiunea for din functia getline de mai sus este aproape de neinteles. Rescrieti programul pentru a-l face mai clar dar pastrati acelasi com- portament la sfirsitul fisierului sau la depasire de buffer. Este acest comportament cel mai adecvat ? 1.11 Rezumat In acest punct am acoperit ceea ce, conventional, poate fi numit esenta lui C. Cu aceste citeva caramizi, este posibil sa scrieti programe utile de marime considerabila dar ar fi o buna idee daca v-ati odihni mai mult inainte de a face asa ceva. Exercitiile care urmeaza au intetia de a va oferi sugestii pentru programe de o complexitate oarecum mai mare decit cele prezentate in acest capitol. Dupa ce reusiti sa aveti sub control aceasta parte a lui C, ar fi demn de efortul dumneavoastra sa cititi mai depar- te, deoarece caracteristicile lui C acoperite in urmatoarele citeva capitole sint cele in care puterea si expresivitatea limbajului devin aparente. Exercitiul 1.19. Scrieti un program "detab" care inlocuieste taburile din intrare cu numarul potrivit de blancuri pentru a sari pina la urmatorul stop de tab. Presupuneti un set fixat de stopuri de tab, fie din n in n pozitii. Exercitiul 1.20. Scrieti un program "entab" care inlocuieste si- ruri de blancuri cu numarul minim de taburi si blancuri pentru a obtine o aceasi spatiere. Folositi aceleasi stopuri de tab ca si detab. Exercitiul 1.21. Scrieti un program pentru a "impaturi" li- niile de intrare lungi dupa ultimul caracter neblanc care apare inainte de a n-a coloana a intrarii, unde n este un parametru. Asigurati-va ca programul dumneavoastra lucreaza inte- ligent cu liniile foarte lungi, chiar daca nu e nici un tab sau blanc inainte de coloana specificata. Exercitiul 1.22. Scrieti un program care sa elimine toate comenta- riile dintr-un program C. Nu uitati sa minuiti adecvat sirurile dintre ghilimele si constantele de caractere. Exercitiul 1.23. Scrieti un program pentru a verfica un program C din punct de vedere al erorilor de sintaxa rudimentare ca de exemplu: paranteze neperechi. Nu uitati ghilimelele, atit cele simple cit si cele duble si comentariile. (Acest program este greu daca il faceti la cazul cel mai general.) CAPITOLUL 2. TIPURI, OPERATORI SI EXPRESII Variabilele si constantele sint obiectele - date de baza mani- pulate intr-un program. Declaratiile listeaza variabilele ce se vor folosi si specifica tipul lor si probabil, valorile lor initiale. Operatorii specifica ce trebuie facut cu ele. Expre- siile combina variabile si constante pentru a produce valori noi. Toate acestea constituie subiectul acestui capitol. 2.1. Nume de variabile Cu toate ca nu am spus-o pina acuma, exista unele restrictii asupra numelor de constante si variabile. Numele sint alca- tuite din litere si cifre; primul caracter trebuie sa fie o litera. Liniuta de subliniere "_" este considerata litera; ea este utila in usurarea citirii numelor lungi de variabile. Literele mari si mici sint caractere distincte; practica traditionala in C foloseste literele mici pentru nume de variabile si literele mari pentru constantele simbolice. Numai primele opt caractere ale unui nume intern sint semni- ficative, cu toate ca se pot folosi mai multe. Pentru numele externe, de exemplu nume de functii si de variabile externe, numarul de caractere poate sa fie mai mic ca 8, deoarece numele externe sint folosite de diferite asambloare si incarcatoare. In Anexa A se dau detalii. Mai mult, cuvinte cheie ca: if, else, int, etc sint rezervate: nu pot fi folosite ca nume de variabile (trebuie sa fie scrise cu litere mici). Natural, e intelept sa alegem numele de variabile astfel incit sa insemne ceva, legat de scopul variabilei, si e nepla- cut sa amestecam litere mari cu mici. 2.2. Tipuri si marimi de date Exista numai citeva tipuri de date de baza in limbajul C: char un singur octet, capabil sa pastreze un caracter din setul local de caractere int un intreg, reflectind tipic marimea efectiva a intregilor pe calculatorul gazda float numar flotant in simpla precizie double numar flotant in dubla precizie. In plus, exista un numar de calificatori care pot fi aplicati tipului "int": short, long si unsigned. short si long se refera la diferite marimi de intregi. Numerele "unsigned" se supun legilor aritmeticii modulo 2^n unde n este numarul de biti dintr-un int; ele sint intodeauna pozitive. Declaratiile pentru cali- ficatori arata astfel: short int x; long int y; unsigned int z; Cuvintul int poate fi omis in astfel de situatii, ceea ce se si intimpla de obicei. Precizia acestor obiecte depinde de calculatorul care le mi- nuieste; tabelul urmator da citeva valori reprezentative: DEC PDP11 Honeywell 6000 IBM/370 Interdata 8/32 ASCII ASCII EBCDIC ASCII char 8 biti 9 biti 8 biti 8 biti int 16 36 32 32 short 16 36 16 16 long 32 36 32 32 float 32 36 32 32 double 64 72 64 64 Intentia e ca short si long sa aiba lungimi diferite de intregi unde e practic; int reflecta normal, cea mai "naturala" lungime pentru un calculator. Asa cum puteti vedea, fiecare compilator este liber sa interpreteze short si long in functie de hardul pe care se executa. Ceea ce trebuie sa notati este ca short nu este niciodata mai lung decit long. 2.3. Constante Constantele int si float au fost deja expuse; notam in plus ca notatia uzuala 123.456e-7 sau notatia stiintifica 0.12E3 pentru numerele flotante sint ambele legale. Orice constanta flotanta este considerata ca fiind de tipul double, asa ca nota- tia "e" serveste atit pentru float cit si pentru double. Constantele lungi sint scrise in stilul 123L. O constanta intreaga normala care este prea lunga pentru un int, este luata deasemenea ca fiind o constanta long. Exista o notatie speciala pentru constantele octale si hexazecimale: un 0 (zero) la inceputul unei constante int inseamna octal; un 0x sau 0X la inceputul unei constante int inseamna hexazecimal. De exemplu, numarul zecimal 31 poate fi scris 037 in octal si 0x1f sau 0X1F in hexazecimal. Constantele octale si hexazecimale pot fi urmate un L pentru a le face "long". O constanta caracter este un caracter singur scris intre ghilimele simple ca, de exemplu, 'x'. Valoarea unei constante caracter este valoarea numerica a caracterului in setul de carac- tere al calculatorului. De exemplu, in setul de caractere ASCII, caracterul zero, sau '0', are valoarea 48, iar in EBCDIC, 240, amindoua valorile fiind diferite de valoarea numerica 0. Scriind '0' in loc de o valoare numerica de tipul 48 sau 240, facem programul independent de o valoare particulara. Constantele caracter participa in operatiile numerice la fel ca oricare alte numere, cu toate ca cel mai adesea ele sint folosite in comparari cu alte caractere. O sectiune viitoare va trata toate regulile de conversie. Anumite caractere negrafice pot fi reprezentate constante caracter cu ajutorul secventelor escape, de exemplu \n (linie noua), \t (tab), \0 (nul), \\ (backspace), \'(ghilimea simpla) etc, care arata ca doua caractere, dar de fapt sint unul singur. In plus, se poate genera orice model de lungime un octet, scriind: '\ddd' unde 'ddd' reprezinta 1 - 3 cifre octale, ca in #define FORMFEED '\014' /* ASCII formfeed */ Constanta caracacter '\0' reprezinta caracterul ce are valoarea '\0' se scrie adesea in locul lui 0 pentru accentua natura caracter a anumitor expresii. O expresie constanta este o expresie care implica numai constante. Astfel de expresii sint evaluate la compilare si nu la executie si ele pot fi folosite in orice loc in care poate apare o constanta, ca in #define MAXLINE 1000 char line[MAXLINE+1]; sau seconds = 60 * 60 * hours; O constanta-sir este o secventa compusa din zero sau mai multe caractere intre ghilimele duble, ca "I am a string" sau "" /* un sir nul */ Ghilimelele duble nu sint parte a sirului ci servesc doar ca delimitatori. Aceleasi secvente escape folosite pentru constan- tele caracter se aplica si la siruri; \" reprezinta caracterul dubla ghilimea. Tehnic, un sir este un tablou ale carui elemente sint carac- tere. Compilatorul plaseaza automat un caracter nul \0 la sfirsi- tul oricarui astfel de sir, astfel ca programele pot determina lesne sfirsitul sirului. Aceasta reprezentare spune ca nu exista o limita reala pentru lungimea unui sir, dar programele trebuie sa parcurga tot sirul pentru a-i determina lungimea. Memoria fizica ceruta este cu o locatie mai mult decit numarul de carac- tere scrise intre ghilimele duble. Functia urmatoare, strlen(s) returneaza lungimea unui sir de carctere s, exclusiv termina- torul \0. strlen(s) /* returneaza lungimea lui s */ char s[]; { int i; i = 0; while (s[i] != '\0') ++i; return(i); } Trebuie distins intre o constanta caracter si un sir care contine un singur caracter: 'x' si "x" nu sint acelasi lucru. Primul este un caracter, folosit pentru a produce valoarea numerica a caracterului x din setul de caractere al calculatoru- lui; al doilea este un sir de caractere care contine un singur caracter (litera x) si un \0. 2.4. Declaratii Toate variabilele trebuie declarate inainte de a fi folosite , cu toate ca anumite declaratii pot fi facute implicit de con- text. O declaratie specifica un tip si este urmata de o lista de una sau mai multe variabile de acel tip, ca in exemplul de mai jos: int lower, upper, step; char c, line[1000]; Variabilele pot apare oricum printre declaratii. Lista de mai sus poate fi scrisa, in mod egal, si astfel: int lower; int upper; int step; char c; char line[1000]; Aceasta ultima forma ocupa mai mult spatiu dar este mai comoda pentru a adauga cite un comentariu la fiecare declaratie sau pentru modificari ulterioare. Variabilele pot fi, deasemenea, initializate in declara- tia lor, cu toate ca exista anumite restrictii. Daca numele este urmat de un semn egal si de o constanta, aceasta serveste la initializare, ca in: char backslash = '\\'; int i = 0; float eps = 1.0e-5; Daca variabila in chestiune este externa sau statica, initializa- rea este facuta o singura data, conceptual inainte ca pro- gramul sa-si inceapa executia. Variabilele automate initializate explicit sint initializate la fiecare apel al functiei in care sint continute. Variabilele automate pentru care nu exista o initializare explicita au valoare nedefinita (adica gunoi). Varia- bilele externe si statice se initializeaza implicit cu zero dar este un bun stil de programare acela de a declara initializrea lor in orice caz. Vom discuta initializarile mai departe pe masura ce se introduc noi tipuri de date. 2.5. Operatori aritmetici Operatorii aritmetici binari sint "+", "-", "*", "/" si operatorul modulo "%". Exista operatorul "-" unar dar nu exista opera- torul unar "+". Impartirea intregilor trunchiaza orice parte frac- tionara. Expresia x % y produce restul cind x se imparte la y si deci este zero cind impartirea este exacta. De exemplu, un an este bisect daca este divizibil cu 4 si daca nu este divizibil cu 100, insa anii divizi- bili cu 400 sint bisecti. Deci if (year % 4 == 0 && year % 100 != 0 || year % 400 == 0) it's a leap year else it's not Operatorul % nu poate fi aplicat la float sau double. Operatorii + si - au aceeasi pondere, care este mai mica decit ponderea (identica) a lui *, / si % care la rindul ei este mai mica decit ponderea operatorului unar -. Operatorii aritmetici se grupeaza de la stinga la dreapta (Tabela de la sfirsitul capitolului rezuma ponderea si asociativitatea pentru toti operatorii). Ordinea de evaluare nu este specificata pentru operatorii asociativi si comutativi de tipul lui * si +. Compilatorul poate rearanja un calcul cu paranteze implicind unul din acestia. Astfel, a+(b+c) poaate fi evaluat ca (a+b)+c. Acest lucru produce rar diferente dar daca se cere o ordine parti- culara, trebuie folosite explicit variabilele temporare. Actiunile care produc depasiri superioare sau inferioare depind in ultima instanta de calculator. 2.6. Operatori relationali si logici Operatorii relationali sint > >= < <=. Ei au toti aceasi pon- dere. Sub ei in tabelul de ponderi se afla operatorii de egali- tate == != , care au o aceeasi pondere. Operatorii relationali au ponderea mai mica decit cei aritmetici, asa ca expresii de tipul i < lim-1 se evalueaza ca i < (lim-1), asa cum ar fi de asteptat. Mai interesanti sint conectorii logici && si ||. Expresiile care-i contin sint evaluate de la stinga la dreapta si evaluarea se opreste in clipa in care se cunoaste adevarul sau falsul rezultatului. Aceste proprietati se dovedesc critice in scrierea programelor. De exemplu iata o bucla luata din functia de intrare getline, pe care am scris-o in Capitolul 1: for (i=0; i= '0' && s[i] <= '9' ; ++i) n = 10 * n + s[i] - '0'; return(n); } Asa cum am vazut in Capitolul 1, expresia s[i]-'0' reprezinta valoarea numerica a caracterului aflat in s[i] deoarece valorile lui '0','1', etc formeaza un sir crescator pozitiv si contiguu. Un alt exemplu de conversie intre char si int il constituie functia lower care transforma literele mari din setul de caractere ASCII in litere mici. Daca intrarea nu este o litera mare, functia o returneaza neschimbata: lower(c) /* conversie ASCII litere mari in litere mici */ int c; { if (c >= 'A' && c <= 'Z') return(c + 'a' - 'A'); else return(c) } Aceasta functie este valabila numai pentru ASCII deoarece pe de o parte intre literele mari si literele mici exista o distanta fixata, ca valoare numerica, iar pe de alta parte ambele alfabete sint contigue - intre A si Z se gasesc numai litere. Aceasta ultima observatie nu este valabila pentru setul de caractere EBCDIC (IBM 360/370), asa incit functia lower esueaza pentru aceste sisteme, ea va converti mai mult decit literele mari. Exista o subtilitate in conversia caracterelor in intregi. Limbajul nu specifica daca o variabila de tip char este o canti- tate cu semn sau fara semn. Cind un char este convertit intr-un int, poate el produce un intreg negativ ? Din pacate, aceasta variaza de la calculator la calculator, reflectind diferen- tele arhitecturale. Pe anumite calculatoare (de exemplu PDP-11) un char al carui cel mai din stinga bit este 1 va fi convertit intr-un intreg negativ ("extensie de semn". Pe altele, un char este convertit intr-un int prin adaugarea de zerouri in partea stinga si astfel el este intodeauna pozitiv. Definitia lui C asigura ca orice caracter din setul stan- dard al masinii nu va fi niciodata negativ, asa ca aceste carac- tere pot fi folosite liber in expresii ca si cantitati pozitive. Dar modele arbitrare de biti memorate in variabile de tip charac- ter pot apare drept negative pe anumite calculatoare si drept pozitive pe altele. Cea mai comuna aparitie a acestei situatii este cind pentru EOF se foloseste -1. Sa consideram codul: char c; c = getchar(); if (c == EOF) ... Pe un calculator care nu face extensie de semn, c este intodeauna pozitiv deoarece el este un char, dar totusi EOF este negativ. In consecinta testul esueaza intodeauna. Pentru a evita aceasta, trebuie sa avem grija atunci cind folosim int in loc de char pentru orice variabila care primeste o valoare returnata de getchar. Adevarata ratiune pentru utilizarea lui int in loc de char nu este legata cu nimic de posibila extensie de semn. Pur si simplu, getchar trebuie sa returneze toate caracterele posibile (astfel incit sa poate fi folosita pentru a citi o intrare arbi- trara) si in plus, o valoare pentru EOF distincta. Astfel, aceasta valoare nu poate fi reprezentata ca si un char dar, in schimb, trebuie memorata ca si un int. O alta forma utila de conversie de tip automata este aceea ca expresiile relationale de tipul i > j si expresiile logice conectate prin && si || se definesc a avea valoarea 1 pentru adevar si 0 pentru fals. Astfel, o asignare: isdigit = c >= '0' && c <= '9'; pune pe isgit pe 1 daca c este o cifra si pe 0 daca nu. (In partea de test a lui if, while ,for, etc, "adevarat" inseamna "nonzero"). Conversiile aritmetice implicite lucreaza in mare masura cum ne asteptam. In general, daca un operator ca + sau * care are doi operanzi (un "operator binar") are operanzi de tipuri diferite, tipul "inferior" este promovat la tipul "superior" ina- inte de executia operatiei. Rezultatul insusi este de tipul supe- rior. Mai precis, pentru fiecare operator aritmetic, se aplica urmatoarea secventa de reguli de conversie: char si short se convertesc in int iar float este conver- tit in double. Apoi, daca un operand este double , celalalt este conver- tit in double iar rezultatul este double. Altfel, daca un operand este long, celalalt este convertit in long iar rezultatul este long. Altfel, daca un operand este unsigned, celalalt este convertit inunsigned, iar rezultatul este unsigned. Altfel, operanzii trebuie sa fie int, iar rezultatul este int. Sa notam ca toti float dintr-o expresie sint convertiti in double; orice calcul flotant in C este facut in dubla precizie. Conversiile se fac in asignari; valoarea partii drepte este convertita la tipul din stinga, care este tipul rezulta- tului. Un caracter este convertit intr-un int fie cu exten- sie de semn, fie nu, asa cum s-a descris mai sus. Operatia inversa, int in char, se comporta bine, pur si simplu, bitii de ordin superior in exces sint eliminati. Astfel, in: int i; char c; i = c; c = i; valoarea lui c ete neschimbata. Acesta este adevarat si cind extensia de semn este implicita si cind nu este implicita. Daca x este float iar i este int, atunci: x = i; si i = x; provoaca amindoua conversii; float in int provoaca trunchierea oricarei parti fractionare. double este convertit in float prin rotunjire. Intregii lungi sint convertiti in scurti sau in char prin pierderea bitilor de ordin superior in exces. Deoarece argumentul unei functii este o expresie, con- versia de tip are loc deasemenea si cind argumentele sint pasate functiei in particular, char si short devin int, iar float devine double. Iata de ce am declarat argumentul functiei ca fiind int si double chiar cind functia este apelata cu char si float. In final, conversia explicita de tip poate fi fortata in orice expresie cu o constructie numita "distribuire"(cast). In constructia: (numedetip) expresie sus. Semnificatia precisa a unei distribuiri este de fapt ca si daca o expresie ar fi asignata la o variabila de tipul specifi- cat, care este apoi folosita in locul intregii constructii. De exemplu, rutina din biblioteca sqrt are nevoie de un argument double si va produce nonsens daca i se da sa minuiasca altceva. Astfel, daca n este un intreg: sqrt((double) n) il converteste pe n in double inainte de a-l pasa lui sqrt. (De notat ca distribuirea produce valoarea n in tipul potrivit; continutul efectiv al lui n nu este alterat ). Operatorul de distribuire are acceasi pondere ca si alti operatori unari, asa cum apare si in tabelul recapitulativ de la sfirsitul capitolu- lui. Exercitiul 2.2. Scrieti o functie htoi(s) care converteste un sir de cifre hexazecimale in valoarea sa intreaga echiva- lenta. Cifrele sint de la 0 la 9, literele de la a la f si de la A la F. 2.8. Operatori de incrementare si decrementare Limbajul C ofera doi operatori neuzuali pentru incrementarea si decrementarea variabilelor. Operatorul de incrementare ++ aduna 1 la operandul sau; operatorul de decrementare -- scade 1. Am folosit frecvent ++ pentru a incrementa variabi- lele, de exemplu: if (c == '\n') ++nl; Aspectul neobisnuit al lui ++ si al lui -- este acela ca ei pot fi folositi atit ca operatori prefix (inaintea variabilei, ca in ++n) cit si ca operatori sufix (dupa variabila, ca in n++). In ambele cazuri, efectul este incrementarea lui n. Dar expresia ++n il incrementeaza pe n inainte de a-i folosi valoarea, in timp ce expresia n++, il incrementeaza pe n dupa ce a fost folo- sita valoarea lui. Aceasta inseamna ca intr-un context in care valoarea este folosita, si nu numai efectul, ++n si n++ sint diferiti. Daca n este 5, atunci: x = n++; il face pe x egal cu 5, dar x = ++n; il face pe x egal cu 6. In ambele cazuri, n devine 6. Operatorii de incrementare si decrementare se pot aplica numai variabilelor. O expresie de tipul x = (i+j)++ este ilegala. Intr-un context in care valoarea nu este folosita, ci numai efectul de incrementare, ca in if (c == '\n') nl++; alegeti modul prefix sau sufix dupa gustul dumneavoastra. Dar exista totusi situatii in care unul sau altul este apelat din adins. De exemplu, sa consideram functia squeeze(s,c) care elimina toate aparitiile lui c din sirul s: squeeze (s,c) /* sterge toate aparitiile lui c din s */ char s[]; int c; { int i, j; for (i = j = 0; s[i] != '\0'; i++) if (s[i] != c) s[j++] = s[i]; s[j] = '\0'; } De fiecare data cind apare un caracter non-c el este copiat in pozitia j curenta si numai dupa aceea j este incrementat pentru a fi gata pentru urmatorul caracter. Aceasta constructie este echivalenta cu urmatoarea: if (s[i] != c) { s[j] = s[i]; j++; } Un alt exemplu de constructie similara este luata din functia getline pe care am scris-o in Capitolul 1, in care putem inlocui if (c == '\n' { s[i]=c; ++i; } cu mult mai compacta constructie: if (c == '\n') s[i++] = c; Ca un al treilea exemplu functia strcat(s,t) care concatenea- za sirul t la sfirsitul sirului s. strcat presupune ca exista suficient spatiu in s pentru a pastra combinatia. strcat (s,t) /* concateneaza pe t la sfirsitul lui s */ char s[], t[]; /* s trebuie sa fie suficient de mare */ { int i, j; i = j = 0; while (s[i] != '\0') /* gaseste sfirsitul lui s */ i++; while ((s[i++] = t[j++]) != '\0') /* copiaza pe t */ ; } Cum fiecare caracter este copiat din t in s, se aplica postfixul ++ atit lui i cit si lui j pentru a fi siguri ca sint pe pozitie pentru urmatorul pas din bucla. Exercitiul 2.3. Scrieti o alta versiune a lui squeeze(s1, s2) care sterge fiecare caracter din s1 care se potriveste cu vreun caracter din s2. Exercitiul 2.4. Scrieti functia any(s1, s2) care returneaza prima locatie din sirul s1 in care apare vreun c acter din sirul s2, sau pe -1 daca s1 nu contine nici un caracter din s2. 2.9. Operatori logici pe biti Limbajul C ofera un numar de operatori pentru manipularea biti- lor; acestia nu se pot aplica lui float si double. & SI bit cu bit | SAU inclusiv bit cu bit ^ SAU exclusiv bit cu bit << deplasare stinga >> deplasare dreapta ~ complement fata de 1 (unar) Operatorul SI bit cu bit "&" este folosit adesea pentru a masca anumite multimi de biti; de exemplu c = n & 0177; pune pe zero toti biti lui n, mai putin bitul 7 (cel mai tare). Operatorul SAU bit cu bit "|" este folosit pentru a pune pe 1 biti: x = x | MASK; pune pe 1 in x bitii care sint setati pe 1 in MASK. Trebuie sa distingeti cu grija operatorii pe biti & si | de conectorii logici && si ||, care implica o evaluare de la stinga la dreapta a unei valori de adevar. De exemplu, daca x este 1 si y este 2, atunci x & y este zero dar x && y este 1. (De ce ?) Operatorii de deplasare << si >> realizeaza deplasari la stinga si la dreapta pentru operandul lor din stinga, cu numarul de pozitii dat de operandul din dreapta lor. Astfel x << 2 deplasea- za la stinga pe x cu doua pozitii, umplind locurile libere cu zero; aceasta este echivalent cu inmultirea cu 4. Deplasind la dreapta o cantitate unsigned, bitii vacanti se umplu cu zero. Deplasind la dreapta o cantitate cu semn, bitii vacanti se umplu cu semnul ("deplasarea aritmetica") pe anumite calculatoare, ca de exemplu PDP-11 si cu 0 ("deplasare logica") pe altele. Operatorul unar ~ da complementul fata de 1 al unui intreg; adica, el converteste fiecare bit de 1 in 0 si vicevesa. Acest operator isi gaseste utilitate in expresii de tipul x & ~077 care mascheaza ultimii 6 biti ai lui x pe 0. De notat ca x & ~077 este independent de lungimea cuvintului si deci prefe- rabil, de exemplu, lui x & 0177700, care presupune ca x este o cantitate cu o lungime de 16 biti. Forma portabila nu implica un cost mai mare, deoarece ~077 este o expresie constanta si deci evaluata la compilare. Pentru a ilustra folosirea unora din operatorii de biti, sa consideram functia getbits(x,p,n) care returneaza (cadrat la dreapta) cimpul de lungime n biti al lui x care incepe la pozitia p. Presupunem ca bitul 0 este cel mai din dreapta si ca n si p sint valori pozitive sensibile. De exemplu, getbits(x,4,3) returneaza 3 biti in pozitiile 4, 3 si 2, cadrati la dreapta. getbits (x, p, n) /* ia n biti de la pozitia p */ unsigned x, p, n; { return( (x >> (p+1-n) ) & ~(~0 << n)); } x >> (p+1-n) muta cimpul dorit la sfirsitul din dreapta al cuvintului. Declarind argumentul x ca fiind unsigned ne asiguram ca atunci cind el este deplasat la dreapta bitii vacanti vor fi umpluti cu 0 si nu cu bitii de semn, independent de calculatorul pe care este executat programul. ~0 este cuvintul cu toti bitii pe 1; deplasindu-l la stinga cu n pozitii prin ~0 << n cream o masca cu zerouri pe cei mai din dreapta n biti si 1 in rest; complemen- tindu-l cu ~ facem o masca de 1 pe cei mai din dreapta n biti. Exercitiul 2.5. Modificati getbits pentru a numara bitii de la stinga la dreapta. Exercitiul 2.6. Scrieti o functie wordlength() care calculea- za lungimea unui cuvint de pe calculatorul gazda, adica numarul de biti dintr-un int. Functia sa fie portabila in sensul ca acelasi cod sursa sa lucreze pe toate calculatoarele. Exercitiul 2.7. Scrieti o functie rightrot(n, b) care roteste intregul n la dreapta cu b pozitii. Exercitiul 2.8. Scrieti o functie invert(x,p,n) care inverseaza (i.e. schimba pe 1 in 0 si viceversa) cei n biti ai lui x care incep de la pozitia p,lasindu-i pe ceilalti neschimbati. 2.10. Operatori si expresii de asignare Expresii de tipul: i = i + 2 in care membrul sting este repetat in membrul drept pot fi scrise intr-o forma condensata: i += 2 folosind operatorul de asignare +=. Majoritatea operatorilor binari (operatori ca +, care au un operand sting si un operand drept) au un operator de asignare corespunzator "op=", unde op este unul din: + - * / % << >> & ^ | Daca e1 si e2 sint doua expresii, atunci: e1 op= e2 este echivalent cu e1 = (e1) op (e2) cu exceptia ca e1 este calculat o singura data. Sa remarcam parantezele din jurul lui e2: x *= y + 1 inseamna de fapt x = x * (y + 1) si nu x = x * y + 1 Dam in continuare, drept exemplu, functia bitcount, care conto- rizeaza numarul de biti pe 1 dintr-un argument intreg. bitcount(n) /* contorizeaza bitii 1 din n */ unsigned n; { int b; for (b = 0; n != 0; n >>= 1) if (n & 01) b++; return(b); } Lasind la o parte conciziunea, operatorii de asignare au avantajul ca ei corespund cel mai bine modului de gindire al oamenilor. Noi spunem "adunam 2 la i" sau " incrementam pe i cu 2" si nu "ia-l pe i, aduna 2, apoi pune rezultatul inapoi in i". Deci i += 2. In plus, pentru o expresie complicata, de tipul: yyval[yypv[p3 + p4] + yypv[p1 + p2]] += 2 operatorul de asignare face codul mai usor de inteles, deoarece cititorul nu trebuie sa verifice sirguincios ca cele doua expresii sint intr-adevar o aceeasi sau sa se intrebe de ce nu sint. In plus, un operator de asignare ajuta chiar compilatorul sa produca un cod mai eficient. Am folosit deja faptul ca o instructiune de asignare are o valoare si ca poate sa apara in expresii; exemplul cel mai comun: while ((c = getchar()) != EOF) ... Asignarile folosind alti operatori de asignare (+=, -=, etc) pot deasemenea sa apara in expresii, cu toate ca acestea se intim- pla mai rar. Tipul unei expresii de asignare este tipul operandului sau sting. Exercitiul 2.9. Intr-un sistem cu numere cu complement fata de 2, x & (x-1) sterge bitul 1 cel mai departe de x. (De ce ?). Folositi aceasta observatie pentru a scrie o versiune mai rapida a lui bitcount. 2.11. Expresii conditionale Instructiunile if (a < b) z = a; else z = b; calculeaza desigur in z maximul dintre a si b. Expresia conditio- nala, scrisa cu operatorul ternar "? :" ofera un mod alternativ pentru a scrie acest lucru precum si constructii similare. In expresia: e1 ? e2 : e3 expresia e1 se evalueaza prima. Daca ea este nonzero (adevara- ta) atunci se evalueaza expresia e2 si aceasta este valoarea expresiei conditionale. Altminteri, se evalueaza e3 si aceasta este valoarea. Numai una din expresiile e2 si e3 se evalueaza. Deci, pentru a pune in z maximul dintre a si b: z = (a > b) ? a : b; /* z = max(a, b) */ Trebuie sa notam ca expresia conditionala este intr-adevar o expresie si ca ea poate fi folosita exact ca oricare alta expre- sie. Daca e2 si e3 sint expresii de tipuri diferite, tipul reultatului se determina dupa regulile de conversie discutate mai inainte in acest capitol. De exemplu, daca f este un float si n este un int, atunci expresia (n > 0) ? f : n este de tipul double, indiferent daca n este pozitiv sau nu. Parantezele nu sint necesare in jurul primei expresii a unei expresii conditionale, deoarece ponderea lui ? : este foarte mica, chiar deasupra asignarii. Ele sint totusi recoman- date, pentru a face partea de conditie a expresiei mai usor de vazut. Expresiile conditionale conduc adesea la un cod succint. De exemplu, bucla urmatoare tipareste N elemente ale unui tablou, 10 pe linie, cu fiecare coloana separata printr-un blanc si cu fiecare linie (inclusiv ultima) terminata cu un singur caracter "linie noua". for (i = 0; i << N; i++) printf("%6d%c", a[i], (i % 10 == 9 || i == N-1) ? '\n' : ' '); Un caracter "linie noua" se tipareste tot dupa al zecelea element si dupa al N-lea element. Toate celelalte elemente sint urmate de un blanc. Cu toate ca seamana cu un truc, este instruc- tiv sa incercati sa scrieti lucrul acesta fara a folosi expresia conditionala. Exercitiul 2.10. Rescrieti functia lower, care converteste literele mari in litere mici, cu o expresie conditionala in locul lui if-else. 2.12. Ponderea si ordinea de evaluare Tabelul de mai jos rezuma regulile de pondere si asociativi- tate pentru toti operatorii, inclusiv pentru aceia pe care nu i- am discutat inca. Operatorii de pe aceeasi linie au aceeasi pondere; liniile sint in ordine de pondere descrescatoare, astfel ca, de exemplu, "*", "/" si "%" au o aceeasi pondere, care este mai mare decit a lui "+" "-". +------------------------------+--------------------------------+ | () [] -> . | de la stinga la dreapta | +------------------------------+--------------------------------+ | ! ~ ++ -- - (tip) * & sizeof | de la dreapta la stinga | +------------------------------+-------------------------------+ | * / % | de la stinga la dreapta | +------------------------------+--------------------------------+ | + - | de la stinga la dreapta | +------------------------------+--------------------------------+ | << >> | de la stinga la dreapta | +------------------------------+--------------------------------+ | < <= > >= | de la stinga la dreapta | +------------------------------+--------------------------------+ | == != | de la stinga la dreapta | +------------------------------+--------------------------------+ | & | de la stinga la dreapta | +------------------------------+--------------------------------+ | ^ | de la stinga la dreapta | +------------------------------+--------------------------------+ | | | de la stinga la dreapta | +------------------------------+--------------------------------+ | && | de la stinga la dreapta | +------------------------------+--------------------------------+ | || | de la stinga la dreapta | +------------------------------+--------------------------------+ | ? : | de la dreapta la stinga | +------------------------------+--------------------------------+ +------------------------------+--------------------------------+ | = += -= etc | de la dreapta la stinga | +------------------------------+--------------------------------+ | , (Capitolul 3) | de la stinga la dreapta | +------------------------------+--------------------------------+ Operatorii -> si. sint folositi pentru a accede membrii structuri- lor; ei vor fi iscutati in Capitolul 6, impreuna cu sizeof (mari- mea unui obiect). Capitolul 5 discuta * (indirectarea) si & (adresa lui ...). Sa notam ca ponderea operatorilor logici pe biti &, | si ^ este sub == i |=. Aceasta implica faptul ca expresiile care testeaza biti, ca de exemplu if (( x & MASK) == 0) ... trebuie sa fie cuprinse in intregime intre paranteze, pentru a da rezultatele steptate. Asa cum am mentionat mai inainte, expresiile ce implica operatori asociativi si comutativi (+, *, &, ^, |) pot fi rearanjate chiar daca sint cuprinse in paranteze. In marea majoritate a cazurilor, aceasta nu da diferente; in situatia in care ar da, se pot folosi variabile temporare explicite pentru a forta o ordine de evaluare particulara. Limbajul C, ca si majoritatea celorlalte limbaje, nu specifica in ce ordine se evalueaza operanzii unui operator. De exemplu, in instructiuni de tipul x = f() + g(); f poate fi evaluat inaintea lui g sau viceversa; deci, daca sau f sau g altereaza o variabila externa de care depinde si cealalta, x poate depinde de ordinea de evaluare. Din nou, rezultatele intermediare pot fi stocate in variabile temporare pentru a fi siguri de o anumita secventa. In mod similar, ordinea in care sint evaluate argumentele unei functii nu este specficata, asa ca instructiunea printf("%d %d\n", ++n, power(2, n)); /* GRESIT */ poate (si o si face) produce rezultate diferite, pe diferite calculatoare, depinzind de faptul daca n este incrementat sau nu inainte de apelul lui power. Solutia, desigur, este sa scriem: ++n; printf("%d %d\n", n, power(2, n)); Apelurile de functii, instructiunile de asignare imbricate, operatorii de incrementare si decrementare provoaca "efecte secu- ndare" - o anumita variabila este modificata ca un produs al unei evaluari de expresie. In orice expresie implicind efecte secundare, pot exista subtile dependente de ordinea in care sint stocate variabilele ce iau parte in expresie. O situatie neferi- cita este ilustrata de instructiunea: a[i] = i++; Chestiunea consta in a sti daca indicele este noua valoare a lui i sau daca este vechea. Compilatorul poate face aceste lucruri in moduri diferite, depinzind de interpretarea sa. Cind efectele secundare (asignare la variabile efective) au loc, sint lasate la discretia compilatorului, caci cea mai buna ordine depinde puternic de arhitectura calculatorului. Morala acestei discutii este aceea, ca scrierea de cod ce depinde de ordinea de evaluare, este o proasta practica de programare in orice limbaj. Natural, e necesar sa stim ce lucruri trebuie evitate, dar daca nu stim cum sint ele facute pe diferite calculatoare, aceasta inocenta ne va ajuta sa ne protejam. (Verificatorul lui C, lint, detecteaza majoritatea de- pendentelor de ordinea de evaluare). CAPITOLUL 3. CONTROLUL FLUXULUI Instructiunile de control al fluxului dintr-un limbaj specifica ordinea in care se fac calculele. Ne-am intilnit deja cu cele mai cunoscute constructii de control al fluxului din limbajul C, in exemplele date in paginile anterioare; in cele ce urmeaza, vom completa setul de instructiuni si vom fi mult mai precisi asupra celor discutate mai sus. 3.1. Instructiuni si blocuri O expresie ca de exemplu x = 0 sau i++ sau printf(...) devine instructiune cind este urmata de punct si virgula, ca in: x = 0; i++; printf(...); In limbajul C, punct-virgula este terminator de instructiune, nu separator, cum este in limbajele de tipul ALGOL. Acoladele { si } sint folosite pentru a grupa impreuna instructiuni si declaratii intr-o instructiune compusa sau bloc, asa ca ele sint sintactic echivalente cu o singura instruc- tiune. Acoladele ce inchid instructiunile unei functii sau cele pentru instructiunile multiple dupa un if, else, while, for sint exemple clare pentru aceasta. (Variabilele pot fi de fapt declarate inlauntrul oricarui bloc; vom discuta despre aceasta in Capitolul 4). Nu se pune niciodata punct si virgula dupa acola- da inchisa care termina un bloc. 3.2. If-Else Instructiunea If-Else este folosita pentru luarea de decizii. Formal, sintaxa ei este: if(expresie) instructiune-1 else instructiune-2 unde partea "else" este optionala. "Expresia" este evalua- ta; daca este "adevarata" (adica, are o valoare nenula), "instructiune-1" este executata. Daca ea este "falsa" ("expresia" este zero) si daca exista partea cu "else", se executa in schimb "instructiune-2". Deoarece un "if" testeaza pur si simplu valoarea numerica a unei expresii, sint posibile anumite prescurtari de cod. Cel mai clar exemplu este scriind if(expresie) in loc de if(expresie != 0) Citeodata, acest lucru este natural si clar. Altadata poate parea cifrat. Deoarece partea cu "else" a unui if-else este optionala, se poate ajunge la o ambiguitate cind se omite un else dintr-o secve- nta imbricata de if-uri. Aceasta este rezolvata, ca de obicei, asa: else este asociat cu if-ul anterior cel mai apropiat, care nu face pereche cu un "if". De exemplu, in: if (n > 0) if (a > b) z = a; else z = b; else face pereche cu if cel mai dinauntru, asa cum am aratat prin tabulare. Daca nu dorim aceasta, trebuie sa folosim acolade pentru a forta asocierea potrivita: if (n > 0) { if (a > b)