Esercizio - Quiz e domande su spostamento, passaggio dei parametri e contenitori STL (primo appello 2023-24, teoria)
In questa pagina 4
Testo. Prova scritta di Laboratorio di Programmazione, primo appello 2023-24, parte di teoria (90 minuti): un quiz di 10 domande, ciascuna con 4 affermazioni da dichiarare vere o false (risposta corretta +1, sbagliata -0,25, non data 0; totale normalizzato a 10), e tre domande a risposta aperta (5, 4 e 4 punti).
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_list
Quiz
Per ogni affermazione: V o F e il perché.
1. In C++ il costruttore di copia di default di una classe:
| affermazione | risposta | motivo |
|---|---|---|
| è disabilitato dalla presenza del costruttore di spostamento definito dal programmatore | V | dichiarando un costruttore (o assegnamento) di spostamento, il costruttore di copia implicito viene cancellato (= delete): chi vuole la copia deve scriverla |
| può essere sovrascritto dal programmatore | V | il programmatore può definire un proprio costruttore di copia, che sostituisce quello generato |
| effettua una deep copy dei membri allocati dinamicamente | F | copia membro per membro: un puntatore viene copiato come valore (shallow copy), i due oggetti condividono il blocco |
| produce un errore di compilazione se usato per oggetti con membri allocati dinamicamente | F | compila: l'errore è a run-time (doppia delete), non di compilazione |
2. In C++ l'operatore =:
| affermazione | risposta | motivo |
|---|---|---|
| per essere usato con un tipo definito dall'utente deve essere definito esplicitamente | F | il compilatore genera l'assegnamento di copia (membro per membro) |
| confronta due variabili | F | confronta ==; = assegna |
| chiama l'operatore di assegnamento di una classe | V | per un oggetto, a = b è a.operator=(b) |
| è un operatore con side effect | V | modifica l'operando di sinistra |
3. La classe std::vector:
| affermazione | risposta | motivo |
|---|---|---|
[] può essere usato solo per l'accesso in lettura |
F | restituisce un riferimento: si può anche scrivere (v[i] = x) |
at() controlla la dimensione e lancia un'eccezione fuori dai limiti |
V | out_of_range |
| ha un numero di elementi costante definito a tempo di compilazione | F | si ridimensiona a run-time (è std::array ad avere dimensione fissa) |
| usa l'allocazione dinamica per il suo funzionamento | V | il buffer è nel free store |
4. Le include guard:
| affermazione | risposta | motivo |
|---|---|---|
| servono a evitare le inclusioni multiple e/o ricorsive | V | |
si rendono con #ifndef ... #define ... #endif, come da standard C++ |
V | è il metodo standard (#pragma once non lo è) |
| servono a proteggere in scrittura una variabile | F | è compito di const |
| si usano solo con classi template | F | in ogni header |
5. La compilazione di un progetto C++:
6. Un function object:
7. Nello stack, i record di attivazione (RA) delle funzioni:
8. In C++ un'enumerazione:
| affermazione | risposta | motivo |
|---|---|---|
| è equivalente a una classe con tutti i membri pubblici | F | è un insieme di costanti con nome |
crea uno scope se è definita come enum class |
V | si scrive Colore::Rosso |
| rende più efficiente l'esecuzione | F | la scelta è di espressività, non di velocità |
| può rendere il codice più espressivo | V | nomi al posto di numeri magici |
9. Una classe astratta in C++:
10. Codice: class Foo { public: void print(void) { std::cout << value << std::endl; } private: int value = 10; }; con main che definisce Foo my_foo; my_foo.print();.
| affermazione | risposta | motivo |
|---|---|---|
il membro value di my_foo è contenuto nello stack |
V | my_foo è una variabile locale automatica e i suoi dati membro sono dentro l'oggetto |
l'eseguibile stampa 10 |
V | inizializzatore del membro = 10 |
my_foo è un oggetto locale automatico |
V | |
la funzione print() ha scope globale |
F | è una funzione membro: scope di classe |
Domanda aperta 1 (5 punti): costruttore e assegnamento di spostamento
Testo. Spiegare che cosa sono il costruttore e l'assegnamento di spostamento e in quali casi sono usati dal compilatore. Riportare un esempio di classe per cui implementare le funzioni di spostamento migliora l'efficienza. Implementare, oltre alle due funzioni citate, anche il costruttore di default e il distruttore. Riportare un esempio di codice in cui è usato il costruttore di spostamento e uno in cui è usato l'assegnamento di spostamento (ignorare la copy elision).
Risposta.
Che cosa sono. Il costruttore di spostamento C(C&&) crea un oggetto "rubando" le risorse di un altro, invece di copiarle; l'assegnamento di spostamento C& operator=(C&&) fa lo stesso su un oggetto già esistente (dopo aver liberato le sue risorse). Il parametro è un riferimento rvalue (&&): si lega a un oggetto temporaneo o a uno che si dichiara "non più necessario" con std::move. L'oggetto sorgente resta valido ma svuotato, pronto per essere distrutto o riassegnato.
Quando le usa il compilatore. Quando l'origine è un rvalue: un temporaneo (Buffer b = make();, valore restituito da funzione, risultato di un'espressione), oppure un std::move(x) esplicito; i contenitori della libreria (come vector) spostano gli elementi quando riallocano, se lo spostamento è noexcept. Per un lvalue il compilatore sceglie la copia. Il compilatore genera lo spostamento da solo solo se la classe non dichiara copia, spostamento o distruttore (regola dei cinque).
Perché migliora l'efficienza. Per una classe che possiede una risorsa (un buffer dinamico di n elementi) la copia costa (alloca e copia), lo spostamento (copia due variabili e azzera la sorgente). Esempio con un contatore delle copie degli elementi:
#include <algorithm>
#include <iostream>
#include <utility>
#include <vector>
// Un array dinamico di int che conta quanti ELEMENTI vengono copiati: cosi' si vede
// la differenza di costo tra copia e spostamento.
class Buffer {
public:
static long element_copies; // contatore globale di copie di elementi
Buffer() : n_{0}, data_{nullptr} {} // costruttore di default
explicit Buffer(std::size_t n) : n_{n}, data_{new int[n]{}} {}
~Buffer() { delete[] data_; } // distruttore
Buffer(const Buffer& o) : n_{o.n_}, data_{new int[o.n_]} // copia profonda: costa O(n)
{
std::copy(o.data_, o.data_ + n_, data_);
element_copies += static_cast<long>(n_);
}
Buffer& operator=(const Buffer& o)
{
if (this != &o) {
int* fresh = new int[o.n_];
std::copy(o.data_, o.data_ + o.n_, fresh);
element_copies += static_cast<long>(o.n_);
delete[] data_;
data_ = fresh;
n_ = o.n_;
}
return *this;
}
Buffer(Buffer&& o) noexcept : n_{o.n_}, data_{o.data_} // spostamento: "ruba" il buffer, costa O(1)
{
o.n_ = 0;
o.data_ = nullptr; // la sorgente resta valida e vuota
}
Buffer& operator=(Buffer&& o) noexcept
{
if (this != &o) {
delete[] data_; // libera il proprio buffer
data_ = o.data_; // prende quello della sorgente
n_ = o.n_;
o.data_ = nullptr;
o.n_ = 0;
}
return *this;
}
std::size_t size() const { return n_; }
private:
std::size_t n_;
int* data_;
};
long Buffer::element_copies = 0;
Buffer make(std::size_t n) { Buffer b{n}; return b; } // restituzione per valore
int main()
{
Buffer a{1000};
std::cout << "costruiti 1000 elementi, copie finora: " << Buffer::element_copies << '\n';
Buffer b = a; // costruttore di COPIA (a e' un lvalue)
std::cout << "copia: copie = " << Buffer::element_copies << ", a.size = " << a.size() << '\n';
Buffer c = std::move(a); // costruttore di SPOSTAMENTO
std::cout << "spostamento: copie = " << Buffer::element_copies << ", a.size = " << a.size()
<< ", c.size = " << c.size() << '\n';
Buffer d;
d = std::move(b); // assegnamento di SPOSTAMENTO
std::cout << "assegn. spost.: copie = " << Buffer::element_copies << ", b.size = " << b.size()
<< ", d.size = " << d.size() << '\n';
Buffer e;
e = c; // assegnamento di COPIA (c e' un lvalue)
std::cout << "assegn. copia: copie = " << Buffer::element_copies << '\n';
Buffer f = make(500); // temporaneo: spostamento o elisione, nessuna copia
std::cout << "da funzione: copie = " << Buffer::element_copies << ", f.size = " << f.size() << '\n';
std::vector<Buffer> v; // il vector, riallocando, sposta se e' noexcept
long prima = Buffer::element_copies;
for (int i = 0; i < 5; ++i) v.push_back(Buffer{100}); // rvalue: push_back(Buffer&&)
std::cout << "5 push_back di temporanei: copie aggiunte = " << Buffer::element_copies - prima << '\n';
}Uscita eseguita (compilato anche per Linux con -Wall -Wextra senza avvisi):
costruiti 1000 elementi, copie finora: 0
copia: copie = 1000, a.size = 1000
spostamento: copie = 1000, a.size = 0, c.size = 1000
assegn. spost.: copie = 1000, b.size = 0, d.size = 1000
assegn. copia: copie = 2000
da funzione: copie = 2000, f.size = 500
5 push_back di temporanei: copie aggiunte = 0Lettura: Buffer b = a; è una copia (1000 elementi copiati). Buffer c = std::move(a); usa il costruttore di spostamento (nessun elemento copiato, a resta vuoto); d = std::move(b); usa l'assegnamento di spostamento; e = c; è una copia perché c è un lvalue; make(500) e i push_back di temporanei non copiano niente.
Esempi richiesti. Costruttore di spostamento: Buffer c = std::move(a); (oppure Buffer c{make(500)} ignorando l'elisione). Assegnamento di spostamento: d = std::move(b); (oppure d = make(500);). Entrambi lasciano la sorgente con data_ == nullptr e n_ == 0.
Punti che conviene dichiarare nella risposta: gli spostamenti sono noexcept (serve ai contenitori per usarli); nello spostamento si controlla l'auto-assegnamento (this != &o) e si libera il proprio buffer prima di prendere quello dell'altro; la sorgente va lasciata in uno stato distruggibile.
Domanda aperta 2 (4 punti): il passaggio degli argomenti a una funzione
Testo. Descrivere i diversi modi con cui avviene il passaggio di argomenti a una funzione in C++. Per ognuno riportare i meccanismi coinvolti e i vantaggi/svantaggi. Corredare ogni modo con un esempio di codice.
Risposta.
| modo | firma | meccanismo | vantaggi | svantaggi |
|---|---|---|---|---|
| per valore | void f(int x) |
il parametro è una copia dell'argomento (costruttore di copia); la funzione lavora sulla copia | l'originale è al sicuro; accetta anche temporanei e costanti | la copia costa per oggetti grandi; non si possono restituire risultati nei parametri |
| per riferimento | void f(int& x) |
il parametro è un alias dell'argomento: nessuna copia | efficiente; permette di modificare l'originale (parametri di uscita); sintassi uguale al valore | non accetta temporanei; dal chiamante non si vede che f può modificare l'argomento |
per riferimento const |
void f(const T& x) |
alias in sola lettura | efficiente e sicuro per i tipi grandi; accetta anche temporanei e costanti | la funzione non può modificare l'argomento |
| per puntatore | void f(int* p) |
si passa una copia dell'indirizzo; la funzione accede all'originale con *p |
permette di modificare l'originale; il puntatore può essere nullptr ("nessun argomento"); è il meccanismo degli array C |
sintassi più pesante (&x, *p); va controllato nullptr; rischio di dangling; la riassegnazione di p è locale |
Esempio completo e verificato:
#include <iostream>
#include <string>
#include <vector>
using namespace std;
void per_valore(int x) { x = 100; cout << " nella funzione x = " << x << '\n'; } // lavora su una COPIA
void per_riferimento(int& x) { x = 100; } // lavora sull'originale
void per_const_ref(const int& x) { cout << " const&: leggo " << x << " senza copiare\n"; }
void per_puntatore(int* p) { if (p) *p = 100; } // lavora sull'originale, puo' essere nullptr
void per_ptr_const(const int* p) { cout << " const*: leggo " << *p << '\n'; }
void somma_grande(const vector<int>& v, int& totale) // uso tipico: grande in ingresso, risultato in uscita
{
totale = 0;
for (int x : v) totale += x;
}
void riassegna_puntatore(int* p, int* altro) // il puntatore e' copiato: la riassegnazione e' locale
{
p = altro;
cout << " nella funzione p punta ad altro: " << (p == altro) << '\n';
}
int main()
{
int a = 1, b = 1, c = 1, d = 1;
per_valore(a);
per_riferimento(b);
per_puntatore(&c);
cout << "dopo le chiamate: a = " << a << " (valore), b = " << b << " (riferimento), c = " << c
<< " (puntatore)\n";
per_const_ref(a);
per_ptr_const(&b);
per_puntatore(nullptr); // lecito: il puntatore puo' non puntare a niente
// per_riferimento(5); // errore: un rvalue non si lega a int&
per_const_ref(5); // ok: const& si lega anche a un temporaneo
vector<int> v{1, 2, 3, 4};
int totale;
somma_grande(v, totale);
cout << "somma = " << totale << '\n';
int *p = &c, *q = &d;
riassegna_puntatore(p, q); // p, nel main, e' ancora &c
cout << "p punta ancora a c: " << (p == &c) << '\n';
}Uscita eseguita:
nella funzione x = 100
dopo le chiamate: a = 1 (valore), b = 100 (riferimento), c = 100 (puntatore)
const&: leggo 1 senza copiare
const*: leggo 100
const&: leggo 5 senza copiare
somma = 10
nella funzione p punta ad altro: 1
p punta ancora a c: 1Il chiamante vede la variabile modificata solo con riferimento (non const) o puntatore (non a const); per valore no. Il puntatore stesso, però, è passato per valore: riassegnarlo dentro la funzione non cambia quello del chiamante (l'ultima riga). Regola d'uso: tipi piccoli (int, double) per valore; oggetti grandi in sola lettura per const&; risultati in uscita per riferimento o puntatore; nullptr come "opzionale" con il puntatore.
Domanda aperta 3 (4 punti): contenitori STL e accesso uniforme
Testo. Descrivere il concetto di contenitore STL. Descrivere il concetto di accesso uniforme ai dati nei contenitori STL e quale strumento viene utilizzato. Riportare due esempi di contenitori STL spiegando come sono strutturati.
Risposta.
Contenitore STL. Un contenitore STL è una sequenza di elementi [begin(), end()) che supporta la copia (costruttore di copia e assegnamento), chiama il tipo degli elementi value_type, ha i tipi iteratore iterator e const_iterator, e offre le operazioni comuni (insert, erase, front, back, push_back, pop_back, size, ...) e i confronti ==, !=, <, ... Esempi: vector, list, deque, map, set, array.
Accesso uniforme. Gli algoritmi (find, sort, accumulate, ...) e i contenitori sono disaccoppiati: l'algoritmo non conosce la struttura dati, il contenitore non conosce l'algoritmo; si parlano solo tramite iteratori. Uno iteratore è un oggetto che identifica un elemento della sequenza (o uno dopo l'ultimo) e supporta * (accesso), ++ (avanzamento), == e !=. Quindi lo stesso codice funziona con vector, list o un array C:
#include <algorithm>
#include <iostream>
#include <list>
#include <string>
#include <vector>
using namespace std;
// Una sola funzione per qualsiasi contenitore: parla solo con gli iteratori [first, last)
template <typename Iterator>
int conta_maggiori(Iterator first, Iterator last, int soglia)
{
int n = 0;
for (Iterator p = first; p != last; ++p)
if (*p > soglia) ++n;
return n;
}
int main()
{
vector<int> v{5, 12, 7, 20};
list<int> l{3, 30, 8};
int a[] = {50, 1, 60};
cout << conta_maggiori(v.begin(), v.end(), 6) << ' ' // vector: 3
<< conta_maggiori(l.begin(), l.end(), 6) << ' ' // list: 2
<< conta_maggiori(a, a + 3, 6) << '\n'; // array C: 2
// gli algoritmi STL usano la stessa interfaccia
cout << (find(v.begin(), v.end(), 7) != v.end()) << (find(l.begin(), l.end(), 7) != l.end()) << '\n'; // 1 0
// insert / erase: stessa notazione, costi diversi (vector sposta, list no)
v.insert(v.begin() + 1, 99);
l.insert(++l.begin(), 99);
cout << v[1] << ' ' << *++l.begin() << '\n';
}Uscita eseguita: 3 2 2, 10, 99 99. La funzione conta_maggiori lavora con tre tipi di sequenza; find si chiama allo stesso modo su vector e list; insert ha la stessa notazione ma costi diversi.
Due contenitori.
vector: un array dinamico: elementi contigui in un buffer nel free store, consizeecapacity; accesso diretto con[](eat()con controllo),push_backammortizzato costante (si raddoppia la capacità riallocando);insertederasenel mezzo sono costosi (spostano gli elementi) e invalidano gli iteratori.list: una lista doppiamente concatenata: ogni elemento sta in un nodo con puntatori al precedente e al successivo (Link {prev, succ, val}), più i puntatori al primo e all'ultimo; non c'è[];insert/erasein tempo costante senza spostare gli altri elementi, e gli iteratori degli altri elementi restano validi. Il suo iteratore è una classe annidata con un puntatore al nodo e gli operatori*,++,--,==,!=.
Versione ripasso
- Quiz (risposte V/F): 1) disabilitato dal move: V; sovrascrivibile: V; deep copy: F; errore di compilazione: F. 2)
=definito esplicitamente: F; confronta: F; chiama l'assegnamento: V; side effect: V. 3)[]solo lettura: F;at()controlla: V; elementi costanti a compile time: F; allocazione dinamica: V. 4) evitano inclusioni multiple: V;#ifndefstandard: V; protezione in scrittura: F; solo template: F. 5) linker segnala sintassi: F; g++ segnala conversioni: V; g++ segnala leak: F; linking = parte mancante: V. 6) function object:operator()V;operator[]F; non copiabile F; predicato V. 7) RA con variabili dinamiche F; con locali automatiche V; dimensione fissa F; RA distinto a ogni chiamata ricorsiva V. 8) enum = classe pubblica F;enum classcrea scope V; più efficiente F; più espressivo V. 9) astratta = almeno una virtuale pura V; niente dati F; solo con allocazione dinamica F; interfaccia comune V. 10)valuenello stack V; stampa 10 V;my_foolocale automatico V;print()scope globale F. - Spostamento:
C(C&&)eoperator=(C&&)rubano le risorse (parametro rvalue, sorgente valida ma vuota,noexcept); usati per temporanei estd::move, non per lvalue; copia , spostamento . Costruttore:Buffer c = std::move(a);; assegnamento:d = std::move(b);.
#include <algorithm>
#include <iostream>
#include <utility>
#include <vector>
// Un array dinamico di int che conta quanti ELEMENTI vengono copiati: cosi' si vede
// la differenza di costo tra copia e spostamento.
class Buffer {
public:
static long element_copies; // contatore globale di copie di elementi
Buffer() : n_{0}, data_{nullptr} {} // costruttore di default
explicit Buffer(std::size_t n) : n_{n}, data_{new int[n]{}} {}
~Buffer() { delete[] data_; } // distruttore
Buffer(const Buffer& o) : n_{o.n_}, data_{new int[o.n_]} // copia profonda: costa O(n)
{
std::copy(o.data_, o.data_ + n_, data_);
element_copies += static_cast<long>(n_);
}
Buffer& operator=(const Buffer& o)
{
if (this != &o) {
int* fresh = new int[o.n_];
std::copy(o.data_, o.data_ + o.n_, fresh);
element_copies += static_cast<long>(o.n_);
delete[] data_;
data_ = fresh;
n_ = o.n_;
}
return *this;
}
Buffer(Buffer&& o) noexcept : n_{o.n_}, data_{o.data_} // spostamento: "ruba" il buffer, costa O(1)
{
o.n_ = 0;
o.data_ = nullptr; // la sorgente resta valida e vuota
}
Buffer& operator=(Buffer&& o) noexcept
{
if (this != &o) {
delete[] data_; // libera il proprio buffer
data_ = o.data_; // prende quello della sorgente
n_ = o.n_;
o.data_ = nullptr;
o.n_ = 0;
}
return *this;
}
std::size_t size() const { return n_; }
private:
std::size_t n_;
int* data_;
};
long Buffer::element_copies = 0;
Buffer make(std::size_t n) { Buffer b{n}; return b; } // restituzione per valore
int main()
{
Buffer a{1000};
std::cout << "costruiti 1000 elementi, copie finora: " << Buffer::element_copies << '\n';
Buffer b = a; // costruttore di COPIA (a e' un lvalue)
std::cout << "copia: copie = " << Buffer::element_copies << ", a.size = " << a.size() << '\n';
Buffer c = std::move(a); // costruttore di SPOSTAMENTO
std::cout << "spostamento: copie = " << Buffer::element_copies << ", a.size = " << a.size()
<< ", c.size = " << c.size() << '\n';
Buffer d;
d = std::move(b); // assegnamento di SPOSTAMENTO
std::cout << "assegn. spost.: copie = " << Buffer::element_copies << ", b.size = " << b.size()
<< ", d.size = " << d.size() << '\n';
Buffer e;
e = c; // assegnamento di COPIA (c e' un lvalue)
std::cout << "assegn. copia: copie = " << Buffer::element_copies << '\n';
Buffer f = make(500); // temporaneo: spostamento o elisione, nessuna copia
std::cout << "da funzione: copie = " << Buffer::element_copies << ", f.size = " << f.size() << '\n';
std::vector<Buffer> v; // il vector, riallocando, sposta se e' noexcept
long prima = Buffer::element_copies;
for (int i = 0; i < 5; ++i) v.push_back(Buffer{100}); // rvalue: push_back(Buffer&&)
std::cout << "5 push_back di temporanei: copie aggiunte = " << Buffer::element_copies - prima << '\n';
}- Passaggio dei parametri: per valore (copia: sicuro, costoso), per riferimento (alias: modifica l'originale, no temporanei), per riferimento
const(efficiente e sicuro in sola lettura), per puntatore (indirizzo copiato, può esserenullptr, la riassegnazione è locale). Il chiamante vede le modifiche solo con riferimento o puntatore nonconst. Esempio:a = 1(valore),b = 100(riferimento),c = 100(puntatore). - Contenitore STL: sequenza
[begin(), end())copiabile convalue_type,iterator,const_iterator,insert,erase,size, confronti. Accesso uniforme = iteratori (*,++,==,!=): algoritmi e contenitori disaccoppiati.vector= array dinamico contiguo ([],insertcostoso, invalida iteratori);list= nodiprev/succ/val(insertsenza spostare, niente[]). Vedi STL - sequenze, iteratori e container (vector, list, string)La STL (Standard Template Library) fornisce contenitori (vector, list, map, ...) e algoritmi generici (sort, find, accumulate, ...) tenuti insieme da una interfaccia unificata: la sequenza, un intervallo [begin, end) di elementi gestito tramite iteratori (oggetti che identificano un elemento: * dereferenzia, ++ avanza, == e != confrontano; end è il primo elemento dopo l'ultimo, quindi una sequenza vuota ha begin == end). Gli algoritmi conoscono solo gli iteratori e i contenitori espongono solo iteratori, così i due mondi sono disaccoppiati e cambiare contenitore non cambia l'algoritmo. Un iteratore di lista è una classe annidata con un puntatore al nodo; auto deduce il tipo dell'iteratore. vector (contiguo, [] e at(), insert/erase costosi e che invalidano gli iteratori), string (un vector di char con + e +=), list (nodi concatenati, insert/erase senza spostare gli altri elementi, niente []) e char[] (array C: dimensione fissa, niente iteratori, == confronta i puntatori) a confronto.STL - sequenze, iteratori e container (vector, list, string) →.
Teoria collegata
- C++ - costruttori, inizializzazione, copia e spostamento
- C++ - funzioni, reference e passaggio dei parametri
- STL - sequenze, iteratori e container (vector, list, string)
- C++ - buona interfaccia e progetti su più file
- STL - algoritmi, predicati e lambda
- C++ - chiamate a funzione e layout della memoria
- C++ - ereditarietà, funzioni virtuali e layout degli oggetti