Salta al contenuto
Note per Studenti Esercizio - MyVector con memoria dinamica (laboratorio 3)

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 classe vector discussa a lezione, che rappresenta vettori di double di 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 membro int che contiene la lunghezza del vettore; b. un costruttore che accetta un int che indica la lunghezza del vettore da costruire; c. l'overloading dell'operatore [], sia in versione const sia non const; d. le funzioni safe_set() e safe_get() che permettono l'accesso agli elementi effettuando il controllo di accesso entro i limiti del vettore; e. il distruttore.

Progetto

elemento scelta perché
dati int size_ e double* buffer_ il puntatore regge il buffer allocato con new double[n]; la lunghezza è un dato separato perché il puntatore non la conosce
costruttore (b) explicit MyVector(int n) un costruttore a un argomento senza explicit permetterebbe MyVector v = 5; e passare 5 dove si aspetta un vettore
inizializzazione ciclo che azzera new double[n] lascia i double indeterminati
operator[] (c) double& e double ... const la versione non const ritorna una reference (si può scrivere v[3] = x), quella const una copia (per oggetti const e const&)
safe_set, safe_get (d) controllano 0 <= i < size_ e lanciano std::out_of_range l'indice fuori dai limiti è un errore del chiamante che la classe non può risolvere da sola: un'eccezione non può essere ignorata (C++ - classi, membri e invariantiuna classe è un tipo definito dal programmatore che rappresenta un concetto con una rappresentazione (dati membro) e delle operazioni (funzioni membro); l'interfaccia pubblica separa chi usa la classe da chi la implementa e i dati privati sono modificabili solo tramite l'interfaccia; struct è una class con membri pubblici di default; l'invariante è la regola che definisce gli stati validi, e il costruttore deve stabilirlo e ogni funzione membro conservarlo; il costruttore (stesso nome della classe) inizializza con una lista di inizializzazione e, se i dati non sono validi, lancia un'eccezione, l'unico modo per far fallire un costruttore; le funzioni membro si definiscono fuori dalla classe (solo le brevissime dentro, dove sono inline); le funzioni che non modificano l'oggetto sono dichiarate const; le funzioni che si possono scrivere con la sola interfaccia pubblica (operator==, next_sunday) sono helper function esterne, spesso nello stesso namespace.C++ - classi, membri e invarianti →)
distruttore (e) delete[] buffer_; il buffer è stato allocato con new[]
copia vietata con = delete la copia generata copia il puntatore e non il contenuto: due oggetti con lo stesso buffer e doppia delete[] (C++ - costruttori, inizializzazione, copia e spostamentouna buona interfaccia è completa e minimale, ha i costruttori giusti, supporta o proibisce la copia, usa tipi adeguati, dichiara const ciò che non modifica e libera le risorse nel distruttore; il costruttore di default (senza argomenti) esiste quando c'è un valore iniziale sensato, si ottiene con valori di default nei membri (in-class initializer) o argomenti di default, e explicit vieta la conversione implicita da un argomento; initializer_list permette la lista {a, b, c} e con le graffe ha la precedenza sul costruttore a un intero (vector{3} è un elemento, vector(3) sono tre); la copia di default è membro a membro (shallow copy) e con un puntatore a un buffer duplica il puntatore invece del contenuto (due oggetti che condividono i dati e una doppia delete), quindi servono il costruttore di copia e l'assegnamento di copia che fanno la deep copy (l'assegnamento prima alloca e copia, poi libera i vecchi dati, e deve tollerare l'auto-assegnamento); this è il puntatore all'oggetto corrente, parametro implicito delle funzioni membro; il costruttore e l'assegnamento di spostamento (T&&, rvalue reference) rubano il buffer della sorgente lasciandola vuota ma valida, il compilatore li sceglie da solo (con la copy elision a volte non servono nemmeno); i cinque (distruttore, copia, spostamento) vanno definiti insieme.C++ - costruttori, inizializzazione, copia e spostamento →); copia e spostamento veri sono il laboratorio 4

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

lab3_myvector.cppcpp
#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 limiti

Lettura:

  • v[i] = i * 1.5 per i=0..4i = 0..4 dà 0,1.5,3,4.5,60, 1.5, 3, 4.5, 6; safe_set(4, 10) sostituisce l'ultimo con 1010. Perciò v[2] = 3, safe_get(4) = 10.
  • cv è una reference const a v: cv[1] sceglie l'overload const e legge 1.51.5; cv[1] = 3; non compila perché quell'overload non restituisce un lvalue.
  • v.safe_set(5, 1.0) e v.safe_get(-1) hanno indici fuori da [0,5)[0, 5): lanciano out_of_range e il catch stampa il messaggio. Al contrario v[5] compilerebbe e scriverebbe oltre il buffer, senza segnalazione.
  • Alla } di main il distruttore libera il buffer (delete[]).

Versione ripasso

  • Dati: int size_; double* buffer_; (il puntatore non conosce la lunghezza). Costruttore explicit MyVector(int n) : size_{n}, buffer_{new double[n]} + ciclo che azzera (new double[n] non inizializza). Distruttore delete[] buffer_;.
  • Copia vietata: MyVector(const MyVector&) = delete; MyVector& operator=(const MyVector&) = delete; perché la copia di default duplicherebbe il puntatore (doppia delete[]); la deep copy è nel laboratorio 4.
  • operator[]: double& operator[](int i) (non const: lettura e scrittura, v[3] = x) e double operator[](int i) const (per const MyVector&); nessun controllo dei limiti.
  • Accesso sicuro:
cpp
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.5 poi safe_set(4, 10) →\to v[2] = 3, safe_get(4) = 10; cv[1] = 1.5; safe_set(5, ...) e safe_get(-1) →\to out_of_range; cv[1] = 3; non compila.
  • Errori tipici: delete al posto di delete[]; lasciare la copia di default; operator[] che ritorna double per valore (v[3] = x non compila); confrontare l'indice solo con size_ e non con 0; costruttore senza explicit.
  • 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 (0,1.5,3,4.5,60, 1.5, 3, 4.5, 6), v.safe_set(4, 10) →\to v[2] = 3, safe_get(4) = 10; const MyVector& cv = v; →\to cv[1] = 1.5.

Esercizi su questo argomento

Teoria collegata