You are on page 1of 14

C++ – noţiuni de limbaj

Autor: Ionuţ Gabriel Popescu


Data: 03 Iunie 2011

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.

Pentru a vă ajuta să descoperiţi probleme în cod, descărcaţi CodeBlocks, mergeţi la „Settings” -


> „Compiler and debugger...” şi bifaţi:

- 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.

Câteva sugestii pe care aş putea să vi le dau:

- când deschideţi o acoladă, o paranteză, o şi închideţi, imediat ce aţi deschis-o. Motivul e


simplu: dacă veţi deschide mai multe, nu veţi ştii ulterior pe care şi unde să o închideţi.

- folosiţi nume de variabile care să vă ajute să înţelegeţi la ce le folosiţi. De exemplu


folosiţi ob pentru obiect şi p_ob pentru un pointer la obiect.

- aranjaţi codul aşa cum vă place vouă, numai să fie cât mai uşor de înţeles.

- folosiţi comentarii! Deşi momentan nu vi se par utile, pe măsură ce dimensiunea codului


creşte o să vă ajute foarte mult să înţelegeţi ce aţi dorit să faceţi.
Probleme:
Exerciţiul 0: caractere speciale

Nu este important, este doar informativ, puteţi sări peste.


Încercaţi să compilaţi următorul cod:

%:include <iostream>
using namespace std;

int main()
<%
int x<:2:> = <%12, 19%>;
cout << x<:0:> << "\n";
return 0;
%>

Ar trebui să meargă. Limbajul permite folosirea acestor înlocuitori pentru #, ,, -, *, +.


De asemenea, ar mai fi permise şi secvenţele „trigraph”, şi s-ar compila şi codul următor:

??=include <iostream>
using namespace std;

int main()
??<
int x??(2??) = ??<12, 19??>
cout << x??(0??) << "\n";
??>

Câteva caractere speciale sunt înlocuite de secvenţe de alte 3 caractere.


Însă va trebui activată această facilitate cu o opţiune în plus pentru compilator.
Nu sunt utile, dar sunt interesante şi e bine să aveţi idee că există.
Cred că se pot folosi din motive de compatibilitate, pentru vremurile în care tastaturile nu
aveau acele caractere speciale, dar nu sunt sigur.

Exerciţiul 1: const

Să începem cu lucruri simple, începem cu modificatorul „const”.

Datele constante trebuie iniţializate la declarare. De exemplu:

const int x; // Nu se va compila – „uninitialized const ‚x’


const int x = 123; // Nu e nicio problema
Pointeri constanţi şi date constante

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”

int * const y = &a; // Pointer constant


// Adresa pointerului nu poate fi modificata
y++; // Eroare la compilare = „increment of read-only variable y”

// Insă
x++; // Valid – Nu e un pointer constant
*y = 1337; // Valid – Datele de la acea adresa nu sunt constante

// Dar putem avea şi aşa


int const * const z = &a; // Pointer constant la date constante
// Sau „const int * const z = &a;”
z++;
*z = 1337;
// Vor genera erori de compilare ca mai sus
// Atât pointerul cât şi datele sunt constante – nu se pot modifica

Obiecte şi metode constante

Pentru obiecte constante ideea e aceeaşi – datele obiectului nu se pot modifica.


De exemplu, creăm o clasă Test cu o metodă normală şi cu o metodă constantă. Metodele
constante, care sunt urmate de un „const” dupa lista de parametri, asigură că nu modifică
starea obiectului, nu modifică datele.
Obiectele constante pot apela doar metode constante. Un alt lucru important e că putem
supraîncărca o funcţie doar prin faptul că una e const şi cea de-a doua nu.

#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;

c_ob.FunctieConstanta(); // Apeleaza metoda const


ob.FunctieConstanta(); // Apeleaza metoda non-const

// c_ob.Functie(); // Eroare la compilare - "no matching function for call" - Nu gaseste


Functie() const deoarece c_ob e constant si poate apela doar metode const

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:

Test RetTest() { return Test(); } // Returneaza un obiect Test


const Test RetTestConst() { return Test(); } // Returneaza un obiect Test constant

Acelasi principiu si pentru obiecte alocate dinamic si returnare de pointeri.


Vom avea următoarele rezultate:
const Test c_ob; // Obiect const

RetTest().FunctieConstanta(); // Apel de functie ce returneaza Test (anonim) - Apeleaza


metoda non-const

RetTestConst().FunctieConstanta(); // Apel de functie ce returneaza const Test (anonim) -


Apeleaza metoda const
(new Test) -> FunctieConstanta(); // Pointer la un obiect anonim non-const Test - Apeleaza
metoda non-const

(new const Test) -> FunctieConstanta(); // Pointer la un obiect anonim const Test - Apeleaza
metoda const

const_cast<Test&>(c_ob).FunctieConstanta(); // Folosim const_cast pentru a trece peste


atributul const - Apeleaza metoda non-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 Test::z = 123;

int main()
{
const Test c_ob; // Obiectul e constant

// z nu face parte din obiect

c_ob.z = 1337; // Valid, la fel ca Test::z = 1337;

return 0;
}

Un alt lucru important legat de „const” îl reprezintă parametrii „const” ai funcţiilor.


Să luăm următoarele funcţii, una cu parametru „const” alta fără.

