15.3. Structuri, reuniuni şi tipuri enumerare
În limbajul C++ numele după cuvântul cheie struct, union respectiv enum poate fi folosit pentru identificarea structurii, reuniunii respectiv a tipului enumerare. De exemplu în limbajul C struct nume formează un tip de date, în limbajul C++ nu mai este nevoie de cuvântul cheie struct pentru a se referi la structura deja declarată.
De exemplu să considerăm structura:
struct Persoana {
char nume[15];
char prenume[15];
long telefon;
};
Atunci următoarea declaraţie este corectă în C++ şi greşită în C.
Persoana Adalbert, Marta;
O altă proprietate importantă este că în limbajul C++ o funcţie poate avea structuri ca şi parametrii. Deasemenea şi valoarea returnată de o funcţie poate fi o structură. În consecinţă valoarea returnată de funcţie şi parametrii unei funcţii pot fi structuri, pointeri către structuri şi referinţe la structuri.
Se pot efectua instrucţiuni de atribuire cu structurile de acelaşi tip. De exemplu:
Persoana A, Adalbert;
A = Adalbert;
În interiorul unei structuri se poate declara o reuniune anonimă în limbajul C++ sub forma:
struct nume {
declaraţii
union {
declaraţii
};
declaraţii
} lisă_de_nume;
În acest caz, referirea la elementele declarate în interiorul reuniunii anonime, se face ca şi referirea la elementele structurii.
În limbajul C++ primul element al unei reuniuni poate fi iniţializat.
Considerăm următorul exemplu. Fişierul union1.cpp:
Rezultă că primul element (vectorul x) poate fi iniţializat la fel cum s-ar fi procedat în cazul iniţializării elementului în sine (adică a unui vector).
În limbajul C++ variabilelor de tip enumerare se pot atribui numai elementele componente ale tipului. Numerele asociate cu aceste elemente nu se pot atribui variabilelor de tip enumerare. Exemplu
enum culori {rosu, galben, verde};
culori Semafor;
...
Semafor = 1; // corect in C, gresit in C++
Semafor = galben; // corect
În continuare ne vom ocupa de facilităţi noi oferite de limbajul C++ referitoare la funcţii.
15.4. Funcţii
15.4.1. Iniţializarea parametrilor formali ai funcţiilor
Parametrii formali ai unei funcţii pot fi iniţializaţi în limbajul C++. Vom spune că parametrii iniţializaţi au o valoare implicită. În lista parametrilor formali elementele iniţializate vor fi declarate în felul următor:
tip nume = expresie
Totuşi există anumite restricţii în acest sens. Dacă există atât parametri formali iniţializaţi cât şi neiniţializaţi, atunci cei neiniţializaţi trebuie să fie înainte de cei iniţializaţi în lista parametrilor formali.
Prezentăm un exemplu în continuare. Fişierul init1.cpp:
Dacă se execută programul se obţine:
Observăm că paramerii formali ora şi minut sunt iniţializaţi cu valorile implicite 12, respectiv 0. Dacă parametrii formali iniţializaţi nu au parametrii actuali corespunzători, atunci se vor folosi valorile lor implicite, în caz contrar se va ignora valoarea implicită şi se vor folosi parametrii actuali corespunzători.
Este interesant de remarcat că la iniţializare se poate folosi orice expresie, deci de exemplu şi apeluri de funcţii. În următorul exemplu apar parametrii formali iniţializaţi în acest fel. Fişierul init2.cpp:
În exemplul de mai sus s-a folosit că în fişierul dos.h este declarată o structură pentru memorarea datelor sub forma:
struct date {
int da_year; // Anul exprimat printr-un întreg.
char da_day; // Caracterul se converteşte în int
// şi astfel se obţine ziua.
char da_mon; // Caracterul se converteşte în int
// şi astfel se obţine luna.
};
Funcţia getdate are un parametru, care este un pointer către structura de mai sus, în care se va returna data curentă.
Fişierul se poate compila cu un compilator C++ sub sistemul de operare Dos sau Windows, şi după execuţie se obţine un rezultat care poate varia în funcţie de data curentă. De exemplu (data curentă: 24 august 1998):
Din rezultate reiese că în cazul în care nu s-a specificat parametru actual, s-a folosit valoarea implicită, deci data curentă.
15.4.2. Funcţii care returnează tipuri referinţă
În limbajul C++ tipul returnat de o funcţie poate fi şi tip referinţă. În acest caz antetul funcţiei va fi de forma:
tip& nume(lista_parametrilor_formali)
Funcţiile care returnează tipuri referinţă pot apare atât pe partea dreaptă cât şi pe partea stângă a operatorilor de atribuire. Se spune că ele pot fi atât operanzi lvalue (left value) cât şi rvalue (right value). Un operand lvalue poate să apară pe partea stângă, iar unul rvalue pe partea dreaptă a operatorilor de atribuire.
Să considerăm următorul exemplu. Într-o urnă se află bile albe şi negre. Se extrage un număr de bile din urnă la întâmplare. Să se scrie un program pentru aflarea numărului de bile albe, respectiv negre extrase. Fişierul refer4.cpp:
După compilarea sub sistemul de operare Dos sau Windows cu Borland C++, şi executarea programului, se obţine un rezultat care depinde de numărul total de bile, număr citit de la intrarea standard. Numărul de bile albe respectiv negre extrase se determină în mod aleator. Deci rezultatul poate fi:
Menţionăm că s-a evidenţiat numărul citit. Observăm că funcţia alb_negru returnează o referinţă către unul din parametrii nr_alb sau nr_negru în funcţie de faptul că bila extrasă este albă sau neagră. Este important de menţionat că parametrii formali ref_alb şi ref_negru ai funcţiei alb_negru trebuie să fie declarate ca şi referinţe la tipul long. În caz contrar, în cadrul funcţiei s-ar încerca returnarea unei referinţe către o variabilă locală (unul din parametrii formali), ceea ce este nepermis.
În cadrul funcţiei main s-a apelat funcţia alb_negru şi totodată s-a folosit şi operatorul de incrementare. Acest lucru este posibil datorită faptului că funcţia returnează o referinţă la o variabilă declarată în funcţia main, şi ca atare apelul funcţiei se comportă ca şi un nume de variabilă.
Bineînţeles această problemă simplă s-ar fi putut rezolva şi fără folosirea unei funcţii, care returnează o referinţă. În varianta fără funcţia care returnează o referinţă, operatorul de incrementare s-ar fi apelat de două ori, pentru două variabile distincte (nr_alb şi nr_negru). În cazul unei probleme mai complexe, de exemplu dacă operatorul de incrementare s-ar fi înlocuit cu operaţii mai complicate, utilizarea unei funcţii, care returnează o referinţă, poate simplifica considerabil programul.
În capitolele care urmează, în cadrul definirii claselor, ne vom întâlni cu situaţii, în care apare necesitatea declarării unor funcţii, care returnează o referinţă către clasa curentă.
15.4.3. Supraîncărcarea funcţiilor
În limbajul C numele funcţiilor distincte trebuie să fie diferit. În limbajul C++ însă putem defini funcţii diferite având numele identic. Această proprietate se numeşte supraîncărcarea funcţiilor. De obicei funcţiile se vor supraîncărca în cazul în care nişte operaţii asemănătoare sunt descrise de mai multe variante ale unor funcţii. În general aceste funcţii se referă la proprietăţi asemănătore, aplicate pentru parametrii formali diferiţi.
Pentru a putea face distincţie între funcţiile supraîncărcate, antetul acestor funcţii trebuie să fie diferit. Astfel tipul parametrilor actuali va determina funcţia care se va apela.
Prezentăm un exemplu legat de variantele funcţiei trigonometrice atan, pentru calcularea numărului π. Fişierul sup_fun1.cpp:
În fişierul de mai sus, în formă de comentariu s-au specificat cele cinci variante ale funcţiei atan. Primele patru sunt declarate în fişierul math.h, ele au numele distincte şi se referă la parametrii formali de tip real. În Borland C++ există funcţia atan, cu acelaşi nume ca şi prima funcţie referitoare la parametrii de tip real. Această funcţie este declarată în fişierul complex.h, şi se apelează în cazul unui parametru actual de tip complex.
După executarea programului obţinem:
Observăm că în cazul în care nu s-au folosit variabile de tip long double (funcţiile atan cu parametrii de tip double respectiv complex, şi funcţia atan2) primele 15 zecimale sunt corecte, iar pentru funcţiile atanl şi atan2l toate cele 18 zecimale obţinute sunt corecte.
Exemplul de mai sus se poate modifica astfel încât toate cele cinci variante ale funcţiei atan să aibă acelaşi nume. Pentru a realiza acest lucru vom scrie trei funcţii cu numele atan, care vor apela funcţiile atan2, atanl respectiv atan2l. Deoarece antetul funcţiilor va fi diferit, se poate efectua supraîncărcarea lor. Prezentăm în continuare această variantă modificată a programului. Fişierul sup_fun2.cpp:
Dacă se execută programul, se obţine acelaşi rezultat ca şi pentru programul precedent. În acest caz numele tuturor funcţiilor este acelaşi, deci funcţia care se va apela, se determină după numărul şi tipul parametrilor actuali. De aceea trebuie specificat cu exactitate tipul parametrilor formali (de exemplu 1.0 este constantă de tip double, şi 1.0L este constantă de tip long double).
În exemplul de mai sus se poate determina întotdeauna cu exactitate funcţia care se va apela, deoarece există o funcţie pentru care atât numărul cât şi tipul parametrilor formali este acelaşi ca şi pentru funcţia respectivă. Nu este însă întotdeauna în aşa fel. De exemplu, dacă în funcţia main din exemplul de mai sus am fi scris
cout << 4 * atan( 1 ) << endl;
atunci deoarece parametrul actual al funcţiei atan este de tip întreg, nici unul din cele cinci funcţii nu are un antet corespunzător. În astfel de cazuri se vor lua în considerare următoarele reguli. Etapele determinării funcţiei care se va apela:
-
Dacă există o funcţie pentru care tipul parametrilor formali coincide cu tipul parametrilor actuali corespunzătoare (deci şi numărul parametrilor formali coincide cu numărul parametrilor actuali), atunci această funcţie se va apela.
-
Altfel, funcţia se va determina folosind conversia implicită pentru tipurile standard (de exemplu tipul int se va converti în double). În acest caz nu vor fi pierderi de informaţii.
-
Dacă nu s-a reuşit determinarea unei funcţii, atunci se încearcă o conversie pentru tipuri standard cu eventuale pierderi de informaţii (de exemplu conversie din tipul long în tipul int).
-
Dacă nici în acest fel nu s-a reuşit determinarea funcţiei, atunci se încearcă aplicarea unor conversii şi tipurilor definite de programator.
Rezultă că în cazul expresiei cout << 4 * atan( 1 ) << endl; se va încerca aplicarea etapei a doua a determinării funcţiei, dar deoarece s-ar putea aplica atât o conversie de la tipul int la double, cât şi la long double, apare o ambiguitate care nu poate fi rezolvată de compilator, şi se va afişa un mesaj de eroare la compilare.
Dacă însă revenim la exemplul din fişierul sup_fun1.cpp putem constata că la compilarea expresiei amintite nu au apărut mesaje de eroare. Acest lucru se datorează faptului că funcţia atan a fost supraîncărcat în aşa fel încât au existat numai două variante ale ei (una cu parametru formal de tip double, iar alta de tip complex). În acest caz există o singură conversie implicită posibilă pentru tipuri standard, deci se va aplica conversia de la tipul int la double, şi se va apela funcţia atan cu un singur parametru de tip double.
15.4.4. Funcţii inline
Apelarea unei funcţii se face printr-un salt la adresa unde este memorată corpul funcţiei, iar după executarea instrucţiunilor funcţiei, se face o revenire la locul apelării. În cazul în care funcţia este foarte simplă operaţiile necesare apelării pot fi mai complicate decît instrucţiunile funcţiei. În limbajul C această problemă se poate rezolva prin folosirea unor macrouri. Ele se definesc în modul următor:
#define nume(listă_de_parametrii) şir_de_caractere
În faza de preprocesare apelurile macrourilor vor fi înlocuite cu şirul de caractere specificat, în care parametrii formali vor fi înlocuiţi cu parametrii actuali. Observăm că apelarea macrourilor seamănă cu apelarea funcţiilor. Totuşi există multe diferenţe. De exemplu apelarea unei funcţii se face printr un salt la adresa corpului funcţiei după care se revine la locul apelării, iar apelarea macrourilor se face prin înlocuirea cu expresia corespunzătoare în faza de preprocesare. Diferenţa cea mai importantă este că în cazul apelării unei funcţii, se verifică corespondenţa dintre tipurile parametrilor formali şi cei actuali, iar dacă este necesar se face şi o conversie la tipul corespunzător. Astfel de verificări nu se fac în cazul macrourilor, ceea ce este un dezavantaj fiindcă poate să conducă la apariţia unor erori. Dacă sunt folosite în mod corect, avantajul macrourilor este că ele pot fi apelate pentru expresii de tipuri diferite.
În următorul exemplu vom defini trei macrouri pentru calculul valorii absolute a unui număr, şi vom atrage atenţia asupra posibilităţii apariţiei unor erori. Fişierul macro1.cpp:
Dacă parantezele ar lipsi din definirea macroului Val_abs(x), în unele expresii ar putea să apară erori. Într-adevăr prin expresia
cout << Val_abs_eronata_1(4000L * y + z ) << endl;
se obţine rezultat necorespunzător (-90000 în loc de 10000), deoarece în faza de preprocesare macroul Val_abs_eronata_1 s-a înlocuit cu:
( 4000L * y + z > 0 ? 4000L * y + z : -4000L * y + z )
şi rezultatul va fi: –4000L * y + z = -90000. Nici în cazul expresiei
cout << 15 + Val_abs_eronata_2( -y );
nu se obţine rezultat corect (5 în loc de 25), deoarece macroul Val_abs_eronata_2 se înlocuieşte cu
(-y) > 0 ? (-y) : -(-y)
şi expresia va deveni:
cout << 15 +(-y) > 0 ? (-y) : -(-y);
Operatorul de adunare fiind mai prioritar decât operatorul ?:, se evaluează mai întâi expresia 15 + (-y), şi se va afişa valoarea 5. În sfârşit, menţionăm că şi în cazul în care macroul este definit corect pot să apară situaţii în care rezultatul nu va fi cel aşteptat. Dacă apelarea macroului s-ar face ca şi în cazul funcţiilor, atunci prin evaluarea expresiei
cout << Val_abs( y++ ) << endl;
s-ar afişa valoarea 10, iar valoarea parametrului y după evaluarea expresiei ar fi 11. În cazul nostru însă s-a afişat valoarea 11, iar după evaluarea expresiei s a obţinut valoarea y = 12. Explicaţia constă şi de această dată în modul de apelare a macrourilor. În faza de preprocesare macroul Val_abs se înlocuieşte cu următorul şir de caractere:
((y++) > 0 ? (y++) : -(y++))
După evaluarea expresiei (y++) > 0, se incrementează valoarea lui y, deci rezultatul expresiei condiţionale va fi egală cu 11, după care se incrementează încă o dată valoarea variabilei y. În consecinţă după evaluarea expresiei se obţine y = 12.
Aceste neajunsuri ale macrourilor pot fi înlăturate cu ajutorul funcţiilor inline. Apelarea funcţiilor inline se face în mod asemănător cu apelarea macrourilor. Apelul funcţiei se va înlocui cu corpul funcţiei, deci operaţia de salt la adresa corpului funcţiei şi revenirea la locul apelării nu sunt necesare. Comparativ cu macrouri, funcţia inline are avantajul, că se va verifica corespondenţa dintre tipurile parametrilor formali şi cei actuali, şi se va face o conversie de tipuri, dacă este necesar. Astfel posibilitatea apariţiei unor erori este diminuată. Declararea sau definirea unei funcţii inline se face în mod obişnuit, dar antetul funcţiei se va începe cu cuvântul cheie inline.
Exemplul de mai sus se poate transcrie folosind o funcţie inline, în modul următor. Fişierul inline1.cpp:
După executarea programului se obţine:
Putem constata că, în acest caz, toate rezultatele obţinute sunt corecte. Menţionăm că o funcţie inline nu se poate folosi numai într-un singur modul, funcţia nu poate să conţină operaţii mai sofisticate, de exemplu nici instrucţiuni de ciclare. În cazul definirii claselor ne vom întâlni foarte des cu funcţii inline.
Dostları ilə paylaş: |