Responsabili
În cadrul acestui laborator ne propunem să ilustrăm conceptele din C++ cu care veți lucra pe parcursul acestui semestru.
Într-un mod extrem de simplist spus C++ este un superset al limbajului C, iar tot ceea ce ați învățat în C la PC se poate compila cu un compilator pentru limbajul C++, funcționalitatea rămânând aceeași.
Ne dorim să:
Pentru că C++ permite implementarea structurilor de date cu tipuri de date generice, prin intermediul template-urilor, într-un mod care nu presupune trecerea la programarea orientată pe obiecte. În cadrul acestui laborator nu ne așteptăm să dobândiți cunoștințe (elementare sau avansate) legate de programarea obiectuală, întrucât în anul II există un curs dedicat acestui lucru.
Vă încurajăm însă să citiți cât mai multe despre C++ pe parcurs și să cereți lămuriri suplimentare din partea asistenților de la laborator.
În cadrul laboratorului de Programarea Calculatoarelor am învățat să declarăm și să folosim tipuri de date complexe, structuri în limbajul C. Pentru a recapitula, iată mai jos un exemplu simplu de astfel de structură, pentru a reprezenta un număr complex.
|
Tipul de date definit mai sus, ca orice alt tip de date, poate fi folosit drept:
Mai jos puteți urmări un exemplu în acest sens:
|
Dându-se o variabilă de tip struct complex vom dori să efectuăm diferite operații asupra acesteia.
#ifndef __COMPLEX_H #define __COMPLEX_H struct complex { double re; double im; }; // Initializeaza campurile unei structuri date. void complex_initialize(struct complex *number, double re, double im); // Intoarce o structura ce contine numarul complex conjugat. struct complex complex_conjugate(struct complex *number); #endif // __COMPLEX_H
Ce observăm că au în comun cele două funcții? Hint: primul parametru, care reprezintă un pointer către o zonă de memorie care reține tipul struct complex
Acest pattern de a defini funcții specifice unui anumit tip de date este extrem de întâlnit. De asemenea, se observă că aceste funcții nu ar putea fi folosite în combinație cu alte structuri de date, ele fiind specifice struct complex.
Așadar, iată cum o structură poate să capete funcții specifice (codul de mai jos este specific C++):
#ifndef __COMPLEX_H #define __COMPLEX_H struct complex { double re; double im; void complex_initialize(double re, double im); struct complex complex_conjugate(); }; #endif // __COMPLEX_H
Astfel, am definit funcţii care operează pe tipul nostru de date și care pot fi apelate întocmai cum se realizează accesarea membrilor de date. Observăm însă că a dispărut primul parametru! De ce?
Funcțiile definite mai sus pot fi apelate numai pe o variabilă de tip struct complex
sau struct complex *
în același mod în care se accesează și variabilele membru. Ne dorim, totuși, ca în corpul acestor funcții membru să putem modifica (sau cel puțin accesa) ceilalți membri ai variabilei pe care facem apelul. Cum putem ști, în corpul funcției, pe ce variabilă am făcut apelul ca să putem accesa informația necesară?
C++ se ocupă (în spate, fără intervenția noastră) să paseze un parametru extra funcției apelate. El este de tip struct complex*
și se numește this; nu face parte efectiv din semnătura funcției și este un cuvânt cheie (rezervat) în C++, deci aveți grijă cum vă numiți variabilele.
Aceste funcții membru cu proprietățile pe care tocmai le-am menționat se numesc metode. Este un termen pe care îl veți folosi de acum încolo în acest context și mai ales în programarea orientată pe obiecte începând cu semestrul următor.
Mai jos observați implementarea metodelor în antetul definit mai sus și folosirea lui this. Compilați și rulați codul de mai jos cu g++.
#ifndef __COMPLEX_H #define __COMPLEX_H struct complex { double re; double im; void complex_initialize(double re, double im) { this->re = re; this->im = im; } struct complex complex_conjugate() { struct complex conjugate; conjugate.complex_initialize(this->re, -(this->im)); return conjugate; } }; #endif // __COMPLEX_H
Formal am făcut deja primii pași mai sus pentru a implementa o clasă în C++, utilizând keyword-ul struct.
Totuși, ce înseamnă o clasă? Nu trebuie decât să ne gândim la ce am făcut mai sus:
Cu această adăugare menționată, putem să ne referim la ceea ce înseamnă o clasă, respectiv un obiect.
Ne referim la o clasă ca fiind o amprentă (blueprint) sau descriere generală. Un obiect sau o instanță a clasei este o variabilă concretă ce se conformează descrierii clasei.
Vom numi clasă tipul de date definit de struct complex sau class complex și obiect o instanțiere (o alocare dinamică sau locală) a tipului de date.
Când discutăm despre tipul de date complex ne referim la clasă. Când discutăm despre variabila number ne referim la un obiect, o instanță a clasei.
Și totuși, C++ adăugă keyword-ul class. Care este diferența între class și struct? Iată cum definim complet clasa de mai sus, separând antetul de implementare și de programul principal.
|
|
Sursele C++ se compilează folosind compilatorul g++. Acesta permite exact aceleași opțiuni de bază ca și gcc, compilatorul utilizat pentru sursele de C.
g++ complex.cc main.cc -o exemplu
Ce observați?
Înlocuiți acum keyword-ul class cu keyword-ul struct și compilați din nou.
Am observat mesajul de eroare în urma compilării fișierelor de mai sus.
Astfel, singura diferență folosirea celor două keyword-uri este nivelul implicit de vizibilitate a metodelor și atributelor.
Membri precedați de label-ul private pot fi folosiți numai în interiorul clasei, în cadrul metodelor acesteia. Ei nu pot fi citiți sau modificați din afara clasei.
Iată cum puteam remedia soluția:
class Complex { public: double re; double im; Complex conjugate(); };
Studiați codul de mai jos.
|
|
Observăm două bucăți din cod în mod special:
Complex::Complex(double re, double im);
Linia de mai sus nu are tip returnat, spre deosebire de celelalte linii. Acesta este constructorul clasei, care este apelat în momentul alocării unui obiect.
Ce operații sunt uzuale în constructor?
A doua bucată observată este:
Complex numar(2, 3);
Până acum nu ați mai alocat astfel structurile. Ce se întâmplă în spate este exact ceea ce intuiți: este apelat constructorul obiectului și se execută instrucțiunile acestuia pentru variabila numar (reprezentată ca pointer prin this, direct în interiorul constructorului).
În constructorul definit mai sus, tot ceea ce se întâmplă este să se inițializeze membri. Pentru asta, C++ vă pune la dispoziție o sintaxă simplă:
Complex::Complex(double real, double imaginar) : re(real), im(imaginar) { } // SAU Complex::Complex(double real, double imaginar) { this->re = real; this->im = imaginar; }
Cei doi constructori sunt (aproape) identici ca funcționalitate.
Așa cum probabil ați observat, constructorul este apelat în mod explicit de către voi. Destructorul însă, în cazul de mai sus, este apelat implicit la terminarea blocului care realizează dealocărea automată a obiectului.
Un destructor nu are parametri și se declară în interiorul clasei astfel:
~Complex();
Dacă în constructor sau în interiorul clasei ați fi alocat memorie, cel mai probabil în destructor ați fi făcut curat și ați fi apelat free pe membrul respectiv.
C++ introduce perechea de keyword-uri new și delete, care se folosesc pentru a aloca dinamic instanțe ale claselor.
Complex *numar = new Complex(2, 3); delete numar;
Keyword-ul new apelează constructorul clasei, iar keyword-ul delete apelează destructorul clasei.
Observație
new[]/delete[]
Complex *numere = new Complex[10]; delete[] numere;
Dacă dorim să alocăm memorie dinamică în mai multe dimensiuni, vom folosi o procedură asemănătoare cu cea folosită în C. Presupunând că vrem să alocăm o matrice de dimensiune NxM
atunci declarăm un dublu pointer, alocăm N
pointeri pentru linii și apoi M
elemente pentru fiecare linie în parte.
int **mat; mat = new int*[N]; for (int i = 0; i < N; i++) { mat[i] = new int[M]; } // .. // Folosire matrice // .. // Dezalocare for (int i = 0; i < N; i++) { delete[] mat[i]; } delete[] mat;
În cazul de mai sus, N
și M
nu trebuie sa fie constante, pot fi (de exemplu) variabile citite de la tastatură.
Atenție Memoria alocată nu va fi toată într-o zonă continuă (doar elementele unei linii alocate cu new int[M]
vor fi continue, dar între două linii consecutive pot exista “spații” în memorie).
Dacă toate dimensiunile (mai puțin prima) sunt constante (cunoscute la compile time) se poate aloca dinamic memoria în felul următor:
int (*mat)[100] = new int[N][100]; // mat va fi alocata în zona de heap (nu pe stivă), într-o zonă continuă de memorie // .. // Folosire matrice // .. // Dezalocare delete[] mat;
Atenție Nu convertiți matricea de mai sus la un pointer dublu, deoarece cele două nu sunt același lucru (prima este un vector de N
pointeri care pointează către liniile matricei, pe când a doua este o zonă continuă de memorie în care compilatorul accesează elementele la fel ca într-o matrice clasică).
Această secțiune nu este punctată și încearcă să vă facă o oarecare idee a tipurilor de întrebări pe care le puteți întâlni la un job interview (internship, part-time, full-time, etc.) din materia prezentată în cadrul laboratorului.
Și multe altele…