This is an old revision of the document!
Autor: Răzvan Cristea
Studentul va fi capabil la finalul acestui laborator să:
În cadrul acestui laborator vom evidenția principalele diferențe dintre limbajele C și C++. Vom analiza, prin exemple simple, modul în care C++ extinde caracteristicile limbajului C și vom pune bazele unei înțelegeri mai profunde a acestuia. Astfel, vom avea un punct de plecare solid pentru paradigma Orientată Obiect (OO) pe care o vom studia începând cu laboratorul următor.
În C pentru citirea și afișare variabilor se realiza utilizând funcțiile scanf și printf. În C++ vom folosi operatorul >> pentru citirea datelor de la tastatură sau din fișiere și operatorul << pentru afișarea datelor în fișiere sau consolă.
#include <stdio.h> int main() { int x; printf("Introduceti un numar: "); scanf("%d", &x); printf("Numarul introdus de utilizator este: %d\n", x); return 0; }
#include <iostream> int main() { int x; std::cout << "Introduceti un numar: "; // cout vine de la console output std::cin >> x; // cin vine de la console input std::cout << "Numarul introdus de utilizator este: " << x << '\n'; return 0; }
Dacă în cazul citirii și afișării variabilelor lucrurile nu erau cu mult diferite, dacă discutăm despre alocare dinamică există câteva diferențe seminficative pe care o sa le observăm în exemplele următoare.
Pentru a aloca dinamic în C folosim la alegere funcția malloc sau funcția calloc, singura diferență între ele fiind faptul că funcția calloc initializează cu 0 valorile. Pentru eliberarea memoriei se folosește funcția free.
#include <iostream> int main() { int* ptr1 = (int*)malloc(sizeof(int)); *ptr1 = 5; // sau folosind calloc int* ptr2 = (int*)calloc(1, sizeof(int)); std::cout << *ptr2 << '\n'; *ptr2 = *ptr1; std::cout << *ptr1 << '\n'; std::cout << *ptr2 << '\n'; free(ptr1); free(ptr2); return 0; }
Dacă dorim să alocăm dinamic a un vector putem proceda în felul următor.
int main() { int nrElemente = 5; int* vector = (int*)malloc(nrElemente * sizeof(int)); vector[0] = 3; vector[1] = 2; vector[2] = -2; vector[3] = 10; vector[4] = 8; std::cout << "Vectorul alocat dinamic este: "; for (int i = 0; i < nrElemente; i++) { std::cout << vector[i] << ' '; } free(vector); return 0; }
Iar dacă vrem să realocăm spațiul din vector folosim funcția realloc.
int main() { int nrElemente = 5; int* vector = (int*)malloc(nrElemente * sizeof(int)); vector[0] = 3; vector[1] = 2; vector[2] = -2; vector[3] = 10; vector[4] = 8; std::cout << "Vectorul alocat dinamic este: "; for (int i = 0; i < nrElemente; i++) { std::cout << vector[i] << ' '; } nrElemente = 8; vector = (int*)realloc(vector, nrElemente * sizeof(int)); vector[5] = 15; vector[6] = 20; vector[7] = 25; std::cout << "\nVectorul realocat dinamic este: "; for (int i = 0; i < nrElemente; i++) { std::cout << vector[i] << ' '; } free(vector); return 0; }
În C++ alocarea și dezalocarea memoriei sunt mai simple, deoarece aici nu mai avem funcții ci operatori specifici. Pentru a aloca memoria în C++ se folosește operatorul new, iar pentru a elibera memoria folosim operatorul delete.
Alocarea dinamică pentru o singură adresă de memorie
În exemplul de mai jos puteți vedea diferite variante de alocare și dezalocare pentru un singur spațiu de memorie.
#include <iostream> int main() { int* ptr1 = new int; // alocare dinamica fara initializare, compilatorul va atribui o valoare in mod aleator int* ptr2 = new int(10); // alocare dinamica cu initializare std::cout << *ptr1 << '\n'; std::cout << *ptr2 << '\n'; /*delete ptr1, ptr2; // desi nu da eroare de compilare va elibera doar spatiul pentru ptr1, nu se recomanda aceasta scriere pentru ca va genera memory leak-uri usor*/ delete ptr1; delete ptr2; return 0; }
Alocarea dinamică pentru un bloc de memorie
Dacă intenționăm să alocăm dinamic un vector în C++ vom folosi tot operatorul new, dar puțin diferit.
#include <iostream> int main() { int nrElemente = 5; int* vector = new int[nrElemente]; // se folosesc [] pentru a anunta compilatorul ca vrem sa alocam spatiu pentru un bloc de memorie continuu for (int i = 0; i < nrElemente; i++) { std::cout << "vector[" << i << "] = "; std::cin >> vector[i]; } std::cout << "\nElementele vectorului alocat sunt: "; for (int i = 0; i < nrElemente; i++) { std::cout << vector[i] << ' '; } delete[] vector; // se folosesc [] pentru a anunta compilatorul ca ne dorim sa eliberam memoria unui bloc contiguu return 0; }
#include <iostream> int main() { int nrElemente = 6; int* vector = new int[nrElemente]; for (int i = 0; i < nrElemente; i++) { std::cout << "vector[" << i << "] = "; std::cin >> vector[i]; } std::cout << "\nElementele vectorului alocat sunt: "; for (int i = 0; i < nrElemente; i++) { std::cout << vector[i] << ' '; } delete[] vector; nrElemente = 3; vector = new int[nrElemente]; std::cout << "\n\n===================================================\n\n"; for (int i = 0; i < nrElemente; i++) { std::cout << "vector[" << i << "] = "; std::cin >> vector[i]; } std::cout << "\nElementele vectorului realocat sunt: "; for (int i = 0; i < nrElemente; i++) { std::cout << vector[i] << ' '; } std::cout << '\n'; delete[] vector; return 0; }
În C++, referințele reprezintă o extensie esențială pentru gestionarea variabilelor. Practic, o referință este un alias pentru o variabilă deja existentă. Spre deosebire de pointeri, care stochează adresa unei variabile și necesită dereferențiere explicită, referințele acționează direct asupra variabilei asociate, eliminând necesitatea manipulării adreselor.
Odată ce am asociat o referință unei variabile, orice operație efectuată asupra acesteia se reflectă direct asupra variabilei originale. Putem privi referința ca pe o persoană care are două prenume: indiferent de numele folosit, este vorba despre aceeași persoană. Astfel, deși se aseamănă cu pointerii, referințele se disting prin simplitate și prin modul mai natural în care permit accesul la date. Diferențele între referințe și pointeri sunt mai multe la număr însă le vom prezenta pe cele mai importante mai jos.
#include <iostream> int main() { int x = 10; int& ref = x; ref = 16; std::cout << x << '\n'; // x devine 16 deoarece ref este un alias pentru el x = 20; std::cout << ref << '\n'; // ref este 20 datorita faptului ca se refera la x int y = 0; ref = y; // poate parea schimbarea referintei dar in realitate este doar atribuirea valorii 0 lui ref std::cout << x << '\n'; // x este 0 din motive evidente return 0; }
Pentru a înțelge mai bine, în desenul de mai jos se poate observa de fapt cine este ref și că nu face altceva decât să partajeze aceeași zonă de memorie ca și x.
Cu ajutorul acestui tip de date putem să ne facem viața mult mai ușoară, deoarece putem trimite parametrii unei funcții prin referință.
#include <iostream> void alocareVector(int*& v, const int& dim) { v = new int[dim]; } void citireVector(int*& v, const int& dim) { for (int i = 0; i < dim; i++) { std::cout << "vector[" << i << "] = "; std::cin >> v[i]; } } void afisareVector(const int* const& v, const int& dim) { std::cout << "Elementele vectorului alocat sunt: "; for (int i = 0; i < dim; i++) { std::cout << v[i] << ' '; } std::cout << '\n'; } void dezalocareVector(int*& v) { if (v != nullptr) { delete[] v; } } int main() { int nrElemente = 5; int* vector = nullptr; alocareVector(vector, nrElemente); citireVector(vector, nrElemente); afisareVector(vector, nrElemente); dezalocareVector(vector); return 0; }
Funcțiile folosesc referințe la pointeri (de exemplu, int*& v) pentru a permite modificarea efectivă a pointerilor în funcția apelantă. Astfel, funcțiile pot aloca memorie sau modifica adresele pointerilor direct în cadrul funcției care le-a apelat, fără a returna un nou pointer.
Parametrii transmiși ca referințe constante (de exemplu, const int& dim) sunt utilizați pentru a asigura că valoarea acestora nu este modificată în interiorul funcției, protejând astfel valorile originale. Acest lucru ajută la prevenirea modificărilor accidentale și la creșterea clarității.
Funcția afisareVector folosește referințe constante la pointeri (const int* const& v), pentru a garanta că atât adresa vectorului, cât și conținutul acestuia nu vor fi modificate în timpul afișării, menținând integritatea datelor.
#include <iostream> int main() { int x = NULL; // valid int* ptr = NULL; // valid ptr = nullptr; // valid x = nullptr; // eroare de compilare x nu este un pointer return 0; }
În C++ avem avantajul de a declara funcții cu același nume dar care să difere prin numărul și/sau tipul parametrilor. Această modalitate de declarare a funcțiilor este cunoscută în Programarea Orientată Obiect sub numele de polimorfism care înseamnă multe forme. Vom oferi mai multe detalii pe parcursul întregului semestru despre acest principiu al POO.
Să urmărim exemplul de cod de mai jos care ilustrează polimorfismul a trei funcții.
#include <iostream> int suma(int a, int b) { return a + b; } float suma(float a, float b) { return a + b; } float suma(float a, int b) { return a + b; } float suma(int a, float b) { return a + b; } int main() { std::cout << suma(2, 5) << '\n'; // prima functie denumita suma std::cout << suma(2.5f, 5.5f) << '\n'; // a doua functie denumita suma std::cout << suma(2.85f, 8) << '\n'; // a treia functie denumita suma std::cout << suma(10, 8.5f) << '\n'; // // a patra functie denumita suma return 0; }
În acest laborator, am explorat în detaliu diferențele fundamentale dintre clase și structuri, subliniind faptul că singura deosebire constă în specificatorul de acces implicit: la structuri, membrii sunt publici, iar la clase sunt privați. Totodată, am învățat cum să creăm obiecte și cum să le gestionăm proprietățile folosind constructori, precum și accesori prin intermediul getter-ilor și al setter-ilor. Acest lucru ne oferă un control mai fin asupra datelor și ne permite o manipulare clară și sigură a obiectelor într-un program care folosește conceptele OOP. Astfel, am dobândit o înțelegere mai bună a principiilor fundamentale ale Programării Orientate Obiect, principii pe care le vom aplica pe întregul parcurs al semestrului.