Esercizio - Classe Image con buffer dinamico (terzo appello 2023-24, programmazione)
In questa pagina 3
Testo (60 minuti, su carta). Progettare e implementare la classe Image che ha l'obiettivo di gestire immagini. Ciascun oggetto gestisce una sola immagine, la cui larghezza e altezza sono salvate nei dati membro w e h. Ciascun pixel è rappresentato da un intero, e il buffer di pixel è allocato dinamicamente e gestito tramite il puntatore data, anch'esso membro della classe. Un invariante della classe è che la dimensione del buffer è sempre uguale a w*h. Image ha un costruttore con gli argomenti width (larghezza, valore di default 100) e height (altezza, valore di default 100). Funzioni membro: set (scrive il valore passato per argomento in tutti i pixel dell'immagine); resize (cambia le dimensioni w e h aggiornando la dimensione del buffer; se la dimensione aumenta, i nuovi pixel hanno lo stesso valore di quelli esistenti). Image possiede inoltre costruttore e assegnamento di copia e spostamento, e tutte le funzioni necessarie per il suo buon funzionamento.
Teoria: 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 →, 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
1. Lettura del testo
Come interpretare resize
La frase "se la dimensione aumenta, i nuovi pixel hanno lo stesso valore di quelli esistenti" ha almeno due letture ragionevoli: (a) i pixel nuovi copiano il valore dei pixel esistenti, in particolare quelli di bordo (la lettura più naturale per un'immagine, ed è quella adottata); (b) in un'immagine uniforme (dopo set) i nuovi pixel valgono lo stesso valore. La scelta (a) soddisfa anche (b). Dettagli scelti e dichiarati nel commento dell'header:
- le coordinate dei pixel si conservano (il pixel
(x, y)resta(x, y)): per allargare o allungare l'immagine non basta ricopiare il buffer lineare, perché un cambio di larghezza sposta le righe; - un pixel nuovo
(x, y)copia il pixel vecchio più vicino: coordinate bloccate al bordo(min(x, w-1), min(y, h-1)); - se le dimensioni diminuiscono si tiene l'angolo in alto a sinistra;
- dimensioni non positive: eccezione
std::invalid_argument(nessun'immagine ha lato nullo); l'unico stato 0 x 0 è quello di un oggetto spostato, valido e distruggibile.
Le cinque operazioni
- Costruttore: controlla le dimensioni, poi
new int[w*h]{}(le{}azzerano i pixel). - Copia: nuovo buffer della stessa dimensione e
std::copy(copia profonda); un oggetto spostato (data_ == nullptr) si copia comenullptr. - Assegnamento di copia: copy-and-swap (copia locale, poi
swapche non lancia): corretto anche per l'auto-assegnamento e con garanzia forte. - Spostamento: costruttore e assegnamento fanno
swapcon la sorgente, che resta 0 x 0. - Distruttore:
delete[] data_.
resize usa lo stesso schema: crea un'immagine temporanea con le nuove dimensioni (se new fallisce l'oggetto non cambia), la riempie leggendo da *this, poi swap; il vecchio buffer muore con il temporaneo. Il prodotto w * h è calcolato come size_t per non andare in overflow con int grandi.
2. Il codice
#ifndef IMAGE_H
#define IMAGE_H
// Immagine di w x h pixel, ciascuno un int, memorizzati per righe in un buffer allocato dinamicamente.
// Invarianti: il buffer ha esattamente w * h elementi; data_ e' nullptr se e solo se w == 0 (e h == 0),
// stato che si ha solo in un oggetto spostato (ogni costruzione normale richiede dimensioni positive).
class Image {
public:
Image(int width = 100, int height = 100); // dimensioni di default 100 x 100; lancia se <= 0
Image(const Image& other); // copia profonda
Image& operator=(const Image& other);
Image(Image&& other) noexcept; // spostamento: other diventa 0 x 0
Image& operator=(Image&& other) noexcept;
~Image();
void set(int value); // scrive value in tutti i pixel
// Cambia le dimensioni. Se aumentano, i pixel nuovi copiano il pixel esistente piu' vicino
// (le coordinate fuori dall'immagine vecchia sono "bloccate" al bordo): in un'immagine uniforme
// restano tutti uguali. Se diminuiscono, si tiene l'angolo in alto a sinistra.
void resize(int new_width, int new_height);
int width() const { return w_; }
int height() const { return h_; }
int& at(int x, int y) { return data_[y * w_ + x]; } // x = colonna, y = riga
int at(int x, int y) const { return data_[y * w_ + x]; }
private:
void swap(Image& other) noexcept;
int w_;
int h_;
int* data_;
};
#endif // IMAGE_H#include "image.h"
#include <algorithm>
#include <stdexcept>
#include <utility>
Image::Image(int width, int height) : w_{width}, h_{height}, data_{nullptr}
{
if (width <= 0 || height <= 0) throw std::invalid_argument("Image: dimensioni non positive");
data_ = new int[static_cast<std::size_t>(w_) * h_]{}; // pixel inizializzati a 0
}
Image::Image(const Image& other)
: w_{other.w_}, h_{other.h_}, data_{other.data_ ? new int[static_cast<std::size_t>(other.w_) * other.h_] : nullptr}
{
std::copy(other.data_, other.data_ + static_cast<std::size_t>(w_) * h_, data_);
}
Image& Image::operator=(const Image& other)
{
Image copy{other}; // puo' lanciare: *this e' ancora intatto
swap(copy); // commit senza eccezioni; la vecchia memoria la libera copy
return *this;
}
Image::Image(Image&& other) noexcept : w_{0}, h_{0}, data_{nullptr}
{
swap(other); // other resta 0 x 0, valido e distruggibile
}
Image& Image::operator=(Image&& other) noexcept
{
swap(other);
return *this;
}
Image::~Image() { delete[] data_; }
void Image::set(int value)
{
std::fill(data_, data_ + static_cast<std::size_t>(w_) * h_, value);
}
void Image::resize(int new_width, int new_height)
{
if (new_width <= 0 || new_height <= 0) throw std::invalid_argument("Image::resize: dimensioni non positive");
Image next{new_width, new_height}; // buffer nuovo: se new fallisce, nulla cambia
for (int y = 0; y < new_height; ++y)
for (int x = 0; x < new_width; ++x)
next.at(x, y) = at(std::min(x, w_ - 1), std::min(y, h_ - 1)); // coordinate bloccate al bordo
swap(next); // il vecchio buffer passa a next e muore con lui
}
void Image::swap(Image& other) noexcept
{
std::swap(w_, other.w_);
std::swap(h_, other.h_);
std::swap(data_, other.data_);
}#include <iostream>
#include <utility>
#include "image.h"
namespace {
void print(const char* title, const Image& im)
{
std::cout << title << " (" << im.width() << "x" << im.height() << ")\n";
for (int y = 0; y < im.height(); ++y) {
std::cout << " ";
for (int x = 0; x < im.width(); ++x) std::cout << im.at(x, y) << (x + 1 < im.width() ? ' ' : '\n');
}
}
}
int main()
{
Image def; // costruttore con argomenti di default
std::cout << "default: " << def.width() << "x" << def.height() << '\n';
def.set(7);
def.resize(150, 120); // immagine uniforme: i pixel nuovi valgono ancora 7
std::cout << "dopo resize: " << def.width() << "x" << def.height()
<< ", pixel(149,119) = " << def.at(149, 119) << ", pixel(0,0) = " << def.at(0, 0) << '\n';
Image im{3, 2};
for (int y = 0; y < 2; ++y)
for (int x = 0; x < 3; ++x) im.at(x, y) = x + 10 * y;
print("3x2", im);
im.resize(5, 3); // allarga: i pixel nuovi copiano il bordo
print("resize 5x3", im);
im.resize(2, 1); // restringe: resta l'angolo in alto a sinistra
print("resize 2x1", im);
Image a{2, 2};
a.set(1);
Image b = a; // copia: buffer separati
a.set(9);
std::cout << "copia: a(0,0) = " << a.at(0, 0) << ", b(0,0) = " << b.at(0, 0) << '\n';
b = a; // assegnamento di copia
std::cout << "dopo b = a: b(1,1) = " << b.at(1, 1) << '\n';
Image& same = b;
b = same; // auto-assegnamento innocuo
Image c = std::move(a); // spostamento: a diventa 0x0
std::cout << "spostata: c = " << c.width() << "x" << c.height() << ", a = " << a.width() << "x" << a.height() << '\n';
a = std::move(c); // assegnamento di spostamento
std::cout << "a = " << a.width() << "x" << a.height() << ", c = " << c.width() << "x" << c.height() << '\n';
}Uscita eseguita (con la throw sostituita da un abort solo per la prova in WebAssembly, che non supporta le eccezioni; i percorsi di errore non vengono mai percorsi; compilato anche per Linux con -Wall -Wextra senza avvisi):
default: 100x100
dopo resize: 150x120, pixel(149,119) = 7, pixel(0,0) = 7
3x2 (3x2)
0 1 2
10 11 12
resize 5x3 (5x3)
0 1 2 2 2
10 11 12 12 12
10 11 12 12 12
resize 2x1 (2x1)
0 1
copia: a(0,0) = 9, b(0,0) = 1
dopo b = a: b(1,1) = 9
spostata: c = 2x2, a = 0x0
a = 2x2, c = 0x0Lettura: dopo set(7) e resize(150, 120) i pixel nuovi valgono ancora 7 (immagine uniforme). L'immagine 3x2 con pixel x + 10y allargata a 5x3 ripete l'ultima colonna (2 e 12) e l'ultima riga: è il blocco al bordo; ridotta a 2x1 tiene 0 1. La copia ha un buffer proprio (a cambia, b no); dopo lo spostamento a è 0x0 e c ha le dimensioni di prima; l'assegnamento di spostamento restituisce i dati ad a.
3. Errori da evitare
- Rompere l'invariante: cambiare
wehprima di avere allocato il nuovo buffer (senewfallisce l'oggetto resta incoerente). - Copia superficiale (puntatore copiato): doppia
delete[]. delete data_senza[].- Argomenti di default nella definizione del
.cppinvece che nella dichiarazione. resizeche ricopia il buffer lineare: le immagini si deformano quando cambia la larghezza.- Spostare lasciando la sorgente con lo stesso puntatore (due proprietari, doppia
delete[]): la sorgente va azzerata, qui lo fa loswapcon un oggetto vuoto. w * hcalcolato inint.
Versione ripasso
- Testo: classe
Imageconw,hedata(buffer diw*hint, invariante); costruttorewidth = 100,height = 100;set(valore);resize(se aumenta, pixel nuovi = valore di quelli esistenti); copia e spostamento. Teoria: 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_listpermette 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 → , 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 →. - Scelte: possiede memoria, quindi cinque operazioni; un costruttore con default nell'header;
setconstd::fill;resizeconserva le coordinate, i pixel nuovi copiano il vecchio più vicino(min(x, w-1), min(y, h-1)), in riduzione resta l'angolo in alto a sinistra; dimensioni non positive →invalid_argument; oggetto spostato 0 x 0 valido. - Operazioni: copia profonda; assegnamento copy-and-swap; spostamento con
swap;delete[]nel distruttore.resize: immagine temporanea nuova, riempimento da*this,swap(garanzia forte).
#include "image.h"
#include <algorithm>
#include <stdexcept>
#include <utility>
Image::Image(int width, int height) : w_{width}, h_{height}, data_{nullptr}
{
if (width <= 0 || height <= 0) throw std::invalid_argument("Image: dimensioni non positive");
data_ = new int[static_cast<std::size_t>(w_) * h_]{}; // pixel inizializzati a 0
}
Image::Image(const Image& other)
: w_{other.w_}, h_{other.h_}, data_{other.data_ ? new int[static_cast<std::size_t>(other.w_) * other.h_] : nullptr}
{
std::copy(other.data_, other.data_ + static_cast<std::size_t>(w_) * h_, data_);
}
Image& Image::operator=(const Image& other)
{
Image copy{other}; // puo' lanciare: *this e' ancora intatto
swap(copy); // commit senza eccezioni; la vecchia memoria la libera copy
return *this;
}
Image::Image(Image&& other) noexcept : w_{0}, h_{0}, data_{nullptr}
{
swap(other); // other resta 0 x 0, valido e distruggibile
}
Image& Image::operator=(Image&& other) noexcept
{
swap(other);
return *this;
}
Image::~Image() { delete[] data_; }
void Image::set(int value)
{
std::fill(data_, data_ + static_cast<std::size_t>(w_) * h_, value);
}
void Image::resize(int new_width, int new_height)
{
if (new_width <= 0 || new_height <= 0) throw std::invalid_argument("Image::resize: dimensioni non positive");
Image next{new_width, new_height}; // buffer nuovo: se new fallisce, nulla cambia
for (int y = 0; y < new_height; ++y)
for (int x = 0; x < new_width; ++x)
next.at(x, y) = at(std::min(x, w_ - 1), std::min(y, h_ - 1)); // coordinate bloccate al bordo
swap(next); // il vecchio buffer passa a next e muore con lui
}
void Image::swap(Image& other) noexcept
{
std::swap(w_, other.w_);
std::swap(h_, other.h_);
std::swap(data_, other.data_);
}- Uscita:
default: 100x100; doposet(7)eresize(150, 120)pixel(149,119) = 7;3x2(0 1 2 / 10 11 12) allargata a5x3ripete l'ultima colonna e riga; ridotta a2x1resta0 1; copia con buffer separato; spostataa = 0x0. - Errori tipici: invariante rotto da
resize, copia superficiale,deletesenza[], default nel.cpp, buffer lineare ricopiato,w * hinint.