Esercizio - MyVector con memoria dinamica (laboratorio 3)
Seconda parte del terzo laboratorio del corso di Laboratorio di Programmazione, Ing. Informatica UniPD: una classe che gestisce un buffer sul free store. La prima parte è Esercizio - Puntatori, array e passaggio dei parametri (laboratorio 3). Teoria: C++ - allocazione dinamica della memoria con new e deletela memoria dinamica si prende dal free store (heap) con new, che restituisce un puntatore all'oggetto o al primo elemento dell'array (new T, new T[n], con inizializzatori {}), e si restituisce con delete (per new) e delete[] (per new[]); il puntatore non sa quanti elementi ci sono, la memoria non ha nome e non ha controllo dei limiti; in C++ non c'è garbage collector (scelta per efficienza e controllo), quindi un'area che perde ogni riferimento diventa garbage e produce un memory leak, e gli errori tipici sono la doppia cancellazione e il dangling pointer (si mette il puntatore a nullptr dopo delete); il distruttore ~T() è chiamato dal compilatore quando l'oggetto esce dallo scope o con delete e libera le risorse, per cui una classe come vector acquisisce la memoria nel costruttore e la rilascia nel distruttore; new alloca e chiama il costruttore, delete chiama il distruttore e libera; operator[] con due versioni (T& e const) dà accesso in lettura e scrittura al buffer; si alloca dinamicamente solo se serve davvero (memoria grande, dimensione nota a run-time, durata controllata).C++ - allocazione dinamica della memoria con new e delete →; la versione che cresce è il Esercizio - MyVector 2.0 con buffer ridimensionabile (laboratorio 4).
Testo. Implementare la classe
MyVector, analoga alla classevectordiscussa a lezione, che rappresenta vettori didoubledi lunghezza decisa in fase di costruzione (e non modificabile in seguito). Il buffer dove sono salvati i dati deve essere allocato dinamicamente, poiché i dati da salvare potrebbero essere numerosi. Includere nella classe: a. un dato membrointche contiene la lunghezza del vettore; b. un costruttore che accetta unintche indica la lunghezza del vettore da costruire; c. l'overloading dell'operatore[], sia in versioneconstsia nonconst; d. le funzionisafe_set()esafe_get()che permettono l'accesso agli elementi effettuando il controllo di accesso entro i limiti del vettore; e. il distruttore.
Progetto
operator[] e safe_get/safe_set convivono: v[i] è veloce e non controlla (come un array), safe_* controlla. È lo stesso compromesso di std::vector fra operator[] e at().
Soluzione
#include <iostream>
#include <stdexcept>
using namespace std;
// Vettore di double di lunghezza fissata alla costruzione. Il buffer sta sul free store.
class MyVector {
public:
explicit MyVector(int n); // b. costruttore con la lunghezza
~MyVector(); // e. distruttore: libera il buffer
MyVector(const MyVector&) = delete; // senza copia scritta a mano la copia di default
MyVector& operator=(const MyVector&) = delete; // farebbe due delete[] sullo stesso buffer: la si vieta
int size() const { return size_; } // a. la lunghezza (dato membro: size_)
double& operator[](int i) { return buffer_[i]; } // c. versione non const: lettura e scrittura
double operator[](int i) const { return buffer_[i]; } // versione const: sola lettura
void safe_set(int i, double value); // d. accesso con controllo dei limiti
double safe_get(int i) const;
private:
int size_;
double* buffer_;
};
MyVector::MyVector(int n) : size_{n}, buffer_{new double[n]}
{
for (int i = 0; i < size_; ++i) buffer_[i] = 0; // new non inizializza i double
}
MyVector::~MyVector()
{
delete[] buffer_;
}
void MyVector::safe_set(int i, double value)
{
if (i < 0 || i >= size_) throw out_of_range("MyVector::safe_set: indice fuori dai limiti");
buffer_[i] = value;
}
double MyVector::safe_get(int i) const
{
if (i < 0 || i >= size_) throw out_of_range("MyVector::safe_get: indice fuori dai limiti");
return buffer_[i];
}
int main()
{
MyVector v(5);
for (int i = 0; i < v.size(); ++i) v[i] = i * 1.5; // operator[] non const
v.safe_set(4, 10);
cout << "v[2] = " << v[2] << ", safe_get(4) = " << v.safe_get(4) << '\n';
const MyVector& cv = v; // riferimento const: serve operator[] const
cout << "cv[1] = " << cv[1] << '\n';
// cv[1] = 3; // errore: la versione const non ritorna un lvalue
cout << "v[5] (fuori dai limiti, nessun controllo): comportamento indefinito, non lo provo\n";
try {
v.safe_set(5, 1.0); // con il controllo: eccezione
cout << "non arrivo qui\n";
}
catch (out_of_range&) {
cout << "safe_set(5, ...): indice fuori dai limiti\n";
}
try {
cout << v.safe_get(-1) << '\n';
}
catch (out_of_range&) {
cout << "safe_get(-1): indice fuori dai limiti\n";
}
return 0;
}Uscita (il programma usa try/catch: in WebAssembly le eccezioni non funzionano, quindi l'uscita è ricavata a mano e confermata eseguendo una copia in cui throw stampa e termina):
v[2] = 3, safe_get(4) = 10
cv[1] = 1.5
v[5] (fuori dai limiti, nessun controllo): comportamento indefinito, non lo provo
safe_set(5, ...): indice fuori dai limiti
safe_get(-1): indice fuori dai limitiLettura:
v[i] = i * 1.5per dà ;safe_set(4, 10)sostituisce l'ultimo con . Perciòv[2] = 3,safe_get(4) = 10.cvè una referenceconstav:cv[1]sceglie l'overloadconste legge ;cv[1] = 3;non compila perché quell'overload non restituisce un lvalue.v.safe_set(5, 1.0)ev.safe_get(-1)hanno indici fuori da : lancianoout_of_rangee ilcatchstampa il messaggio. Al contrariov[5]compilerebbe e scriverebbe oltre il buffer, senza segnalazione.- Alla
}dimainil distruttore libera il buffer (delete[]).
Versione ripasso
- Dati:
int size_; double* buffer_;(il puntatore non conosce la lunghezza). Costruttoreexplicit MyVector(int n) : size_{n}, buffer_{new double[n]}+ ciclo che azzera (new double[n]non inizializza). Distruttoredelete[] buffer_;. - Copia vietata:
MyVector(const MyVector&) = delete; MyVector& operator=(const MyVector&) = delete;perché la copia di default duplicherebbe il puntatore (doppiadelete[]); la deep copy è nel laboratorio 4. operator[]:double& operator[](int i)(nonconst: lettura e scrittura,v[3] = x) edouble operator[](int i) const(perconst MyVector&); nessun controllo dei limiti.- Accesso sicuro:
void MyVector::safe_set(int i, double value)
{ if (i < 0 || i >= size_) throw out_of_range("MyVector::safe_set: indice fuori dai limiti");
buffer_[i] = value; }
double MyVector::safe_get(int i) const
{ if (i < 0 || i >= size_) throw out_of_range("MyVector::safe_get: indice fuori dai limiti");
return buffer_[i]; }- Prova:
v[i] = i * 1.5poisafe_set(4, 10)v[2] = 3, safe_get(4) = 10;cv[1] = 1.5;safe_set(5, ...)esafe_get(-1)out_of_range;cv[1] = 3;non compila. - Errori tipici:
deleteal posto didelete[]; lasciare la copia di default;operator[]che ritornadoubleper valore (v[3] = xnon compila); confrontare l'indice solo consize_e non con 0; costruttore senzaexplicit. - Dichiarazione della classe:
class MyVector { public: explicit MyVector(int n); ~MyVector(); MyVector(const MyVector&) = delete; MyVector& operator=(const MyVector&) = delete; int size() const; double& operator[](int i); double operator[](int i) const; void safe_set(int i, double v); double safe_get(int i) const; private: int size_; double* buffer_; }; - Uso e uscita:
v[i] = i * 1.5(),v.safe_set(4, 10)v[2] = 3,safe_get(4) = 10;const MyVector& cv = v;cv[1] = 1.5.