Professional Documents
Culture Documents
Introducere
Salut. Scopul principal al acestui articol e pregatirea pentru examenul de POO (Programare
Orientată pe Obiecte) din cadrul Facultăţii de Matematică-Informatică Bucureşti, însă poate fi
util oricui vrea să înveţe mai multe despre limbajul C++, lucruri mărunte şi nu foarte des
întâlnite dar care ar trebui înţelese.
Pe lângă partea de teorie, examenul constă în evaluarea – atât „compilarea” cât şi „execuţia”
unor secvenţe de cod – pe foaie. Adică vezi codul, trebuie să specifici dacă se compilează, dacă
se compilează să specifici ce afişează, în ce ordine se apelează constructorii etc., iar dacă nu, să
specifici unde intervine eroarea şi de ce.
Deşi nu sunt tocmai expert în acest domeniu am decis să scriu acest articol pentru că POO este
singura materie care îmi place şi la care mă descurc.
Articolul va fi în mare parte bazat pe exemple cu explicaţiile de riguare. Sunt om, pot greşi,
aştept orice idee, sugestie sau critică cu plăcere.
Voi încerca să acopăr cât mai multe noţiuni, de la lucruri banale, elementare, la lucruri mai
complicate şi mai rar întâlnite.
Înainte de toate, vreau să înţeleagă toată lumea care e diferenţa dintre un IDE (Integrated
Development Environment) şi un compilator: IDE-ul este doar o interfaţă grafică pentru un
compilator, e programul care îţi permite să scrii codul sursă şi foloseşte un compilator pentru a
compila respectiva sursă. De exemplu, am văzut că este extrem de folosit Dev C++. Acest IDE
foloseşte compilatorul MinGW (Minimalist GNU for Windows) şi este de fapt varianta pentru
Windows a compilatorului pentru Linux pentru C++. Sfatul meu este să folosiţi CodeBlocks care
foloseşte acelaşi compilator însă este mai prietenos, mai elegant şi are mai multe opţiuni. De
asemenea puteţi încerca NetBeans sau Eclipse. Visual C++ din Visual Studio, pe lângă IDE vine cu
propriul compilator.
Mediul de lucru
Iniţial voi face testele doar pe compilatorul MinGW, cu el şi cu opţiunile sale sunt obişnuit să
lucrez. IDE-ul folosit va fi CodeBlocks, în special pentru că permite selectarea unor opţiuni utile
pentru compilator.
- Enable all compiler warnings – Va activa o gamă largă de avertismente care nu apar
implicit şi aceste avertismente vă pot ajuta foarte mult în descoperirea problemelor din
codul vostru.
- Enable extra compiler warnings – Activează un alt set de avertismente care nu sunt
activate nici de –Wall (cel de mai sus) şi care pot fi utile
Bine, eu am activat un larg set de opţiuni, dar mai bine decideţi singuri ce să activaţi şi ce nu,
însă aveţi grijă ce opţiuni selectaţi. Mai bine aruncaţi o privire aici:
http://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html#Warning-Options
De asemenea, un lucru extrem de important e modul în care scrieţi codul. Am văzut că foarte
multe persoane nu sunt deloc ordonate şi nu au un stil propriu pe care să îl urmeze, scriu cod
fără să îl indenteze, apoi nu mai înţeleg ce au scris acolo.
- aranjaţi codul aşa cum vă place vouă, numai să fie cât mai uşor de înţeles.
%:include <iostream>
using namespace std;
int main()
<%
int x<:2:> = <%12, 19%>;
cout << x<:0:> << "\n";
return 0;
%>
??=include <iostream>
using namespace std;
int main()
??<
int x??(2??) = ??<12, 19??>
cout << x??(0??) << "\n";
??>
Exerciţiul 1: const
int a = 123;
const int *x = &a; // La fel ca „int const *x” – Pointer la date constante
// Datele de la adresa x nu pot fi modificate
*x = 1337; // Va genera o eroare – „assignment of read-only location”
// Insă
x++; // Valid – Nu e un pointer constant
*y = 1337; // Valid – Datele de la acea adresa nu sunt constante
#include <iostream>
using namespace std;
class Test
{
int x;
public:
Test() {} // Necesar pentru a nu primi eroare la compilare - la crearea obiectului
constant va spune ca nu e initializat in lipsa unui constructor
void Functie() { x = 123; }
void FunctieConstanta() const
{
cout << "Metoda const\n";
/* Ilegal: x = 1337; Nu poate modifica datele obiectului din moment ce e
o metoda constanta */
}
void FunctieConstanta() { cout << "Metoda non-constanta\n"; }
// Dupa cum vedeti singura diferenta intre cele doua functii este faptul ca una e
„const” si cealalta nu, dar nu e nicio problema de compilare
};
int main()
{
const Test c_ob; // Obiect constant
Test ob;
return 0;
}
Legat de funcţiile care returnează un obiect „const” şi de obiectele anonime, nişte lucruri bine
de înţeles şi de ţinut minte ar fi următoarele:
(new const Test) -> FunctieConstanta(); // Pointer la un obiect anonim const Test - Apeleaza
metoda const
De asemenea, dacă există date publice definite în clase, aceastea nu vor putea fi modificate prin
intermediul obiectelor constante.
O excepţie ar fi desigur datele statice. Dacă de exemplu definim o dată publică „z” ca static, o
vom putea modifica prin intermediul obiectelor constante deoarece fiind statică, acea dată nu
aparţine de obiect ci aparţine de clasă, exista un singur „z” pentru toate obiectele de tip Test.
Vom putea acţiona astfel: „c_ob.z = 1337;” va fi acelaşi lucru ca „Test::z = 1337;”, nu vom primi
eroare la compilare.
class Test
{
public:
Test() {}
static int z;
};
int main()
{
const Test c_ob; // Obiectul e constant
return 0;
}
int main()
{
const Test c_ob;
return 0;
}
Dar nu ne putem opri aici. Un lucru foarte importanta: clasa noastră, clasa Test nu are definit un
constructor de copiere. În acest caz va fi generat unul de către compilator, astfel la apelul
funcţiilor noastre (dat fiind faptul că parametrul este transmis prin valoare) va fi apelat
constructorul generat de compilator.
Problema ar fi dacă am defini noi un constructor de copiere şi dacă l-am defini greşit:
Dacă îl vom defini aşa, nu vom putea apela funcţia „AltaFunctie” deoarece ar încerca să apeleze
constructorul de copiere (acesta) – deoarece functia primeste ca parametrul obiectul Test prin
valoare - care are ca parametru o referinţă la un obiect „const” şi astfel ajungem la ce ne
intereseaza: nu se poate apela o funcţie care are ca parametru o referinţă la un obiect non-
const, cu un parametru const. Vom rescrie funcţia astfel:
Pe scurt: nu sunt probleme la transferul prin valoare, problemele apar la transferul parametrilor
prin referinţă, deoarece const asigură că obiectul nu poate fi modificat, iar funcţia având ca
parametru o referinţă la un obiect non-const nu asigură că nu îl modifică.
Exemplul 2: Constructori:
Să începem cu un lucru simplu. Avem o clasă Test şi creăm un obiect ob1 global, şi unul local, să
vedem cum se apelează constructorii şi destructorii:
#include <iostream>
using namespace std;
class Test
{
int x;
public:
Test(int i) : x(i) { cout << "Constructor: " << i << "\n"; }
~Test() { cout << "Destructor: " << x << "\n"; }
} ob1(1); // Se apeleaza constructorul inainte de maine
int main()
{
cout << "Main\n";
// Dupa main se apeleaza destructorii, mai intai pentru ob2 apoi pentru ob1
Să luăm un alt exemplu. Avem clasa Test, ca cea de mai sus şi o clasă derivată Test2. Când
creăm un obiect Test2, se apelează mai întâi constructorul clasei Test.
Însă trebuie să fim atenţi la astfel de cazuri, în care clasa părinte nu are constructor fără
parametri:
#include <iostream>
using namespace std;
class Test
{
int x;
public:
Test(int i) : x(i) { cout << "Test: " << i << "\n"; }
~Test() { cout << "~Test: " << x << "\n"; }
};
int main()
{
Test2 x; // Eroare la compilare
// Cand se apeleaza constructorul, se incearca apelarea constructorului clasei
parinte care nu are constructor fara parametri
// Putem crea un constructor pentru Test fara parametru sau redefinim
constructorul din clasa Test2, unde apelam explicit constructorul clasei Test cu o anumita
valoare, sa zicem 0, adica:
// Test2() : Test(0) { cout << "Test2\n"; }
return 0;
}
Eroare la compilare se va primi chiar dacă nu vom defini niciun obiect de tipul Test2.
Un alt exemplu simplu ar fi initializarea unui vector de obiecte. Folosim tot clasa Test cu
parametru int implicit 0, şi creăm un vector de 4 elemente.
#include <iostream>
using namespace std;
class Test
{
int x;
public:
Test(int i = 0) : x(i) { cout << "Test: " << i << "\n" ; }
~Test() { cout << "~Test: " << x << "\n" ; }
};
int main()
{
// Se va crea un vector cu 4 elemente
// Doar primul obiect va fi creat apeland constructorul cu valoarea 1
// Pentru celelalte obiecte se va apela constructorul cu valoare implicita, adica 0
return 0;
}
Se va afişa: Test: 1, Test: 0, Test: 0, Test: 0 – cele 4 obiecte, apoi destructorii în ordine inversă.
Dacă am fi avut: Test x[4] = {1, 2}; primul obiect ar fi fost construit cu valoarea 1, al doilea cu
valoarea doi, iar celelalte două cu valoarea implicită 0.
Să luăm acum un exemplu mai interesant. Luăm clasele Test şi Test2, care e derivată din Test,
ambele cu parametru int implicit 0. Şi creăm un obiect x şi încă unul y = x.
#include <iostream>
using namespace std;
class Test
{
int x;
public:
Test(int i = 0) : x(i) { cout << "Test: " << i << "\n" ; }
~Test() { cout << "~Test: " << x << "\n" ; }
};
int main()
{
Test2 x(3), y = x;
return 0;
}
Test: 0 - Pentru x, mai întâi se apelează constructorul clasei de bază cu valoarea implicită
Test2: 3 - Apoi constructorul său, Test2, cu valoarea 3
~Test2: 3 - Destructor y
~Test: 0 - Destructorul clasei de bază
~Test2: 3 - Destructor x
~Test: 0 - Destructorul clasei de bază
Însă lucrurile devin mai interesante dacă definim un constructor de copiere pentru Test2. Să îl
definim şi să incrementăm cu 1 valoarea lui y (din Test2) pentru a face diferenţa între obiectele
x şi y.
La rulare, se va afişa:
Tot legat de constructori, e important unde un obiect se declară şi cât rămâne acesta în viaţa.
Să luăm exemplul următor, în care creăm un obiect (aceeaşi clasă Test ca mai sus) într-un for:
int main()
{
cout << "Main\n" ;
Se va afişa:
Main
Test: 1
~Test: 1
Test: 2
~Test: 2
Gata main
Ceea ce nu e deloc greu de înţeles: domeniul de vizibilitate al obiectului x este interiorul for-
ului. Se crează când se intră în for, apoi se distruge.
Ar afişa:
Main
Test: 1
~Test: 1
Gata main
int main()
{
cout << "Main\n" ;
Se va afişa:
Main
Test: 1
Gata main
~Test: 1
Adică se va crea un singur obiect şi va fi distrus la ieşirea din main. Obiectul se crează prima
oară când se ajunge cu execuţia la el, şi e „valid” pe toată execuţia programului, adică e distrus
la ieşirea din main.
#include <iostream>
using namespace std;
class Test
{
Test() { cout << "Test: \n" ; }
};
int main()
{
Test x; // Cream un obiect Test
return 0;
}