void AltaFunctie(Test ceva)


{
cout << "AltaFunctie \n";
ceva.FunctieConstanta(); // "ceva" nu va fi constant, se va apela metoda non-const
}

void AltaFunctieConst(const Test ceva)


{
cout << "AltaFunctieConst \n";
ceva.FunctieConstanta(); // "ceva" e const, se apeleaza metoda const
}

Şi în main, declarăm un obiect „const” şi apelam aceste funcţii cu obiectul constant ca


parametru:

int main()
{
const Test c_ob;

AltaFunctie(c_ob); // Nu va genera eroare la compilare, obiectul „const” este convertit


la un obiect non-const iar în funcţie se va apela metoda non-const.
AltaFunctieConst(c_ob); // Apel normal

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:

Test(Test &) { cout << "Copiere\n"; }

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:

void AltaFunctie(Test &ceva) // Parametru e referinta la un obiect Test


{
cout << "AltaFunctie \n";
ceva.FunctieConstanta(); // "ceva" nu va fi constant, se va apela metoda non-const
}
Nu vom putea apela această funcţie folosind ca parametru un obiect const. Dacă avem „ const
Test &ceva” ca parametru atunci nu mai e nicio problemă. In acest caz, dat fiind faptul că
transferul se face prin referinţa, nu se mai apelează constructorul de copiere şi vom primi
eroare la compilare dacă apelăm funcţia cu un parametru const.

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:

Pentru început: variabilele se pot iniţializa ca şi obiectele:

int x(5); // x e o variabila int cu valoarea 5, nu un vector cu 5 elemente, am vazut ca se


fac confuzii
// char y[4]("POO"); // Stupid, ilegal - Vectorii nu se pot initializa asa. Analog pentru int
z[2](0);
cout << x << "\n" ; // Va afisa 5

E ca şi cum am avea o clasă int, şi am apela constructorul cu valoarea 5.

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";

Test ob2(2); // Constructorul

cout << "Gata main\n";


return 0;
}

// Dupa main se apeleaza destructorii, mai intai pentru ob2 apoi pentru ob1

E simplu şi uşor de înţeles.

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"; }
};

class Test2: public Test


{
public:
Test2() { cout << "Test2\n"; }
~Test2() { cout << "~Test2\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

Test x[4] = {1};

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" ; }
};

class Test2: public Test


{
int y;
public:
Test2(int i = 0) : y(i) { cout << "Test2: " << i << "\n" ; }
~Test2() { cout << "~Test2: " << y << "\n" ; }
};

int main()
{
Test2 x(3), y = x;

return 0;
}

În ce ordine se vor apela constructorii? E simplu:

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ă

Ce se întâmplă apoi? E simplu: pentru y nu se apelează constructorul normal, ci se apelează


constructorul de copiere, care nu este definit de noi. Deci se crează y, un al doilea obiect, de
aceea vedem că se apelează de două ori seria ~Test2/~Test.

Î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.

Test2(Test2 &ob) { cout << "Copiere Test2\n" ; y = ob.y + 1; }

La rulare, se va afişa:

Test: 0 - Pentru x - baza


Test2: 3 - Pentru x – constructorul din derivată
Test: 0 - Interesant. Acum, înaintea constructorului de copiere care se apelează la crearea lui
y, se apelează constructorul din clasa de bază pentru y. Motivul e simplu: se crează un nou
obiect Test2 şi asta presupune apelul constructorului clasei de bază, clasa Test. În primul caz,
în care nu aveam un constructor de copiere, x-ul se copia bit cu bit in y şi nu se mai apela
niciun constructor al clasei de bază. Aşadar, atenţie la astfel de lucruri mărunte
Copiere Test2 - Se apelează şi constructorul de copiere
~Test2: 4 - Destructorul lui y mai întâi, destructorii se apelează în ordine inversă
~Test: 0 - Destructorul din bază al lui y
~Test2: 3 - Destructorul lui x
~Test: 0 - Destructorul din bază al lui x

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" ;

for(int i = 1; i <= 2; i++)


{
Test x(i);
}

cout << "Gata main\n" ;


return 0;
}

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.

De reţinut că acoladele definesc practic un domeniu de vizibilitate.


De exemplu, codul următor:

cout << "Main\n" ;


{
Test x(1);
}
cout << "Gata main\n" ;

Ar afişa:

Main
Test: 1
~Test: 1
Gata main

Adică obiectul e distrus la ieşirea din expresie.

Un exemplu interesant, în cazul for-ului ar fi următorul:

int main()
{
cout << "Main\n" ;

for(int i = 1; i <= 5; i++)


{
static Test x(i);
}

cout << "Gata main\n" ;


return 0;
}

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.

Un exemplu banal ar fi următorul:

#include <iostream>
using namespace std;

class Test
{
Test() { cout << "Test: \n" ; }
};

int main()
{
Test x; // Cream un obiect Test
return 0;
}

Ce se va întâmpla? Eroare la compilare. Ar fi trebuit să observaţi ca avem un constructor privat,


ceea ce nu permite instanţierea unui obiect. La fel de important e faptul că dacă nu am crea
niciun obiect Test nu ar fi nicio eroare la compilare, deoarece nu s -ar încerca apelarea
constructorului.

Cam acestea ar fi câteva exemple legate de constructori...

You might also like