Documente Academic
Documente Profesional
Documente Cultură
========
Daniel Dragulici
februarie - mai 2013
==============================================================================
Detalii administrative:
=======================
Notare:
- o nota la examenul scris (S);
- o nota la laborator (L);
Conditii de promovabilitate: S >= 5, L>=5
Nota finala la aceasta disciplina (F), care se trece in catalog, se
calculeaza dupa regula:
F = parte_intreaga_superioara((S + L) / 2) + bonus
unde "bonus" este 0 sau 1, valoarea 1 acordandu-se celor care s-au
remarcat in mod deosebit de-a lungul semestrului.
Bibliografie:
* Herbert Schildt: "C++ manual complet", Teora, 1997
* Muslea Ionut: "Initiere in C++ Programare orientata pe oiecte",
Ed. Cluj-Napoca, 1992, "C++ pentru avansati 33", Ed. Microinformatica,
1994
* Bjarne Stroustrup: "The C++ programming language", Addison-Wesley, 1997
(aparuta si la Ed. Teora)
* Bruce Eckel: "Thinking in C++" (2 vol), Prentice Hall (dar se gaseste
gratis pe Internet)
* ANSI/ISO C++ standard (nu este gratis, dar exista draft-uri gratis)
* diverse resurse pe Internet:
Wikipedia (http://en.wikipedia.org/wiki/Main_Page)
http://www.cplusplus.com/
http://publib.boulder.ibm.com/infocenter/lnxpcomp/
http://www.tutorialspoint.com
etc.
Materialele de referinta pentru examen sunt:
* Cartea lui Herbert Schildt mentionata in bibliografie
* Acest curs
(orice informatie prezenta in reuniunea acestor doua materiale poate fi
subiect de examen).
Lectia 1 - Conceptul de OOP
===========================
I. Paradigme ale programarii
============================
O PARADIGMA este un tipar de gandire si de concepte.
O PARADIGMA A PROGRAMARII este un stil fundamental de programare a
calculatoarelor. Principalele paradigme sunt:
- programarea orientata pe obiecte (OOP);
- programarea imperativa;
- programarea functionala;
- programarea logica.
Ele au ca fundament modele distincte de calcul:
- masina Turing, pentru programarea orientata pe obiecte si cea imperativa;
- lambda calculul, pentru programarea functionala;
- logica de ordinul intai, pentru programarea logica.
Exista si alte paradigme, avand intr-o masura mai mare sau mai mica
legatura cu cele patru de mai sus: programarea structurata si cea
procedurala (variante ale programarii imperative), programarea declarativa
(opusa celei imperative), calculul paralel si concurent, etc.
- PROGRAMAREA IMPERATIVA descrie calculul in termenii unor INSTRUCTIUNI
ce schimba STAREA programului; baza programarii imperative este
instructiunea de ATRIBUIRE; programul este constituit din secvente de
comenzi (instructiuni) pe care le are de executat calculatorul.
In programarea imperativa clasica, programul este o lunga lista de
instructiuni ce opereaza pe niste date globale.
In PROGRAMAREA STRUCTURATA se cauta imbunatatirea claritatii, calitatii
si a timpului de dezvoltare a codului, impunand ca listele de instructiuni
sa aiba o organizare in subrutine, blocuri si cicluri (in contrast cu
folosirea unor simple teste si salturi, care pot conduce la un "spaghetti
code" dificil de inteles si intretinut).
In PROGRAMAREA PROCEDURALA lista de instructiuni si datele sunt impartite
in mai multe SUBRUTINE (proceduri, functii), avand fiecare o anumita
interfata formata din parametri si valoare returnata, si care pot fi
apelate (cu parametri actuali adecvati) ca si cand ar fi instructiuni
obisnuite (iar atunci se executa codul incorporat, cu parametrii actuali
furnizati); din interiorul unei subrutine putem apela alte subrutine; un
desiderat urmarit in programarea procedurala este MODULARITATEA, prin
care cerem ca instructiunile dintr-o subrutina sa opereze doar pe date
locale subrutinei (care nu sunt accesibile din alte subrutine) iar
comunicarea subrutinei cu apelantii ei sa se efectueze doar prin interfata
acesteia (parametri, valoare returnata) - astfel, codul este mai usor
de intretinut si reutilizat.
- OOP reprezinta conceptele ca "OBIECTE", avand campuri de date (ATRIBUTE
sau PROPRIETATI care descriu obiectul) si proceduri asociate (numite
METODE); obiectele sunt manevrate ca un tot unitar, ca si cand ar fi niste
date obisnuite;
tipul unui obiect s.n. CLASA (obiectele sunt INSTANTE ale claselor);
clasele/obiectele se pot defini unele pe baza altora prin MOSTENIRE,
formand astfel IERARHII; un program este o colectie de clase si obiecte
care interactioneaza si descrie aceasta interactiune.
Astfel, daca in programarea imperativa conventionala programul are
aspectul unei liste lungi de de instructiuni ce opereaza pe niste date
globale, in OOP programul are aspectul unui sistem complex de declaratii
(fib 10)
si va afisa:
34
In practica, diferenta dintre o functie matematica si notiunea de functie
folosita in programarea imperativa este ca functiile imperative pot avea
efecte laterale (side effects) ce pot schimba starea programului; de aceea,
ele nu au transparenta referentiala (o expresie are transparenta
referentiala daca poate fi inlocuita in cod cu valoarea ei fara sa se
schimbe comportamentul programului), deci o expresie poate produce valori
diferite la momente diferite, pentru aceleasi argumente, in functie de
starea de executie a programului. In programarea functionala valoarea
furnizata de o functie depinde doar de argumentele acesteia (deci, daca
o vom apela de mai multe ori cu aceleasi argumente, va furniza acelasi
rezultat); comportamentul programului este astfel mai usor de inteles si
mai predictibil.
- PROGRAMAREA LOGICA utilizeaza instructiuni logice pentru a descrie un
calcul sau modul in care rezolvarea unei probleme se reduce la rezolvarea
altor probleme; ea poate fi privita ca o forma de programare declarativa,
dar si procedurala.
Un exemplu de instructiune logica este clauza Horn; de exemplu:
p(X, Y) if q(X) and r(Y)
poate semnifica "q(X) si r(Y) implica p(X, Y)" sau "pentru a rezolva
p(X, Y), intai rezolvam q(X) si apoi r(Y)".
In general, calculul este privit ca un rationament automatizat (automated
reasoning) asupra unui corpus de cunostinte; ipotezele asupra domeniului
problemei sunt exprimate prin formule logice(scrise ca instructiuni logice)
si formeaza programul, iar rularea acestuia consta in aplicarea de reguli
de inferenta (deductie) asupra lor pana este gasit un raspuns la problema
sau colectia de formule se dovedeste inconsistenta (contradictorie).
Exemplu: program Fibonacci recursiv, in Prolog:
fib(0, 0).
fib(1, 1).
fib(X, Y) :X > 1,
X2 is X 2, fib(X2, Y2),
X1 is X 1, fib(X1, Y1),
Y is Y1 + Y2.
atunci, programul poate fi apelat astfel:
fib(10, 34).
si va afisa:
yes
sau astfel:
fib(10, 35).
si va afisa:
no
- PROGRAMAREA DECLARATIVA descrie logica unui calcul fara a descrie fluxul
ei de control (ordinea in care operatiile sunt executate); ea exprima "ce"
trebuie sa faca programul, fara a prescrie "cum" sa o faca, in termenii
unor secvente de actiuni ce trebuie efectuate. Programarea functionala si
cea logica sunt considerate forme de programare declarativa.
- PROGRAMAREA GENERICA este un stil de programare in care algoritmii sunt
definiti deasupra unor tipuri ce urmeaza a fi precizati mai tarziu si sunt
instantiati ulterior, atunci cand este nevoie de ei, pentru niste tipuri
particulare date ca parametri; astfel, daca mai multe fragmente de cod
difera doar prin tipurile de date asupra carora opereaza, ele nu mai
trebuie scrise separat, ci doar o singura data, generic (deasupra unor
tipuri date ca parametri) - se evita astfel fenomenul de DUPLICARE A
CODULUI (duplicate code), adica aparitia de mai multe ori a unei secvente
de cod in cadrul unui program sau al unui grup de programe ce apartin sau
sunt intretinute de o aceeasi entitate.
Aceasta paradigma poate fi adaugata unor limbaje imperative, ex: Ada,
Delphi, C++, Java, C#, Visual Basic .NET, dar si unor limbaje functionale,
ex: Haskell, Clean.
Multe paradigme ale programarii sunt la fel de bine cunoscute pentru
tehnicile pe care le interzic, ca si pentru cele pe care le introduc.
De exemplu, programarea functionala interzice utilizarea efectelor
laterale si schimbarea valorii variabilelor prin atribuire (propunand
folosirea in schimb a recursiei), iar programarea structurata interzice
folosirea instructiunii "goto". Interzicerea anumitor tehnici poate fi
perceputa ca o constrangere de catre programatori, dar face programele mai
usor de inteles si inlesneste demonstrarea unor teoreme privind
proprietatile acestora.
Unele limbaje de programare suporta mai multe paradigme. De exemplu un
program in Object Pascal sau C++ poate fi pur procedural, sau pur orientat
pe obiecte, sau continand elemente ale ambelor paradigme.
Exemple de limbaje de programare:
- BASIC, FORTRAN, COBOL: imperative, procedurale;
- C, Pascal: imperative, structurate, procedurale;
- C++, Java, Object Pascal, Turbo Pascal (v5.5+), Visual BASIC: imperative,
structurate, procedurale, OOP;
- Lisp: familie de limbaje functionale: Common Lisp, Scheme; CommonLOOPS
este o forma de Lisp orientata pe obiecte;
- Prolog, Datalog: limbaje de programare logica declarativa, bazata pe
clauze Horn;
OBJ, CafeOBJ, Maude: limbaje de programare logica declarativa, bazate pe
logica ecuationala; Full Maude este o extensie a lui Maude orientata
pe obiecte;
SQL: limbaj de interogare baze de date; SQL1999 (cunoscut anterior ca
SQL3) este un SQL orientat pe obiecte.
Evolutia limbajelor de programare a avut urmatoarele etape:
- Asocierea dinamica.
In ceea ce priveste stilul de programare intr-un limbaj obiectual, acesta
difera de cel specific programarii imperative procedurale clasice - de
exemplu sa facem o comparatie C versus C++:
In C programatorul priveste prelucrarea din interior, avand o perceptie
dinamica, operativa, a acesteia, si o exprima explicitand seria de operatii
ce trebuie efectuate sub forma unei lungi liste de instructiuni, impartita
eventual in mai multe functii; programul are o dezvoltare pe orizontala,
avand aspectul unei colectii de functii, care contin liste lungi de
instructiuni.
In C++ programatorul priveste prelucrarea din exterior, avand o perceptie
statica, descriptiva, a acesteia, si o exprima descriind legaturile logice
(dependentele) intre operatiile ce trebuie efectuate, sub forma unui sistem
complex de declaratii; programul are o dezvoltare pe verticala, avand
aspectul unei ierarhii de clase si obiecte, care reutilizeaza extensiv cod
mostenit (metodele noi adaugate contin de fiecare data cod putin).
Am putea face o paralela intre modul in care definim un sir prin formula
termenului general sau prin recurenta.
Cand folosim formula termenului general suntem in C: scriem o expresie
comlpexa care expliciteaza toate operatiile ce trebuie efectuate pentru
calcularea termenului curent.
Cand folosim formula de recurenta, suntem in C++: scriem o expresie mult
mai simpla care descrie logica dependentei dintre termenul curent si cativa
termeni anteriori (declaratie prin mostenire); expresia simpla contine
aceeasi informatie ca si cea complexa de mai inainte, dar nu o expliciteaza
- toate calculele din expresia complexa se vor generea si se vor executa
la run time; astfel, putem descrie aceleasi calcule cu cod mai putin
(creste expresivitatea limbajului).
Evident, deoarece C++ extinde C, putem programa in C++ in stil C (folosind
functii independente, care nu sunt metode ale obiectelor, punand cod mult
in metode putine si dezvoltand programul pe orizontala, etc.) dar atunci nu
vom putea beneficia de toate avantajele acestui limbaj. Diferenta se va
simti mai ales la programele de dimensiuni mari, unde avem nevoie de o
expresivitate a limbajului si o productivitate a programarii sporite.
si de asemenea:
int *j = malloc(sizeof(int) * 5);
/* Conversie implicita de la
void* la int* */
void fn(void)
{
goto eticheta;
int i = 1;
eticheta:
;
}
De notat ca intercalarea declaratiilor de variabile cu instructiuni
(ca in exemplul de mai sus) nu era permisa la primele versiuni de C
(acolo declaratiile de variabile trebuiau puse la inceputul blocului iar
instructiunile la sfarsitul sau), dar in C99 este permisa.
(4) Daca operandul drept al operatorului virgula este o "l-value" (entitate
ce poate fi folosita in stanga unei atribuiri), atunci operatorul va
produce o "l-value" in C++ si doar o "r-value" (entitate ce poate fi
folosita in dreapta unei atribuiri) in C; exemplu:
int i, j, x, y;
i = 1; j = 2; x = 3; y = 4;
x = (i, j); /* valid si in C si in C++; x primeste valoarea 2 */
(i, j) = y; /* valid in C++ (j primeste valoarea 4), invalid in C */
(5) C nu permite duplicarea unui typedef in acelasi domeniu (scope), in timp
ce C++ permite typedef-uri repetate;
scope = context de includere unde sunt sunt asociate valori si expresii
(enclosing context where values and expressions are associated);
exemplu:
typedef int MyInt;
typedef int MyInt;
my_counter = 0;
/* Comportament nedefinit */
(7) Identificatorii C++ nu pot contine doi sau mai multi underscore
(caracterul "_") consecutivi, in orice pozitie; identificatorii C nu pot
incepe cu doi sau mai multi underscore consecutivi, dar ii pot contine in
alte pozitii.
Nota: cateva teste efectuate cu compilatoarele gcc si g++ versiunea
4.5.1 20101208 nu au raportat erori la folosirea identificatorilor
de forma mentionata.
(8) In fisierele header ale bibliotecii standard C++ unele functii din
biblioteca standard C au declaratiile modificate pentru a fi mai sigure din
punct de vedere al tipurilor (type-safe) atunci cand sunt folosite in C++;
de exemplu sunt adaugati calificatori const suplimentari.
Astfel, declaratia functiei din biblioteca standard C:
/* <string.h> */
extern char * strchr(const char *s, int c);
este inlocuita in biblioteca C++ cu urmatoarele declaratii:
/* <cstring> */
/* valid in C si in C++ */
(9) Atat in C cat si in C++ putem declara tipuri structura incuibate (nested)
in alte structuri, dar domeniul numelui lor (scope) este interpretat diferit:
in C el se extinde si in afara domeniului structurii exterioare, in C++ el
este definit doar in domeniul (scope)/spatiul de nume (namespace) structurii
exterioare.
In general, in C++ declaratiile de structura sau clasa au propriul lor
domeniu (scope), ceea ce nu este valabil si in C; aceasta afecteaza toate
tipurile structura, uniune, enumerare sau alte clase declarate in interior.
Exemplu:
struct Outer
{
struct Inner
/* declaratie de structura incuibata */
{
int
a;
float
f;
}
in;
enum E
/* declaratie de tip enumerare incuibata */
{
UKNOWN, OFF, ON
}
state;
};
struct Inner
si;
enum E
et;
/* cod C++ */
Outer::Inner
Outer::E
si;
et;
enum E
{
UKNOWN, OFF, ON
};
struct Outer
{
struct Inner
enum E
};
in;
state;
De exemplu, urmatorul cod este valid in C90, dar nu si in C99 sau in C++:
/* Nu exista o declaratie anterioara a lui bar()
in domeniul (scope) curent */
void foo(void)
{
bar(); /* declaratie implicita: extern int bar() */
}
De obicei, compilatoarele (vechi) de C, la intalnirea unei functii
nedeclarate anterior, presupun (implicit) un prototip in care tipurile
parametrilor formali coincid cu cele ale parametrilor actuali furnizati
(determinati cu ajutorul regulii conversiilor implicite folosite la
evaluarea expresiilor) iar tipul returnat este int; ca atare, apelantul va
incarca pe stiva valorile parametrilor actuali si va recupera la revenire
valoarea returnata in concordanta cu aceste tipuri; cand se face saltul
in corpul functiei apelate, aceasta va consulta stiva si va returna o
valoare in concordanta cu tipurile folosite la definirea ei (ea poate fi
definita cu antet si corp in acelasi fisier, dar sub apelul respectiv,
sau in alt fisier linkeditat cu cel in care se afla apelul), iar daca
aceste tipuri difera, valorile vor fi comunicate alterat.
TODO: exemplu
(11) Declararea tipurilor struct, union si enum in prototipurile functiilor
este permisa in C, nu si in C++; exemplu:
/* cod valid in C, invalid in C++ */
extern void foo(const struct info { int typ; int sz; } *s);
int bar(struct point { int x, y; } pt)
{ ... }
De asemenea, declararea tipurilor structura ca tipuri returnate de functii
este permisa in C, nu si in C++; exemplu:
/* cod valid in C, invalid in C++ */
extern struct pt { int x; } pos(void);
Motivul pentru care C++ nu permite declaratiile de mai sus este ca domeniul
(scope) unei structuri declarate astfel nu se extinde in afara declaratiei
sau definitiei functiei, facand imposibila definirea de obiecte de acel
tip structura care ar putea fi pasate ca parametri functiei sau asignarea
valorilor returnate de functie cu obiecte de tipul respectiv.
Atat C cat si C++ permit declararea tipurilor structura incomplete in
prototipurile functiilor si ca tipuri returnate de functie:
void frob(struct sym *s); // OK, pointer catre tip incomplet
struct typ * fraz(void); // idem
Nota: teste efectuate cu compilatoarele gcc si g++ versiunea 4.5.1 20101208
au raportat urmatoarele:
- in cazul functiilor foo, bar si frob, gcc afisaza un warning de tipul:
warning: 'struct info'/'struct point'/'struct sym' declared inside
parameter list; its scope is only this definition or declaration,
which is probably not what you want
in cazul functiilor pos si fraz nu semnaleaza warning sau error, iar in
main se pot defini variabile de tip struct pt si struct typ *;
- g++ semnaleaza eroare in cazul functiilor foo, bar, pos (si accepta
frob si fraz).
memb;
next;
/* int */
/* pointer la struct */
/* parametri nespecificati */
/* fara parametri */
}
/* cod C++ */
extern int xyz();
extern int xyz(void); /* la fel ca xyz() in C++,
diferit si invalid in C */
Nota: gcc versiunea 4.5.1 20101208 accepta prezenta ambelor declaratii ale
functiei xyz intr-un program C, dar la intalnirea unui apel va lua in
const int i = 1;
/* external linkage */
extern const int j = 2; /* 'extern' este optional */
static const int k = 3; /* 'static' este obligatoriu */
/* cod C++ */
const int i = 1;
extern const int
static const int
/* internal linkage */
j = 2; /* 'extern' este obligatoriu */
k = 3; /* 'static' este optional */
dupa el; ca accesibilitate, membrii (atribute sau metode) pot fi: privati
(private), protejati (protected), publici (public). Membrii privati pot fi
invocati doar in metodele clasei si in functiile prietene (friend) acesteia,
cei publici pot fi invocati din orice parte a programului; in lipsa unei
specificari de acces, membrii clasei sunt implicit privati (atentie, ca la
un tip structura sunt implicit publici). Pentru alte detalii, a se vedea
capitolul II din aceasta lectie.
Pentru o instanta a clasei, membrii se pot accesa folosind sintaxa cu '.',
ca in cazul structurilor:
<Nume obiect>.<Nume membru>
Exemplu:
========
#include<iostream>
using namespace std;
class complex{
double re, im; // implicit privati
public:
void setre(double x) {re = x;} // metoda publica, implicit functie inline O
BS All the member function declared and defined within class are Inline by defa
ult. So no need to define explicitly.
void setim(double);
double getre();
double getim();
} a, b[3];
// metoda publica
// metoda publica
// metoda publica
}
Comentarii:
- in program avem cinci obiecte de clasa 'complex': a, b[0], b[1], b[2]
(globale) si c (locala lui 'main'); 'a' si vectorul 'b' au fost declarate
odata cu clasa, 'c' ulterior (vedem ca nu a mai fost nevoie sa adaugam
cuvantul 'class');
- fiecare dintre cele cinci obiecte are proprii sai membri 're', 'im',
'setre', 'setim', 'getre', 'getim' iar daca vrem sa ne referim la un anumit
membru al unui anumit obiect, trebuie sa calificam numele membrului cu
numele obiectului si '.'; asadar, 'a.re' este o alta entitate decat 'c.re';
daca intr-o metoda nestatica a unui obiect invocam un membru al aceleiasi
clase necalificat cu vreun obiect, se considera ca este vorba de membrul
obiectului care a apelat metoda; asadar, instructiunea 're = x' din corpul
metodei 'setre' a lui 'a' executa de fapt 'a.re = x' iar cea din corpul
aceleiasi metode a lui 'c' executa de fapt 'c.re = x'; implementarea
acestui comportament se bazeaza pe folosirea parametrului ascuns 'this',
a se vedea mai jos;
- atributele 're' si 'im' sunt implicit private (deoarece nu exista nici un
specificator de acces deasupra lor) iar metodele 'setre', 'setim', 'getre',
'getim' sunt publice (deoarece cel mai apropiat (si singurul) specificator
de acces aflat deasupra lor este 'public');
de aceea, membrul privat 're' a putut fi invocat in functiile 'setre',
'getre', deoarece sunt metode ale clasei, si nu a putut fi invocat in
functia 'main', deoarece aceasta nu este metoda a clasei si nici prietena
a acesteia (conteaza doar invocarea numelui 're', nu are relevanta prezenta
vreunei calificari cu un obiect si '.');
- observam ca obiectele se pot manevra ca un tot unitar, prin atribuire cu
'=', comparare cu '==', etc.; este insa necesar sa existe supraincarcari
ale acestor operatori cu parametri de tip 'complex'; pentru '=',
compilatorul adauga o supraincarcare implicit (dar o putem inlocui cu o
alta), pentru '==' nu (trebuie sa o adaugam explicit); pentru alte detalii
a se vedea lectia V.
Asa cum am spus mai sus, obiectele unei clase au locatii disjuncte pentru
atributele nestatice si locatii comune pentru cele statice; exemplu:
class c{
public:
int x;
static int y;
} a,b;
int c::y;
a.x = 1; b.x = 2; cout << a.x << " " << b.x << endl; // afisaza: 1 2
a.y = 3; b.y = 4; cout << a.y << " " << b.y << endl; // afisaza: 4 4
intr-adevar, 'a.x' si 'b.x' au locatii disjuncte, 'a.y' si 'b.y' au
aceeasi locatie.
Metodele unei clase nu au mai multe copii, cate una pentru fiecare obiect,
ci fiecare exista in cate un singur exemplar program, iar toate invocarile ei
de catre diverse obiecte sunt traduse de compilator prin grupuri de
instructiuni care contin un 'call' (instructiunea de apel de procedura in
limbaj masina) catre o aceeasi adresa (a corpului metodei).
Astfel, in programul anterior cu numere complexe exista un singur exemplar
al functiei 'setre', iar atat apelul 'a.setre(3.14)' cat si apelul
'c.setre(1)' produc salt catre o aceesi adresa (a corpului lui 'setre',
determinata de compilator si scrisa ca atare, ca o constanta, in programul
// afisaza: abcdefgh
// afisaza: abcdefghijkl
public:
void Set_a(int a) {
// Pointerul 'this' este folosit pentru regasirea lui 'xobj.a'
// mascat de parametrul 'a'
this->a = a;
}
void Print_a() { cout << "a = " << a << endl; }
};
int main() {
X xobj; int a = 5;
xobj.Set_a(a); xobj.Print_a(); // afisaza: a = 5
}
Daca numele unui membru al clasei nu este mascat, invocarea numelui
respectiv este echivalenta cu invocarea numelui respectiv calificat cu
'this->'.
Putem invoca membrii unei clase cu specificarea domeniului (scope) acestora,
'clasa::'. Acest lucru este util atunci cand numele unor membri este mascat de
alte nume (de exemplu pentru a distinge intre un membru mostenit si unul nou
adaugat, cu acelasi nume) si pentru invocarea membrilor statici fara
precizarea obiectului in functii ce nu sunt metode ale clasei (membrii
statici au locatii comune pentru toate obiectele clasei si pot fi invocati
fara a preciza un obiect, dar cand facem acest lucru in afara domeniului
clasei trebuie precizat domeniul lor).
Exemplu:
========
#include<iostream>
using namespace std;
class baz{
public:
int i, j, k, t;
};
class der: public baz{
public:
int i, j, a, b;
void f();
};
// 'der' are doi 'i' si doi 'j' (cate unul mostenit si unul nou);
// mai are 'k', 't' (mosteniti), 'a', 'b', 'f'
void der::f(){
int i, k, a; // variabile locala; sunt diferite de membrii 'i', 'k' 'a'
i = 1;
// 'i' local
der::i = 2;
// membrul 'i' nou
baz::i = 3; der::baz::i = 30; // membrul 'i' mostenit
j = 4; der::j = 40;
// membrul 'j' nou
baz::j = 5; der::baz::j = 50; // membrul 'j' mostenit
k = 6;
// 'k' local
der::k = 7; baz::k = 70; der::baz::k = 700; // membrul 'k'
a = 8;
// 'a' local
"
"
"
"
"
"
<<
<<
<<
<<
<<
<<
der::i
der::j
der::k
der::a
der::t
der::b
<<
<<
<<
<<
<<
<<
}
int main(){
der x; x.f();
}
Comentarii:
- Variabila locala 'i' mascheaza membrul nou 'i', care mascheaza membrul
mostenit 'i'; prin precizarea domeniului 'clasa::' le putem accesa.
- Celelalte exemple arata ca precizarea domeniului 'clasa::' in cazul unei
entitati vizibile implicit in acel domeniu este inutila (ex. 'j' este
echivalent cu 'der::j'.
Exemplu:
========
#include<iostream>
using namespace std;
class baz{
public:
int i;
int f(int);
int f(int, int);
int g(); int k;
void h(int);
};
int baz::f(int x){cout << "int baz::f(int)\n"; return 0;}
int baz::f(int x, int y){cout << "int baz::f(int, int)\n"; return 0;}
int baz::g(){cout << "int baz::g()\n"; return 0;}
void baz::h(int x){cout << "int baz::h(int)\n";}
class der: public baz{
public:
double i;
double f(int);
void f(int, int);
int g; int k();
void h(int, int);
void afis();
};
double der::f(int x){cout << "double der::f(int)\n"; return 12.3;}
void der::f(int x, int y){cout << "void der::f(int, int)\n";}
int der::k(){cout << "int der::k()\n"; return 0;}
void der::h(int x, int y){cout << "void der::h(int, int)\n";}
void der::afis(){
baz::i = 1; i = 2;
cout << baz::i << " " << der::i << " " << endl;
// afisaza: 1 2
// deci atributul 'i' din clasa 'der' a mascat atributul 'i' din clasa
// 'baz', chiar daca au tipuri diferite si am folosit valori de tipuri
// corespunzatoare (cu 'i = 2' s-a atribut tot 'baz::i');
int a, b;
a = f(3); b = baz::f(4);
a = f(5, 6); // eroare, nu se poate atribui 'void' unei variabile 'int'
a = baz::f(5, 6);
// afisaza:
// double der::f(int)
// int baz::f(int)
// int baz::f(int, int)
// deci metodele 'f' din clasa 'der' mascheaza metodele 'f' din clasa
// 'baz' chiar daca difera tipul returnat iar 'a' si 'b' sunt de tipul
// 'int' (tipul returnat de metodele din 'baz');
h(6); // eroare, nu este gasita o metoda cu signatura potrivita in 'der'
baz::h(7);
h(8, 9);
// afisaza:
// int baz::h(int)
// void der::h(int, int)
// deci metoda 'h' din clasa 'der' o mascheaza pe cea din clasa 'baz'
// chiar daca difera signatura si am apelat cu numar adecvat de parametri
// actuali
a = g(); // eroare, 'g' nu este functie
a = baz::g();
k = 11; // eroare, membrul stang nu este l-value intreaga
baz::k = 10;
g = k(); // OK, sunt 'g' si 'k' din clasa 'der'
cout << a << " " << baz::k << " " << g << endl;
// afisaza:
// int baz::g()
// int der::k()
// 0 10 0
// deci numele din clasa 'der' le mascheaza pe cele din clasa 'baz', chiar
// daca un nume desemneaza intr-o clasa un atribut si in cealalta o
// metoda
}
int main(){
der ob;
ob.afis();
}
Comentariu: numele declarate in clasa derivata le pot masca pe cele declarate
in clasa de baza chiar daca desemneaza entitati de natura diferita
(atribute/metode) si tip/signatura diferita; in toate cazurile, invocarea
cu specificarea domeniului 'baz::' ne ajuta sa le accesam si pe cele mascate.
Exemplu:
========
class cls{
public:
static int i;
void f();
};
int cls::i;
// 'i' are locatie comuna pentru toate obiectele lui 'cls';
// el se poate invoca si fara a preciza obiectul, dar cand facem aceasta
// in afara clasei trebuie precizat domeniul 'cls::'
void cls::f(){
i = 1; cls::i = 2;
}
// in 'f' suntem in domeniul clasei, deci precizarea domeniului
// 'cls::' este inutila; 'i' este echivalent cu 'cls::i'
int main(){
cls x;
x.i = 3; x.cls::i = 4; cls::i = 5;
i = 6; // eroare, numele 'i' nu este necunoscut aici
// in'main' nu suntem in domeniul clasei ('main' nu este metoda a ei),
// asa ca desi putem invoca 'i' fara precizarea obiectului,
// cand omitem obiectul trebuie sa precizam clasa;
// deci, putem scrie 'cls::i' dar nu doar 'i';
// 'x.i', 'x.cls::i', 'cls::i' sunt echivalente
x.f(); x.cls::f();
f();
// eroare, numele 'f' nu este necunoscut aici
cls::f(); // eroare, trebuie precizat un obiect
// 'f' este nestatica, deci depinde de obiect, iar 'main' nu are un
// obiect curent; 'x.f()' si 'x.cls::f()' sunt echivalente
}
In programul de mai sus 'i' si 'cls::i' din 'f', 'x.i', x.cls::i' si
'cls::i' din 'main' desemneaza toate cinci aceeasi locatie.
O DECLARATIE DE CLASA INCOMPLETA (INCOMPLETE CLASS DECLARATION) este o
declaratie de clasa ce nu specifica nici un membru (si nici acoladele).
Pana cand declaratia clasei nu va fi scrisa ulterior complet, nu se pot
declara obiecte ale clasei si nici nu se pot invoca membrii sai.
O declaratie incompleta ne permite insa sa ne referim la clasa inainte de a
o declara complet, cu conditia ca dimensiunea (sizeof) clasei sa nu trebuiasca
a fi cunoscuta; de exemplu putem declara referinte sau pointeri la ea.
Exemplu:
========
class first;
class second{
// declaratie completa a clasei 'second'
first* oneptr;
// pointer la clasa 'first'; se refera la clasa 'first' inaintea
// declaratiei sale complete (implementarea lui 'oneptr' nu necesita
// cunoasterea lui 'sizeof(first)' deoarece toti pointerii au aceeasi
// dimensiune
first &oneref;
// similar
first one;
// eroare, nu putem declara un obiect al unei clase declarate incomplet
int x, y;
public:
second();
};
class first{
// declaratia completa a clasei 'first'
second two; // se defineste un obiect al clasei 'second'
int z;
};
first ob;
second::second():oneref(ob){}
Comentariu: Definitia lui 'ob' si cea a lui 'second' (anume liniile
'first ob;' si 'second::second():oneref(ob){}') trebuie puse dupa
declaratia completa a lui 'first', altfel se semnaleaza aroare.
Daca declaram o clasa cu lista vida de membri (deci sunt prezente acoladele),
aceasta este o declaratie de clasa completa.
Exemplu:
========
class X;
class Z {};
class Y{
public:
X yobj;
Z zobj;
};
// eroare
// corect
// eroare
// eroare
// eroare
I.2 Structuri
=============
In C++, tipurile structura sunt foarte asemanatoare cu clasele; in particular
ele pot avea atribute si metode, constructori si destructori, specificatori de
acces, metodele nestatice au 'this', etc.; ele pot mosteni alte tipuri
structura sau clase; de asemenea, clasele pot mosteni tipuri structura sau
clase.
De aceea, sintaxa folosita in legatura cu tipurile structura este
asemanatoare cu cea folosita in cazul claselor, dar in locul cuvantului
'class' se foloseste cuvantul 'struct' (similar, el este necesar doar la
declararea tipului structra, nu si la utilizarea sa, unde este optional).
Singurele diferente intre clase si tipuri structura sunt:
- in absenta unei specificari de acces, membrii unei clase sunt implicit
privati, iar membrii unei structuri sunt implicit publici; de exemplu,
urmatoarele declaratii descriu tipuri similare:
class c{
int a;
public:
void set(int x){a = x;}
int get(){return a;}
};
struct s{
void set(int x){a = x;}
int get(){return a;}
private:
int a;
};
// implicit privat
// explicit public
// explicit public
// implicit public
// implicit public
// explicit privat
int main(){
cout << offsetof(c1,b) << " "
<< offsetof(c2,b) << " "
<< offsetof(c3,b) << endl; // afisaza: 4 0 4
}
I.7. Clase imbricate si clase locale
====================================
O CLASA IMBRICATA (NESTED CLASS) este o clasa declarata in interiorul altei
clase. Numele clasei imbricate este local clasei care o include (este valid
doar in domeniul acesteia).
Intr-o clasa imbricata putem invoca nume (inclusiv de tipuri, membri,
enumerari), din clasa care o include.
Membrii unei clase imbricate nu au drepturi de acces speciale fata de
membrii altei clase imbricate in aceeasi clasa. Metodele clasei care include
nu au drepturi de acces speciale fata de membrii clasei imbricate.
Exemplu:
========
class A {
int x;
enum E {E1, E2};
class B { };
class C {
B b;
// OK
int y;
void f(A* p, int i) {
p->x = i; // OK
}
};
void g(C* p) {
C q;
// OK
E r = E1;
// OK
int z = p->y; // eroare, 'A::C::y' este privat
}
};
int main() {
B u;
// eroare, numele 'B' nu este declarat in acest domeniu (scope)
A::B v; // eroare, 'A::B' este privat
}
Putem defini atributele statice si metodele unei clase imbricate in
exteriorul claselor daca folosim nume de tip calificate - mai exact, daca
folosim specificari ale domeniilor imbricate: 'clasa1::clasa2::membru'.
Putem defini un nume cu 'typedef' pentru a reprezenta un nume de clasa
calificat. Ulterior, putem folosi numele definit cu 'typedef' impreuna cu
operatorul '::' (scope resolution) pentru a ne referi la o clasa sau membru
imbricat.
Exemplu:
========
class outside{
public:
class nested{
public:
static int x;
static int y;
int f();
int g();
};
};
int outside::nested::x = 5;
int outside::nested::f() { return 0; }
typedef outside::nested outnest; // definim un nume cu 'typedef'
int outnest::y = 10;
// folosim numele definit cu 'typedef' cu '::'
int outnest::g() { return 0; };
Observatie: Folosirea unui nume definit cu 'typedef' pentru a reprezenta
numele unei clase imbricate ascunde informatii si poate face codul mai greu
de inteles.
Un nume definit cu 'typedef' nu poate fi folosit intr-un specificator de tip
elaborat. Pentru ilustrare, in exemplul de mai sus nu putem folosi urmatoarea
declaratie:
class outnest obj;
dar putem folosi:
outnest obj;
O clasa imbricata poate mosteni alta clasa imbricata in aceeasi clasa.
Exemplu:
========
class A {
private:
class B { };
B *z;
class C : private B {
private:
B y;
A::B y2;
C *x;
A::C *x2;
};
};
Clasele imbricate sunt folosite rareori. Datorita flexibilitatii si puterii
mecanismului de mostenire, practic nu este nevoie de clase imbricate.
O CLASA LOCALA (LOCAL CLASS) este o clasa declarata in cadrul definitiei
(corpului) unei functii. Numele ei este local functiei respective (este valid
doar in domeniul acesteia).
Claselor locale li se aplica mai multe restrictii:
- Clasa locala nu poate invoca variabilele automatice ale functiei in care
este declarata; ea poate invoca nume de tipuri, enumerari, variabile statice
din domeniul functiei respective, si variabile si functii externe.
Exemplu:
========
int x;
// variabila globala
void f()
// definitia functiei
{
static int y;
// variabila statica 'y' poate fi folosita de clasa locala
int x;
// variabila automatica 'x' nu poate fi folosita de clasa locala
extern int g();
// functia externa 'g' poate fi folosita de clasa locala
class local
// clasa locala
{
int g() { return x; }
// eroare, variabila locala 'x' nu
int h() { return y; }
// valid,
int k() { return ::x; }
// valid,
int l() { return g(); }
// valid,
};
}
int main()
{
local* z;
// eroare: numele 'local' nu este declarat in acest domeniu (scope)
}
- Metodele unei clase locale nu pot fi definite in afara declaratiei clasei
ci doar in interiorul acesteia; in consecinta, ele sunt automat functii
inline (nu mai este nevoie sa folosim cuvantul cheie 'inline').
- Clasele locale nu pot avea atribute statice.
- Functia in care este declarata o clasa locala nu au drepturi speciale de
acces la membrii acesteia.
Exemplu:
=======
void f(){
class local{
int f();
// eroare, metoda non-inline a clasei locale
int g() {return 0;} // valid, metoda inline
static int a;
// eroare, atribut static al clasei locale
int b;
// valid, atribut nonstatic
} x;
int k = x.b; // eroare, 'int f()::local::b' este privat
}
Exemplu:
========
#include <iostream>
using namespace std;
class cls {
public:
int a;
void f(int);
};
void cls::f(int b) {cout << "Valoarea lui b este "<< b << endl;}
int main() {
int cls::*ptiptr = &cls::a;
// 'ptiptr' este pointer la atribute de tip 'int' ale lui 'cls'
void (cls::* ptfptr) (int) = &cls::f;
// 'ptfptr' este pointer la metode cu signatura
// 'void (cls::)(int)' ale lui 'cls'
// Obs. ca putem initializa pointerii la membri chiar daca inca nu exista
// obiecte ale clasei respective
cls ob; // 'ob' este un obiect al clasei 'cls'
ob.*ptiptr = 10;
// se initializeaza atributul 'a' al lui 'ob';
// este echivalent cu 'ob.a = 10;';
cout << "Valoarea lui a este " << ob.*ptiptr << endl;
// afisaza: Valoarea lui a este 10
(ob.*ptfptr) (20);
// se apeleaza metoda 'f' a lui 'ob';
// este echivalent cu 'ob.f(20);';
// afisaza: Valoarea lui b este 20
// parantezele au fost necesare deoarece '.*' este mai slab decat
// '()' (apelul de functie) si s-ar fi interpretat
// 'ob.*(ptfptr (20));'
}
Pentru a simplifica scrierea, putem asocia un nume cu 'typedef' tipului
pointerilor la un anumit membru al unei clase.
Exemplu: putem modifica exemplul anterior astfel:
========
typedef int cls::*my_pointer_to_member;
typedef void (cls::*my_pointer_to_function) (int);
int main() {
my_pointer_to_member ptiptr = &cls::a;
my_pointer_to_function ptfptr = &cls::f;
cls ob;
ob.*ptiptr = 10;
cout << "Valoarea lui a este " << ob.*ptiptr << endl;
(ob.*ptfptr) (20);
}
int *pv;
// 'pv' este pointer la variabile de tip 'int'
void (*pf) (int); // 'pf' este pointer la functii cu singatura 'void (int)'
int cls::*pa;
// 'pa' este pointer la atribute ale clasei 'cls' de tip 'int'
void (cls::*pm)(int);
// 'pm' este pointer la metode ale clasei 'cls' cu signatura 'void (int)'
int main(){
p = &a;
pv = &a.i;
// 'pv' ia ca valoare adresa de memorie a variabilei 'a.i';
// valoarea este de tip 'int*'
pa = &a.i;
// eroare: nu se poate converti 'int*' la 'int cls::*'
pa = &cls::i;
// 'pa' ia ca valoare offestul membrului 'i' in obiectele clasei 'cls';
// valoarea este de tip 'int cls::*'
cout << p << " " << pv << " " << pa << endl;
// afisaza: 0x804a0d8 0x804a0dc 1
// locatia lui 'a.i' este la 4 octeti dupa inceputul locatiei lui 'a'
// deoarece inaintea sa este locatia lui 'a.x'
a . i = 1;
cout << a.i << endl; // afisaza: 1
* pv = 2;
cout << a.i << endl; // afisaza: 2
a .* pa = 3; cout << a.i << endl; // afisaza: 3
p ->* pa = 4; cout << a.i << endl; // afisaza: 4
// am accesat aceeasi variabila 'a.i'
// direct, cu un pointer, cu un obiect si un pointer la membru,
// cu un pointer la obiect si un pointer la membru
pv = &a.j; // echivalent cu: 'pv = &cls::j'
// 'pv' ia ca valoare adresa de memorie a variabilei 'cls::j';
// valoarea este de tip 'int*'
pa = &a.j; pa = &cls::j;
// erori: nu se poate converti 'int*' la 'int cls::*'
// deci nu se poate lua offsetul unui atribut static, ci doar adresa de
// memorie a lui ca variabila (independenta de obiecte)
a . j = 5;
cout << cls::j << endl; // afisaza: 5
cls :: j = 6; cout << cls::j << endl; // afisaza: 6
* pv = 7;
cout << cls::j << endl; // afisaza: 7
// am accesat aceeasi variabila 'cls::j' (independenta de obiecte)
// direct (cu 'a.j' si, echivalent, cu 'cls::j') si cu un pointer
pf = &a.f;
// eroare: nu se poate lua adresa de memorie a unei metode nestatice
pm = &cls::f; // nu puteam zice fara '&': 'pm = cls.f;'
// 'pm' ia ca valoare offestul metodei 'f' in obiectele clasei 'cls';
// valoarea este de tip 'void (cls::*)(int)';
// nu puteam zice: 'pm = &a.f;' sau 'pm = a.f;'
cout << pm << endl; // afisaza: 1
a . f (1);
// afisaza: f
(a .* pm) (1); // afisaza: f
(p ->* pm) (1); // afisaza: f
// am apelat aceeasi functie 'a.f()'
// direct, cu un obiect si un pointer la membru,
// cu un pointer la obiect si un pointer la membru;
// la ultimele doua instructiuni parantezele au fost necesare
// deoarece '.*' si '->*' sunt mai slabi ca '()' si s-ar fi
// interpretat 'a .* (pm (1));', 'p ->* (pm (1));'
pf = &a.g; // echivalent cu: 'pf = &cls::g;', 'pf = a.g;', 'pf = cls::g;'
// 'pf' ia ca valoare adresa de memorie a functiei 'cls::g';
// valoarea este de tip 'void (*) (int)'
pm = &cls::g;
// eroare: nu se poate lua offsetul unei metode statice
cout << pf << endl; // afisaza: 1
a . g (2);
// afisaza: g
cls :: g (2); // afisaza: g
pf (2);
// echivalent cu: '(*pf)();' afisaza: g
// am apelat aceeasi functie 'a.g()'
// direct (cu 'a.g()' si, echivalent, cu 'cls::g()'),
// si cu un pointer la functie
}
Comentariu: exemplul de mai sus arata ca:
- in cazul atributelor statice:
nu se poate lua offsetul;
'&obiect.atribut' si '&clasa::atribut' inseamna adresa de memorie;
- in cazul atributelor nestatice:
'&clasa::atribut' inseamna offsetul;
'&obiect.atribut' inseamna adresa de memorie;
- in cazul metodelor statice:
nu se poate lua offsetul;
'&obiect.functie', 'obiect.functie','&clasa::functie', 'clasa::functie'
inseamna adresa de memorie;
- in cazul metodelor nestatice:
'&clasa::metoda' inseamna offsetul;
nu se poate lua adresa de memorie.
Exemplu:
========
#include<iostream>
using namespace std;
class cls{
public:
int i,j,k;
void f(int); void g(int); void h(int);
};
void cls::f(int x){cout << "f\n";}
void cls::g(int x){cout << "g\n";}
void cls::h(int x){cout << "h\n";}
int& accesare(cls *po, int cls::* pa){
return po ->* pa;
}
void aplica(cls *po, void (cls::*pm)(int), int x){
(po ->* pm)(x);
}
int main(){
int cls::*p1=&cls::i, cls::*p2=&cls::j, cls::*p3=&cls::k;
cout << p1 << " " << p2 << " " << p3 << endl;
cout << (p1 == p2) << " " << (p2 == p3) << " " << (p1 == p3) << endl;
// afisaza: 1 1 1
//
0 0 0
// deci afisarea nu arata corect valoarea pointerilor la membri,
// asa cum se intampla in cazul pointerilor obisnuiti
cls ob;
accesare(&ob, &cls::i) = 1;
cout << accesare(&ob, &cls::i) << " " << ob.i << endl; //afisaza: 1 1
// intrucat functia 'accesare' returneza referinta la 'int',
// expresia 'accesare(&ob, &cls::i)' este lvalue
aplica(&ob, &cls::f, 10); // afisaza: f
void (cls::*v[])(int) = {&cls::f, &cls::g, &cls::h};
// vector de pointeri la metode
for(int i=0; i<3; ++i) (ob.*v[i])(100);
// afisaza:
// f
// g
// h
// operatorul '.*' este mai slab decat '()', deci scrierea fara
// paranteze '(ob.*v[i])(100)' ar fi insemnat 'ob.*(v[i](100))'
}
Urmatorul exemplu ilustreaza sensurile diferite pe care le pot avea
expresiile care contin operatorii '.*' si '->*' daca adaugam sau nu paranteze
suplimentare (de exemplu '(x.*p)[i]' versus 'x.*p[i]'):
Exemplu:
========
#include<iostream>
using namespace std;
class cls{
public:
int i, j, k, v[3];
void h(int);
};
void cls::h(int x) {cout << "cls::h(" << x << ")\n";}
int cls::* p(int selector){
switch(selector){
case 0: return &cls::i;
case 1: return &cls::j;
case 2: return &cls::k;
}
}
int main(){
cls x; int i;
{
// aici, 'p' este functia ce returneaza pointer la atribut
// definita mai sus
for(i = 0; i < 3; ++i)
x.*p(i) = i*10;
cout << x.i << " " << x.j << " " << x.k << endl;
// afisaza: 0 10 20
}{
// declaram 'p' ca pointer la metoda
void (cls::* p) (int) = &cls::h;
(x.*p)(7);
// afisaza: cls::h(7)
}{
// declaram 'p' ca vector de pointeri la atribute
int cls::* p[] = {&cls::i, &cls::j, &cls::k};
for(i = 0; i < 3; ++i)
x.*p[i] = i*100;
cout << x.i << " " << x.j << " " << x.k << endl;
// afisaza: 0 100 200
}{
// declaram 'p' ca pointer la atribut-vector
int (cls::* p) [3] = &cls::v;
for(i = 0; i < 3; ++i)
(x.*p)[i] = i*1000;
cout << x.v[0] << " " << x.v[1] << " " << x.v[2] << endl;
// afisaza: 0 1000 2000
}
}
Comentariu: pe parcursul lui 'main', numele 'p' a fost folosit cu
patru sensuri diferite; pentru a nu aparea conflicte intre
declaratiile respective, le-am inclus in instructiuni compuse
separate (fiind deci locale acestora).
II. Accesibilitate, incapsulare, functii si clase prieten (friend)
==================================================================
Una dintre caracteristicie membrilor claselor/obiectelor (in particular
structurilor, uniunilor) este ACCESIBILITATEA; din acest punct de vedere
ei pot fi: PRIVATI (PRIVATE), PROTEJATI (PROTECTED), PUBLICI (PUBLIC).
Accesibilitatea membrilor se poate specifica inserand in declaratia clasei,
printre declaratiile membrilor, SPECIFICATORII DE ACCES (cuvinte cheie)
'private', 'protected', 'public' (cu sensul evident), urmati de ':'; fiecare
0;
//
//
//
// accesare permisa
accesare permisa
accesare permisa
accesare permisa
0;
0;
0;
//
//
//
// accesare permisa
// accesare permisa
// accesare permisa
accesare permisa
accesare permisa
accesare permisa
int main(){
cls ob, *pob = &ob;
ob.a = 1; pob -> a = 1; // eroare ('a' este privat)
ob.b = 1; pob -> b = 1; // accesare permisa
ob.e = 1; pob -> e = 1; // eroare ('e' este protejat)
ob.f(1); pob -> f(1); // eroare ('f' este privat)
ob.g(); pob -> g(); // accesare permisa
ob.h(); pob -> h(); // eroare ('h' este protejat)
}
Comentarii:
- observam ca dintr-o metoda a clasei 'cls' (indiferent daca este privata,
protejata sau publica) putem accesa orice membru (data sau functie) al clasei
'cls' (indiferent daca este privat, protejat sau public); din 'main', care
nu este nici metoda si nici friend al clasei 'cls', nu putem accesa decat
membrii sai publici;
- dreptul de accesare are lagatura doar cu scrierea numelui membrului in
locul respectiv, nu conteaza calificarea ('ob.', 'pob->', etc.);
evident, 'main' nefiind metoda a clasei, nu are 'this' si deci nu putem
invoca membrii necalificati - de ex. nu mai merge 'b = 1' sau 'g()', dar
merge 'ob.b = 1', 'pob -> b = 1', 'ob.g()', 'pob -> g()'.
Exemplu:
========
class cls{
private:
int a;
protected: int b;
public:
int c;
};
void cls::fa(){a =
void cls::fb(){a =
void cls::fc(){a =
d
a
b
c
=
=
=
=
1;
1;
1;
1;
e = 1;
fa(); // erori, membri existenti, dar inaccesibili direct
fb();
fc();
}
int main(){
clsder ob;
ob.a = 1; ob.fa(); // erori, membri inaccesibili
ob.b = 1; ob.fb(); // erori, membri protected
ob.c = 1; ob.fc();
ob.d = 1; ob.fd(); // erori, membri privati
ob.e = 1; ob.fe();
}
Comentarii:
- clasa 'clsder' mosteneste clasa 'cls', deci are si ea membrii 'a', 'b',
'c', 'fa', 'fb', 'fc', cu aceleasi tipuri/signaturi; in plus, are si membrii
'd', 'e', 'fd', 'fe';
- membrii 'a', 'fa', fiind privati in clasa 'cls', desi exista in clasa
'clsder', nu sunt accesibili direct, nici din metodele nou adaugate, nici din
alte functii (indiferent daca sunt sau nu friend ai lui 'cls' sau 'clsder');
ei pot fi accesati doar indirect, prin metode mostenite ramase accesibile
(de ex. 'fd' apeleaza 'fb', care a ramas accesibila si care invoca 'a');
- ceilalti membrii mosteniti din clasa 'cls', adica 'b', 'c', 'fb', 'fc',
au accesibilitatea influentata de modul cum s-a facut mostenirea (privat,
protejat sau public); intucat mostenirea s-a facut public (am scris
'public cls'), ei isi pastreaza accesibilitatea din 'cls'; in particular,
ei sunt accesibili din metodele noi 'fd', 'fe', dar numai 'c' si 'fc' sunt
accesibili si din 'main' (fiind publici).
Pentru alte detalii legate de mostenire si cum afecteaza ea accesibilitatea,
a se vedea lectia ***.
De obicei, la declararea unei clase se specifica (cel mult) cate o singura
sectiune cu membri privati, protejati sau publici. Folosirea mai multor
sectiuni cu membri de aceeasi accesibilitate are ca singur avantaj acela de
a vedea grupari ale diferitelor parti ale clasei, facilitand citirea si
intelegera programului de catre alte persoane. Pentru compilator insa, nu are
importanta folosirea mai multor specificatori de acces identici.
Exemplu:
========
class complex{
private:
double re;
public:
void setre(double);
double getre();
private:
double im;
public:
void setim(double);
double getim();
};
void complex::setre(double x) {re = x;}
double complex::getre() {return re;}
cout <<
<<
<<
cout <<
cout <<
membru privat
membru privat
membru privat
afisaza: 0
afisaza: 20
}
Comentariu: daca baza de date 'clienti' ar fi fost publica, din 'main'
s-ar fi putut consulta/modifica toate conturile; asa, un cont (ex. 'Nelu')
poate fi consultat/modificat doar daca i se cunoaste parola ('456').
- Impiedicarea initializarii obiectelor cu date invalide.
Exemplu:
========
class rational{
int numarator, numitor;
public:
void set(int, int);
int get_numarator();
int get_numitor();
};
void rational::set(int pnumarator, int pnumitor){
numarator = pnumarator;
numitor = pnumitor != 0 ? pnumitor : 1;
}
int rational::get_numarator() {return numarator;}
int rational::get_numitor() {return numitor;}
int main(){
rational r;
r.numitor = 0; // eroare: membru privat
r.set(10,0); // OK, 'r' devine numarul rational 10/1
}
Comentariu: daca atributele 'numarator', 'numitor' ar fi publici, nimeni
n-ar putea impiedica utilizatorul ('main') sa seteze numitorul cu 0,
facand ca numarul rational sa fie invalid; metoda publica 'set' va
seta numarul rational numai cu date valide.
- Daca algoritmul implementat este proiectat a.i. sa se disocieze logic partea
de comunicare cu exteriorul (intrare date, furnizare rezultate) de partea de
procesare, acesta se poate implementa sub forma unei clase a.i. partea de
comunicare sa fie inclusa in metode publice de interfata iar partea de
procesare in membri privati, iar metodele de interfata nu vor reflecta in
signatura lor detaliile mecanismelor interne de procesare.
Astfel, putem imbunatati ulterior algoritmica clasei (adaugand/eliminand/
modificand membrii privati) fara sa-i modificam interfata, anume numarul sau
signatura metodelor publice (s-ar putea modifica corpul metodelor publice,
dar acest lucru nu este vizibil pentru codul care utilizeaza clasa).
Atunci, codul existent, care utiliza vechea versiune a clasi poate fi
refolosit, fara modificari, si cu noua versiune (mai performanta) a clasei.
Exemplu: consideram o implementare a clasei 'rational', inzestrata cu o
======== metoda privata de normalizare (care simplifica fractia si face
numitorul pozitiv), a.i. orice numar rational nou calculat sa fie furnizat
in forma normalizata, si un cod (functia 'main') care o foloseste:
#include<iostream>
using namespace std;
class rational{
int numarator, numitor;
void normalize();
public:
void set(int, int);
int get_numarator();
int get_numitor();
};
void rational::normalize(){
if(numarator == 0) {numitor = 1; return;}
if(numitor < 0) {numarator = -numarator; numitor = - numitor;}
int negativ;
if(numarator < 0){negativ = 1; numarator = - numarator;}
else negativ = 0;
for(int i = 2; i <= numarator && i <= numitor; )
if(numarator % i == 0 && numitor % i == 0){
numarator /= i; numitor /= i;
}else
++i;
if(negativ) {numarator = - numarator;}
}
void rational::set(int pnumarator, int pnumitor){
numarator = pnumarator;
numitor = pnumitor != 0 ? pnumitor : 1;
normalize();
}
int rational::get_numarator() {return numarator;}
int rational::get_numitor() {return numitor;}
int main(){
rational r;
r.set(-36,-60);
cout << r.get_numarator()<< "/"<<r.get_numitor()<<endl; // afisaza: 3/5
}
Metodele 'set'/'get' din interfata nu reflecta in signatura lor normalizarea
care se aplica numerelor nou create; astfel, putem imbunatati algoritmica
interna a clasei inlocuind metoda de normalizare cu o alta, mai performanta,
care aplica algoritmul lui Euclid, fara a modifica interfata; atunci, codul
care folosea vechea clasa poate fi refolosit cu noua clasa fara modificari
(functie 'main' ramane la fel):
#include<iostream>
using namespace std;
class rational{
int numarator, numitor;
void normalize();
public:
void set(int, int);
int get_numarator();
int get_numitor();
};
void rational::normalize(){
if(numarator == 0) {numitor = 1; return;}
if(numitor < 0) {numarator = -numarator; numitor = - numitor;}
int a = numarator > 0 ? numarator : - numarator,
b = numitor;
while(a != b)
if(a > b) a -= b;
else b -= a;
numarator /= a; numitor /= b;
}
void rational::set(int pnumarator, int pnumitor){
numarator = pnumarator;
numitor = pnumitor != 0 ? pnumitor : 1;
normalize();
}
int rational::get_numarator() {return numarator;}
int rational::get_numitor() {return numitor;}
int main(){
rational r;
r.set(-36,-60);
cout << r.get_numarator()<< "/"<<r.get_numitor()<<endl; // afisaza: 3/5
}
Accesibilitatea membrilor, ca si pointerul 'this', sunt tehnici prin care
limbajul C++ implementeaza caracteristica de INCAPSULARE, specifica OOP;
ca si concept de programare, incapsularea presupune doua aspecte (a se vedea
lectia 1):
- o constructie de limbaj ce faciliteaza impachetarea datelor cu operatiile
definite asupra lor, creandu-se legaturi stranse intre ele;
in C++ aceasta cerinta este atinsa prin faptul ca putem grupa date si cod
in obiecte, care apoi sunt manevrate ca un tot unitar, ca si cand ar fi
niste date obisnuite; membrii unui obiect sunt mai strans legati intre ei
decat datele/functiile independente, deoarece de exemplu metodele unui
obiect pot accesa membrii aceluiasi obiect fara specificari suplimentare,
prin utilizarea implicita a pointerului 'this' (functiilor independente
trebuie sa le comunicam explicit toate informatiile);
- un mecanism de limbaj pentru restrictionarea accesului la unele
componente ale pachetului;
in C++ aceasta cerinta este atinsa prin folosirea diverselor tipuri de
accesibilitate: privat, protejat, public.
In general, stilul de programare OOP cere sa respectam PRINCIPIUL
INCAPSULARII DATELOR: atributele sa fie private, iar accesul la ele sa
fie permis doar prin metode publice.
Exemple: clasele 'banca' si 'rational' din exemplele precedente respecta
aceasta cerinta; prezentam alte doua exemple:
Exemplu:
========
#include<iostream>
using namespace std;
class complex{
double re, im;
public:
void read();
void write();
complex operator+(complex);
};
z = x + y;
write(z);
}
Comentarii:
- Am rescris clasa 'complex' din exemplul precedent a.i. interfata
ei sa contina metode putine, dar foarte flexibile, a.i. prin combinarea lor
sa poate fi valorificata toata functionalitatea clasei: 'setre', 'setim',
'getre', getim'; operatiile cu numere complexe 'read', 'write', '+' au fost
scrise deasupra acestor metode, considerate ca primitive (operatii
elementare) - ele nu trecut de aceasta granita, ca sa acceseze direct 're',
'im'; functia 'main', care rezolva o problema de adunare, a fost scrisa
desupra acestor operatii.
- Codul este organizat astfel pe trei niveluri, fiecare avand o interfata
proprie (formata din operatii definite pe nivelul respectiv) si accesand
nivelurile inferioare doar prin interfata lor.
Acest stil de organizare a codului pe niveluri il face mai usor de
intretinut, reutilizat, depanat (putem face modificari pe un nivel fara sa
afectam celelalte niveluri, putem localiza mai usor o eroare) dar mai putin
performant (de exemplu, in functia 'operator+' instructiunea
'c.setre(a.getre() + b.getre());' face mai multe operatii decat ar fi facut
'c.re = a.re + b.re;').
Implementarea atributelor ca publice nu este in concordanta cu stilul de
programare OOP, dar uneori este utila - de exemplu, dorim ca un anumit atribut
des folosit sa fie accesibil direct (nu mediat de apelul unor metode), pentru
a obtine timpi de rulare mai buni.
TODO: exemplu
Putem permite unei functii (in particular unui operator) care nu este metoda
a unei clase sa poata invoca membrii privati si protejati ai clasei, declarand
functia respectiva ca FUNCTIE PRIETEN (FRIEND FUNCTION) a clasei; in acest
scop, scriem in cadrul declaratiei clasei (nu conteaza in care loc) prototipul
functiei precedat de cuvantul cheie 'friend'. Functia prietena va putea invoca
toate numele declarate in clasa respectiva, inclusiv nume de tipuri sau
constante ale tipurilor enumerare.
Atentie: o functie independenta declarata ca friend a unei clase nu devine
metoda a clasei respective; in consecinta:
- nu are parametrul ascuns 'this'; deci, ea nu poate invoca membrii clasei
careia ii este friend fara calificare (cu 'obiect.' sau 'clasa::', in cazul
membrilor statici).
- daca este un operator, trebuie declarat cu un numar de parametri egal cu
aritatea (nu cu aritatea - 1).
Exemplu:
========
class complex{
double re, im;
public:
void setre(double);
void setim(double);
double getre();
double getim();
friend void read(complex &);
};
void complex::setre(double x) {re = x;}
void complex::setim(double x) {im = x;}
class complex{
double re, im;
public:
friend istream& operator>>(istream&, complex&);
friend ostream& operator<<(ostream&, complex);
complex operator+(complex); // adunare complex + complex
complex operator+(double); // adunare complex + double
friend complex operator+(double, complex); // adunare double+complex
};
complex complex::operator+(complex b){
complex c;
c.re = re + b.re; c.im = im + b.im;
return c;
}
complex complex::operator+(double b){
complex c;
c.re = re + b; c.im = im;
return c;
}
complex operator+(double a, complex b){
complex c;
c.re = a + b.re; c.im = b.im;
return c;
}
istream& operator>>(istream& s, complex& c){
s >> c.re >> c.im;
return s;
}
ostream& operator<<(ostream& s, complex c){
s << c.re << "+i*" << c.im;
return s;
}
int main(){
complex x, y, z1, z2, z3; double a, b;
cin >> x >> y >> a >> b;;
z1 = x + y; z2 = x + a; z3 = b + x;
cout << z1 << ", " << z2 << ", " << z3 << endl;
}
Comentariu: Observam ca operatorul '+' intre 'double' si 'complex' nu poate fi
definit ca metoda, deoarece ar fi metoda a lui 'double', care nu este clasa
si, oricum, este un tip predefinit (nu poate fi modificat); similar, '>>' si
'<<' ar trebui adaugate ca metode claselor 'istream', 'ostream', care insa
sunt predefinite (nu pot fi modificate).
Putem transforma fortat codul de mai sus a.i. sa nu lucreze decat cu clase
si metode si sa respecte incapsularea datelor, in felul urmator (exercitiu !):
- in loc sa lucram direct cu 'double', scriem o clasa 'real' cu un membru
'double' si in care sa declaram ca metoda operatorul 'real + complex';
- cream clasele 'intrare', 'iesire', care mostenesc 'istream', respectiv
'ostream', si in care declaram ca metode operatorii 'intrare >> real',
'intrare >> complex', respectiv 'iesire << real', 'iesire << complex',
iar in loc de 'cin', 'cout' vom lucra cu doua instante ale noilor clase.
Intrucat functiile friend constituie o incalcare a principiului incapsularii,
este recomandat sa le folosim doar cand este strict necesar.
Putem declara ca friend a unei clase si o metoda a altei clase (nu a
aceleiasi clase).
Evident, aceasta functie va avea parametrul ascuns 'this', dar in legatura
cu clasa din care face parte ca metoda, nu cu clasa careia ii este friend;
asadar, ca si in cazul functiilor independente, ea nu va putea invoca
membrii clasei careia ii este friend fara calificare (cu 'obiect.' sau
'clasa::', in cazul membrilor statici).
Exemplu:
========
#include<iostream>
using namespace std;
class cls1;
class cls2{
public:
void g(int, cls1&);
};
class cls1{
int n;
static int s;
public:
void set(int);
int get();
friend void f(cls1);
friend void cls2::g(int, cls1&);
};
int cls1::s = 0;
void cls1::set(int x){n = x;}
int cls1::get(){return n;}
void cls2::g(int x, cls1 &y){
y.n = x;
cls1::s = x;
}
void f(cls1 x){
cout << x.n << " " << cls1::s << endl;
}
int main(){
cls1 ob1; cls2 ob2;
ob2.g(10, ob1);
f(ob1); // afisaza: 10 10
}
Comentarii:
- Clasa 'cls1' are ca functii friend pe 'f' (functie independenta) si pe 'g'
(metoda a clasei 'cls2'); ambele pot accesa membrii privati ai clasei
'cls1', 'n' (membru nestatic) si 's' (membru static); deoarece 'f' si 'g'
nu au parametru 'this' in legatura cu obiecte ale clasei 'cls1' ('g' are
'this' legat de clasa 'cls2' si care la apelul 'ob2.g(10, ob1)' primeste
ca valoare '&ob2') si atunci in aceste functii 'n' si 's' nu pot fi invocate
decat calificate (am calificat 'n' cu 'y.' si 's' cu 'cls1::').
- Atentie la ordinea in care am scris declaratiile: declaratia incompleta
(vida) 'class cls1' trebuie sa preceada declaratia completa (cu membri) a
sah pion::joc;
- Prietenia nu se mosteneste:
* o functie sau clasa friend a clasei de baza nu este automat friend a
clasei derivate; la fel si viceversa;
* daca clasa de baza este friend a altei clase, clasa derivata nu este
automat friend a acelei clase; la fel si viceversa;
- Accesul in baza prieteniei se mosteneste: un friend al clasei derivate
poate invoca membrii mosteniti din clasa de baza, dar numai pe aceia care
sunt accesibili direct in metodele noi adaugate (i.e. membrii care in
clasa de baza nu au fost privati)
Alte restrictii legate de functiile friend:
- O functie friend nu poate fi declarata 'static' sau 'extern'.
III. Membri statici
===================
O instanta a unei clase consta dintr-o colectie de instante pentru membrii
clasei.
Putem declara un membru (atribut sau metoda) a unei clase ca fiind STATIC,
daca precedam declaratia sa cu cuvantul cheie 'static'; altfel, este NESTATIC.
- Un membru nestatic este asociat unei instante a clasei (obiect);
in particular:
* un atribut nestatic are instante diferite (locatii diferite) in obiecte
diferite; aceste instante apar odata cu obiectele respective, deci
definitia atributului este parte a definitiei obiectelor;
declaratia atributului in cadrul declaratiei clasei nu poate contine
initializari;
* o metoda nestatica are parametrul ascuns 'this', prin care se poate referi
la obiectul care a apelat-o si la membrii sai;
* membrii nestatici pot fi invocati fara precizarea obiectului doar din
metodele nestatice ale aceleiasi clase;
intr-o metoda nestatica putem invoca fara precizarea obiectului orice
membru al clasei respective (iar el va fi calificat implicit cu 'this->',
deci va fi considerat de la obiectul apelant);putem invoca de asemenea
direct numele tipurilor si constantelor de enumerare si ale altor tipuri
incuibate in clasa.
- Un membru static este asociat clasei, dar nu unei instante (obiect) anume el este comun tuturor instantelor clasei; in particular:
* un atribut static are o instanta comuna (o aceeasi locatie) pentru toate
obiectele clasei; aceasta instanta apare inainte de a exista obiecte ale
clasei, astfel incat atributul necesita o definitie separata, scrisa in
afara declaratiei clasei (aceasta definitie fiind in afara domeniului
clasei, numele membrului trebuie invocat cu precizarea domeniului
'clasa::'); definitia trebuie sa fie globala la nivelul fisierului
(file scope);
declaratia atributului in cadrul declaratiei clasei poate contine
initializari doar daca atributul este si 'const'; definitia exterioara
a atributului poate contine initializari; in absenta initializarii, el
este initializat cu o valoare implicita (ex. 0 pentru numere);
* o metoda statica nu are parametrul ascuns 'this';
* membrii statici pot fi invocati fara precizarea obiectului din orice
metoda a clasei si din functiile care nu sunt metode ale acesteia, in
ultimul caz trebuind insa precizat domeniul lor cu 'clasa::';
cout
<< i << " " << cls::i << " " << this -> i << " " << this -> cls::i << " "
<< j << " " << cls::j << " " << this -> j << " " << this -> cls::j << " "
<< a.i << " " << a.cls::i << " " << a.j << " " << a.cls::j << endl;
// afisaza: 4 4 4 4 2000 2000 2000 2000 200 200 2000 2000
// deoarece 'i' are locatii diferite pentru obiectul curent si 'a'
// iar 'j' are aceeasi locatie pentru obiectul curent si 'a'
}
void cls::s(){
cls a;
i = 11; cls::i = 22; f(); cls::f();
// erori: 'i' si 'f' sunt nestatici (deci depind de obiect)
// iar 's' este statica (deci nu are un obiect curent)
this -> i = 33; this -> cls::i = 44; this -> f(); this -> cls::f();
this -> j = 55; this -> cls::j = 66; this -> g(); this -> cls::g();
// erori: 's' este statica, deci nu are 'this'
j = 77; cls::j = 88; g(); cls::g();
// 'j' si 'g' comune tuturor obiectelor lui 'cls';
// fiind statice (deci nu depind de obiect), se pot invoca din metode
// statice (care n-au obiect curent) fara precizarea unui obiect
a.i = 110; a.cls::i = 220; a.f(); a.cls::f();
// 'i' si 'f' ale obiectului 'a'
a.j = 330; a.cls::j = 440; a.g(); a.cls::g();
// 'j' si 'g' comune tuturor obiectelor lui 'cls'
cout
<< j << " " << cls::j << " "
<< a.i << " " << a.cls::i << " " << a.j << " " << a.cls::j << endl;
// afisaza: 440 440 220 220 440 440
// deoarece 'i' are locatii diferite pentru obiectul curent si 'a'
// iar 'j' are aceeasi locatie pentru obiectul curent si 'a'
}
int main(){
cls b, x;
i = 111; f(); j = 222; g();
// erori: 'i', 'j', 'f', 'g' sunt nume necunoscute in acest
// domeniu (scope)
cls::i = 333; cls::f();
// erori: 'i' si 'f' sunt nestatici (deci depind de obiect)
// iar 'main' nu este metoda a lui 'cls' (deci nu are un obiect
// al lui 'cls' curent)
this -> i = 444; this -> cls::i = 555; this -> f(); this -> cls::f();
this -> j = 666; this -> cls::j = 777; this -> g(); this -> cls::g();
// erori: 'main' nu este metoda a lui 'cls', deci nu are 'this'
cls::j = 888; cls::g();
// 'j' si 'g' comune tuturor obiectelor lui 'cls';
// fiind statice (deci nu depind de obiect), se pot invoca din functii
// ne-metode ale lui 'cls' (care n-au un obiect al lui 'cls' curent)
// fara precizarea unui obiect, dar trebuie precizat domeniul lor,
// 'cls::'
b.i = 1110; b.cls::i = 2220; b.f(); b.cls::f();
// 'i' si 'f' ale obiectului 'b'
al obiectului curent
comun obiectelor lui 'der'
'i' mostenit, al obiectului curent
'j' mostenit, comun obiectelor lui
'der' si obiectelor lui 'baz'
int main(){
baz ob1, ob2; der od1, od2;
ob1.f(1); ob2.f(10); od1.g(100); od2.g(1000);
cout << ob1.i << " " << ob2.i << " "
<< od1.i << " " << od1.baz::i << " "
<< od2.i << " " << od2.baz::i << " "
<< baz::j << " " << der::j << " " << der::baz::j << endl;
// afisaza: 2 20 200 600 2000 6000 8000 4000 8000
// intr-adevar, in program avem 8 locatii diferite de tip 'int':
// - 'ob1.i', 'ob2.i', 'od1.i', 'od1.baz::i', 'od2.i', 'od2.baz::i',
// 6 locatii diferite;
// - 'ob1.j', 'ob2.j', 'od1.baz::j', 'od2.baz::j', locatii identice;
// - 'od1.j', 'od2.j', locatii identice;
}
Exemplu:
========
#include<iostream>
using namespace std;
class baz{
public:
int i; static int j;
};
// 'baz' are:
// - un 'i', cu locatii diferite pentru obiecte diferite;
// - un 'j', cu locatii comune pentru obiecte diferite;
class der: public baz{
public:
static int i; int j;
};
// 'der' are:
// - doi 'i' (unul mostenit, unul nou);
//
'i' mostenit are locatii diferite in obiecte diferite
//
(indiferent daca sunt obiecte ale lui 'baz' sau ale lui 'der');
//
'i' nou are locatii comune pentru obiectele lui'der' si nu este
//
prezent in obiectele lui 'baz';
// - doi 'j' (unul mostenit, unul nou);
//
'j' mostenit are locatii comune pentru obiectele lui 'der'; aceasta
//
locatie si locatia lui 'j' cel declarat in clasa 'baz', comuna
//
obiectelor lui 'baz', coincid;
//
'j' nou are locatii diferite pentru obiecte ale lui 'der' diferite si
//
nu este prezent in obiectele lui 'baz';
int baz::j, der::i; // definitiile membrilor statici
int main(){
baz b1, b2; der d1, d2;
b1.i = 1; b1.j = 2; b2.i = 3; b2.j = 4;
=
=
=
=
=
=
C::f();
C::i;
c.f();
c.j;
c.a;
1;
//
//
//
//
//
//
initializare
initializare
initializare
initializare
initializare
initializare
class Y : private C {} y;
int C::m = Y::f();
int C::n = Y::r;
// eroare
// eroare
cu
cu
cu
cu
cu
cu
o metoda statica
un alt atribut static
o metoda a unui obiect
un atribut al unui obiect
un atribut nestatic al unui obiect
o valoare constanta
// eroare
// eroare
Comentarii:
- In expresiile de initializare 'C::f()', 'C::i', 'c.f()', 'c.j', 'c.a'
sunt invocati membri privati ai clasei 'C', desi ele nu sunt scrise
intr-o metoda a lui 'C'.
- Cele patru erori din final apar ca urmare a mostenirii private; daca
scriam 'class Y : public C {} y', atunci membrii lui 'Y' erau accesibili
membrilor lui 'C' si nu se mai semnala eroare.
Daca un atribut static este de un tip intreg 'const' sau enumerare 'const',
se poate specifica un initializator constant chiar in declaratia membrului din
cadrul declaratiei clasei. Acest initializator constant trebuie sa fie o
expresie care produce o valoare intreaga constanta. Initializatorul constant
nu este o definitie, atributul trebuie in continuare definit in afara clasei
cu 'clasa::'.
Nota: Teste efectuate cu gcc/g++ versiunea 4.5.1 20101208 sub Linux au
aratat ca tipul atributului poate fi si altul decat intreg sau enumerare
(ex. 'const double'), iar daca am specificat un initializator in declaratia
din clasa, putem omite definitia exterioara; nu putem insa specifica
initializatori si in declaratie si in definitie.
Atributele statice 'const' TREBUIE sa aiba initializare (fie indeclaratia
din clasa, fie in definitia exterioara).
Exemplu:
========
#include<iostream>
using namespace std;
class c0{public: static const int n;};
const int c0::n;
// eroare: constanta neinitializata 'c0::n'
class c1{public: static const int n = 1;};
const int c1::n;
class c2{public: static const int n;};
const int c2::n = 2;
class c3{public: static const int n = 3;};
const int c3::n = 3;
// eroare: initializare duplicata pentru 'c3::n'
class c4{public: static const int n = 4;};
class c{public: static const double n;};
const double c::n = 3.14;
int main() {
cout << c1::n << " " << c2::n << " " << c4::n << " " << c::n << " " << endl;
// afisaza: 1 2 4 3.14
}
Una dintre utilizarile obisnuite ale membrilor statici este de a asigura
controlul accesului mai multor obiecte la unele resurse comune. Metodele
statice se pot apela chiar inainte de a exista obiecte ale clasei respective
int main(){
scriitori::init("f.txt");
scriitori s1('A'), s2('B'), s3('C');
const char *ns[] = {"s1", "s2", "s3"};
// vector cu numele variabilelor-obiect
const char *na[] = {"ocupe fisierul", "scrie", " elibereze"};
// vector cu numele activitatilor desfasurate de metode
scriitori *s[] = {&s1, &s2, &s3};
// vector de pointeri la obiecte ale clasei 'scriitori',
// initializat cu adresele obiectelor 's1', 's2', 's3'
bool (scriitori::*p[]) ()
= {&scriitori::ocupa, &scriitori::scrie, &scriitori::elibereaza};
// vector de pointeri la metode ale clasei 'scriitori',
// initializat cu adresele metodelor 'ocupa', 'scrie', 'elibereaza'
int i,j; // variabile de lucru
srand(time(NULL));
// initializeaza generatorul de nr. pseudoaleatoare furnizate de 'rand'
// cu seed-ul dat de momentul curent, furnizat de 'time' ca nr. de
// secunde care au trecut de la 1 ianuarie 1970, ora 00:00 UTC
for(int k = 0; k < 10; ++k){
i = rand() % 3; j = rand() % 3;
if((s[i] ->* p[j]) ())cout << ns[i] << " reuseste sa " << na[j] << endl;
else cout << ns[i] << " incearca sa " << na[j] << endl;
}
// 'rand' genereaza urmatorul nr. pseudoaleator din secventa,
// in intervalul 0 .. RAND_MAX
scriitori::final();
}
La rulare, o evolutie posibila este:
s2
s2
s2
s1
s3
s1
s1
s3
s1
s2
reuseste
reuseste
reuseste
reuseste
incearca
reuseste
reuseste
incearca
reuseste
reuseste
sa
sa
sa
sa
sa
sa
sa
sa
sa
sa
ocupe fisierul
scrie
elibereze
ocupe fisierul
elibereze
scrie
scrie
scrie
elibereze
ocupe fisierul
inversa crearii.
- Un atribut nestatic descrie o variabila care este parte a unui obiect al
clasei.
El se instantiaza la fiecare instantiere a clasei, instanta sa fiind parte
a obiectului respectiv; de aceea, definitia sa este parte a definitiilor
obiectelor.
O instanta a unui atribut nestatic are aceeasi clasa de alocare (storage
class) ca si obiectul in care a fost creata: statica, automatica, dinamica,
etc.
La crearea unui obiect, instantele membrilor sai nestatici sunt create in
ordinea in care au fost declarati in clasa; la distrugerea obiectului,
ei sunt distrusi in ordinea inversa crearii lor.
Discutia de mai sus este valabila si in cazul atributelor care sunt masive
(vectori, matrici, etc.), doar ca in acest caz numele atributului desemneaza o
adresa, nu o locatie.
Atributele pot fi non-obiecte (de ex. 'int', 'double', etc.) sau obiecte (de
diverse clase).
Instantierea unui atribut non-obiect presupune doar alocarea unei locatii
si initializarea cu o valoare; aceasta valoare poate fi una implicita sau
una specificata in cadrul definitiei atributului:
- In cazul atributelor non-obiect statice, valoarea initiala se poate
specifica printr-o expresie de initializare scrisa in definitia exterioara
clasei:
<Tip> <Nume clasa>::<Nume atribut> = <Expresie>;
Daca expresia lipseste:
<Tip> <Nume clasa>::<Nume atribut>;
atributul static primeste implicit valoarea nula.
- In cazul atributelor non-obiect nestatice, valoarea initiala se poate
specifica printr-o expresie de initializare in cadrul unei liste de clauze
de initializare scrisa in definitia obiectului (daca este o structura
agregat):
<Nume clasa> <Nume obiect> = {<Expresie 1>, ..., <Expresie n>};
sau in listele de initializatori ale constructorilor clasei:
<Nume clasa>::<Nume clasa> (<Parametri formali>):
<Initializator 1>, ..., <Initializator atribut>, ...,<Initializator n>
{<corp>}
unde <Initializator atribut> poate fi:
<Nume atribut>(<Expresie>)
Obiectul poate fi definit fara lista de clauze de initializare:
<Nume clasa> <Nume obiect>;
De asemenea, unii dintre constructorii clasei pot sa nu aiba initializatori
pentru toate atributele nestatice:
Exemplu:
========
#include<iostream>
using namespace std;
class cls1{
public:
int i, j;
static int s,t;
};
int cls1::s, cls1::t = 1; // 'cls1::s' primeste implicit valoarea 0
cls1 a, b = {10, 20};
// 'a' si 'b' sunt stocat static;
// 'a.i','a.j' primesc implicit valoarea 0 ('a' fiind stocat static);
// 'b.i' primeste valoarea 10, 'b.j' primeste valoarea 20;
class cls2{
public:
int i,j;
cls2(int);
cls2(int, int);
};
cls2::cls2(int p): i(100), j(200) {}
cls2::cls2(int p, int q): i(1000) {} // 'j' primeste o valoare implicita
// puteam scrie: cls2::cls2(int p, int q) {i = 1000;}
// si atunci mai intai 'i' si 'j' ar fi primit o valoare implicita,
// apoi 'i' devenea 1000 (iar 'j' ramanea cu valoarea implicita)
cls2 c(123), d(123, 456);
// 'c' si 'd' sunt stocate static;
// lui 'c' i se aplica constructorul 'cls2(int)', deci 'c.i' primeste
// valoarea 100 iar 'c.j' primeste valoarea 200
// lui 'd' i se aplica constructorul 'cls2(int, int)', deci 'c.i' primeste
// valoarea 1000 iar 'c.j' primeste implicit valoarea 0 ('d' fiind
// stocat static)
int main(){
cls1 x;
// 'x' este stocat automatic; ca atare, in lipsa clauzelor de
// initializare, 'x.i' si 'x.j' au implicit valoarea aflata pe
// stiva in momentul alocarii
cls2 y(12, 34);
// 'y' este stocat automatic si i se aplica constructorul
// 'cls2(int ,int)'; ca atare, 'y.i' primeste valoarea 10000
// iar 'y.j' are implicit valoarea aflata pe stiva in momentul alocarii
cout << cls1::s << " " <<
<< a.i << " " << a.j
<< b.i << " " << b.j
<< x.i << " " << x.j
<< c.i << " " << c.j
<< d.i << " " << d.j
<< y.i << " " << y.j
// afisaza: 0 1 0 0 10 20
}
Exemplu:
========
#include<iostream>
using namespace std;
class clsob{
public:
int n;
clsob();
clsob(int);
clsob(int, int);
};
clsob::clsob(): n(1) {}
clsob::clsob(int p): n(p) {}
clsob::clsob(int p, int q) {} // 'n' primeste o valoare implicita
class cls{
public:
clsob x, y;
cls();
cls(int);
cls(int, int);
};
cls::cls(): x(10), y(20, 30) {}
cls::cls(int p): x(40) {}
// echivalent cu a scrie:
// cls::cls(int p): x(40), y() {}
cls::cls(int p, int q) {}
// echivalent cu a scrie:
// cls::cls(int p): x(), y() {}
cls
//
//
//
//
//
//
//
//
//
//
//
//
int main(){
cout << a.x.n <<
<< b.x.n <<
<< c.x.n <<
// afisaza: 10 0
}
decat daca declaram explicit unul. Pentru alte detalii, a se vedea lectia ***.
Exemplu:
========
#include<iostream>
using namespace std;
class clsob{public: int k; clsob(int); ~clsob();};
clsob::clsob(int n): k(n) {cout << "Constructor " << k << endl;}
clsob::~clsob() {cout << "Destructor " << k << endl;}
class cls2{public: static clsob n2;};
class cls1{public: static clsob n1;};
clsob a(0);
clsob cls1::n1(1);
clsob cls2::n2(2);
class cls3{
public:
clsob i,j;
cls3(int);
cls3(int, int);
};
cls3::cls3(int p): i(10), j(20) {}
cls3::cls3(int p, int q): j(200), i(100) {}
int main(){
cout << "Incepe main\n";
clsob b(11);
cls3 c(11), d(22, 33);
}
Programul afisaza:
Constructor 0
Constructor 1
Constructor 2
Incepe main
Constructor 11
Constructor 10
Constructor 20
Constructor 100
Constructor 200
Destructor 200
Destructor 100
Destructor 20
Destructor 10
Destructor 11
Destructor 2
Destructor 1
Destructor 0
Comentarii:
- Variabilele stocate static se creaza in ordinea in care au fost definite:
intai 'a', apoi 'cls1::n1', apoi 'cls1::n2' (chiar daca clasele lui 'n1'
si 'n2' au fost declarate in ordine inversa); crearea lor se face inainte
de a se executa 'main'; ele se distrug dupa terminarea lui 'main' in
ordinea inversa crearii.
- Atributele nestatice ale lui 'cls3' se instantiaza la crearea obiectelor
'b' si 'c' in ordinea declararii lor in clasa: intai 'i', apoi 'j',
indiferent de ordinea in care au fost invocati in lista de initializatori
ale constructorilor; ele se distrug la distrugerea obiectelor gazda,
in ordinea inversa crearii.
V. Membri 'const' si atribute 'mutable'
=======================================
V.1. Constante, pointeri la zone constante, pointeri constanti
==============================================================
O CONSTANTA este o data read-only (se poate consulta, nu se poate modifica);
ea se poate declara ca o variabila obisnuita, masiv, etc., a carui tip de
baza este insotit de cuvantul cheie 'const' ('const tip' sau 'tip const').
In legatura cu o constanta sunt permise doar operatiile de consultare, nu si
cele de modificare (ex. '++').
De aceea, constanta este initializata la crearea sa (prin definitia sa) si
va pastra valoarea respectiva pe toata durata existentei sale; in particular:
- Daca constanta este non-obiect, definitia sa trebuie sa contina o
initializare de forma:
<Tip> const <Nume_constanta> = <Valoare>;
sau:
<Tip> const <Nume_constanta>(<Valoare>);
si atunci constanta va avea valoarea respctiva; initializarea nu poate lipsi.
- Daca constanta este obiect (deci la creare i se aplica automat un
constructor), definitia sa poate fi insotita de o liste de clauze de
initializare intre '{}' (daca clasa obiectului este o clasa agregat):
<Nume clasa> const <Nume_constanta> = {<Expresie 1>, ..., <Expresie n>};
(clauzele se refera la atributele nestatice) sau de o lista de parametri
actuali intre '()' care sa permita selectarea constructorului (dupa regulile
generale de la supraincarcarea functiilor):
<Nume clasa> const <Nume_constanta> (<Parametri actuali>);
sau echivalent, cand exista un singur parametru actual:
<Nume clasa> const <Nume_constanta> = <Parametru actual>;
Initializarea poate lipsi:
<Nume clasa> const <Nume_constanta>;
doar daca clasa are un constructor implicit (i.e. fara parametri sau cu toti
cls1 const a;
// eroare, constanta neinitializata
// intr-adevar, nu exista clauze intre '{}' iar clasa nu are constructor
// implicit definit de utilizator (are un constructor implicit generat
// automat de compilator, dar nu este suficient)
cls1 const b = {2, 3}, c = {4};
// OK, exista clauze intre '{}';
// 'b.i' si 'b.j' primesc valorile date explicit 2, respectiv 3;
// 'c.i' primeste valoarea data explicit 4,
// 'c.j' primeste valoarea implicita conform clasei de alocare a lui 'd',
//
anume 0 ('c' are storage class static)
cls1 const d[] = {{5},{6, 7},{8}}, e[3] = {{9, 10}}, f[3] = {};
// OK, se definesc 9 obiecte, atributele lor nestatice pentru care nu
// exista clauze de initializare primesc valoarea implicita 0 (cele 9
// obiecte au storage class static)
cls1 const g[3];
// eroare, constanta neinitializata,
// deoarece nu exista nici lista de clauze de initializare intre '{}',
// nici un constructor implicit definit de utilizator in 'cls1'
// (exista unul implicit generat automat de compilator, dar nu este
// suficient)
class cls2{
public:
int n;
cls2();
cls2(int);
};
// clasa non agregat,
// nu se poate instantia cu clauze de initializare intre '{}'
cls2::cls2(){n = 1;}
cls2::cls2(int x){n = x;}
cls2 const h, x(11), y[3];
// OK in toate cele trei cazuri;
// pentru 'h' si componentele lui 'y' se foloseste constructorul implicit
// 'cls2()' definit de utilizator (care asigneaza atributul 'n' cu
// valoarea 1), iar pentru 'x' constructorul 'cls2(int)'
int main(){
c.i = 100; e[1].i = 200; x.n = 300; y[1].n = 400;
// erori, 'c.i', 'e[1].i', 'x.n', 'y[1].n' sunt read-only
int k;
cout << b.i << " " << b.j << endl
<< c.i << " " << c.j << endl;
// afisaza: 2 3
//
4 0
for(k = 0; k < 3; ++ k) cout << d[k].i << " " << d[k].j << endl;
// afisaza: 5 0
//
6 7
//
8 0
for(k = 0; k < 3; ++ k) cout << e[k].i << " " << e[k].j << endl;
// afisaza: 9 10
//
0 0
//
0 0
for(k = 0; k < 3; ++ k) cout << f[k].i << " " << f[k].j << endl;
// afisaza: 0 0
//
0 0
//
0 0
cout << h.n << endl << x.n << endl;
// afisaza: 1
//
11
for(k = 0; k < 3; ++ k) cout << y[k].n << endl;
// afisaza: 1
//
1
//
1
}
Comentarii:
- acest exemplu ne arata ca in definitia obiectelor/masivelor de obiecte
constante:
* daca exista lista de clauze de initializare intre '{}', chiar daca ea
nu specifica valori pentru toate atributele nestatice si/sau toate
obiectele, nu se semnaleaza eroare iar atributele nestatice omise primesc
valoarea implicita conform storage class-ului obiectelor;
* daca nu exista lista de clauze de initializare intre '{}' dar exista
in clasa un constructor implicit definit de utilizator, nu se semnaleaza
eroare iar pentru obiectul/componentele masivului de obiecte respectiv
se foloseste acest constructor;
* eroare se semnaleaza daca nici nu exista lista de clauze de initializare
intre '{}', nici nu exista in clasa un constructor implicit definit de
utilizator.
- daca defineam constructorul implicit al clasei 'cls2' astfel:
cls2::cls2(){}
(cu alte cuvinte nu precizam pe nici o cale valoarea lui 'n'), atributul
'n' al obiectelor 'h', 'y[0]', 'y[1]', 'y[2]' ramanea cu valoarea
implicita conform clasei de alocare a lui 'h' si 'y', anume 0 ('h' si 'y'
au storage class static); daca in clasa 'cls2' eliminam cu totul
constructorul implicit definit de utilizator, la 'h' si 'y' se semnala
eroare (constanta neinitializata).
Exemplu:
=======
#include<iostream>
using namespace std;
class cls{
public:
static int i;
int j;
};
int cls::i = 1;
int main(){
cls const ob = {10};
// clauzele de initializare se refera la atributele nestatice
cout << ob.i << " " << ob.j << endl; // afisaza: 1 10
ob.i = 2; // OK, chiar daca 'ob' este constant, 'i' este alta variabila
ob.j = 20; // eroare, 'ob.j' este read-only
cout << ob.i << " " << ob.j << endl; // afisaza: 2 10
}
Comentarii:
- Clauzele de initializare intre '{}' privesc atributele nestatice; cele
statice, care sunt variabile de sine statatoare, sunt initializate separat,
in definitia lor.
const*',
mod read-only
read-only
read-only
r[2] = &b;
cout << *r[2] << endl; // afisaza: 1
// '[]' este prioritar fata de '*', deci nu sunt necesare paranteze
// suplimentare
f(&a);// afisaza: 20
}
Comentariu: constatam ca lui 'q', 'r[2]', 'x' le putem schimba valoarea, dar
zonei accesate prin '*q', '*r[2]', '*x' nu; daca zona accesata nu este ea
insasi declarata ca o constanta, ii putem schimba valoarea, insa pe alta
cale decat prin intermediul pointerului constant (de exemplu a mers
'a = 20;', dar nu si '*q = 30;', desi era vorba de aceeasi zona).
Un POINTER CONSTANT este un pointer read-only (se poate consulta, nu se poate
modifica); el se poate declara ca un pointer obisnuit, folosind insa
operatorul declarativ '* const' in loc de '*'; el este similar constantelor
tratate mai sus, in particular trebuie initializat in definitia sa, iar
ulterior nu i se poate schimba valoarea (pointeaza o aceeasi zona pe toata
durata existentei sale).
Zona pointata de un pointer constant nu este neaparat read-only (decat daca
l-am definit ca pointer constant catre zona constanta), cu alte cuvinte desi
nu puteam schimba valoarea pointerului (nu il putem face sa pointeze o alta
zona), putem modifica zona pointata prin intermediul lui.
Exemplu:
========
#include<iostream>
using namespace std;
int a, b;
// non constante
int const c = 1; // constanta
int * const p, * const v[3];
// erori, constante neinitializate
int * const q = &c;
// eroare, nu este permisa conversia de la 'const int*' (adresa lui 'c')
// la 'int*' (tipul constantei 'q')
int * const r = &a, * const w[3] = {&a, &b};
// 'r' este pointer constant la 'int', initializat cu adresa lui 'a'
// 'w' este vector de pointeri constanti la 'int', ale carui componente
// sunt initializate cu respectiv adresa lui 'a', adresa lui 'b', NULL
// (ultima fiind valoare implicita pentru storage class-ul static)
void f(int * const x){
x = &a; // eroare, 'x' este read-only
*x = 10;
cout << *x << endl;
}
int main(){
r = &a;
// eroare, 'r' este read-only
*r = 20;
// 'a' primeste valoarea 20
w[1] = &a; // eroare, 'w[1]' este read-only
*w[1] = 30; // 'b' primeste valoarea 30
cout << a << " " << b << " "
<< *r << " " << *w[0] << " " << *w[1] << endl;
// afisaza: 20 30 20 20 30
cout << *w[2] << endl;
// la executare poate provoaca terminarea anormala a programului,
// deoarece 'w[2]' are valoarea NULL
f(&a); // afisaza: 10
f(&b); // afisaza: 10
}
};
cls::cls():j(1){}
int cls::* p;
// 'p' este pointer la membru al clasei 'cls' de tip 'int'
int const cls::* q;
// 'q' este pointer la membru al clasei 'cls' de tip 'int' constant
int cls::* const r = &cls::i;
// 'r' este pointer constant la membru al clasei 'cls' de tip 'int';
// in particular, trebuie initializat la definire si nu merge cu
// '&cls::j' (nu este permisa conversia de la 'const int cls::*'
// la 'int cls::*'
int const cls::* const t = &cls::j;
// 'r' este pointer constant la membru al clasei 'cls' de tip 'int'
// constant;
// in particular, trebuie initializat la definire si merge si cu
// '&cls::i' (este permisa conversia de la 'int cls::*' la
// 'const int cls::*')
Pentru a intelege mai usor o declaratie in care apare de mai multe ori
'*' si 'const', reamintim urmatoarele reguli de formare si citire a unei
declaratii, valabila si in limbajul C:
- o declaratie are forma:
<Tip> <Expresie declarativa 1>, ..., <Expresie declarativa n>;
(daca dorim definitii, expresiile declarative pot fi insotite de
initializatori);
- fiecare <Expresie declarativa> declara un singur nume, folosind operatorii
declarativi unari '*' (pointer, se pune in stanga operandului),
'[dimensiune]' (vector, se pune in dreapta operandului), '(parametri)'
(functie, se pune in dreapta operandului);
ordinea prioritatilor acestora este: '*' < '[]' < '()';
se pot folosi paranteze pentru a modifica ordinea implicita de asociere pe
baza prioritatilor;
- declaratia se citeste/interpreteaza concentric, de la numele declarat spre
exterior, aplicand operatorii declarativi in ordinea intalnirii; daca se
intalnesc doi simultan (unul in stanga si unul in dreapta) se aplica in
ordinea descrescatoare a prioritatilor;
- nu sunt permise: vector de functii, functie ce returneaza vectori, functie
ce returneaza functii;
- nu este permisa mixarea definitiilor de functii (antet + corp) cu alte
declaratii/definitii; putem mixa insa declaratii de functii (doar prototip)
cu alte declaratii/definitii;
Un caz particular de 'Tip' este cel de forma 'Tip const'; cazuri particulare
de '*' (pointeri) sunt cele de forma '* const', 'clasa::*', 'clasa::* const'.
Exemplu:
========
int *a;
int const *a;
int * const a = &x;
}
V.2. Atribute constante
=======================
O clasa poate avea atribute constante; ele sunt read-only si se declara
asemanator atributelor obisnuite, dar cu 'tip const' sau 'const tip'.
Atributele constante sunt initializate la instantierea lor (prin definitia
lor) iar instantele vor pastra valoarea respectiva pe toata durata existentei
lor; in particular:
- Daca atributul constant este static, declaratia sa din clasa sau definitia
sa exterioara clasei (nu ambele) trebuie sa contina initializarea; scrierea
este asemanatoare celei pentru constante care nu sunt atribute (a se vedea
inceputul sectiunii V.1), mai exact (ilustram cazul definitiei exterioare):
* daca atributul constant static este non-obiect, definitia va fi:
<Tip> const <Nume clasa>::<Nume atribut> = <Valoare>;
sau:
<Tip> const <Nume clasa>::<Nume atribut>(<Valoare>);
in declaratia interioara clasei nu se poate folosi varianta cu
paranteze: (<Valoare>);
* daca atributul constant static este obiect, definitia va fi:
<Nume clasa atribut> const <Nume clasa>::<Nume atribut>
= {<Expresie 1>, ..., <Expresie n>};
(daca clasa obiectului atribut este structura agregat)
sau (pentru a selecta un constructor adecvat):
<Nume clasa atribut> const <Nume clasa>::<Nume atribut>
(<Parametri actuali>);
sau echivalent, cand exista un singur parametru actual:
<Nume clasa atribut> const <Nume clasa>::<Nume atribut>
= <Parametru actual>;
in declaratia interioara clasei nu se poate face initializarea;
* initializarea atributului constant static poate lipsi atat din declaratia
interioara clasei cat si din definitia exterioara, in una din urmatoarele
situatii:
** atributul respectiv nu va fi accesat (nici prin 'atribut', din metodele
clasei, nici prin 'obiect.atribut' sau 'clasa::atribut'); atunci
definitia exterioara poate chiar lipsi cu totul;
** atributul este obiect iar clasa sa are un constructor implicit (i.e.
fara parametri sau cu toti parametrii impliciti) definit de utilizator
(cel generat automat de compilator nu este suficient);
atunci vom scrie:
<Nume clasa atribut> const <Nume atribut>;
i = x; f();
cls a; a.j = x; a.g1(); a.g2();
// intr-o metoda statica putem accesa membrii statici si membrii
// const si non const ai unor obiecte mentionate explicit;
// metodele statice nu au 'this';
}
void h() const {} // eroare, functiile independente nu pot fi const
int main(){ob.test1(10); ob.test2(20); ob.test3(30);}
Obiectelor constante nu li se pot apela metodele nestatice neconstante (ci
doar cele nestatice constante si cele statice).
Inr-adevar, aceste metode au 'this' de tipul 'clasa * const' iar adresa
obiectului constant este de tip 'clasa const * const'.
Exemplu:
========
#include<iostream>
using namespace std;
class cls{
public:
static int i;
int j;
static void f();
void g1();
void g2() const;
};
int cls::i;
void cls::f(){}
void cls::g1() {}
void cls::g2() const {}
cls const ob = {10}; // se initializeza doar 'j' (care este nestatic)
int main(){
ob.i = 1; cout << ob.i << endl;
// OK, 'i' este independent de obiectele lui 'cls' si nu este 'const';
// afisaza: 1
ob.j = 2; // eroare, 'ob.j' este read-only (deoarece 'ob' este 'const')
cout << ob.j << endl; // Ok, afisaza: 10
ob.f(); // OK, 'f' este static
ob.g1(); // eroare, 'ob' este 'const' iar 'g1' este nestatica neconstanta
ob.g2(); // OK
}
V.4. Atribute mutabile
======================
Atributele mutabile se specifica astfel la declarare insotind tipul lor de
baza de cuvantul cheie 'mutable'.
Un atribut mutabil poate fi modificat chiar daca este parte a unui obiect
read-only (de exemplu declarat ca o constanta sau accesat printr-un pointer
la zona constanta); in particular, metodele constante pot modifica atributele
mutabile invocate fara precizarea obiectului (i.e. accesate la obiectul
curent prin 'this').
class cls{
...
} a, b;
...
a = x; // operator de atribuire
a = b; // operator de atribuire prin copiere
Pentru orice clasa definita de utilizator, daca programatorul nu a declarat
explicit un operator de atribuire prin copiere, compilatorul genereaza automat
unul, numit OPERATOR DE ATRIBUIRE PRIN COPIERE IMPLICIT (DEFAULT COPY
ASSIGNMENT OPERATOR), a.i. pentru orice doua obiecte ale clasei 'a', 'b' va
avea sens automat expresia 'a = b'.
Daca clasa este 'cls', operatorul de atribuire prin copiere implicit este o
metoda nestatica inline publica cu signatura:
cls& cls::operator= (cls const &);
care copiaza membru cu membru (memberwise) atributele nestatice ale
obiectului sursa (dat ca parametru) in cele ale obiectului destinatie
(obiectul curent). Pentru atributele nestatice non obiect se foloseste
copierea bit cu bit; pentru cele obiect, se foloseste operatorul lor propriu
de atribuire prin copiere, care poate fi cel implicit sau unul definit de
utilizator; de asemenea, daca 'cls' mosteneste alte clase, operatorul de
atribuire prin copiere implicit apeleaza operatorii de atribuire prin copiere
ai claselor de baza (care pot fi cei impliciti sau unii definiti de
utilizator), pentru a copia partea mostenita. Operatorii de atribuire prin
copiere ai claselor de baza se apeleaza in ordinea declararii acestor clase
in lista claselor mostenite si inaintea operatorilor de atribuire prin
copiere ai atributelor nestatice obiect; operatorii de atribuire prin copiere
ai atributelor nestatice obiect se apeleaza in ordinea declararii acestor
atribute in clasa. In final, operatorul de atribuire prin copiere implicit
returneaza prin referinta obiectul curent.
Daca toata informatia proprie unui obiect se afla in locatia sa, operatorul
de atribuire prin copiere implicit este satisfacator (duplica informatia in
concordanta cu sensul intuitiv al notiunii de copiere). Daca o parte din
aceasta informatie se afla in alta parte (de exemplu in niste zone alocate
dinamic si doar pointate din locatia obiectului, sau in niste fisiere iar in
locatia obiectului se retin descriptorii acestora), operatorul implicit nu
mai este satisfacator intotdeauna si trebuie folosit unul definit de
utilizator (obtinut prin supraincarcarea explicita a lui '='), care sa faca
o 'copiere in profunzime' (deep copy).
Operatorul '=' se poate supraincarca doar ca metoda nestatica a unei clase
(pentru detalii privind supraincarcarea operatorilor, a se vedea lectia ***).
Daca intr-o clasa am scris un operator '=' prin copiere (i.e. care are ca
parametru un obiect de aceeasi clasa, nu conteaza daca este prin
valoare/referinta, cu/fara 'const', etc.) definit de utilizator el ia
locul celui implicit (compilatorul nu-l mai genereaza pe acela). Operatorul
'=' prin copiere definit de utilizator nu mai apeleaza insa automat
operatorii '=' prin copiere ai claselor de baza si ai atributelor obiect, ci
doar daca o cerem noi explicit prin codul sau.
Daca in clasa am scris operatori '=' care nu sunt de copiere, ei vor
coexista cu cel implicit (compilatorul il genereaza si pe acela). Fiecare
operator '=' va fi selectat ulterior conform regulilor generale de la
supraincarcarea functiilor si operatorilor.
Exemplu:
========
#include<iostream>
using namespace std;
class complex{
double re, im;
public:
void setre(double); void setim(double);
double getre(); double getim();
};
void complex::setre(double x) {re = x;}
void complex::setim(double x) {im = x;}
double complex::getre() {return re;}
double complex::getim() {return im;}
int main(){
complex a, b;
a.setre(1); a.setim(2);
b = a;
cout << b.getre() << " " << b.getim()
// afisaza: 1 2
b.setre(10); b.setim(20);
cout << a.getre() << " " << a.getim()
<< b.getre() << " " << b.getim()
// afisaza: 1 2 10 20
a + b; // eroare, '+' nu este definit
}
<< endl;
<< " "
<< endl;
intre complex si complex
Comentarii:
- a fost suficient doar sa definim clasa 'complex' si automat capata
sens expresia 'a = b' intre doua obiecte ale sale; intr-adevar,
compilatorul a adaugat automat clasei o metoda operator '=' (operatorul
de atribuire prin copiere implicit):
complex& complex::operator=(complex const &);
acest lucru nu se intampla si cu alti operatori, de ex. '+'; de aceea,
daca vrem sa aiba sens si expresia 'a + b' trebuie sa adaugam explicit
un '+' definit de utilizator;
- in cazul clasei 'complex' operatorul '=' de copiere implicit este
satisfacator, deoarece toata informatia proprie unui obiect se afla in
membrii sai 're' si 'im', care sunt in locatia obiectului;
intr-adevar, afisarile au aratat ca prin 'b = a' se duplica informatiile
lui 'a', iar 'b' are o copie proprie a acestor informatii, care daca se
modifica nu sunt afectate informatiile lui 'a'.
Exemplu:
========
#include<iostream>
#include<string.h>
using namespace std;
class string1{
char s[20];
public:
int set(char const *);
char const *get();
char& operator[] (int);
};
int string1::set(char const *t){
Exemplu:
========
#include<iostream>
using namespace std;
class cls1{
public:
int i;
cls1(int);
};
cls1::cls1(int x):i(x) {}
// 'cls1' are operator '=' prin copiere implicit
cls1 h1(){cls1 ob(10); return ob;}
// functie ce returneaza prin valoare un obiect de clasa 'cls1';
// acesta va fi creat ca un obiect temporar
class cls2{
public:
int i;
cls2(int);
void operator=(cls2 &);
};
cls2::cls2(int x):i(x) {}
void cls2::operator=(cls2 &p) {
i = p.i;
cout << "cls2, atribuire utilizator\n";
}
// 'cls2' are operator '=' prin copiere definit de utilizator
cls2 h2(){cls2 ob(20); return ob;}
// functie ce returneaza prin valoare un obiect de clasa 'cls2';
// acesta va fi creat ca un obiect temporar
int main(){
cls1 a(1), b(2), c(3); cls2 d(4), e(5), f(6);
a = b; a = b = c; a = h1(); // OK
d = e; // afisaza: cls2, atribuire utilizator
d = e = f;
// eroare, nu se poate atribui valoarea returnata de 'e = f',
// adica 'void', lui 'd'
d = h2();
// eroare, nu se poate pasa obiectul temporar returnat de 'h2' ca
// parametru prin referinta la zona neconstanta lui '='
}
Comentarii:
- Operatorul '=' definit de utilizator pentru 'cls2' este unul de copiere
(deoarece parametrul sau este obiect al aceleiasi clase), chiar daca
nu are aceeasi singatura ca cel implicit - parametrul este referinta la
zona neconstanta si returneaza 'void'; astfel, pentru 'cls2' compilatorul
nu a mai generat operatorul implicit (la 'd = e' s-a afisat:
'cls2, atribuire utilizator').
- Operatorul '=' de copiere implicit pentru 'cls1' a putut fi inlantuit,
deoarece returneaza referinta la obiectul curent; cel definit de utilizator
pentru 'cls2' nu, deoarece returneaza 'void'; ca sa mearga, in loc de
'd = e = f;' am fi putut scrie: 'e = f; d = e;'.
- Lui 'a' i-am putut atribui obiectul temporar returnat prin valoare de 'h1',
= din clsob3
= din clsob2
= din cls2
notam ca 'p' poate fi pasat ca parametru actual celor trei '=' deoarece
o referinta la o clasa de baza poate fi initializata cu un obiect al
clasei derivate (dar va accesa in el doar membri mosteniti), a se vedea
lectia ***.
Exemplu:
========
#include<iostream>
using namespace std;
class clsbaz1{public: void operator=(clsbaz1); void operator=(clsbaz1 &);};
void clsbaz1::operator=(clsbaz1 p)
{cout << "void clsbaz1::operator=(clsbaz1)\n";}
void clsbaz1::operator=(clsbaz1 &p)
{cout << "void clsbaz1::operator=(clsbaz1 &)\n";}
// 'clsbaz1' are doi operatori '=' de copiere definiti de utilizator
class clsbaz2{void operator=(clsbaz2 const &);};
void clsbaz2::operator=(clsbaz2 const &p)
{cout << "void clsbaz2::operator=(clsbaz2 const &)\n";}
// 'clsbaz2' are un operator '=' de copiere definit de utilizator,
// dar este privat
class clsob {public: void operator=(clsob const &);};
void clsob::operator=(clsob const &p)
{cout << "void clsob::operator=(clsob const &)\n";}
// 'clsob' are un operator '=' de copiere definit de utilizator
class cls1: public clsbaz1 {};
class cls2: public clsbaz2 {};
class cls3 {clsob n;};
// 'cls1, 'cls2' si 'cls3' au operatori '=' de copiere impliciti
int main(){
clsbaz1 x, y; cls1 a, b;
x = y;
// eroare de ambiguitate intre
// void clsbaz1::operator=(clsbaz1)
// void clsbaz1::operator=(clsbaz1&)
a = b;
// afisaza: void clsbaz1::operator=(clsbaz1)
cls2 c, d;
c = d;
// eroare, 'void clsbaz2::operator=(const clsbaz2&)' este privata
cls3 e, f;
e = f;
// afisaza: void clsob::operator=(clsob const &)
}
Comentarii:
- La 'x = y' s-a semnalat eroare de ambiguitate deoarece ambii operatori
'=' de copiere ai clasei 'clsbaz1' au acelasi grad de compatibilitate
cu tipul parametrului actual 'y';
- La 'a = b', operatorul '=' de copiere implicit al clasei 'cls1',
trebuind sa apeleze un operator '=' de copiere al clasei de baza,
Ctor
Ctor
--Ctor
Ctor
Ctor
Ctor
Dtor
--Ctor
Ctor
Ctor
Dtor
Dtor
Ctor
Ctor
Dtor
Dtor
Dtor
Ctor
Ctor
Ctor
Dtor
Dtor
Ctor
Ctor
Dtor
Dtor
Dtor
--Dtor
Dtor
Dtor
Dtor
Dtor
Dtor
cls::cls(2), id: 2
cls::cls(3, 4), id: 3
cls::cls(8), id: 4
cls::cls(9), id: 5
cls::cls(10), id: 6
cls::cls(11, 12), id: 7
cls::~cls(), id: 6
cls::cls(13), id: 8
cls::cls(5), id: 9
cls::cls(6, 7), id: 10
cls::~cls(), id: 10
cls::~cls(), id: 9
cls::cls(5), id: 11
cls::cls(6, 7), id: 12
cls::~cls(), id: 12
cls::~cls(), id: 11
cls::~cls(), id: 8
cls::cls(13), id: 13
cls::cls(5), id: 14
cls::cls(6, 7), id: 15
cls::~cls(), id: 15
cls::~cls(), id: 14
cls::cls(5), id: 16
cls::cls(6, 7), id: 17
cls::~cls(), id: 17
cls::~cls(), id: 16
cls::~cls(), id: 13
cls::~cls(),
cls::~cls(),
cls::~cls(),
cls::~cls(),
cls::~cls(),
cls::~cls(),
id:
id:
id:
id:
id:
id:
5
4
3
2
1
0
*/
Comentarii:
- La fiecare creare a unui nou obiect, constructorul apelat asigneaza
acestuia o identificare unica 'id' calculata cu ajutorul atributului
static 'genid', care se incrementeaza de fiecare data.
- La fiecare creare a unui nou obiect se apeleaza un constructor
ales de compilator dupa regulile generale de la supraincarcarea functiilor
privind compatibilitatea ca numar si tip intre parametrii actuali (care
insotesc definitia obiectului) si cei formali (din declaratia
constructorului); de exemplu pentru 'cls a' s-a apelat constructorul fara
parametri, pentru 'b(1)' si 'c = 2' cel cu un parametru, pentru 'd(3, 4)'
cel cu doi parametri.
Notam echivalenta intre a scrie 'cls c = 2' si 'cls c(2)'.
- Pentru fiecare obiect, la inceputul domeniului sau de existenta i s-a
apelat un constructor, la sfarsitul domeniului sau de existenta i s-a
destructorul, iar domeniile de existenta ale obiectelor sunt in
concordanta cu clasa lor de alocare:
* 'a', 'b', 'c', 'd' sunt alocate static, deci au fost create la inceputul
executarii programului, inainte de 'main', in ordinea definirii lor, si
au fost distruse la sfarsitul executarii programului, dupa 'main', in
ordinea inversa crearii lor.
* 'g', 'h' sunt alocate automatic in 'main', deci au fost create dupa
intrarea in apelul lui 'main', la intalnirea definitiei lor (deci dupa ce
for(int i=0;i<3;++i){
cout << "Incepe '{}'\n";
static cls d(3);
cout << "---\n";
static cls e(4);
}
cout << "Se termina 'main'.\n";
}
/* Afisaza:
Ctor 2
Ctor 3
Ctor 4
Incepe 'main':
Incepe 'f':
Ctor 0
Ctor 1
Incepe 'f':
Incepe 'for':
Incepe '{}'
Ctor 5
--Ctor 6
Incepe '{}'
--Incepe '{}'
--Se termina 'main'.
Dtor 6
Dtor 5
Dtor 1
Dtor 0
Dtor 4
Dtor 3
Dtor 2
*/
Comentariu: constatam ca obiectele statice globale au fost create la
inceputul programului in ordinea definirii lor, cele statice locale in
momentul primei executari a blocului in care au fost definite (nu si la
urmatoarele executari ale acestuia), la intalnirea definitiei lor (deci
dupa ce s-a afisat "Incepe..."), iar la sfarsitul programului au fost
distruse toate obiectele statice in ordinea inversa crearii lor in general,
indiferent daca erau globale, locale in 'f' sau in instructiunea compusa
din 'main'.
Urmatorul exemplu ilustreaza alte aspecte legate de ordinea crearii/
distrugerii obiectelor automatice:
Exemplu:
========
#include<iostream>
using namespace std;
class cls{
int n;
public:
cls(int);
~cls();
};
cls::cls(int x):n(x){cout << "Ctor " << n << endl;}
cls::~cls(){cout << "Dtor " << n << endl;}
void f(cls a, cls b,
cout << "=== 'f' 1
cls e(111);
cout << "=== 'f' 2
cls f(222);
cout << "=== 'f' 3
}
cls c){
===\n";
===\n";
===\n";
int main(){
cout << "=== 'main' 1 ===\n";
cls a(1);
cout << "=== 'main' 2 ===\n";
cls b(2);
cout << "=== 'main' 3 ===\n";
f(10,20,30);
f(100,200,300);
cout << "=== 'main' 4 ===\n";
for(int i = 0; i < 2; ++i){
cout << "=== 'for' 1 ===\n";
cls c(3);
cout << "=== 'for' 2 ===\n";
cls d(4);
cout << "=== 'for' 3 ===\n";
}
cout << "=== 'main' 5 ===\n";
}
/* Afisaza:
=== 'main' 1 ===
Ctor 1
=== 'main' 2 ===
Ctor 2
=== 'main' 3 ===
Ctor 30
Ctor 20
Ctor 10
=== 'f' 1 ===
Ctor 111
=== 'f' 2 ===
Ctor 222
=== 'f' 3 ===
Dtor 222
Dtor 111
Dtor 10
Dtor 20
Dtor 30
Ctor 300
Ctor 200
Ctor 100
=== 'f' 1 ===
Ctor 111
=== 'f' 2 ===
Ctor 222
=== 'f' 3 ===
Dtor 222
Dtor 111
Dtor 100
Dtor 200
Dtor 300
=== 'main' 4 ===
=== 'for' 1 ===
Ctor 3
=== 'for' 2 ===
Ctor 4
=== 'for' 3 ===
Dtor 4
Dtor 3
=== 'for' 1 ===
Ctor 3
=== 'for' 2 ===
Ctor 4
=== 'for' 3 ===
Dtor 4
Dtor 3
=== 'main' 5 ===
Dtor 2
Dtor 1
*/
Comentariu: constatam ca obiectele automatice se creaza/distrug la fiecare
executare a blocului in care au fost definite; ele se creaza la intalnirea
definitiei, se distrug la sfarsitul blocului, iar ordinea in care se distrug
este inversa celei in care au fost create:
'a', 'b' se creaza/distrug o data, la singura executare a lui 'main';
'a', 'b', 'c', 'e', 'f' se creaza/distrug de doua ori, la cele doua
executari ale lui 'f';
'c' si 'd' se creaza/distrug de doua ori, la cele doua executari ale
instructiunii compuse.
In cazul lui 'f', parametrii au fost creati la inceputul apelului (deci
inainte de crearea lui 'e', 'f'), in ordinea inversa declararii:
'c', 'b', 'a', apoi s-a executat apelul si la intalnirea definitiilor
lui 'e' si 'f' s-au creat aceste obiecte, iar la sfarsitul apelului
au fost distruse obiectele automatice create cu ocazia acestui apel
in ordinea inversa crearii lor in general, indiferent daca erau
variabile locale sau parametri: 'f', 'e', 'a', 'b', 'c'.
Pentru alocarea dinamica (i.e. la cerere) de memorie, in C++ avem la
dispozitie atat functiile de biblioteca din C: 'malloc', 'calloc', 'realloc',
'free', cat si operatorii specifici C++: 'new', 'delete'. Prezentam mai jos
anumite particularitati legate de acestea:
- Functiile 'malloc', 'calloc', 'realloc', simplist vorbind, primesc ca
parametru o dimensiune de memorie, aloca zona respectiva retinand intr-o
tabela interna adresa de inceput si dimensiunea acesteia, si returneaza
adresa de inceput a zonei, ca 'void *'; in C, aceasta poate fi atribuita
direct oricarei variabile pointer fara cast; in C++, ea nu poate fi
atribuita unei variabile pointer la non-void decat cu cast; in continuare,
adresa poate fi atribuita de la un pointer la altul, eventual cu cast,
de fiecare data ne transmitandu-se si vreo indicatie despre dimensiunea
zonei; pentru a pasa aceasta adresa ca parametru lui 'free' (in scopul
dezalocarii zonei) nu este nevoie sa folosim aceeasi variabila pointer
in care am primit adresa de la 'malloc'/'calloc'/'realloc', nici macar
nu este nevoie sa fie o variabila de acelasi tip pointer - practic, se
se va transmite doar o adresa; 'free' va cauta adresa in tabela interna
si de acolo va determina dimensiunea zonei ce trebuie dezalocate (evident,
va elimina din tabela adresa si dimensiunea de memorie asociata); de
// mai apare 'beta' (acum este adresa libera), dar 's' retine in
// continuare 'beta'
t = (long long int *) malloc(sizeof(long long int));
// se aloca 8 octeti; intamplator (nu este obligatoriu), sunt tot de la
// adresa 'beta'; in tabela interna se retine perechea (beta, 8), in
// 't' se retine 'beta';
// acum 't' si 's' pointeaza aceeasi zona
cout << hex;
// setam 'cout' a.i. in continuare toate valorile intregi sa se afiseze
// hexa
cout << t << " " << s << endl;
// se afisaza adresele retinute in 't' si 's', anume 'beta' (vedem ca
// cele doua adrese sunt egale);
// de exemplu se afisaza: 0x9901038 0x9901038
*t = 0x0000000100000001ll;
// atribuim o valoare 'long long' zonei pointate de 't'; fara 'll'
// literalul este considerat implicit de tip 'int' iar valoarea
// sa ar putea fi considerata prea mare
cout << *t << endl;
// afisaza: 100000001
*s = 0x00000002;
// atribuim o valoare 'int' zonei pointate de 's'; practic, se
// suprascrie jumatatea low a zonei pointete de 't'
cout << *t << endl;
// afisaza: 100000002
// observam ca continutul zonei pointete de 't' s-a modificat, desi nu
// am folosit '*t'
free(s);
// 'free' primeste ca parametru 'beta', gaseste in tabela interna
// asociat 8 octeti si dezaloca zona de 8 octeti de la adresa 'beta';
// astfel, zona este dezalocata corect (cu dimensiunea cu care s-a
// alocat la 't = (long long int *) malloc(sizeof(long long int))'),
// desi n-am folosit 'free(t)'
free(t);
// comportament imprevizibil, de exemplu terminare anormala a
// programului, deoarece 'beta' nu mai este in tabela interna
}
Daca dorim sa folosim 'malloc'/'calloc'/'realloc' pentru a aloca dinamic
un obiect sau vector de obiecte, aceste functii vor aloca doar memoria
necesara locatiei acestora (conform 'sizeof'-ului clasei), fara a le apela
vreun constructor - astfel, nu se vor face anumite initializari si nu se vor
aloca resurse suplimentare, de exemplu alte zone de memorie alocate dinamic
pentru care in locatia obiectelor se retine (in niste atribute) doar adresa
de inceput.
Corespunzator, 'free' dezaloca doar memoria alocata locatiei obiectelor,
fara a le apela destructorul - astfel, daca de exemplu obiectele detineau
zone de memorie alocate dinamic pentru care in locatie se retinea doar
adresa, aceste zone vor ramane ne dezalocate (si vor ocupa heap-ul).
Exemplu:
========
#include<iostream>
#include<stdlib.h>
using namespace std;
class cls{
void *p;
public:
cls();
~cls();
void set(void *);
};
cls::cls() {p = NULL; cout << "Ctor\n";}
cls::~cls() {free(p); cout << "Dtor\n";}
void cls::set(void *x) {free(p); p = x;}
int main(){
cls *a, *b; int *n;
a = (cls *) malloc(sizeof(cls));
// se aloca sizeof(cls) octeti de memorie de la o adresa 'alpha',
// cat pentru un obiect al clasei 'cls', dar nu i se apeleaza
// acestuia constructorul;
// in particular, atributul 'p' al obiectului nu primeste valoarea NULL
// (ramane cu o valoare implicita, conform clasei sale de alocare
// dinamica) si nu se afisaza nimic;
n = (int *) malloc(sizeof(int)); *n = 10;
// se aloca sizeof(int) octeti de memorie de la o adresa 'beta',
// cat pentru o variabila de tip 'int', si se initializeaza aceasta
// variabila cu 10;
a -> set(n);
// mai intai se apeleaza 'free' pentru adresa necontrolata continuta in
// atributul 'p', efectul fiind imprevizibil; in continuare, daca
// programul nu s-a terminat anormal, se executa 'p = x' asa incat in
// final obiectul dinamic pointeaza cu atributul 'p' zona de la
// adresa 'beta';
free(a);
// se dezaloca cei sizeof(cls) octeti de la adresa 'alpha', adica
// obiectul dinamic, dar nu i se apeleaza acestuia destructorul;
// in particular, zona dinamica de la adresa 'beta' (pointata de
// atributul 'p' al obiectului) ramane nedezalocata si nu se afisaza
// nimic;
b = (cls *) malloc(3 * sizeof(cls));
// se aloca memorie cat pentru 3 obiecte ale clasei 'cls', dar nu li se
// apeleaza acestora constructorul; atributele 'p' ale acestor obiecte
// cu niste valori implicite, necontrolate, si nu se afisaza nimic;
for(int i=0; i<3; ++i){
n = (int *) malloc(sizeof(int)); *n = i * 100;
b[i].set(n);
}
// se apeleaza 'free' pentru adresele necontrolate continute in atributele
// 'p', efectul fiind imprevizibil; daca programul nu s-a terminat
// anormal, in final atributele 'p' ale celor 3 obiecte dinamice vor
// contine adresele 'beta1', 'beta2', 'beta3' ale celor 3 variabile
// dinamice de tip 'int' alocate cu 'malloc';
free(b);
// se dezaloca toate cele 3 obiecte dinamice (in total 3 * sizeof(cls)
// octeti), dar nu li se apeleaza acestora destructorul;
// in particular, zonele dinamice de la adresele 'beta1', 'beta2',
// 'beta3' raman nedezalocate si nu se afisaza nimic;
}
- Operatorul 'new' se poate apela sub formele:
new <Nume tip>
free(q);
poate fi inlocuit cu codul:
p = new T[n1] ();
for(i = 0; i < n1; ++i) p[i] = i * 10;
q = new T[n2];
for(i = 0; i < n1; ++i) q[i] = p[i];
delete [] p;
for(i = n1; i < n2; ++i) q[i] = i * 100;
for(i = 0; i < n2; ++i) cout << q[i] << " ";
cout << endl;
delete [] q;
Notam ca 'realloc' incearca sa redimensioneze zona dinamica veche, iar daca
aceasta nu se poate aloca alta zona, copiaza in ea informatia din zona
veche si dezaloca zona veche; cand lucram cu 'new'/'delete' copierea
informatiei si dezalocarea zonei vechi trebuie facute explicit.
Daca 'T' ar fi fost o clasa simularea ar fi fost mai dificila, deoarece
alocarea/copierea presupun apelarea unor constructori si operatori de
atribuire (exercitiu !).
In alte privinte, modul de functionare al lui 'malloc'/'calloc'/'realloc'/
'free' si 'new'/'delete' se aseamana: si aici se foloseste o tabela
interna, 'new' retine in ea adresa si dimensiunea zonei returnand doar
adresa, aceasta adresa poate fi atribuita (eventual cu cast) unor pointeri
diversi (nu neaparat spre acelasi tip) fara a se transmite si dimensiunea
zonei, 'delete' primeste ca operand doar adresa (nu si dimensiunea) si
determina dimensiunea zonei din tabela interna (in particular trebuie sa
gaseasca adresa acolo, altfel comportamentul este imprevizibil), eliminand
adresa din tabela; daca operandul lui 'delete' este o variabila pointer, ea
isi pastreaza in continuare valoarea, pointand zona respectiva chiar daca
este dezalocata (iar ulterior folosita partial sau total in alte alocari).
In plus, pot aparea probleme suplimentare legate de apelarea
constructorilor/destructorilor necorespunzatori. De exemplu, ca si in cazul
lui 'free', cand adresa ajunge la 'delete' ea nu trebuie neaparat sa aiba
acelasi tip de baza ca atunci cand a fost returnata de 'new'; 'delete' va
dezaloca corect exact cat a alocat 'new' (dimensiunea zonei este luata din
tabela interna), dar pentru zona respectiva va apela destructorul noului
tip de baza.
Urmatorul exemplu reia primul exemplu legat de 'malloc'/'calloc'/'realloc'/
'free' pentru a ilustra fenomene asemanatoare legate de 'new'/'delete':
Exemplu:
========
#include<iostream>
using namespace std;
int main(){
int *p; double *q; int a;
p = new int [10];
// daca 'sizeof(int)' este 4, atunci se aloca 40 octeti,
// de la o adresa 'alpha'; observam ca nu este necesar
// castul '(int *)' ('new' returneaza adresa gata castata);
// intr-o tabela interna, se retine perechea (alpha, 40);
// 'int' nefiind o clasa, nu se apeleaza nici un constructor
q = (double *) p;
// se atribuie doar 'alpha'
delete q;
// se paseaza ca parametru actual doar 'alpha'; 'delete'
// cauta 'alpha' in tabela interna, gaseste asociat 40, si
// dezaloca 40 octeti de la adresa 'alpha';
// observam ca la 'delete' nu a fost nevoie sa folosim tot 'p'
// (ca la 'new') si nici macar un pointer de acelasi tip
// ('p' este 'int *', 'q' este 'double *');
// 'double' nefiind o clasa, nu se apeleaza nici un destructor;
// in urma acestui 'delete', 'q' isi pastreaza valoarea 'alpha'
delete q;
// comportament imprevizibil, de exemplu terminare anormala a
// programului, deoarece 'alpha' nu mai este in tabela interna
delete &a;
// comportament imprevizibil, de exemplu terminare anormala a
// programului deoarece adresa lui 'a' nu este (n-a fost niciodata)
// in tabela interna
int *s; long long int *t;
// tipul 'long long int' este disponibil in 'gcc/g++';
// in versiunea considerata de 'g++', 'int' si 'long long int'
// inseamna intreg cu semn de 4, respectiv 8 octeti
s = new int;
// se aloca 4 octeti de la o adresa 'beta'; in tabela interna se
// retine perechea (beta, 4), in 's' se retine 'beta'
delete s;
// se elibereaza cei 4 octeti de la adresa 'beta'; in tabela interna nu
// mai apare 'beta' (acum este adresa libera), dar 's' retine in
// continuare 'beta'
t = new long long int;
// se aloca 8 octeti; intamplator (nu este obligatoriu), sunt tot de la
// adresa 'beta'; in tabela interna se retine perechea (beta, 8), in
// 't' se retine 'beta';
// acum 't' si 's' pointeaza aceeasi zona
cout << hex;
// setam 'cout' a.i. in continuare toate valorile intregi sa se afiseze
// hexa
cout << t << " " << s << endl;
// se afisaza adresele retinute in 't' si 's', anume 'beta' (vedem ca
// cele doua adrese sunt egale);
// de exemplu se afisaza: 0x95a9038 0x95a9038
*t = 0x0000000100000001ll;
// atribuim o valoare 'long long' zonei pointate de 't'; fara 'll'
// literalul este considerat implicit de tip 'int' iar valoarea
// sa ar putea fi considerata prea mare
cout << *t << endl;
// afisaza: 100000001
*s = 0x00000002;
// atribuim o valoare 'int' zonei pointate de 's'; practic, se
// suprascrie jumatatea low a zonei pointete de 't'
cout << *t << endl;
// afisaza: 100000002
// observam ca continutul zonei pointete de 't' s-a modificat, desi nu
// am folosit '*t'
delete s;
// 'delete' primeste ca parametru 'beta', gaseste in tabela interna
// asociat 8 octeti si dezaloca zona de 8 octeti de la adresa 'beta';
// astfel, zona este dezalocata corect (cu dimensiunea cu care s-a
// alocat la 't = new long long int'), desi n-am folosit 'delete t';
// 'int' nefiind o clasa, nu se apeleaza niciun destructor;
delete t;
a se vedea:
http://en.wikipedia.org/wiki/Smart_pointer
* Folosirea COLECTORULUI DE GUNOI BOEHM (BOEHM GARBAGE COLLECTOR) in locul
functiilor standard de alocare a memoriei; astfel, sunt evitati complet
pointerii dangling prin faptul ca sunt dezactivate 'free'-urile iar
obiectele sunt recuperate prin 'garbage collection', a se vedea:
http://en.wikipedia.org/wiki/Boehm_garbage_collector
- O SCURGERE DE MEMORIE (MEMORY LEAK) apare atunci cand o zona de memorie
este alocata dar nu pote fi accesata de catre codul care ruleaza.
Ea are simptome similare cu un numar de alte probleme si in general poate
fi diagnosticata doar prin analiza codului de catre programator.
Deoarece scurgerile de memorie pot epuiza memoria disponibila pe masura
ce programul ruleaza, ele sunt un factor de imbatranire software;
IMBATRANIREA SOFTWARE (SOFTWARE AGING) = degradarea progresiva a
performantei sau caderea/blocarea brusca a unui sistem software, cauzata de
epuizarea resurselor sistemului de operare, fragmentare si acumulare de erori.
In mod tipic in C++ o scurgere de memorie apare atunci cand o zona de
memorie alocata dinamic devine inaccesibila, deoarece pointerii care
retineau adresa ei au fost suprascrisi sau distrusi.
Scurgerile de memorie sunt mai serioase atunci cand se aloca frecvent memorie
noua pentru scopuri singulare, cum ar fi randarea cadrelor (frame) unui
joc sau animatii.
Exemplu:
========
#include<iostream>
#include<stdlib.h>
#include<string.h>
using namespace std;
char *upcase(char *s){
char *temp;
temp = (char *) malloc((strlen(s)+1) * sizeof(char));
strcpy(temp, s);
free(s);
for(int i = 0; i < strlen(temp); ++i)
if('a' <= temp[i] && temp[i] <= 'z')
temp[i] -= ' ';
// daca 'temp[i]' este litera mica, devine majuscula corespunzatoare
return temp;
}
int main(){
int i;
int *p, *q;
p = (int *) malloc(sizeof(int)); *p = 1;
cout << p << " " << *p << endl;
// afisaza adresa si continutul zonei: 0x9d7c008 1
q = (int *) malloc(sizeof(int)); *q = 2;
cout << q << " " << *q << endl;
// afisaza adresa si continutul zonei: 0x9d7c018 2
p = q;
// apare un memory leak, deoarece zona de la adresa 0x9d7c008
// a ramas alocata, dar nu mai poate fi accesata (de exemplu pentru a
// o dezaloca cu 'free') deoarece adresa ei nu mai este retinuta
// nicaieri;
// acum 'p' si 'q' pointeaza zona de la adresa 0x9d7c018
free(q);
// se elibereaza zona de la adresa 0x9d7c018; 'p' si 'q' raman dangling
char *x;
x = (char *) malloc(100 * sizeof(char));
cin.getline(x, 100);
// se citeste un sir in zona alocata, de exemplu: aB1
cout << upcase(x) << endl;
// afisaza sirul citit trecut la majuscule, de exemplu: AB1
// In acest moment zona pointata de 'x' a fost dezalocata in
// functia 'upcase' (iar 'x' este dangling) iar zona alocata in
// functia 'upcase' a ramas alocata, dar i s-a pierdut urma,
// deoarece 'temp' a fost distrus (memory leak)
char **v = NULL; int n = 0;
do{
char buf[100];
cout << "Introduceti urmatorul sir (ENTER pentru terminare):";
cin.getline(buf, 100);
if(strlen(buf) == 0) break;
v = (char **) realloc(v, ++n * sizeof(char *));
v[n - 1] = (char *) malloc((strlen(buf)+1) * sizeof(char));
strcpy(v[n - 1], buf);
} while(1);
cout << "Am citit:\n";
for(i = 0; i < n; ++i) cout << v[i] << endl;
free(v);
// se dezaloca zona de n * sizeof(char *) pointata de 'v', dar nu se
// dezaloca automat si zonele pointate de v[0], ..., v[n-1] (memory
// leak);
// o abordare corecta ar fi fost ca in loc de:
//
//
free(v);
//
//
sa scriem:
//
//
for(i = 0; i < n; ++i) free(v[i]);
//
free(v);
class linie{
char *l;
public:
linie(char const *p)
{l = new char [(strlen(p) +1) * sizeof(char)]; strcpy(l, p);}
~linie()
{delete(l);}
operator char const * ()
{return l;}
};
class text{
linie **t; int n;
public:
text() {t = NULL; n = 0;}
~text() {
for(int i = 0; i < n; ++i) delete t[i];
free(t);
}
text& operator<< (char const *p) {
t = (linie **) realloc(t, ++n * sizeof(linie *));
t[n - 1] = new linie(p);
return *this;
}
char const * operator[] (int i) {return *t[i];}
} *w;
w = new text;
do{
char buf[100];
cout << "Introduceti urmatorul sir (ENTER pentru terminare):";
cin.getline(buf, 100);
if(strlen(buf) == 0) break;
*w << buf;
} while(1);
cout << "Am citit:\n";
for(i = 0; i < n; ++i) cout << (*w)[i] << endl;
delete w;
// o varianta obiectuala a codului anterior; apelurile 'delete' pe care
// le executa destructorii apelati fac ca sa nu mai avem memory leak;
// pentru membrul 't' am folosit 'realloc' in loc de 'new' deoarece
// codul pentru prelungirea zonei pointate se scrie mai usor;
// in scrierea 'cout << (*w)[i] << endl' parantezele '()' sunt necesare
// deoarece '*' are prioritate mai mare decat '[]';
{
text w1;
w1 << "abc" << "defg" << "h";
cout << w1[0] << endl << w1[1] << endl << w1[2] << endl;
// afisaza:
//
abc
//
defg
//
h
{text w2;
w2 = w1;
w2 << "ijk" << "lm";
cout << w2[0] << endl << w2[1] << endl << w2[2] << endl
<< w2[3] << endl << w2[4] << endl;
// afisaza:
//
abc
//
defg
//
h
//
ijk
//
lm
}
cout << w1[0] << endl << w1[1] << endl << w1[2] << endl
<< w1[3] << endl << w1[4] << endl;
// efect imprevizibil; de exemplu poate afisa tot:
//
abc
//
defg
//
h
//
ijk
//
lm
// sau programul se poate termina anormal;
}
// daca mai devreme programul nu s-a terminat anormal si se ajunge aici,
// iarasi efectul este imprevizibil;
// intr-adevar, clasa 'text' are operator '=' prin copiere implicit,
// deci la 'w2 = w1' se copiaza bit cu bit atributul 't' al lui 'w1'
// (adica adresa zonei dinamice pointate de 't') in cel al lui 'w2',
// fara a se duplica zona pointata de 't', a.i. in final 'w1' si 'w2'
// pointeaza acelasi vector de linii si aceleasi linii; de aceea, cand
// am modificat 'w2' (prin adaugare de linii) s-a modificat si 'w1', iar
// cand s-au dezalocat zonele dinamice asociate lui 'w2' prin apelarea
// destructorului (la iesirea din domeniul sau de existenta, care este
// instructiunea compusa interioara) s-au dezalocat si cele asociate lui
// 'w1'; deci, cand am incercat sa afisam din nou 'w1' s-a incercat
// accesarea unor zone dezalocate, iar cand s-a apelat destructorul
// pentru 'w1' (la iesirea din domeniul sau de existenta, care este
// instructiunea compusa exterioara) s-a incercat dezalocarea unor zone
// dezalocate, ambele incercari avand efect imprevizibil;
// daca la apelarea constructorului pentru 'w1' (la intalnirea definitiei
// sale de la inceputul instructiunii compuse exterioare) s-ar fi alocat
// memorie (in loc sa se faca 't = NULL'), in urma atribuirii 'w2 = w1'
// s-ar fi pierdut accesul la aceasta memorie (memory leak); intrucat
// la iesirea din instructiunea compusa interioara obiectul 'w1' ajunge
// sa pointeze zone dezalocate, avem de-a face si cu fenomenul de
// dangling pointer;
// pentru a evita atat memory leak-urile cat si dangling pointerii care
// apar mai sus, ar trebui ca atat clasa 'text' cat si clasa 'linie',
// ale caror atribute pointeaza zone dinamice, sa aiba operator '='
// de copiere definit de utilizator care sa duplice aceste zone
// (destructori definiti de utilizator care dezaloca corect aceste zone
// au deja);
}
Pentru a evita fenomenul de memory leak, o regula de urmat este sa ne
asiguam ca la orice rulare a programului, pentru fiecare 'malloc'/'calloc'
executat se va executa si cate un 'free', si pentru fiecare 'new'/'new []'
executat se va executa si cate un 'delete'/'delete[]'; un 'realloc'
redimensioneaza sau inlocuieste o zona alocata cu 'malloc'/'calloc', nu
creste numarul zonelor curent alocate, deci lui nu trebuie sa-i corespunda
un 'free' (decat daca eeste un 'realloc(NULL, ...)', care are efectul lui
'malloc').
O alta recomandare este: ce s-a alocat cu 'malloc'/'calloc'/'realloc' sa se
dezaloce cu 'free' (nu cu 'delete'), iar ce s-a alocat cu 'new' sa se
dezaloce cu 'delete' (nu cu 'free'); o explicatie ar fi ca tabelele interne
folosite de 'malloc'/'calloc'/'realloc' si 'free', respectiv 'new'/'delete',
pentru a retine adresele zonelor alocate si dimensiunile lor nu sunt neaparat
aceleasi.
Exemplul urmator ilustreaza fenomene legate de crearea vectorilor de obiecte,
independenti sau membrii ai unor obiecte, cu diverse clase de alocare
(statici, automatici, dinamici), evidentiind ordinea in care sunt considerate
componentele pentru a li se aplica constructorii/destructorii:
Exemplu:
========
#include<iostream>
using namespace std;
class cls{
static int gen; int n;
public:
cls();
~cls();
};
int cls::gen = 0;
cls::cls() {n = ++gen; cout << "Ctor cls: " << n << " " << this << endl;}
cls::~cls() {cout << "Dtor cls: " << n << " " << this << endl;}
class cls1{
static int gen; int n;
cls va[2];
public:
cls1();
~cls1();
};
int cls1::gen = 0;
cls1::cls1()
{n = gen +=100; cout << "Ctor cls1: " << n << " " << this << endl;}
cls1::~cls1() {cout << "Dtor cls1: " << n << " " << this << endl;}
cls vg[2]; // vector static de obiecte
int main() {
cout << "--- 1 ---\n";
static cls1 og; // obiect static (local) cu atribut vector de obiecte
cout << "--- 2 ---\n";
{
cls vl[2]; // vector automatic de obiecte
// (local unei instructiuni compuse)
cout << "--- 3 ---\n";
cls1 ol;
// obiect automatic cu atribut vector de obiecte
// (local unei instructiuni compuse)
cout << "--- 4 ---\n";
}
cout << "--- 5 ---\n";
cls *p = new cls[2]; // vector dinamic de obiecte
delete []p;
cout << "--- 6 ---\n";
cls1 *q = new cls1[2]; // vector dinamic de obiecte cu atribut vector
// de obiecte
delete []q;
cout << "--- 7 ---\n";
}
Comentariu: programul afisaza:
Ctor cls: 1 0x804b100
Ctor cls: 2 0x804b104
--- 1 --Ctor cls: 3 0x804b11c
Ctor cls: 4 0x804b120
Ctor cls1: 100 0x804b118
--- 2 --Ctor cls: 5 0xbf883610
Ctor cls: 6 0xbf883614
--- 3 --Ctor cls: 7 0xbf883608
Ctor cls: 8 0xbf88360c
Ctor cls1: 200 0xbf883604
--- 4 --Dtor cls1: 200 0xbf883604
Dtor cls: 8 0xbf88360c
Dtor cls: 7 0xbf883608
Dtor cls: 6 0xbf883614
Dtor cls: 5 0xbf883610
--- 5 --Ctor cls: 9 0x84b500c
Ctor cls: 10 0x84b5010
0;}
y; c = 0;}
x; b = y; c = z;}
<< b << " " << c << endl;}
0
0
0
3
0
0
0
3
public:
clsbaz1(int);
~clsbaz1();
};
clsbaz1::clsbaz1(int x): n(x) {cout << "Ctor baz1 " << n << endl;}
clsbaz1::~clsbaz1() {cout << "Dtor baz1 " << n << endl;}
class clsbaz2{
public:
clsbaz2();
~clsbaz2();
};
clsbaz2::clsbaz2() {cout << "Ctor baz2\n";}
clsbaz2::~clsbaz2() {cout << "Dtor baz2\n";}
class clsatr{
int n;
public:
clsatr(int = 1000);
~clsatr();
};
clsatr::clsatr(int x): n(x) {cout << "Ctor atr " << n << endl;}
clsatr::~clsatr() {cout << "Dtor atr " << n << endl;}
class cls: clsbaz1, clsbaz2{
clsatr a1;
int n;
clsatr a2;
public:
cls(int);
cls(int, int);
~cls();
};
cls::cls(int x):
a2(3), n(x), clsbaz2(), clsbaz1(4), a1(5)
{cout << "Ctor cls " << x << "," << n << endl;}
cls::cls(int x, int y):
a1(x), clsbaz1(y)
{cout << "Ctor cls " << x << "," << y << "," << n << endl;}
cls::~cls(){cout << "Dtor " << n << endl;}
int main(){
{cls ob1(10);}
cout << "---\n";
{cls ob2(100, 200);}
}
Comentarii:
- Obiectele 'ob1', 'ob2' sunt locale automatice in cadrul unor instructiuni
compuse incluse in 'main', a.i. 'ob2' se distruge inainte de a se crea
'ob1'; astfel, putem urmari mai usor ordinea apelarii constructorilor si
destructorilor.
- Programul afisaza:
Ctor
Ctor
Ctor
Ctor
Ctor
Dtor
baz1 4
baz2
atr 5
atr 3
cls 10,10
10
Dtor
Dtor
Dtor
Dtor
--Ctor
Ctor
Ctor
Ctor
Ctor
Dtor
Dtor
Dtor
Dtor
Dtor
atr 3
atr 5
baz2
baz1 4
baz1 200
baz2
atr 100
atr 1000
cls 100,200,-1218110981
-1218110981
atr 1000
atr 100
baz2
baz1 200
class cls1{
public:
cls1();
~cls1();
};
cls1::cls1() {cout << "Ctor1\n";}
cls1::~cls1() {cout << "Dtor1\n";}
class cls2: cls1{};
// 'cls2' are constructor implicit si destructor generati automat
class cls3{
public:
cls3(int = 0);
cls3();
~cls3();
};
cls3::cls3(int x) {cout << "Ctor3 " << x << endl;}
cls3::cls3() {cout << "Ctor3 " << endl;}
cls3::~cls3() {cout << "Dtor3\n";}
class cls{
cls1 a; cls2 b; cls3 c;
public:
cls();
};
cls::cls():c(1) {cout << "Ctor cls\n";}
// 'cls' are destructor generat automat
int main(){
cls ob;
}
Comentarii:
- Programul afisaza:
Ctor1
Ctor1
Ctor3 1
Ctor cls
Dtor3
Dtor1
Dtor1
Intr-adevar, conform regulilor prezentate mai sus, la crearea lui 'ob'
mai intai se instantiaza atributul 'a', apoi atributul 'b', apoi atributul
'c', apoi se executa corpul constructorului folosit din clasa 'cls'; el
trebuie sa fie un constructor implicit, deoarece definitia lui 'ob' nu
contine parametri actuali; in clasa 'cls' exista un singur constructor
implicit, iar acesta este definit de utilizator; in lista sa de initializare
exista initializator doar pentru 'c'; de aceea, pentru 'a' si 'b' se
foloseste un constructor implicit.
In cazul lui 'a', singurul constructor implicit al clasei 'cls1' este cel
definit de utilizator (intrucat clasa are constructori definiti de
utilizator, copilatorul nu ii mai adauga constructorul implicit generat
automat); acesta este apelat si se afisaza 'Ctor1'.
In cazul lui 'b', singurul constructor implicit al clasei 'cls2' este cel
generat automat de compilator (intrucat clasa nu are constructori definiti
de utilizator); acesta trebuie sa apeleze un constructor pentru clasa de
//
initializarea cu o valoare implicita);
// ambii constructori se pot folosi;
cls2 a2, b2 = a2;
// pentru 'a2' se foloseste constructorul implicit,
// pentru 'b2' cel de copiere;
class cls3{
public:
int const n; int &r;
cls3(cls3 const &x): n(x.n), r(x.r){}
};
// 'n' si 'r' nu accepta valori implicite;
// 'cls3' nu are constructor implicit (deoarece are constructori definiti
// utilizator si nici unul nu este implicit) si are constructor de
// copiere definit de utilizator; in definirea lui 'cls3(cls3 const &)'
// initializatorii lui 'n' si 'r' nu pot fi omisi (deoarece s-ar
// presupune ca invoca tot initializarea cu o valoare implicita, nu
// copierea valorii bit cu bit, cum s-ar intampla in cazul cand
// constructorul de copiere al lui 'cls3' ar fi cel generat automat);
// acest constructor de copiere se poate folosi (daca avem pe cine copia);
cls3 a3 = *(cls3*)buf;
// pentru 'a3' se foloseste constructorul de copiere, copiindu-se un obiect
// fictiv presupus a se afla la adresa 'buf';
class cls4{
public:
int const n; int &r;
cls4(): n(0), r(glob) {}
cls4(cls4 const &x): n(x.n), r(x.r) {}
};
// 'n' si 'r' nu accepta valori implicite;
// 'cls4' are constructor implicit si constructor de copiere definiti de
// utilizator; in definirea acestora, initializatorii lui 'n' si 'r' nu
// pot fi omisi (deoarece s-ar presupune ca invoca initializarea cu o
// valoare implicita);
// ambii constructori se pot folosi;
cls4 a4, b4 = *(cls4*)buf;
// pentru 'a4' se foloseste constructorul implicit,
// pentru 'b4' cel de copiere;
class cls1a : public cls1{};
class cls1b : public cls1a {
public:
cls1b(): cls1a(*(cls1a*)buf) {}
cls1b(cls1b const &x): cls1a() {}
};
class cls_unu{
public:
cls1b c1;
};
// 'cls_unu' are constructor implicit si constructor de copiere generati
// automat;
// constructorul implicit al lui 'cls_unu'(generat automat) invoca (in
// absenta specificarii) constructorul implicit al atributului nestatic
// 'c1', care este un constructor definit de utilizator; acesta invoca
// (conform specificarii) constructorul de copiere al clasei de baza
// 'cls1a', care este un constructor generat automat; acesta invoca (in
// absenta specificarii) constructorul de copiere al clasei de baza
implicit se semnaleaza eroare, iar daca are mai multi constructori impliciti
(de exemplu unii care difera prin lista tipurilor parametrilor, ca sa fie
permisa supraincarcarea, dar au toti parametrii impliciti), se semnaleaza de
asemenea eroare (de ambiguitate).
Daca pentru o clasa (in particular tip structura, uniune) utilizatorul nu a
definit nici un constructor, compilatorul genereaza automat un constructor
implicit; acest constructor este declarat implicit daca clasa necesita un
constructor si este definit implicit atunci cand compilatorul il foloseste
pentru a crea un obiect al clasei respective; el se mai numeste CONSTRUCTOR
IMPLICIT IMPLICIT (IMPLICIT DEFAULT CONSTRUCTOR, sau IMPLICITLY-DECLARED
DEFAULT CONSTRUCTOR, sau DEFAULTED DEFAULT CONSTRUCTOR).
Daca insa pentru clasa respectiva utilizatorul a definit constructori,
compilatorul nu-l mai adauga si pe cel implicit generat automat, a.i. clasa
nu va avea constructori impliciti decat daca i-a definit utilizatorul.
Constructorul implicit generat automat este echivalent cu un constructor
implicit definit de utilizator avand corp vid; in plus, este inline, public,
are lista de parametri si lista de initializare vide.
Exemplu:
========
#include<iostream>
using namespace std;
class cls1{};
// 'cls1' are constructor implicit generat automat
class cls2{
public:
cls2(int);
cls2();
};
cls2::cls2(int x) {cout << "cls2(int)\n";}
cls2::cls2() {cout << "cls2()\n";}
// 'cls2' are constructor implicit definit de utilizator
class cls3{
public:
cls3(int);
};
cls3::cls3(int x) {cout << "cls3(int)\n";}
// 'cls3' nu are constructori impliciti
class cls_unu: cls1, cls2, cls3{
cls1 a; cls2 b; cls3 c;
public:
cls_unu();
cls_unu(int);
};
cls_unu::cls_unu():
cls1(),
cls3(1),
b(), c(2)
{cout << "cls_unu()\n";}
/*
constructor implicit definit de utilizator; el apeleaza in ordine
constructori pentru clasele:
'cls1', ca prima clasa de baza;
'cls2', ca a doua clasa de baza;
'cls3', ca a treia clasa de baza;
int main() {
cls3 *ob = reinterpret_cast <cls3 *> (malloc(100));
ob -> x = 1; ob -> n.x = 2;
}