Sunteți pe pagina 1din 11

Capitolul3: INSTRUCTIUNI DE CONTROL

Instructiunile de control ale unui limbaj specifica ordinea in care sint realizate calculele. Am intilnit deja cele mai obisnuite constructii in exemplele anterioare; aici vom completa setul si vom fi mai precisi in privinta unora ce au fost discutate mai inainte.

3.1

Instructiuni si blocuri

O expresie, precum x = 0 sau i++ sau printf(...), devine instructiune cind este urmata de caracterul ; (punct si virgula), ca de exemplu: x = 0; i++; printf(...); In C caracterul ; este terminator de instructiune si nu un separator cum este el in limbaje ca Pascal. Acoladele { si } sint folosite pentru a grupa declaratii si instructiuni intr-o instructiune compusa , sau bloc , astfel incit ele sint sintactic echivalente unei instructiuni singulare. Acolade- le care incadreaza instructiunile unei functii reprezinta un exemplu evident; acoladele ce incadreaza instructiunile ce urmeaza dupa if, else, while sau for reprezinta un alt exemplu. (Variabilele pot fi declarate in interiorul oricarui bloc; vom vorbi despre acest lucru in Cap. 4). Nu se pune caracterul ; dupa acolada din dreapta care sfirseste un bloc.

3.2

If-else

Instructiunea if-else este folosita pentru a exprima decizii. Formal, sintaxa este urmatoarea: if (expresie) instructiunea1 else instructiunea2 unde partea else este optionala. Este calculata expresia dintre paranteze; daca este adevarata (adica daca expresia are valoare diferita de zero) este executata instructiunea1. Daca este falsa (expresia are valoarea 0) si daca exista o parte else atunci va fi executata instructiunea2 in locul instructiunii1. Deoarece un if testeaza pur si simplu valoarea numerica a unei expresii, sint posibile anumite scurtari in scrierea programelor. Cea mai evidenta rezulta prin scrierea: if (expresie) in loc de: if (expresie != 0)

