Biologie | Chimie | Didactica | Fizica | Geografie | Informatica | |
Istorie | Literatura | Matematica | Psihologie |
EXPRESII SI INSTRUCTIUNI
C++ are un set mic, dar flexibil, de tipuri de instructiuni pentru controlul programului si un set bogat de operatori pentru manipularea datelor. Un singur exemplu complex introduce cele mai frecvente facilitati utilizate. Dupa aceea sint rezumate expresiile si conversiile explicite de tip si este prezentata in detaliu utilizarea memoriei libere. Apoi sint rezumate instructiunile, iar in final se discuta stilul de decalare si comentare a textului.
Un calculator de birou
Instructiunile si expresiile se introduc prin prezentarea programului calculatorului de birou care furnizeaza cele patru operatii aritmetice standard ca operatori infix asupra numerelor flotante. Utilizatorul poate, de asemenea, defini variabile. De exemplu, dindu-se intrarea:
r = 2.5
area = pi * r * r
(pi este predefinit), programul calculator va scrie:
unde 2.5 este rezultatul primei linii de intrare, iar 19.635 este rezultatul celei de a doua. Calculatorul consta din patru parti principale: un analizor, o functie de intrare, o tabela de simboluri si un driver. In realitate este un compilator miniatura cu un analizor care face analiza sintactica, functia de intrare realizind intrarea si analiza lexicala, tabela de simboluri pastrind informatia permanenta, iar driverul facind initializarea, iesirea si tratind erorile. Exista multe facilitati care pot fi adaugate la acest calculator pentru a-l face mai util, dar codul este destul de lung intrucit are 200 de linii si cele mai multe facilitati noi ar adauga cod fara a furniza aspecte
noi in utilizarea lui C++.
Analizorul
Iata o gramatica pentru limbajul acceptat de calculator:
program:
END //END este sfirsitul intrarii
expr_list END
expr_list:
expression PRINT //PRINT este 'n' sau ;
expresion PRINT expr_list
expression:
expression + term
expression - term
term
term:
term / primary
term * primary
primary
primary:
NUMBER //numar in flotanta din C++
NAME //nume din C++ fara subliniat
NAME = expression
_primary
(expression)
Cu alte cuvinte, un program este un sir de linii. Fiecare linie consta din una sau mai multe expresii separate prin punct- virgula. Unitatile de baza ale unei expresii sint numere, nume si operatorii *, /, +, - (atit unar cit si binar) si =. Numele nu trebuie sa fie declarate inainte sa fie utilizate. Stilul analizei sintactice utilizate este de obicei numit analiza descendenta recursiva. Este o tehnica top-down directa. Intr-un limbaj cum este C++ in care apelurile de functii sint relativ ieftine, aceasta este o tehnica eficienta. Pentru fiecare productie din gramatica exista o functie care apeleaza alte functii. Simbolurile terminale (de exemplu END, NUMBER, + si -) se recunosc prin analizorul lexical, get_token(), iar simbolurile neterminale sint recunoscute prin functiile analizorului sintactic expr(), term() si prim(). De indata ce ambii operanzi ai unei (sub)expresii sint cunoscuti, ei se evalueaza. Intr-un compilator real se genereaza codul in acest punct.
Analizorul utilizeaza o functie get_token() pentru a obtine o intrare. Valoarea ultimului apel a lui get_token() poate fi gasita in variabila curr_tok. Aceasta este o valoare de enumerare de tip token_value:
enum token_value;
token_value curr_tok;
Fiecare functie a analizorului presupune ca get_token() a fost apelat astfel incit curr_tok sa pastreaze tokenul (lexicul) urmator de analizat. Aceasta permite analizorului sa vada un lexic inainte si obliga fiecare functie a analizorului sa citeasca totdeauna un lexic in plus fata de cele pe care le utilizeaza productia pe care o trateaza ea. Fiecare functie a analizorului evalueaza expresia ei si returneaza o valoare. Functia expr() trateaza adunarea si scaderea. Ea consta dintr-un singur ciclu care cauta termeni de adunat sau scazut:
double expr()
}
Aceasta functie in realitate nu face ea insasi foarte mult. Intr-o maniera tipica pentru functii de nivel mai inalt dintr-un program mare, ea apeleaza alte functii pentru a face 'greul'. Sa observam ca o expresie de forma 2 - 3 + 4 se evalueaza ca (2 - 3) + 4, asa cum se specifica in gramatica.
Notatia curioasa for(;;) este modul standard de a specifica un ciclu infinit. O alternativa este while(1). Instructiunea switch se executa repetat pina cind nu se mai gaseste + sau - si in acest caz se executa instructiunea return din default.
Operatorii += si -= se utilizeaza pentru a trata adunarea si scaderea.
left = left + term();
left = left - term();
ar putea fi utilizate fara a schimba intelesul programului. Cu toate acestea
left += term();
left -= term();
sint nu numai mai scurte, dar exprima direct operatia intentionata. Pentru un operator binar @, o expresie x @= y inseamna x = x @ y si se aplica la operatorii binari:
+ - * / % & | ^ << >>
asa ca sint posibili urmatorii operatori de atribuire:
= += -= *= /= %= &= |= ^= <<= >>=
Fiecare este un lexic separat, asa ca a + = 1; este o eroare din cauza spatiului dintre + si =. (% este modulo sau restul impartirii, &, |, ^ sint operatorii logici pe biti and, or si xor, << si >> sint deplasari stinga si dreapta).
Functiile term() si get_token() trebuie sa fie declarate inainte de expr().
Capitolul patru discuta cum sa se organizeze un program ca un set de fisiere. Cu o singura exceptie, declaratiile pentru acest exemplu de calculator de birou pot fi ordonate in asa fel incit fiecare este declarata exact o data inainte de a fi utilizata. Exceptie face expr(), care apeleaza term(), care apeleaza prim(), care la rindul ei apeleaza expr(). Acest ciclu trebuie sa fie intrerupt cumva. O declaratie:
double expr(); inaintea definitiei lui prim() va fi nimerita.
Functia term() trateaza inmultirea si impartirea:
double term() //inmultire si impartire
}
Testul pentru a ne asigura ca nu se face impartirea prin zero este necesar deoarece rezultatul in acest caz nu este definit. Functia error(char*) este descrisa mai tirziu. Variabila d este introdusa in program acolo unde este nevoie de ea si este initializata imediat. In multe limbaje, o declaratie poate apare numai in antetul unui bloc. Aceasta restrictie poate conduce la erori. Foarte frecvent o variabila locala neinitializata este pur si simplu o indicatie de un stil rau. Exceptii sint variabilele care se initializeaza prin operatii de intrare si variabilele de tip vector sau structura care nu pot fi initializate convenabil printr-o atribuire simpla. Sa observam ca = este operatorul de asignare, iar == este operatorul de comparare.
Functia prim() trateaza un primar; deoarece este la un nivel mai inferior in ierarhia de apeluri, ea face un pic mai multa 'munca' si nu mai este necesar sa cicleze.
double prim()
return look(name_string)->value;
case MINUS : get_token(); //minus unar
return _prim();
case LP : get_token();
double e = expr();
if(curr_tok != RP)
return error(') expected'); get_token(); return e;
case END : return 1;
default : return error('primary expected');
}
}
Cind se4 gaseste un NUMBER (adica o
In acelasi mod in care valoarea ultimului NUMBER intilnit este tinut in number_value, reprezentarea sirului de caractere a ultimului NAME intilnit este tinut in name_string. Inainte de a face ceva unui nume, inainte calculatorul trebuie sa vada daca el este asignat sau numai utilizat. In ambele cazuri se consulta tabela de simboluri. Tabela este prezentata in &1. Aici trebuie sa observam ca ea contine intrari de forma:
struct name;
unde next se utilizeaza numai de functiile care mentin tabela:
name* look(char*); name* insert(char*);
Ambele returneaza un pointer la un nume care corespunde la parametrul sir de caractere. Functia look() semnaleaza daca numele nu a fost definit. Aceasta inseamna ca in calculator un nume poate fi utilizat fara o declaratie prealabila, dar prima lui utilizare trebuie sa fie partea stinga a unei atribuiri.
Functia de intrare
----- ----- --------
Citirea intrarii este adesea cea mai incurcata parte a unui program. Motivul este faptul ca daca un program trebuie sa comunice cu o persoana, el trebuie sa invinga capriciile, conventiile si erorile unei persoane sau a mai multora. Incercarea de a forta persoana sa se comporte intr-o maniera mai convenabila pentru masina este adesea, pe drept cuvint, considerata ofensiva.
Sarcina unei rutine de intrare de nivel inferior este de a citi caractere unul dupa altul si sa compuna unitati de nivel mai inalt. Aici intrarea de nivel inferior se face cu get_token().
Regulile pentru intrarile in calculator au fost deliberat alese asa ca sa fie ceva incomod pentru sirul de functii care le manevreaza. Modificari neimportante in definitiile unitatilor ar face pe get_token() foarte simpla.
Prima problema este aceea ca, caracterul newline 'n' este semnificativ pentru calculator, dar sirul de functii de intrare il considera un caracter whitespace. Adica, pentru acele functii, 'n' este un terminator de unitate lexicala. Pentru a invinge aceasta trebuie examinate spatiile albe (spaces, tab, etc):
char ch;
dowhile(ch!='n' && isspace(ch));
Apelul cin.get(ch) citeste un singur caracter din sirul de la intrarea standard in ch. Testul if(!cin.get(ch)) esueaza daca nici un caracter nu poate fi citit de la intrare (din cin). In acest caz se returneaza END pentru a termina sesiunea de calcul. Operatorul ! (not) se utilizeaza intrucit get() returneaza o valoare nenula in caz de succes. Functia isspace() din <ctype.h>
furnizeaza testul standard pentru spatiu alb (&8.4.1). Functia isspace(c) returneaza o valoare nenula daca c este un caracter alb, zero altfel. Testul este implementat ca o tabela de cautare, astfel, utilizind isspace este mai rapid decit daca s-ar testa individual caracterele spatiu alb. Acelasi lucru se aplica la functiile isalpha(), isdigit() si isalnum() utilizate in get_token().
Dupa ce s-a facut avans peste caracterele albe, se utilizeaza caracterul urmator pentru a determina ce fel de unitate lexicala incepe in sirul de intrare. Sa ne oprim la niste cazuri separate inainte de a prezenta functia completa. Expresiile terminatoare 'n' si ';' sint tratate astfel:
switch(ch)
Saltul peste caractere albe (din nou) nu este necesar, dar daca ar trebui s-ar repeta apeluri ale lui get_token(). WS este un obiect de spatiu alb declarat in <stream.h>. El este utilizat numai pentru a indeparta spatiile albe. O eroare la intrare sau la sfirsitul intrarii nu va fi detectata pina la apelul urmator a lui get_token().
Sa observam modul in care diferite etichete ale lui case pot fi utilizate pentru un singur sir de instructiuni care trateaza acele cazuri. Se returneaza unitatea PRINT si se pune in curr_tok in ambele cazuri. Numerele se trateaza astfel:
case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case '.': cin.putback(ch);
cin >> number_value; return curr_tok = NUMBER;
Scrierea etichetelor orizontal in loc de vertical,
in general, nu este o idee buna deoarece este mai greu de citit, dar nu este
nimerit in cazul de fata sa avem o linie pentru fiecare cifra. Deoarece
operatorul >> este deja definit pentru a citi constante in virgula
flotanta dubla precizie, codul este trivial. Intii caracterul initial (o cifra
sau un punct) se pune inapoi in cin si apoi
Un nume, care este un lexic de tip NAME, este definit ca o litera care este posibil sa fie urmata de litere sau cifre:
if(isalpha(ch))
Aceasta construieste un sir terminat cu zero in name_string. Functiile isalpha() si isalnum() sint furnizate in <ctype.h>, isalnum(c) este diferit de zero daca c este o litera sau o cifra si zero altfel.
Iata in final functia de intrare completa:
token_value get_token()
while(ch != 'n' && isspace(ch)); switch(ch)
error('bad token'); return curr_tok = PRINT;
}
}
Intrucit token_value al unui operator a fost definit prin valoarea intreaga a operatorului, toate alternativele (case) pentru operator se trateaza trivial.
Tabela de simboluri
O singura functie are acces la tabela de simboluri:
name* look(char* p, int ins = 0);
Cel de al doilea argument indica daca sirul de caractere presupus trebuie sa fie in prealabil inserat sau nu.
Folosirea argumentului implicit da pentru look posibilitatea convenabila de a scrie look('sqrt2') in loc de look('sqrt2', 0), adica se doreste cautare, nu inserare. Pentru a obtine notatii convenabile pentru inserare se defineste o a doua functie:
inline name* insert(char* s)
Asa cum s-a mentionat inainte, intrarile in tabela sint de forma:
struct name;
Elementul next se utilizeaza pentru a face inlantuirea numerelor in tabela. Tabela insasi este pur si simplu un vector de pointeri spre obiecte de tip nume:
const TBLSZ = 23;
name* table[TBLSZ];
Deoarece toate obiectele statice sint implicit initializate cu zero, aceasta declaratie triviala a lui table asigura de asemenea si initializarea. Pentru a gasi o intrare pentru un nume din tabela, look() utilizeaza un cod hash simplu (numele cu acelasi cod hash se inlantuie):
int ii = 0;
char* pp = p;
while(*pp)
ii = ii << 1 ^ *p++; if(ii < 0)
ii = -ii; ii %= TBLSZ;
Fiecare caracter din sirul de intrare p este 'adaugat' la ii ('suma' caracterelor precedente) printr-un sau exclusiv. Un bit din x^y este setat daca si numai daca bitii corespunzatori din operanzii x si y sint diferiti. Inainte de a face un sau exclusiv, ii se deplaseaza cu un bit in stinga pentru a elimina utilizarea numai a unui octet din el. Aceasta se poate exprima astfel:
ii <<= 1;
ii ^= *pp++;
Utilizarea lui ^ este mai rapida decit a lui +. Deplasarea este esentiala pentru a obtine un cod hash rezonabil in ambele cazuri. Instructiunile:
if(ii < 0)
ii = -ii; ii %= TBLSZ;
asigura ca ii sa fie in domeniul 0 TBLSZ - 1, (% este opera torul modulo, numit si rest).
Iata functia completa:
extern int strlen(const char*);
extern int strcmp(const char*, const char*);
extern char* strcpy(const char*, const char*);
name* look(char* p, int ins = 0)
Dupa ce codul hash a fost calculat in ii, numele este gasit printr-o cautare simpla prin intermediul cimpurilor next. Fiecare nume este verificat folosind functia strcmp() de comparare a sirurilor. Daca este gasit, se returneaza numele lui; altfel se adauga un nume nou.
Adaugarea unui nume implica crearea unui obiect cu numele nou intr-o zona de memorie libera folosind operatorul new (vezi &2.6), initializarea si adaugarea lui la lista de nume. Adaugarea se face punind noul nume in capul listei deoarece aceasta se poate face fara a testa daca exista sau nu o lista. Sirul de caractere care alcatuieste numele trebuie si el pastrat intr-o zona libera. Functia strlen() se foloseste pentru a gasi cit de multa memorie este necesara, operatorul new pentru a aloca memorie, iar functia strcpy() pentru a copia sirul in zona respectiva.
Tratarea erorilor
Intrucit programul este atit de simplu, tratarea erorilor nu este o preocupare majora. Functia eroare pur si simplu numara erorile, scrie un mesaj de eroare si returneaza:
int no_of_errors;
double error(char* s)
Motivul pentru care se returneaza o valoare este faptul ca erorile de obicei apar in mijlocul evaluarii unei expresii, asa ca ar trebui sau sa se faca un abandon al acelei evaluari sau sa se returneze o valoare care in continuare sa fie putin probabil sa cauzeze erori. Ultima varianta este adecvata pentru acest calculator simplu. Daca get_token() ar tine numerele de linie, error() ar putea informa utilizatorul aproximativ asupra locului unde a aparut eroarea. Aceasta ar fi util la o folosire interactiva a calculatorului. Adesea un program trebuie sa fie terminat dupa o eroare deoarece nu
exista o cale adecvata care sa permita continuarea executiei. Acest lucru se poate face apelind functia exit(), care la inceput videaza lucrurile de tipul fisierelor de iesire (&8.2) dupa care se termina programul iar valoarea returnata de el este argumentul lui exit(). Un mod mai drastic de terminare a programului este apelul lui abort() care termina imediat sau imediat dupa pastrarea undeva a informatiei pentru debugger (vidaj de memorie).
Driverul
Cu toate bucatile programului construite noi avem nevoie numai de un driver care sa initializeze si sa porneasca tot procesul. In acest exemplu simplu functia main() poate fi construita astfel:
int main() //insereaza nume predefinite
return no_of_errors;
}
Prin conventie, main() returneaza zero daca programul se termina normal si altfel, o valoare diferita de zero, asa ca returnarea numarului de erori se potriveste bine cu aceasta conventie. Aici singurele initializari sint numerele predefinite pentru 'pi' si 'e' care se insereaza in tabela de simboluri.
Sarcina primordiala a ciclului principal este sa citeasca expresii si sa scrie raspunsul. Aceasta se obtine prin linia:
cout << expr() << 'n';
Testind pe cin la fiecare pas al ciclului se asigura ca programul sa se termine daca ceva merge rau in sirul de intrare iar testul pentru END asigura ca ciclul sa se termine corect cind get_token() intilneste sfirsitul de fisier. O instructiune break provoaca iesirea din instructiunea switch sau din ciclul care o contine (adica o instructiune for, while sau do). Testul pentru PRINT (adica pentru 'n' si ';') elibereaza pe expr() de necesitatea de a prelucra expresii vide. O instructiune continue este echivalenta cu trecerea la sfirsitul ciclului, asa ca in acest caz:
while(cin)
este echivalent cu :
while(cin)
(ciclurile se descriu in detaliu in &r9).
Argumentele liniei de comanda
Dupa ce programul a fost scris si testat, am observat ca tastarea expresiilor la intrarea standard a fost adesea mai mult decit necesar, deoarece in mod frecvent a trebuit sa se evalueze o singura expresie. Daca este posibil ca aceasta expresie sa fie prezentata ca un argument al liniei de comanda, atunci multe accese cheie ar fi fost eliminate. Asa cum s-a mentionat in prealabil, un program incepe prin apelul lui main(). Cind aceasta s-a facut, main() primeste doua argumente, care specifica numarul de argumente si care de obicei se numeste argc si un vector de argumente, care de obicei se numeste argv. Argumentele sint siruri de caractere, asa ca tipul lui argv este char *[argc]. Numele unui program (intrucit el apare pe linia de comanda) se paseaza ca argv[0], asa ca argc este intotdeauna cel putin 1. De exemplu, pentru comanda:
dc 150/1.1934 argumentele au aceste valori:
argc 2
argv[0] 'dc'
argv[1] '150/1.1934'
Nu este dificil sa fie pastrata linia de comanda ca argument. Problema este cum sa se foloseasca fara a face reprogramare. In acest caz, este trivial intrucit un sir de intrare poate fi limitat la un sir de caractere in loc de un fisier (&8.5). De exemplu, cin poate fi facut sa citeasca caractere dintr-un sir in loc de intrarea standard:
int main(int argc, char* argv[])
// ca inainte
}
Programul este neschimbat exceptind adaugarea argumentelor la main() si utilizarea lor in instructiunea switch. S-ar putea usor modifica main() pentru a accepta diferite argumente in linia de comanda, dar acest lucru nu este necesar, deoarece diferite expresii pot fi pasate ca un singur argument:
dc 'rate=1.1934;150/rate;19.75/rate217/rate'
Ghilimelele sint necesare aici din cauza ca ';' este separator de comenzi in sistemul UNIX.
Sumar de operatori
Operatorii C++ sint descrisi sistematic si complet in &r7. Aici, este un sumar al lor si niste exemple. Fiecare operator este urmat de unul sau mai multe nume utilizate in comun pentru el si de un exemplu de utilizare a lui. In aceste exemple class_name este numele unei clase, member este un nume al unui membru, un object este o expresie care produce un obiect, un pointer este o expresie care produce un pointer, o expr este o expresie, iar o lvalue este o expresie ce noteaza un obiect neconstant. Un type poate fi un nume de tip general complet (cu *, (), etc.) numai cind el apare in paranteze. Altfel exista restrictii.
Operatorii unari si operatorii de atribuire se asociaza de la dreapta; toti ceilalti se asociaza de la stinga.
Adica
a = b = c inseamna a = (b = c),
a + b + c inseamna (a + b) + c,
iar
*p++ inseamna *(p++), nu (*p)++.
SUMAR DE OPERATORI |
domeniu de existenta class_name::member |
global ::name |
| -> selectare de membru pointer->member |
indexare pointer[expr] |
apel de functie expr(expr_list) |
constructie de valoare type(expr_list) |
| sizeof dimensiunea unui obiect sizeof expr |
| sizeof dimensiunea unui tip sizeof(type) |
increment postfixat lvalue++ |
increment prefixat ++lvalue |
decrement postfixat lvalue-- |
decrement prefixat --lvalue |
complement ~expr |
negare !expr |
minus unar -expr |
| + plus unar +expr |
| & adresa &lvalue |
indirectare *expr |
| new creaza(aloca) new type |
| delete distruge(dealoca) delete pointer |
| delete[] distruge un vector delete[expr]pointer|
| (type) conversie de tip (type)expr |
inmultire expr * expr |
impartire expr / expr |
modulo(rest) expr % expr |
adunare(plus) expr + expr |
scadere(minus) expr - expr |
| << deplasare stinga expr << expr |
| >> deplasare dreapta expr >> expr |
| < mai mic expr < expr |
| <= mai mic sau egal expr <= expr |
| > mai mare expr > expr |
| >= mai mare sau egal expr >= expr |
egal expr == expr |
diferit expr != expr |
| & si pe biti expr & expr |
| ^ sau exclusiv pe biti expr ^ expr |
| | sau pe biti expr | expr |
| && si logic expr && expr |
sau logic expr || expr |
| ? : if aritmetic expr ? expr : expr |
asignare simpla lvalue = expr |
inmultire si asignare lvalue *= expr |
impartire si asignare lvalue /= expr |
modulo si asignare lvalue %= expr |
adunare si asignare lvalue += expr |
scadere si asignare lvalue -= expr |
| <<= deplasare stinga si asignare lvalue <<= expr |
| >>= deplasare dreapta si asignare lvalue >>= expr |
| &= si pe biti si asignare lvalue &= expr |
| |= sau pe biti si asignare lvalue |= expr |
| ^= sau exclusiv pe biti si asignare lvalue ^= expr |
virgula(succesiune) expr, expr |
Fiecare dreptunghi contine operatori cu aceeasi prioritate. Un operator are o prioritate mai mare decit operatorii aflati in dreptunghiuri inferioare.
De exemplu:
a + b * c
inseamna
a + (b * c)
deoarece * are prioritate mai mare decit +, iar a + b - c inseamna (a + b) - c deoarece + si - au aceeasi prioritate, dar operatorii + si - sint asociati de la stinga spre dreapta.
Paranteze rotunde
Parantezele rotunde sint suprasolicitate in sintaxa lui C++. Ele au un numar mare de utilizari: includ argumentele in apelurile de functii, include tipul intr-o conversie de tip, includ nume de tipuri pentru a nota functii si, de asemenea, pentru a rezolva conflictul prioritatilor intr-o expresie. Din fericire, ultimul caz nu este necesar foarte frecvent deoarece regulile cu nivelele de prioritate si de asociativitate sint astfel definite ca expresiile sa 'functioneze' asa cum ne asteptam (adica sa re flecte utilizarile cele mai frecvente). De exemplu:
if(i <= 0 || max < i)
are intelesul obisnuit. Cu toate acestea, parantezele ar trebui utilizate ori de cite ori un programator este in dubiu despre acele reguli:
if((i <= 0)||(max < i))
//.
Utilizarea parantezelor este mai frecventa cind subexpresiile sint mai complicate; dar subexpresiile complicate sint o sursa de erori, asa ca daca simtim nevoia de a folosii paranteze am putea sa descompunem expresiile utilizind variabile auxiliare. Exista, de asemenea, cazuri cind prioritatea operatorilor nu conduce la o interpretare 'evidenta'. De exemplu:
if(i & mask == 0)
//.
nu aplica o masca la i si apoi testeaza daca rezultatul este zero. Intrucit == are o prioritate mai mare decit &, expresia este interpretata ca: i & (mask == 0). In acest caz parantezele sint importante:
if((i & mask) == 0)
//.
De asemenea, poate fi util sa observam ca secventa de mai jos nu functioneaza in modul in care s-ar astepta un utilizator naiv:
if(0 <= a <= 99)
//
este legal, dar se interpreteaza ca:
(0 <= a) <= 99
si rezultatul primei comparatii este 0 sau 1 si nu a (daca a diferit de 1). Pentru a testa daca a este in domeniul 0..99 se poate folosi:
if(0 <= a && a <= 99)
//.
Ordinea de evaluare
Ordinea de evaluare a subexpresiilor intr-o expresie este nedefinita. De exemplu:
int i = 1;
v[i] = i++;
poate fi evaluata sau ca v[1] = 1, sau ca v[2] = 1. Un cod mai bun se poate genera in absenta restrictiilor asupra ordinii de evaluare a expresiilor. Ar fi mai bine daca compilatorul ne-ar avertiza despre astfel de ambiguitati;majoritatea compilatoarelor nu fac acest lucru. Operatorii && si || garanteaza faptul ca operandul lor sting se evalueaza inaintea celui drept. De exemplu, b = (a = 2, a + 1) atribuie lui b valoarea
Exemple de utilizare a lui && si || se dau in paragraful &1. Sa observam ca operatorul virgula este logic diferit de virgula folosita pentru a separa argumente intr-un apel de functie. Sa consideram:
f1(v[i], i++); //doua argumente
f2((v[i], i++)); //un argument
Apelul lui f1 are doua argumente, v[i] si i++, iar ordinea de evaluare a expresiilor argument este nedefinita. Ordinea de evaluare a expresiilor argument este neportabila si nu este precizata. Apelul lui f2 are un singur argument si anume expresia (v[i], i++). Parantezele nu pot fi utilizate pentru a forta ordinea de evaluare; a*(b/c) poate fi evaluata ca (a*b)/c deoarece * si / au aceeasi precedenta. Cind ordinea de evaluare este importanta, se pot introduce variabile temporare. De exemplu:
(t = b / c, a * t)
Incrementare si Decrementare
Operatorul ++ se utilizeaza pentru a exprima o incrementare directa in schimbul exprimarii ei folosind o combinatie intre adunare si atribuire. Prin definitie, ++lvalue inseamna: lvalue += 1 care din nou inseamna lvalue = lvalue + 1 cu conditia ca lvalue sa nu aiba efecte secundare. Expresia care noteaza obiectul de incrementat se evalueaza o singura data. Decrementarea este exprimata similar prin operatorul --. Operatorii ++ si -- pot fi utilizati ambii atit prefix cit si postfix. Valoarea lui ++x este noua valoare a lui x (adica cea incrementata). De exemplu y = ++x este echivalent cu y = (x += 1). Valoarea lui x++ este valoarea veche a lui x. De exemplu y=x++ este echivalent cu y = (t=x, x+=1, t), unde t este o variabila de acelasi tip cu x.
Operatorii de incrementare sint utili mai ales pentru a incrementa si decrementa variabile in cicluri. De exemplu se poate copia un sir terminat cu zero astfel:
inline void cpy(char* p, const char* q) Sa ne amintim ca incrementind si decrementind pointeri, ca si adunarea sau scaderea dintr-un pointer, opereaza in termenii elementelor vectorului spre care pointeaza pointerul in cauza; p++ face ca p sa pointeze spre elementul urmator. Pentru un pointer de tip T*, are loc prin definitie:
long(p + 1) == long(p) + sizeof(T);
Operatori logici pe biti
Operatorii logici pe biti &, |, ^, ~, >> si << se aplica la intregi; adica obiecte de tip char, short, int, long si corespunzatoarele lor fara semn (unsigned), iar rezultatele lor sint de asemenea intregi. O utilizare tipica a operatorilor logici pe biti este de a implementa seturi mici (vectori de biti). In acest caz fiecare bit al unui intreg fara semn reprezinta numai un membru al setului, iar numarul de biti limiteaza numarul de membri. Operatorul binar & este interpretat ca intersectie, | ca reuniune si ^ ca diferenta. O enumerare poate fi utilizata pentru a numi membri unui astfel de set. Iata un mic exemplu imprumutat din implementarea (nu interfata utilizator) lui <stream.h>:
enum state_value;
Definirea lui _good nu este necesara. Eu numai am dorit sa existe un nume adecvat pentru starea in care nu sint probleme. Starea unui sir poate fi resetata astfel:
cout.state = _good;
Se poate testa daca un sir a fost deformat sau o operatie a esuat, ca mai jos:
if(cout.state & (_bad | _fail)) //nu este bine
Parantezele sint necesare deoarece & are o precedenta mai mare decit |.
O functie care intilneste sfirsitul intrarii poate sa indice acest lucru astfel: cin.state |= _eof. Se utilizeaza operatorul |= deoarece sirul ar putea fi deformat deja (adica state == _bad) asa ca: cin.state = _eof ar fi sters conditia respectiva. Se poate gasi modul in care difera doua stari astfel:
state_value diff = cin.state ^ cout.state;
Pentru tipul stream_state o astfel de diferenta nu este foarte folositoare, dar pentru alte tipuri similare ea este mai utila. De exemplu, sa consideram compararea unui vector de biti care reprezinta setul de intreruperi de prelucrat cu un altul care reprezinta setul de intreruperi ce asteapta sa fie prelucrat.
Sa observam ca utilizind cimpurile (&2.5.1) se obtine o prescurtare convenabila pentru a deplasa masca si a extrage cimpuri de biti dintr-un cuvint. Aceasta se poate face, evident, utilizind operatorii logici pe biti.
De exemplu, se pot extrage 16 biti din mijlocul unui int de 32 de biti astfel:
unsigned short middle(int a)
Sa nu se faca confuzie intre operatorii logici pe biti cu cei logici &&, || si !. Acestia din urma returneaza sau 0 sau 1 si ei sint in primul rind utili pentru a scrie teste in if, while sau for (&1). De exemplu !0 (negatia lui 0) are valoarea 1, in timp ce ~0 (complementul lui zero) reprezinta valoarea -1 (toti biti sint unu).
Conversia tipului
Uneori este necesar sa se converteasca o valoare de un tip spre o valoare de un alt tip. O conversie de tip explicit produce o valoare de un tip dat pentru o valoare a unui alt tip. De exemplu:
float r = float(1);
converteste valoarea 1 spre valoarea flotanta 1.0 inainte de a face atribuirea. Rezultatul unei conversii de un tip nu este o lvalue deci nu i se poate face o asignare (numai daca tipul este un tip referinta).
Exista doua notatii pentru conversia explicita a tipului: notatia traditionala din C (de exemplu (double)) si notatia functionala (double(a)).
Notatia functionala nu poate fi folosita pentru tipuri care nu au un nume simplu. De exemplu, pentru a converti o valoare spre un pointer se poate folosi notatia din C:
char* p = (char*)0777; sau sa se defineasca un nume de tip nou:
typedef char* pchar; char* p = pchar(0777);
Dupa parerea mea, notatia functionala este preferabila pentru exemple netriviale. Sa consideram aceste doua exemple echivalente:
pname n2 = pbase(n1->tp)->b_name; pname n3 = ((pbase)n2->tp)->b_name;
Intrucit operatorul -> are prioritate mai mare decit (tip), ultima expresie se
interpreteaza astfel:
((pbase)(n2->tp))->b_name
Utilizind explicit conversia de tip asupra tipurilor pointer este posibil sa avem pretentia ca un obiect sa aiba orice tip. De exemplu:
any_type* p = (any_type*)&some_object;
va permite ca some_object sa fie tratat ca any_type prin p.
Cind o conversie de tip nu este necesara ea trebuie eliminata. Programele care utilizeaza multe conversii explicite sint mai greu de inteles decit programele care nu le utilizeaza. Totusi, astfel de programe sint mai usor de inteles decit programele care pur si simplu nu utilizeaza tipuri pentru a reprezenta concepte de nivel mai inalt (de exemplu, un program care opereaza cu un registru de periferic folosind deplasari si mascari de intregi in loc de a defini o structura corespunzatoare si o operatie cu ea; vezi &2.5.2). Mai mult decit atit, corectitudinea unei conversii explicite de tip depinde adesea in mod esential de intelegerea de catre programator a modului in care diferite tipuri de obiecte sint tratate in limbaj si foarte adesea de detaliile de
implementare. De exemplu:
int i = 1; char* pc = 'asdf'; int* pi = &i; i = (int)pc;
pc = (char*)i; // nu se recomanda: pc s-ar putea sa-si
// schimbe valoarea. Pe anumite masini
// sizeof(int) < sizeof(char*)
pi = (int*)pc;
pc = (char*)pi; // nu se recomanda: pc s-ar putea sa-si
// schimbe valoarea. Pe anumite masini
// char* se reprezinta diferit de int*
Pe multe masini nu se va intimpla nimic rau, dar pe altele rezultatul va fi dezastruos. In cel mai bun caz, un astfel de cod nu este portabil. De obicei este gresit sa presupunem ca pointerii la diferite structuri au aceeasi reprezentare. Mai mult decit atit, orice pointer poate fi asignat la un void* (fara un tip explicit de conversie) si un void* poate fi convertit explicit la un pointer de orice tip.
In C++, conversia explicita de tip nu este necesara in multe cazuri in care in C este necesara. In multe programe conversia explicita de tip poate fi complet eliminata, iar in multe alte programe utilizarea ei poate fi localizata in citeva rutine.
Memoria libera
Un obiect denumit este sau static sau automatic (vezi &2.1.3). Un obiect static se aloca cind incepe programul si exista pe durata executiei programului! Un obiect automatic se aloca de fiecare data cind se intra in blocul lui si este eliminat numai cind se iese din bloc. Adesea este util sa se creeze un obiect nou care exista numai cit timp este nevoie de el. In particular, adesea este util sa se creeze un obiect care poate fi utilizat dupa ce se revine dintr-o functie in care el a fost creat. Operatorul new creaza astfel de obiecte, iar operatorul delete poate fi folosit pentru a le distruge mai tirziu. Obiectele alocate prin new se spune ca sint in memoria libera. Astfel de obiecte sint de exemplu nodurile unui arbore sau a unei liste inlantuite care sint parte a unei sructuri de date mai mari a carei dimensiune nu poate fi cunoscuta la compilare.Sa consideram modul in care s-ar putea scrie un compilator in stilul folosit la calculatorul de birou. Functiile de analiza sintactica ar putea construi o reprezentare sub forma de arbore a expresiilor, care sa fie utilizata de generatorul de cod. De exemplu:
struct enode;
enode* expr()
}
Un generator de cod ar putea utiliza arborele rezultat astfel:
void generate(enode* n)
}
Un obiect creat prin new exista pina cind este distrus explicit prin delete dupa care spatiul ocupat de el poate fi reutilizat prin new. Nu exista 'colectarea rezidurilor'. Operatorul delete se poate aplica numai la un pointer returnat de new sau la zero. Aplicarea lui delete la zero nu are nici un efect. Se pot, de asemenea, crea vectori de obiecte prin intermediul lui new. De exemplu:
char* save_string(char* p)
Sa observam ca pentru a dealoca spatiul alocat prin new, delete trebuie sa fie capabil sa determine dimensiunea obiectului alocat. De exemplu:
int main(int argc, char* argv[])
Aceasta implica faptul ca un obiect alocat utilizind implementarea standard prin new va ocupa putin mai mult spatiu decit un obiect static (de obicei un cuvint in plus).
Este de asemenea, posibil sa se specifice dimensiunea unui vector explicit intr-o operatie de stergere. De exemplu:
int main(int argc, char* argv[])
Dimensiunea vectorului furnizata de utilizator se ignora exceptind unele tipuri definite de utilizator (&5.5.5).
Operatorii de memorie libera se implementeaza prin functiile (&r7.2.3):
void* operator new(long);
void operator delete(void*);
Implementarea standard a lui new nu initializeaza obiectul returnat. Ce se intimpla daca new nu gaseste memorie de alocat. Intrucit chiar memoria virtuala este finita, uneori se poate intimpla acest lucru; o cerere de forma:
char* p = new char[100000000];
de obicei va cauza probleme. Cind new esueaza, ea apeleaza functia spre care pointeaza pointerul _new_handler (pointerii spre functii vor fi discutati in &4.6.9). Noi putem seta acel pointer direct sau sa utilizam functia set_new_handler(). De exemplu:
#include <stream.h>
void out_of_store()
typedef void (*PF)(); //pointer spre tipul functiei
extern PF set_new_handler(PF);
main()
de obicei niciodata nu va scrie done dar in schimb va produce:
operator new failed: out of store
Un _new_handler ar putea face ceva mai destept decit pur si simplu sa termine programul. Daca noi stim cum lucreaza new si delete, de exemplu, deoarece noi furnizam operatorii nostri proprii new() si delete(), handlerul ar putea astepta sa gaseasca memorie pentru new. Cu alte cuvinte, un utilizator ar putea furniza un colector de reziduri, redind in utilizare zonele sterse. Aceasta evident nu este o sarcina pentru un incepator. Din motive istorice, new pur si simplu returneaza pointerul 0 daca el nu gaseste destula memorie si nu a fost specificat un _new_handler. De exemplu:
#include <stream.h>
main()
va produce
done, p= 0
Noi am avertizat! Sa observam ca furnizind _new_handler se verifica depasirea memoriei pentru orice utilizare a lui new in program (exceptind cazul cind utilizatorul furnizeaza rutine separate pentru tratarea alocarii obiectelor de tipuri specifice definite de utilizator; vezi &5.5.6).
Sumarul instructiunilor
Instructiunile C++ sint descrise sistematic si complet in &r.9. Cu toate acestea, dam mai jos un rezumat si citeva exemple.
Sintaxa instructiunilor:
statement:
declaration
expression_opt; if(expression) statement if(expression) statement else statement switch(expression) statement while(expression) statement do statement while(expression);
for(statement expression_opt; expression_opt)
statement
case constant_expression: statement
default: statement
break;
continue;
return expression_opt;
goto identifier;
identifier: statement
statement_list:
statement
statement statement_list
Sa observam ca o declaratie este o instructiune si ca nu exista nici o instructiune de atribuire sau de apel; atribuirea si apelul functiei se trateaza ca expresii.
Teste
O valoare poate fi testata sau printr-o instructiune if sau printr-o instructiune switch:
if(expression) statement
if(expression) statement else statement
switch(expression) statement
Nu exista in C++ tipul boolean separat.
Operatorii de comparare == != < <= > >= returneaza valoarea 1 daca compararea este adevarata si 0 altfel. Nu este ceva iesit din comun ca sa consideram ca true se defineste ca 1 si false ca 0.
Intr-o instructiune if se executa prima (sau singura) instructiune daca expresia este diferita de zero si altfel se executa cea de a doua instructiune (daca este prezenta). Aceasta implica faptul ca orice expresie intreaga poate fi utilizata ca o conditie. In particular, daca a este un intreg:
if(a)
//..
este echivalent cu
if(a != 0)
//..
Operatorii logici &&, || si ! sint cei mai utilizati in conditii. Operatorii && si || nu vor evalua cel de al doilea argument al lor numai daca este necesar. De exemplu:
if(p && 1 < p->count)
//..
intii testeaza ca p nu este nul si numai daca este asa se testeaza 1 < p->count.
Anumite instructiuni if simple pot fi inlocuite convenabil inlocuindu-le prin expresii if aritmetice. De exemplu:
if(a <= b)
max = b;
else
max = a;
este mai bine sa fie exprimat prin
max = (a<=b) ? b:a;
Parantezele in jurul conditiei nu sint necesare, dar codul este mai usor de citit cind sint utilizate.
Anumite instructiuni switch simple pot fi scrise prin mai multe instructiuni if. De exemplu:
switch(val)
se poate scrie
if(val==1)
f(); else if(val==2)
g(); else h();
Intelesul este acelasi, dar prima versiune (cu switch) este de preferat din cauza ca natura operatiei (testul unei valori fata de un set de constante) este explicita in acest caz. Aceasta face ca instructiunea switch sa fie mai usor de citit.
Sa avem grija ca un case al unui switch trebuie terminat cumva daca nu dorim ca executia sa continue cu case-ul urmator. De exemplu:
switch(val)
cu val == 1 va imprima
case 1
case 2
default: case not found
spre marea surprindere a neinitiatilor. Cel mai frecvent mod de intrerupere al unui case este terminarea prin break, dar se poate adesea folosi o instructiune return sau goto. De exemplu:
switch(val)
Apelat cu val == 2, produce
case 2
case 1
Sa observam ca o scriere de forma goto case 1; este o eroare sintactica.
Goto
C++ are faimoasa instructiune goto.
goto identifier;
identifier: statement
Are putine utilizari in limbajele de nivel inalt, dar poate fi foarte util cind un program C++ este generat printr-un program in loc ca programul sa fie scris direct de catre o persoana; de exemplu, goto-urile pot fi utilizate intr-un analizor generat dintr-o gramatica printr-un generator de analizoare.
Goto poate fi, de asemenea, important in acele cazuri cind eficienta optimala este esentiala, de exemplu, in ciclul interior al unei aplicatii de timp real.
Una din putinele utilizari bune ale lui goto este iesirea dintr-un ciclu imbricat sau switch (instructiunea break intrerupe numai ciclul sau switch-ul cel mai interior care o contine). De exemplu:
for(int i=0; i<n; i++)
for(int j=0; j<m; j++)
if(nm[i][j] == a)
goto found;
// not found
//..
found:
// nm[i][j] == a;
Exista de asemenea instructiunea continue, care transfera controlul la sfirsitul instructiunii ciclice, asa cum s-a explicat in &1.5.
Comentarii si Decalari
Utilizarea judicioasa a comentariilor si utilizarea consistenta a decalarilor poate face sarcina citirii si intelegerii unui program mai placuta. Exista diferite stiluri ale decalarilor. Autorul nu vede motive fundamentale pentru a prefera un stil fata de altul (deci, ca multi altii, eu am preferintele mele). Acelasi lucru se aplica si la stilurile de comentare.
Comentariile pot fi omise, dar atunci citirea programului va fi serios afectata. Compilatorul nu intelege continutul unui comentariu, asa ca nu exista nici o cale de a ne asigura ca un comentariu:
[1] este de neinteles;
[2] descrie programul;
[3] este pus la zi.
Multe programe contin comentarii care sint incomprehensibile, ambigue si chiar eronate. Comentariile rele pot fi mai rele decit daca nu ar exista. Daca ceva poate fi exprimat in limbajul insusi, ar trebui sa fie mentionat in el, nu numai intr-un comentariu. Aceasta remarca este intarita de comentariile de mai jos:
//variabila 'v' trebuie sa fie initializata
//variabila 'v' trebuie sa fie folosita numai de functia 'f()'
//apeleaza functia 'init()' inainte de a apela
//orice alta functie din acest fisier
//apeleaza functia 'cleanup()' la sfirsitul programului
//sa nu se utilizeze functia 'wierd()'
//functia 'f()' are doua argumente
Astfel de comentarii pot adesea sa fie interpretate ca necesare printr-o utilizare corespunzatoare a lui C++. De exemplu, s-ar putea utiliza regulile de linkere (&4.2) si vizibilitate, initializare si curatire pentru clase (vezi &5.5.2) pentru a face exemplele precedente redondante. Odata ce a fost afirmat ceva clar in limbaj, nu ar trebui mentionat a doua oara intr-un comentariu. De exemplu:
a = b+c // a devine b+c
count++ // se incrementeaza count
Astfel de comentarii sint mai rele decit redondanta: ele maresc cantitatea de text pe care trebuie sa o citeasca programatorul si ele adesea fac mai obscura structura programatorului.
Preferintele autorului sint pentru:
[1] Un comentariu pentru fiecare fisier sursa care sa afirme ce declaratii din el se utilizeaza in comun, scopuri generale pentru mentinere, etc.
[2] Un comentariu pentru fiecare functie netriviala care sa indice scopul ei, algoritmul utilizat (daca nu este evident) si poate ceva despre mediul de executie al ei.
[3] Citeva comentarii in locurile unde codul nu este evident si/sau neportabil.
[4] Foarte mici alternative else.
De exemplu:
// tbl.c: Implementarea tabelei de simboluri
/* Eliminare Gauss prin pivotare
partiala. Vezi Ralston:pg
*/
//swap() presupune utilizarea stivei la un AT&T 3B20.
/ ** ** ** ** *****
Copyright (c) 1984 AT&T. Inc.
All rights reserved
Un set de comentarii bine ales si bine scris este o parte esentiala a unui program bun. Scrierea de comentarii bune poate fi tot atit de dificil ca si scrierea programului insusi.
Sa observam, de asemenea, ca daca se folosesc comentariile cu // intr-o functie, atunci orice parte a acelei functii poate fi comentata utilizind stilul de comentarii /**/ si viceversa.
Exercitii
(*1). Sa se scrie instructiunea urmatoare ca o instructiune while echivalenta:
for(i = 0; i < max_length; i++)
if(input_line[i] == '?')
quest_count++;
Sa se rescrie utilizind un pointer ca si variabila de control; adica asa ca testul sa fie unul de forma *p == '?'.
(*1). Sa se includa complet in paranteze expresiile urmatoare:
a = b + c * d << 2 & 8
a & 077 != 3
a == b || a == c && c < 5
c = x != 0
0 <= i < 7
f(1, 2) + 3
a = -1+ +b-- -5
a = b == c++
a = b = c = 0
a[4][2] *= *b ? c : *d * 2
a - b, c = d
(*2). Sa se gaseasca 5 constructii C++ diferite pentru care sensul este nedefinit.
(*2). Sa se gaseasca 10 exemple de cod C++ neportabile.
(*1). Ce se intimpla daca se face o impartire cu zero pe sistemul d-voastra? Ce se intimpla in cazul unei depasiri superioare sau inferioare.
(*1). Sa se includa complet in paranteze expresiile urmatoare:
*p++
*--p
+++a--
(int*)->m
*p.m
*a[i]
(*2). Sa se scrie functiile strlen() care returneaza lungimea unui sir, strcpy() care copiaza un sir in altul si strcmp() care compara doua siruri. Sa se considere ce tipuri de argumente si ce tipuri se cuvine sa se returneze, apoi sa se compare cu versiunile standard asa cum sint declarate in <string.h>.
(*1). Vedeti cum reactioneaza compilatorul d-voastra la aceste erori:
a := b+1;
if(a = 3)
if(a & 077 == 0)
(*2). Sa se scrie o functie cat() care are doua argumente de tip sir si returneaza un sir care este concatenarea argumentelor. Sa se utilizeze new pentru a gasi memorie pentru rezultat. Sa se scrie o functie rev() care are un argument de tip sir si reutilizeaza caracterele din el. Adica, dupa rev(p), ultimul caracter a lui p va fi primul, etc.
(*2). Ce face exemplul urmator?
void send(register* to, register* from, register count)
while(--n > 0);
}
}
De ce ar vrea cineva sa scrie un astfel de program?
(*2).
Sa se scrie o functie atoi() care are ca argument un sir ce contine cifre si
returneaza int-ul corespunzator. De exemplu, atoi('123') este 12 Sa
se modifice atoi() pentru a trata sirurile octale din C++ si in plus si cele
hexazecimale. Sa se modifice atoi() pentru a trata caracterele C++ utilizate
intr-o notatie de
(*2). Sa se rescrie get_token() (&1.2) asa ca sa citeasca o linie la un moment dat intr-un buffer si apoi sa compuna unitatile citind caracterele din buffer.
(*2). Sa se adauge functii de forma sqrt(), log() si sin() la calculatorul de birou din &1. Sa se predefineasca numele si apelul functiilor printr-un vector de pointeri spre functii. Sa nu se uite sa se verifice argumentele dintr-o functie call.
(*3). Sa se permita unui utilizator sa defineasca functii in calculatorul de birou:
Scop: Sa se defineasca o functie ca un sir de operatii exact asa cum un utilizator ar trebui sa o scrie. Un astfel de sir poate fi memorat sau ca un sir de caractere sau ca o lista de unitati. Apoi se citeste si se executa acele operatii cind functia este apelata. Daca noi dorim ca o functie utilizator sa aiba argumente, noi trebuie sa inventam o notatie pentru aceasta.
(*1.5). Sa se converteasca calculatorul de birou pentru a utiliza un simbol structura in loc sa se utilizeze variabilele statice name_string si number_value:
struct symbol;
(*2.5). Sa se scrie un program care elimina comentariile de tip C++ din program. Adica, citeste din cin si elimina atit comentariile de forma //, cit si cele de forma /*..*/ si scrie rezultatul in cout. Trebuie sa avem grija de // si /*..*/ din comentarii, siruri si constante caracter.
Politica de confidentialitate |
Copyright © 2024 - Toate drepturile rezervate