This is an old revision of the document!


Laborator 02 - Diferențe C/C++

Autor: Răzvan Cristea

Obiective Specifice

Studentul va fi capabil la finalul acestui laborator să:

  • recunoască diferențele dintre C și C++
  • realizeze unui program în C++ care să valorifice caracteristicile și facilitățile specifice limbajului

Introducere

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

Diferențe C/C++

Citirea și afișarea variabilelor

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

Citirea și afișarea în C
#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;
}
Citirea și afișarea în C++
#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;
}

Este de menționat faptul ca în C++ și funcțiile scanf și printf se pot utiliza, dar ca și recomandare ar fi mai indicată utilizarea operatorilor de citire și afișare ai limbajului, deoarece sunt mai specializați pentru ceea ce vom învăța pe parcursul semestrului.

Alocarea dinamică a memoriei

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.

Alocare dinamică în C

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;
}
Alocare dinamică în C++

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

Pentru a face o eliberare corectă a memoriei numărul de delete-uri trebuie să fie egal cu numărul de new-uri. Dacă această regulă este respectată atunci nu vor apărea memory leak-uri (scurgeri de memorie).

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

În C++ nu există operator pentru realocarea memoriei. Nu este recomandată utilizarea funcției realloc pe un vector care a fost alocat cu operatorul new, deoarece va duce la un comportament nedefinit. Dacă vrem să realocăm vectorul va trebui mai întâi să îl dezalocăm și apoi să îl realocăm cu noua dimensiune.

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

Tipul referință

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

  1. Referința când este declarată trebuie instant inițializată
  2. După inițializare referința nu mai poate fi schimbată
  3. Referințele nu pot fi nule
  4. Referințele nu trebuie dereferențiate
#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.

C++ introduce posibilitatea de a inițializa pointerii cu nullptr, care este specific doar pentru acest tip de date. Acesta funcționează similar cu NULL, dar cu un avantaj important: nullptr este un tip de date dedicat pointerilor, ceea ce previne atribuirea sa accidentală altor tipuri de variabile, cum se putea întâmpla cu NULL în C++. În C++, NULL este definit doar ca un macro care reprezintă valoarea 0 și poate fi atribuit chiar și variabilelor care nu sunt pointeri, lucru care poate duce la confuzii nedorite.

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

Funcții cu același nume

În C++ avem avantajul de a declara funcții, procedeu cunoscut sub numele de supraîncărcare a funcțiilor, 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;
}

Trebuie subliniat faptul că tipul de return al unei funcții nu este luat în considerare în contextul polimorfismului prin supraîncărcare. Ceea ce definește polimorfismul în acest caz este exclusiv semnătura funcției, adică numele împreună cu lista și tipurile parametrilor. De aceea, două funcții care diferă doar prin tipul valorii returnate nu sunt considerate supraîncărcări valide. În schimb, diferențele în numărul, tipul sau chiar ordinea parametrilor constituie forme acceptate de polimorfism, așa cum se poate observa în cazul ultimelor două funcții din exemplul de mai sus.

Concluzii

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

poo-is-ab/laboratoare/02.1758656146.txt.gz · Last modified: 2025/09/23 22:35 by razvan.cristea0106
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0