Uneori acest lucru este firesc si clar; alte ori poate fi criptic. Deoarece partea else dintr-un if-else este optionala, exista o ambiguitate cind else este omis dintr-o serie de if-uri cuibarite. Aceasta se rezolva prin asocierea lui else cu cel mai apropiat dintre if-urile care nu au partea else. De exemplu in: if (n > 0) if (a > b) z = a; else z = b; else merge cu if-ul interior, asa cum am aratat prin indentare. Daca nu doriti acest lucru, atunci trebuie neaparat folosite acoladele pentru a forta asocierea corecta: if (n > 0) { if (a > b) z = a; { else z = b; Ambiguitatea este in special pernicioasa in situatii ca:

if (n >= 0) for (i = 0; i < n; i++) if (s[i] > 0) { printf("..."); return i; } else /* WRONG */ printf("error -- n is negative\n"); Indentarea arata fara echivoc ceea ce doriti, dar compilatorul nu primeste mesajul si asociaza else cu if-ul interior. Acest gen de eroare este dificil de descoperit ; este o buna idee de a folosi acolade cind apar if-uri cuibarite. De notat ca, dupa z = a, din: if (a > b) z = a; else z = b; exista caracterul ; (punct si virgula). Aceasta deoarece, gramatical lui if ii urmeaza o instructiune iar o instructiune de forma z = a este totdeauna terminata prin caracterul ; (punct si virgula).

3.3

Else-if

Constructia: if (expresie) instructiune else if (expresie) instructiune else if (expresie) instructiune

else if (expresie) instructiune else instructiune apare atit de des incit este de folos o scurta discutie separata. Aceasta secventa de instructiuni if este modul cel mai general de a scrie o decizie cu mai multe cai. Expresiile sint calculate in ordine; daca o expresie este adevarata atunci se executa instructiunea asociata si aceasta termina intregul lant. Ca intodeauna codul pentru fiecare instructiune este fie o instructiune singulara fie un grup incadrat de acolade. Ultimul else actioneaza in cazul cind nici una dintre conditiile anterioare nu este satisfacuta. Uneori, in astfel de cazuri nu trebuie nici o actiune, atunci: instructiune pot fi omise sau pot fi utilizate pentru verificarea unei erori pentru a prinde o conditie "imposibila". Pentru a ilustra o decizie cu trei cai, iata o functie de cautare binara care decide daca o valoare particulara x apare intr- un tablou sortat v. Elementele lui v trebuie neaparat sa fie in ordine crescatoare. Functia returneaza pozitia (un numar intre 0 si n-1) daca x apare in v si -1 daca nu apare. Cautarea binara compara mai intii valoarea x de la intrare cu elementul din mijlocul tabloului v. Daca x este mai mic decit acesta, cautarea continua in jumatatea de jos a tabloului, altminteri continua in jumatatea de sus. In ambele cazuri, pasul urmator este compararea lui x cu elementul din mijlocul jumatatii selectate. Acest proces de impartire a domeniului in doua continua pina ce valoarea este gasita sau domeniul devine vid. /* binsearch: find x in v[0] <= v[1] <= ... <= v[n-1] */ int binsearch(int x, int v[], int n) { int low, high, mid; low = 0; high = n - 1; while (low <= high) { mid = (low + high) / 2; if (x < v[mid]) high = mid - 1; else if (x > v[mid]) low = mid + 1; else /* found match */ return mid; } return -1; /* no match */ else

Decizia fundamentala este daca x este mai mic decit, mai mare decit sau egal cu elementul din mijloc v[mid] la fiecare pas; acest lucru este natural pentru else-if. Exercitiul 3-1. Cautarea noastra binara face doua teste in interiorul buclei de ciclare, cind unul ar fi suficient (cu pretul mai multor teste in afara). Scrieti o versiune cu numai un test in interiorul buclei si masurati diferenta de timp la executie.

3.4

Switch

Instructiunea switch este o decizie cu mai multe cai care testeaza daca o expresie este egala cu una dintr-un numar de valori intregi constante, in care caz urmeaza calea respectiva:

switch (expresie) { case expr-const : instructiuni case expr-const : instructiuni default : instructiuni } Fiecare case este etichetat cu una sau mai multe constante cu valoare intreaga. Daca un case se potriveste cu valoarea expresiei, atunci executia incepe la acest case. Toate expresiile case trebuie neaparat sa fie diferite. Cazul cu denumirea default este executat daca nici unul dintre celelalte case-uri nu este satisfacut. Un default este optional; daca el nu exista si nici un case nu este satisfacut nu are loc nici o actiune. Case-urile si default-ul pot apare in orice ordine. In Cap. 1 am scris un program pentru a numara aparitiile fiecarei cifre, fiecarui spatiu alb (blanc, tab si newline) si toate celelalte caractere, folosind o secventa de if...else, if...else. Mai jos avem acelasi program cu un switch: #include <stdio.h> main() /* count digits, white spaces, others */ { int c, i, nwhite, nother, ndigit[10]; nwhite = nother = 0; for (i = 0; i < 10; i++) ndigit[i] = 0; while ((c = getchar()) != EOF) { switch (c) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': ndigit[c-'0']++; break; case ' ': case '\n': case '\t': nwhite++; break; default: nother++; break; } } printf("digits ="); for (i = 0; i < 10; i++) printf(" %d", ndigit[i]); printf(", white space = %d, other = %d\n", nwhite, nother); return 0; } Instructiunea break provoaca o iesire imediata din switch. Deoarece caseurile servesc doar ca etichete, dupa ce codul de dupa un case s-a executat, executia cade pe codul case-ului urmator, in afara de cazul ca s-a intreprins o actiune explicita pentru a-l evita. break si return sint cele mai obisnuite cai pentru a parasi un switch. O instructiune break poate fi de asemenea utilizata pentru a forta o iesire dintr-una din buclele de ciclare while, for si do asa cum se va discuta mai tirziu in acest capitol. Executarea case-urilor urmatoare este o fericire amestecata. Partea pozitiva consta in aceea ca se permite ca o serie de case-uri sa fie atasate unei singure

actiuni, precum cazul cifrelor din acest exemplu. Dar acest lucru impune ca, in mod normal, fiecare case sa se termine cu un break pentru a preveni continuarea executiei cu codul case-ului urmator. Trecerea cu executia dintr-un case intraltul nu este un lucru prea bun, predispunind la dezintegrare cind programul este modificat. Cu exceptia etichetelor multiple pentru un singur calcul, trecerea executiei de la un case la altul ar trebui folosita cu zgircenie si comentata. Ca o problema de stil puneti un break dupa fiecare ultim case (default in cazul de fata) chiar daca logic nu este necesar. Intr-o zi, cind va trebui sa adaugati in final un alt case acest exces de prudenta in programare va va salva. Exercitiul 3-2. Scrieti o functie escape(s,t) care sa converteasca caractere de felul newline si tab in secvente vizibile escape de forma \n si \t in momentul cind copiazasirulde caractere t in s. Folositi un switch. Scrieti o functie de acelasi gen dar in directie opusa, care sa converteasca secvente escape in caractere reale.

3.5

Bucle de ciclare while si for

Deja am intilnit buclele while si for. In: while (expresie) instructiune mai intii este calculata expresia; daca ea este diferita de zero, este executata instructiunea apoi expresia este calculata din nou. Ciclul se repeta pina ce expresia devine zero, in care caz executia se reia dupa instructiune. Instructiunea for: for (expr1; expr2; expr3) instructiune este echivalenta cu: expr1; while (expr2) { instructiune expr3; } cu exceptia comportarii lui continue, care este descris in Sectiunea 3.7. Gramatical, cele trei componente ale unei bucle for sint expresii. Cel mai obisnuit, expr1 si expr3 sint atribuiri sau apeluri de functii iar expr2 este o expresie relationala. Oricare dintre cele trei parti poate fi omisa, desi cele doua caractere ; trebuie neaparat sa ramina. Daca expr1 sau expr3 este omisa, ea lipseste pur si simplu din dezvoltare. Daca testul expr2 lipseste el este considerat, in mod permanent, drept adevar, astfel ca: for (;;) { ... } este o bucla de ciclare "infinita", presupus a fi sparta prin alte mijloace, cum ar fi un break sau un return. Alegerea lui while sau for este, in mare masura, o problema de preferinta personala. De exemplu, in: while ((c = getchar()) == ' ' || c == '\n' || c == '\t') ; /* skip white space characters */

nu exista nici o initializare sau reinitializare, astfel ca while este cel mai natural. for este preferat cind exista o initializare si o incrementare deoarece el tine instructiunile de control ale ciclarilor grupate si la loc vizibil, in virful buclei de ciclare. Acest lucru este cel mai evident in: for (i = 0; i < n; i++) ... care este un idiom al lui C pentru prelucrarea primelor n elemente dintr-un tablou, analogul lui DO din FORTRAN sau al lui for din Pascal. Analogia nu este perfecta, totusi, deoarece indicele si limita unei bucle for din C pot fi modificate in interiorul buclei iar variabila indiciala i retine valoarea sa cind bucla se termina indiferent de motiv. Deoarece componentele lui for sint expresii arbitrare, buclele for nu sint restrictionate la progresii aritmetice. Cu toate acestea, este un prost obicei de a forta calcule necorelate in initializarea si incrementarea unui for, care, cel mai bine, sint destinate pentru operatiuni de control al buclei de ciclare. Ca un exemplu mai dezvoltat, avem aici o alta versiune a lui atoi pentru convertirea unui sir de caractere cifre in numarul sau echivalent. Aceasta versiune este putin mai generala decit aceea din Cap. 2. Ea trateaza spatiul alb (blank, tab sau newline) optional din fata si semnele optionale + sau -. (Cap. 4 arata pe atof, care face acelasi lucru pentru numere cu punct zecimal mobil). Structura programului reflecta forma intrarii: sare spatiile albe, daca exista obtine semnul, daca exista obtine partea intreaga si o converteste Fiecare pas isi indeplineste sarcina sa si lasa lucrurile intr-o stare curata pentru pasul urmator. Intregul proces se termina pe primul caracter care n-ar putea fi parte a unui numar. #include <ctype.h> /* atoi: convert s to integer; version 2 */ int atoi(char s[]) { int i, n, sign; for (i = 0; isspace(s[i]); i++) ; sign = (s[i] == '-') ? -1 : 1; if (s[i] == '+' || s[i] == '-') i++; for (n = 0; isdigit(s[i]); i++) n = 10 * n + (s[i] - '0'); return sign * n; /* skip white space */ /* skip sign */

Biblioteca standard furnizeaza o functie mai elaborata strtol pentru conversia sirurilor in intregi lungi; vezi Sectiunea 5 din Apendicele B . Avantajele tinerii controlului buclei, centralizat, este chiar mai evident cind exista citeva bucle de ciclare cuibarite. Functia urmatoare este o sortare Shell pentru sortarea unui tablou de intregi. Ideea de baza a acestui algoritm de sortare, care a fost inventata in 1959 de catre D. L. Shell, consta in aceea ca, in fazele de inceput se compara elementele mai indepartate intre ele si nu cele alaturate ca in sortarile cu interschimburi mai simple. Aceasta tinde sa elimine o mare cantitate de dezordine in mod rapid, astfel ca in fazele ulterioare sa avem mai putina munca de facut. Intervalul dintre elementele comparate descreste succesiv pina la unitate, in care punct sortarea devine in mod efectiv o metoda de interschimburi adiacente. /* shellsort: sort v[0]...v[n-1] into increasing order */ void shellsort(int v[], int n) {

int gap, i, j, temp; for (gap = n / 2; gap > 0; gap /= 2) for (i = gap; i < n; i++) for (j=i-gap;j>=0 && v[j]>v[j+gap]; j-=gap) { temp = v[j]; v[j] = v[j+gap]; v[j+gap] = temp; } } Exista trei bucle de ciclare cuibarite. Cea mai exterioara controleaza gap-ul (intervalul) dintre elementele comparate, miscsorindu-l de la n/2, printr-o impartire succesiva cu 2, pina ce devine zero. Bucla mijlocie paseste de-a lungul elementelor. Bucla cea mai interioara compara fiecare pereche de elemente care este separata prin gap si face interschimbul daca nu este in ordine. Deoarece gap-ul este, eventual, redus la unitate, toate elementele sint, eventual, corect ordonate. De notat cum generalitatea lui for face ca bucla exterioara sa aiba aceeasi forma cu celelalte, chiar daca ea nu este o progresie aritmetica. Un operator final in C este virgula ",", care, cel mai adesea, isi gaseste utilizare in instructiunea for. O pereche de expresii, separate printr-o virgula, este calculata de la stinga la dreapta si tipul si valoarea rezultatului sint de tipul si valoarea operandului din dreapta. Astfel, intr-o instructiune for este posibil sa se plaseze expresii multiple in diferite parti, de exemplu pentru prelucrarea a doi indici in paralel. Aceasta se ilustreaza in functia reverse(s), care inverseaza sirul s pe loc. #include <string.h> /* reverse: reverse string s in place */ void reverse(char s[]) { int c, i, j; for (i = 0, j = strlen(s)-1; i < j; i++, j--) { c = s[i]; s[i] = s[j]; s[j] = c; }

Virgulele care separa argumentele unei functii, variabilele in declaratii, etc., nu sint operatori virgula si nu garanteaza calculele de la stinga la dreapta. Operatorii virgula ar trebui folositi cu zgircenie. Cele mai potrivite utilizari sint pentru constructii puternic legate una de alta precum in cazul buclei de ciclare for din reverse si in macrouri unde un calcul cu mai multi pasi trebuie sa fie o singura expresie. O expresie cu operatori virgula ar putea fi, de asemenea, potrivita pentru schimbul de elemente din reverse, unde schimbul poate fi gindit cu o singura operatie: for (i = 0, j = strlen(s)-1; i < j; i++, j--) c = s[i], s[i] = s[j], s[j] = c; Exercitiul 3-3. Scrieti o functie expand(s1,s2) care extinde notatiile stenografice de forma a-z din sirul s1 intr-o lista echivalenta completa abc...xyz in sirul s2. Sa fie permise litere, mari sau mici, cifre si sa fie pregatita de a manipula constructii de forma a-b-c si a-z0-9 si -a-z. Sa se aranjeze ca o liniuta "-" pusa la inceput sau la sfirsit sa fie luata literal.

3.6

Bucle de ciclare do-while

Dupa cum s-a discutat in Cap. 1, buclele while si for testeaza conditia de terminare la inceputul buclei. Dimpotriva, cea de a treia bucla din C, do-while, testeaza la sfirsit, dupa fiecare traversare a corpului buclei; corpul este intodeauna executat cel putin o data. Sintaxa lui do este: do instructiune while (expresie); Instructiunea este executata si apoi este calculata expresia. Daca este adevarata, instructiunea este executata din nou s.a.m.d. Cind expresia devine falsa, ciclarea se incheie. Cu exceptia sensului testului, do-while este echivalent cu instructiunea repeat-until din Pascal. Experienta arata ca do-while este mult mai putin utilizat decit while si for. Cu toate acestea, din cind in cind, el este valoros precum in functia itoa, care converteste un numar intr-un sir de caractere (inversa lui atoi). Treaba este usor mai complicata decit s-ar putea gindi la inceput, deoarece metodele usoare de generare a cifrelor le genereaza in ordine inversa. Am ales generarea sirului de cifre in ordine inversa si apoi inversarea lui. /* itoa: convert n to characters in s */ void itoa(int n, char s[]) { int i, sign; if ((sign = n) < 0) /* record sign */ n = -n; /* make n positive */ i = 0; do { /* generate digits in reverse order */ s[i++] = n % 10 + '0'; /* get next digit */ } while ((n /= 10) > 0); /* delete it */ if (sign < 0) s[i++] = '-'; s[i] = '\0'; reverse(s); } Do-while este necesar, sau cel putin convenabil, deoarece cel putin un caracter trebuie neaparat instalat in tabloul s, chiar daca n este zero. De asemenea am utilizat acoladele in jurul singurei instructiuni care mobileaza corpul lui dowhile, chiar daca ele nu sint necesare, astfel incit cititorul grabit sa nu confunde partea while cu inceputul unei bucle while. Exercitiul 3-4. Intr-o reprezentare a unui numar prin complementul lui 2, versiunea noastra a lui itoa nu prelucreaza cel mai mare numar negativ, adica valoarea n = -2 la puterea (marimea cuvintului - 1). Explicati de ce ! Modificati programul pentru ca sa tipareasca corect aceasta valoare, indiferent de masina pe care ruleaza. Exercitiul3-5. Scrieti functia itob(n,s,b) care sa converteasca intregul n intr-o baza b reprezentat prin caractere in sirul s. In particular, itob(n,s,16) formateaza pe n ca un intreg hexazecimal in s. Exercitiul 3-6. Scrieti o versiune a lui itoa care accepta trei argumente in loc de doua. Cel de al treilea argument este o marime minima de cimp; numarul convertittrebuie neaparat completat cu blancuri la stinga, daca este necesar pentru a avea marimea minima prescrisa.

3.7

Break si continue

Uneori este comod de a fi in masura sa iesim dintr-o bucla altfel decit prin testarea de la inceput sau de la sfirsit. Instructiunea break asigura o iesire timpurie din for, while si do intocmai ca la switch. Un break provoaca o iesire imediata din cea mai interioara bucla de ciclare sau din switch. Urmatoarea functie, trim, inlatura ultimele blancuri, tab-uri si newline-uri de la sfirsitul unui sir, folosind un break pentru a iesi dintr-o bucla de ciclare in momentul intilnirii celui mai din dreapta caracter non-blanc sau nontab sau non-newline. /* trim: remove trailling blanks, tabs, newlines */ int trim(char s[]) { int n; for (n = strlen(s)-1; n >= 0; n--) if (s[n] != ' ' && s[n] != '\t' && s[n] != '\n') break; s[n+1] = '\0'; return n; } strlen returneaza lungimea unui sir. Bucla for incepe la sfirsitul sirului si exploreaza inapoi cautind primul caracter care nu este nici blanc, nici tab si nici newline. Ciclarea este intrerupta cind a fost gasit un prim astfel de caracter sau cind n devine negativ (adica daca intregul sir a fost explorat). Ar trebui verificat ca functia are o comportare corecta cind sirul este vid sau contine numai spatii albe (blanc, tab, sau newline). Instructiunea continue este legata de break, dar mai putin utilizata; ea provoaca inceperea urmatoarei iteratii pentru ciclul for, while sau do care o cuprinde. In while si do, aceasta inseamna ca partea de test este executata imediat; in for, controlul trece la pasul de incrementare. O instructiune continue se aplica numai buclelor de ciclare, nu si lui switch. Un continue in interiorul unui switch care se gaseste in interiorul unei bucle de ciclare provoaca inceperea urmatoarei iteratii. Ca exemplu, acest fragment prelucreaza numai elementele non- negative din sirul a; valorile negative sint ignorate: for (i = 0; i< n; i++) { if (a[i] < 0) /* skip negative elements */ continue; ... /* do positive elements */ } Instructiunea continue este adesea utilizata cind partea din bucla care urmeaza este complicata, astfel ca, inversind un test si indentind un alt nivel, am cuibari programul prea in adincime.

3.8 Goto si etichete. C furnizeaza instructiunea goto folosita in mod cu totul abuziv. In mod formal, goto nu este niciodata necesara si, in practica, este aproape intodeauna usor de a scrie un program fara ea. Noi nu am utilizat goto in aceasta carte. Cu toate acestea, exista putine situatii unde goto-urile isi pot gasi locul. Cel mai obisnuit este abandonul prelucrarii in vreo structura cuibarita prea adinc: cum ar fi intreruperea si parasirea dintr-o data a doua sau mai multe bucle de ciclare. Instructiunea break nu poate fi utilizata in mod direct deoarece ea realizeaza iesirea numai din bucla cea mai interioara. Astfel:

for (...) for(...) { ... if (disaster) goto error; } ... error: descurca lucrurile Aceasta organizare este usor de minuit daca codul de tratare a erorii nu este banal si daca erorile pot aparea in citeva locuri. O eticheta are aceasi forma ca numele de variabila si este urmata de caracterul : (doua puncte). Ea poate fi atasata oricarei instructiuni din aceasi functie in care se gaseste goto-ul respectiv. Domeniul unei etichete este intreaga functie. Intr-un alt exemplu, sa consideram problema determinarii daca doua siruri a si b au un element comun. O posibilitate este:

for (i = 0; i < n; i++) for (j = 0; j < n; j++) if (a[i] == b[j]) goto found; /* didn't find any common element */ ... found: /* got one: a[i] == b[j] */ ... Codul ce implica un goto poate intodeauna fi scris fara el, desi probabil cu pretul unor teste repetate sau a unei variabile suplimentare. De exemplu, cautarea in sir devine: found=0; for (i = 0; i < n && !found; i++) for (j = 0; j < m && !found; j++) if (a[i] == b[j]) found = 1; if (found) /* got one: a[i-1] == b[j-1] */ ... else /* didn't find any common element */ ... Cu putine exceptii, ca acelea citate aici, codul care se sprijina pe instructiuni goto, in general, este mai greu de inteles si de intretinut decit codul fara goto-uri. Desi nu sintem dogmatici pe aceasta tema, se pare ca instructiunile goto ar trebui folosite cit mai rar daca nu de loc.