[1] "Hello" şi "Goodbye" - constructor, destructor şi this în C++
Setări pentru formulele matematice (click-dreapta pe o formulă): Scale All Math 90%; Math Renderer HTML-CSS
Programa şcolară pentru clasa a XI-a "matematică-informatică" prevede la sfârşit, un capitol intitulat Elemente de programare orientată pe obiecte (OOP). Însă locul acestui item al programei nu este nicidecum, la sfârşitul anului şcolar; OOP este implicat deja - fie şi numai în mod implicit - atunci când începi să desfăşori itemul "Grafuri", iar ca stil de dezvoltare a lucrurilor - OOP este implicat permanent la "matematică".
Momentul cel mai potrivit pentru a aborda OOP este acela în care elevii au asimilat un nou "tip de date". Putem profita de faptul că în primele câteva săptămâni, la matematică se studiază capitolul "Matrice" şi avem un excelent prilej de a demara OOP: să modelăm în C++, ceea ce la matematică se notează prin $\mathcal{M}_{n,m}\left(\mathbb{R}\right)$ - mulţimea matricelor "de tip $n\times m$ peste $\mathbb{R}$", împreună cu anumite operaţii posibile (subânţelese) între elementele acestei mulţimi.
Putem să luăm aminte de la desfăşurarea lucrurilor la matematică: se dau întâi, definiţiile şi notaţiile corespunzătoare ("matrice de tip $n\times m$", mulţimile de matrice de acelaşi tip peste anumite mulţimi numerice, cum se scriu matricele) şi totodată, se definesc anumite operaţii între matrice; apoi, matricele sunt folosite ca atare în diverse contexte de lucru.
Declarând "fie $A,\,B\in\mathcal{M}_{2,3}\left(\mathbb{R}\right)$" - ştim deja că este vorba de a considera două matrice de acelaşi tip şi ştim deja să le adunăm şi să scriem rezultatul; întâlnind "$A + B$", ştim din context că este vorba de "adunarea matricelor de acelaşi tip" definită la început (şi nu confundăm cu "adunarea numerelor").
Imitând această desfăşurare, am avea de constituit întâi o bibliotecă "matrice.cpp" definitorie pentru noul tip de date; ca şi tipurile de date predefinite ("int", "double", etc.) acest nou tip de date trebuie să aibă un nume şi alegem să-l numim "matrix" (amintind de o comandă prevăzută de LaTeX pentru redarea unei matrice). Incluzând această bibliotecă, ar trebui să putem lucra cu matricele cam la fel cum folosim tipurile de date obişnuite:
1 2 3 4 5 6 7 8 9 10 11 12 | #include "matrice.cpp" // unde se defineşte $\mathcal{M}_{n,m}(\mathbb{R})$, prin "matrix" #include <fstream> // biblioteca standard pentru "fişier" int main() { int n, m; ifstream input("matrici.txt"); // n (linii), m (coloane) şi n*m valori ofstream output("out.txt"); input >> n >> m; // citeşte din "matrici.txt" n, m matrix A(n, m), B(n, m); // $A, B\in \mathcal{M}_{n,m}(\mathbb{R})$ input >> A >> B; // citeşte valorile matricelor A şi B output << "suma:\n" << A + B; // adună A cu B şi scrie matricea A+B output << "produsul:\n" << A*B // înmulţeşte şi scrie matricea AB } |
Directiva #include
prevede paranteze unghiulare în cazul bibliotecilor standard (linia 2) şi ghilimele în cazul altor biblioteci (linia 1).
Am presupus că în fişierul "matrici.txt" s-a precizat domeniul de definiţie $\Delta$ (prin indicarea "tipului" matricelor) şi apoi, valorile câtorva matrice; în liniile 7 şi 8 se definesc două funcţii $A,B\colon\Delta\to\mathbb{R}$ (deci matrice de tipul respectiv), ale căror valori particulare sunt apoi preluate din fişier, în linia 9.
În fond, am întâlnit şi anterior asemenea demersuri de modelare a unui nou tip de date (şi nu-i târziu să remarcăm că de multe ori ajungem să ne folosim de lucruri pe care încă "nu le-am învăţat"). Pe la sfârşitul clasei a IX-a (respectând "programa") s-a învăţat despre "Citirea şi afişarea datelor folosind fişiere text": incluzi în program biblioteca standard "fstream" şi foloseşti construcţii precum cele din liniile 5 şi 6, după care poţi folosi ">>" şi "<<" (ca în liniile 7 şi 10) la fel cum s-au folosit acestea de la bun început pentru "citire/scriere" cu tastatura/ecranul.
Biblioteca standard "fstream" defineşte tipuri de date ca "ifstream" şi "ofstream", modelând lucrul cu fişiere; variabila "input" (din linia 5) reprezintă un obiect de tip "ifstream" (se mai zice instanţă a clasei "ifstream") - capabil să deschidă pentru citire fişierul indicat, să citească din fişier într-un buffer propriu, să constate sfârşitul fişierului, etc. - la fel cum n şi m (din linia 4) reprezintă obiecte de tip "int" şi la fel, A şi B (linia 8) sunt instanţe ale clasei "matrix".
Analog cu [1] (unde vizam struct), imaginăm următorul experiment (vizând acum class):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | #include <iostream> class matrix { int rows, cols; // date private ale obiectului public: matrix(int n, int m) { // constructor (creează un obiect de tip "matrix") rows = n; // this->rows = n; cols = m; } void info(const char id[]) { // funcţie membră ("metodă") a clasei std::cout << id << " at address:" << this; std::cout << "; rows=" << rows << ", cols=" << cols << "\n"; } }; matrix A(20, 30); // variabilă globală (în segmentul de date) int main() { // cout << A.rows; // ERROR: ‘int matrix::rows’ is private A.info("obiectul global A:"); { // context local (în segmentul de stivă) matrix C = A, A(3, 4); C.info("copiază obiectul global A, creând C:"); A.info("obiectul local A:"); } // exercită destructorul asupra obiectelor locale A şi C A.info("obiectul global A:"); // C.info("object C:"); // ERROR: ‘C’ was not declared in this scope } |
vb@vb:~/clasa11/OOP/Matrice$ ./test L20 obiectul global A: at address:0x804a0d4; rows=20, cols=30 L23 copiază obiectul global A, creând C: at address:0xbfec7c80; rows=20, cols=30 L24 obiectul local A: at address:0xbfec7c88; rows=3, cols=4 L26 obiectul global A: at address:0x804a0d4; rows=20, cols=30 vb@vb:~/clasa11/OOP/Matrice$
Orice obiect de tip "matrix" conţine doi întregi, identificaţi prin matrix::
rows şi matrix::
cols - v. linia 4 (neglijăm deocamdată, cele rows*cols
valori ale matricei); astfel, obiectul global A (v. L20
) este alocat în segmentul de date (la adresa 0x804a0d4
, ocupând 2*sizeof(int)
octeţi) şi are ca "date membre" valorile 20 şi 30 (pentru rows
şi cols
), iar obiectul local A (v. L24
) este alocat în segmentul de stivă (la adresa 0xbfec7c88
) şi are ca date proprii valorile 3 şi 4.
Aceste valori sunt în mod implicit "private" - nu pot fi accesate în mod direct, din exterior: A.rows
din linia 19 produce eroare, la compilarea programului (spre deosebire de struct
, unde toţi membrii sunt accesibili din exteriorul structurii). Dacă nu foloseam modificatorul de acces "public" (linia 5), atunci toţi membrii clasei ar fi fost "privaţi" - consecinţa fiind că tentativa de a construi o variabilă de tip "matrix" (de exemplu în linia 16) eşuează ("error: ‘matrix::matrix(int, int)’ is private")
.
Un obiect al clasei este creat (liniile 16 şi 22) în urma apelării unuia dintre constructorii prevăzuţi în definiţia clasei, sau eventual prin folosirea unui constructor "standard" (pus la dispoziţie în mod implicit, pentru orice obiect). Un constructor (cum avem aici în liniile 6-9) este o funcţie membră denumită la fel ca şi clasa, atributată de regulă cu acces "public" şi care are rolul de a permite alocarea şi iniţializarea datelor membre, pentru obiectul respectiv.
Construcţia obiectului înseamnă în plus că adresa de memorie la care este alocat obiectul este consemnată unei variabile de tip "pointer" denumite this şi care este asociată în mod tacit clasei (v. linia 11). Funcţiile membre ale clasei sunt comune pentru toate obiectele acelei clase, dar fiecare dintre aceste funcţii deţine this ca parametru implicit; în interiorul definiţiei clasei, this -> membru
permite accesarea unui "membru" din cadrul instanţei curente, oricare ar fi aceasta la momentul invocării (fie cea de la adresa 0x804a0d4
, fie cea de la adresa 0xbfec7c88
). O invocare din exterior de forma A.info()
determină apelarea metodei publice "info()" pentru obiectul A, cu this
referind zona alocată acestuia (liniile 10-13, 20, L20, 25, L24).
În linia 22, pentru construirea obiectului C este implicat constructorul de copiere (sau "atribuire"): datele din obiectul A sunt copiate (v. L23
) la o altă adresă (care devine this-ul noului obiect); nu avem unul în definiţia "matrix" de mai sus, încât constructorul utilizat pentru această atribuire este cel furnizat implicit de către compilator (pentru toate obiectele, indiferent de tip).
Obiectele create (printr-un constructor sau altul) au o anumită durată de existenţă, în segmentele de memorie în care au fost alocate. Variabilele globale sunt vizibile în tot programul (fiind create chiar înainte de intrarea în execuţie a funcţiei main()
), până la încheierea execuţiei; variabilele locale sunt create în segmentul de stivă al programului şi sunt vizibile până la ieşirea din blocul - secvenţă de instrucţiuni delimitată de {
şi }
- în care au fost instituite (fiind vizibile numai în acel bloc).
Liniile 21-25 introduc un context local (un "bloc") în care se construieşte (în segmentul de stivă) un obiect C (prin copierea obiectului global A) şi un obiect A ("mascând" în interiorul blocului, variabila globală A) - cum se vede la L23
şi L24
; odată cu ieşirea din acest bloc, aceste obiecte "dispar" - vezi linia L27
(iar variabila globală A redevine vizibilă - v. linia L26
).
Această eliminare (eliberarea zonelor de memorie alocate obiectelor) se produce prin apelarea unui destructor - fie (ca în cazul de faţă) cel setat în mod implicit de către compilator, fie (dacă există) cel definit ca funcţie membră a clasei (cu acelaşi nume ca şi clasa, dar prefixat cu ~)
.
Constructorii şi destructorul oferiţi în mod implicit de către compilator (suplinind absenţa lor în definiţia clasei, cum am evidenţiat mai sus) alocă şi respectiv dealocă automat, zone de memorie alocate obiectelor în momentul compilării programului - în segmentul de date (globale) şi în stivă. Însă alocarea de zone suplimentare pe parcursul execuţiei programului (în "memoria liberă") şi dealocarea acestora necesită constructori şi destructori explicitaţi corespunzător în definiţia clasei.
Fişierul matrice.h
expune prototipurile care vor trebui urmate pentru a institui matricele necesare şi pentru a opera cu acestea, în cadrul unui program sau altul:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | #include <fstream> using namespace std; class matrix { public: matrix(int, int); // matrix A(2,3), B(3); matrix(const matrix&); // invocat de "return Matrice" matrix& operator = (const matrix&); // matrix A = B + C; ~matrix(); // dealocă zona alocată valorilor matricei friend ostream& operator << (ostream&, const matrix&); // out << A; friend istream& operator >> (istream&, matrix&); // matrix A(2,3); in >> A; friend matrix operator + (const matrix&, const matrix&); // matrix A=B+C; friend matrix operator * (const matrix&, const matrix&); // matrix A=B*C; private: int rows, cols; double** cont; // (content) indică tabloul pointerilor liniilor matricei inline void alloc() { // constituie zona valorilor matricei cont = new double* [rows]; // tabloul pointerilor de linii for(int i=0; i < rows; i++) cont[i] = new double [cols]; // tabloul valorilor din linie } }; |
Orice matrice (obiect de tip "matrix") va avea atributele rows
, cols
şi cont
(liniile 18-20) cu propriile valori, respectiv pentru numărul de linii şi cel de coloane şi pentru conţinutul propriu-zis al ei.
N-am considerat necesar să prevedem şi metode de acces (ca int get_rows() {return this->rows;}
), astfel că aceste valori nu vor putea fi accesate din exteriorul clasei 'matrix'.
Constructorul prevăzut în linia 6 va defini o zonă de memorie pentru a păstra valorile parametrilor primiţi (drept "rows" şi "cols"), precum şi adresa de bază "cont" a tabloului destinat să înregistreze cele rows*cols
valori ale matricei; adresa zonei rezultate va fi pasată ca parametru "this", în cazul apelării din obiectul construit (prin operatorul de selecţie ".
") a unei metode publice a clasei.
Alocarea în cursul execuţiei (în zona "heap"), a tabloului pentru cele rows*cols
valori ale matricei este necesitată în toţi constructorii (din liniile 6-8) - aşa că am prevăzut în acest scop o funcţie alloc()
(liniile 22-26); neavând de ce să o furnizăm ca metodă publică, am instituit-o în secţiunea atributată cu "private".
Declaraţia ei ca inline
(linia 22) reflectă o mică improvizaţie oferită de C++, dar una foarte eficientă: funcţia nu va fi apelată în modul obişnuit (inserând codul de apel, gestionarea "cadrului-stivă" şi codul final de "curăţire" a stivei) - ci chiar în momentul compilării, codul ei va fi inserat ca atare în locul în care este invocată (similar preprocesării în C la întâlnirea unui #define
).
Această "improvizaţie" merge de fapt şi mai departe: metodele clasei sunt tratate în mod implicit ca şi cum ar fi fost declarate "inline"!
Cerând în constructori, alocarea de memorie suplimentară în zona "heap" - devine obligatorie prevederea unui destructor (linia 10), cerând sistemului de operare să elibereze din "heap" zona indicată de pointerul "cont". Destructorul va fi apelat automat nu numai la încheierea execuţiei întregului program, dar şi în momentul încheierii unui subprogram, pentru a elimina din "heap" zonele asociate obiectelor locale create în interiorul subprogramului.
Constructorii şi destructorii "impliciţi" vizează numai segmentele de memorie "standard", asociate programului în momentul compilării (segmentul de date "globale", cel de "stivă"). Dacă obiectele noastre necesită şi zone din "memoria liberă" (la momentul curent al execuţiei programului) - devine obligatorie prevederea unor constructori şi destructori proprii clasei respective.
Constructorii din liniile 7 şi 8 asigură operaţiile de "copiere" şi respectiv, "atribuire"; aceste operaţii sunt implicate în diverse contexte şi uneori, în mod tacit; de exemplu, funcţia care va returna suma a două matrice (modelând A = B + C
) va trebui să creeze local (în stivă) matricea-sumă, urmând să o returneze în final - ori returnarea matricei-sumă înseamnă tocmai copierea datelor acesteia (din "cadrul-stivă", care urmează să fie eliminat) în zona-destinaţie (cea afectată matricei A
).
Aici intervine următoarea subtilitate: nu este suficientă copierea din cadrul-stivă a valorilor "row", "col" şi "cont" - în ideea că, deţinând adresa "cont" a zonei din memoria "heap" în care există valorile matricei-sumă locale, matricea A
deja ar deţine şi aceste valori.
Este adevărat că încheierea execuţiei funcţiei atrage eliminarea cadrului-stivă asociat acesteia, dar nu afectează în mod direct zonele din "heap" - şi am putea dispune de acestea şi după încheierea execuţiei subprogramului; însă nu este aşa: odată cu revenirea dintr-un apel de funcţie (şi de fapt, la ieşirea din contextul unui bloc oarecare) este invocat în mod automat destructorul specific clasei respective, iar acesta (ca în cazul nostru) elimină zonele alocate în cadrul blocului în memoria "heap" - încât "cont" care a fost copiat în A
, indică de fapt o zonă devenită indisponibilă.
De aceea, constructorii de copiere şi de atribuire trebuie să apeleze şi ei funcţia alloc()
pentru crearea matricei-destinaţie A
şi trebuie să asigure şi copierea în zona alocată astfel, a valorilor din "heap" ale matricei-sumă (încât prevederea lor prin liniile 7-8 este absolut necesară).
Pentru introducerea şi pentru redarea valorilor unei matrice avem de folosit operatorii definiţi în liniile 12-13. Se presupune că valorile iniţiale ale matricei au fost înscrise în prealabil într-un fişier text şi atunci operatorul "<<
" (linia 13) va citi aceste valori şi le va înscrie în zona "cont" a matricei indicate. Calificatorul friend
asigură că aceşti operatori pot accesa datele interne din ambele clase (atât ale obiectului "stream" asociat fişierului, cât şi pe ale matricei).
Analog, în liniile 15-16 sunt indicaţi operatorii de adunare (pentru matrice "de acelaşi tip") şi de înmulţire (pentru matrici "înlănţuite").
E de înţeles că aici am prezentat schiţa de bază - suficientă - pentru a lucra cu matrice în diverse programe. Era bine să fi prevăzut şi un constructor matrix::matrix(int, int, double[])
, care nu numai să aloce memorie (urmând ca valorile să fie înscrise ulterior, prin citire din fişier) - dar şi să înscrie valorile matricei (preluîndu-le din tabloul unidimensional transmis ca parametru).
Deasemenea, era bine să fi prevăzut pentru anumite necesităţi ale programelor şi construcţia unor matrici speciale, precum "matricea unitate".
În linia a doua a fişierului matrice.cc
redat mai jos, am prevăzut "macroul" FORij
pentru cele două instrucţiuni "for" cu care s-ar parcurge elementele unei matrice - simplificând astfel scrierea definiţiilor constructorilor şi a operatorilor ">>", "+" şi "*".
Să observăm totuşi că puteam plasa acest macrou chiar în fişierul "header" matrice.h
, făcându-l astfel disponibil şi programului care ar include biblioteca noastră (aceasta trebuie să conţină "matrice.h", dar nu neapărat şi fişierul-sursă "matrice.cc", ci doar fişierul-obiect matrice.o
- în care macroul a fost deja expandat).
#include "matrice.h" #define FORij for(int i=0; i < rows; i++) for(int j=0; j < cols; j++) matrix::matrix(int n=0, int m=0) { // matrix A(2, 3), B; if(n && !m) m = n; // matrix A(3); (pătratică) rows = n; cols = m; alloc(); FORij cont[i][j] = 0; // iniţializează valorile matricei } matrix::matrix(const matrix& M) { // matrix T(*this); ...; return T; rows = M.rows; cols = M.cols; alloc(); FORij cont[i][j] = M.cont[i][j]; // copiază valorile din M } matrix& matrix::operator = (const matrix& M) { // B = A; B=C=A; if(this == &M) return *this; // evită auto-atribuirea (matrix M = M) if(rows != M.rows || cols != M.cols) { this -> ~matrix(); // îşi şterge din HEAP tabloul alocat rows = M.rows; cols = M.cols; alloc(); // preia datele lui M } FORij cont[i][j] = M.cont[i][j]; // copiază valorile din M return *this; } matrix::~matrix() { // cere SO să elibereze zona HEAP alocată for(int i=0; i < rows; i++) delete [] cont[i]; delete [] cont; } ostream& operator << (ostream& out, const matrix& M) { for(int i = 0, n=M.rows; i < n; i++) { for(int j = 0, m=M.cols; j < m; j++) { out.width(10); out << M.cont[i][j]; } out << '\n'; } out << '\n'; return out; } istream& operator >> (istream& inp, matrix& M) { int rows = M.rows, cols = M.cols; FORij inp >> M.cont[i][j]; return inp; } matrix operator + (const matrix& A, const matrix& B) { if(A.rows != B.rows || A.cols != B.cols) // dacă n-au acelaşi tip, return matrix(); // atunci returnează matricea vidă int rows = A.rows, cols = A.cols; matrix C(rows, cols); FORij C.cont[i][j] = A.cont[i][j] + B.cont[i][j]; return C; } matrix operator * (const matrix& A, const matrix& B) { if(A.cols != B.rows) // dacă nu-s înlanţuite, return matrix(); // atunci returnează matricea vidă int rows=A.rows, cols=B.cols; matrix C(rows, cols); FORij for(int k=0; k < A.cols; k++) C.cont[i][j] += A.cont[i][k] * B.cont[k][j]; return C; }
În vederea folosirii ulterioare în diverse programe a clasei "matrix", am putea proceda acum în mai multe moduri. Cel mai simplu este de a furniza ambele surse, matrice.h
şi matrice.cc
; în acest caz, programul va trebui să prevadă #include "matrice.cc"
- astfel că "matrice.cc" va fi (re)compilat odată cu programul respectiv.
Sau putem să constituim o "bibliotecă" (eliminând necesitatea recompilării ulterioare a fişierului "matrice.cc") şi anume (în cea mai modestă formă) astfel:
vb@vb:~/clasa11/OOP/Matrice$ g++ -c matrice.cc
Obţinem astfel fişierul obiect matrice.o
(opţiunea -c
cere compilatorului doar să compileze fişierul sursă indicat, suprimând invocarea mai departe a link-editorului).
"Biblioteca" este constituită din fişierele matrice.h
şi matrice.o
, iar pentru a folosi clasa "matrix" într-un program "test.cpp" - acesta trebuie să folosească de data aceasta #include "matrice.h"
şi să fie compilat prin:
vb@vb:~/clasa11/OOP/Matrice$ g++ test.cpp matrice.o -o test
ceea ce produce într-o primă fază fişierul intermediar test.o
şi apoi asigură "legarea" acestuia cu fişierul-obiect existent matrice.o
- producând în final fişierul executabil indicat de opţiunea "-o".
Pentru calculul puterilor unei matrice putem înmulţi repetat matricea dată, cu ea însăşi - dar mai eficientă este modelarea relaţiei $A^n=A^{\lfloor\frac{n}{2}\rfloor}\cdot A^{\lceil \frac{n}{2}\rceil}$ - de exemplu, $A^5=A^2\cdot A^3=A^2\cdot A^2\cdot A$ - ceea ce constituie algoritmul lui Al Kashî; acesta poate să fie formulat la fel de scurt - prin funcţia recursivă pow(), cuprinsă în program - ca şi calculul obişnuit:
#include "matrice.h" // tipul "matrix" #include <cstdlib> // atoi() - conversia parametrilor din linia de comandă ifstream input("matrici.txt"); // matricea pătratică iniţială ofstream output("out.txt"); matrix pow(const matrix& A, int n) { // produce $A^n$ (algoritmul Al Kashî) if(n == 1) return A; matrix T = pow(A, n/2); return n & 1 ? T*T*A : T*T; // n & 1 (bitul_0 din n) reflectă n % 2 } int main(int argc, char** argv) { // test 3 25 int dim = atoi(argv[1]); // ordinul matricei din "matrici.txt" (=3) int n = atoi(argv[2]); // exponentul (=25) matrix A(dim); input >> A; output.setf(ios_base::fixed, ios_base::floatfield); output.precision(0); // matricea iniţială conţine numere întregi output << "A^" << n << ":\n" << pow(A, n); }
Compilând cum am arătat anterior şi invocând de pe linia de comandă \.test 3 25 obţinem acest rezultat (pentru matricea redată alături, ale cărei valori le-am înscris în prealabil în "matrici.txt"):
A^25: 144053027 35602847 -104719458 64121346 10815070 -45593377 37055938 -1453091 -24787777
$A=\left\lgroup\matrix{3&-1&-2\\2&-2&-1\\2&-3&-1}\right\rgroup$
Ne putem convinge că acesta este rezultatul corect, folosind de exemplu Octave:
octave:1> A = [3,-1,-2; 2,-2,-1; 2,-3,-1]; octave:2> A^25 ans = 144053027 35602847 -104719458 64121346 10815070 -45593377 37055938 -1453091 -24787777
Desigur, ar fi de observat că pentru lucrul efectiv cu matrice (şi cu diverse alte "tipuri" matematice) putem dispune direct (fără să mai "modelăm" noi înşine)
de diverse pachete software (precum Octave, exemplificat mai înainte) - şi foarte competente şi uşor de folosit.
vezi Cărţile mele (de programare)