Esercizio - Quiz e domande su smart pointer, function object e aritmetica dei puntatori (terzo appello 2023-24, teoria)
In questa pagina 4
Testo. Prova scritta di Laboratorio di Programmazione, terzo appello 2023-24, parte di teoria (90 minuti): quiz di 10 domande da 4 affermazioni vere/false (corretta +1, sbagliata -0,25, non data 0) e tre domande aperte (5, 4 e 4 punti).
Teoria: C++ - smart pointerGli smart pointer (classi template di
Quiz
1. Il problema del dangling pointer:
| affermazione | risposta | motivo |
|---|---|---|
| causa un errore di linking | F | non è un errore di costruzione del programma |
| causa un errore di compilazione | F | il compilatore non lo vede |
| può causare l'accesso a zone di memoria già deallocate | V | è la definizione: un puntatore a memoria rilasciata, poi usato |
| si verifica solo in una chiamata all'interno di classi virtuali pure | F | non c'entra con le classi astratte |
2. Una variabile data come argomento a una funzione che ne modifica il valore:
| affermazione | risposta | motivo |
|---|---|---|
| il chiamante la vede modificata se è passata per valore | F | la funzione modifica una copia |
| il chiamante la vede modificata se è passata con un puntatore ad essa | V | *p = ... scrive sull'originale |
| viene generato un errore di compilazione se è passata per riferimento costante | V | la funzione la modifica: un const& lo vieta |
| in nessun caso è possibile modificare il valore di una variabile passata come argomento | F | con riferimento o puntatore non const si può |
3. Il move constructor in C++:
4. La conversione di una variabile da double a int:
| affermazione | risposta | motivo |
|---|---|---|
| è una conversione sicura | F | narrowing: si perde la parte decimale e si può uscire dall'intervallo |
| causa un errore di compilazione | F | è lecita (con {} l'inizializzazione è invece rifiutata) |
| è non sicura solo su architetture ARM | F | indipendente dall'architettura |
| è sicura solo su architetture a 64 bit | F | idem |
5. In C++ il downcasting:
6. Un'enumerazione:
| affermazione | risposta | motivo |
|---|---|---|
| specifica un set di valori | V | |
è equivalente a una struct ma i membri sono privati |
F | non è una struttura: è un insieme di costanti con nome |
| associa un valore numerico a ciascun elemento elencato | V | di default 0, 1, 2, ... |
crea uno scope se è definita come enum class |
V |
7. L'istruzione int age = 42;:
| affermazione | risposta | motivo |
|---|---|---|
| è una definizione di variabile | V | |
| è una dichiarazione di variabile | V | ogni definizione è anche una dichiarazione |
| può riservare spazio nello stack | V | variabile locale automatica |
| può riservare spazio nel free store | F | il free store si usa solo con new |
8. Data una classe base B e una derivata D, una funzione virtuale pura dichiarata in B:
| affermazione | risposta | motivo |
|---|---|---|
deve essere definita in B |
F | è "pura": nessuna definizione obbligatoria |
deve essere definita (con override) in D |
V | l'override è obbligatorio perché D sia istanziabile; se D non la ridefinisce resta astratta a sua volta |
impedisce di definire oggetti di classe B |
V | B è astratta |
impedisce di definire classi derivate da D |
F | si può derivare quanto si vuole |
9. Una variabile locale statica definita in una funzione f: static int i = 9;:
| affermazione | risposta | motivo |
|---|---|---|
| ha scope globale di file | F | scope locale alla funzione (la durata è statica) |
è inizializzata a 9 a ogni chiamata di f |
F | l'inizializzazione avviene una volta sola |
| è memorizzata nel record di attivazione nello stack | F | sta nell'area dei dati statici |
| mantiene il suo valore tra una chiamata e la successiva | V |
10. La direttiva #include <MyLibrary.h>:
| affermazione | risposta | motivo |
|---|---|---|
| è gestita dal compilatore | V | la tratta il preprocessore, nella fase di compilazione |
provoca il linking della corrispondente libreria MyLibrary |
F | include solo le dichiarazioni; il linking è un'altra fase |
| può trovarsi in un file header | V | |
| può causare un errore di esecuzione | F | al più un errore di compilazione (file non trovato) |
Domanda aperta 1 (5 punti): smart pointer
Testo. Descrivere il concetto di smart pointer in C++ e quali problemi permette di risolvere. Descrivere le due tipologie di smart pointer della standard library viste a lezione e per ognuna riportare le caratteristiche peculiari. Descrivere il comportamento delle due tipologie quando un oggetto smart pointer è distrutto.
Risposta. Uno smart pointer è una classe template (header <memory>) che detiene un puntatore, si usa come un puntatore (*, ->) e ne gestisce automaticamente la deallocazione quando esce dallo scope (RAII). Risolve i memory leak (anche in presenza di eccezioni o return anticipati) e il rischio di dangling pointer e di doppia deallocazione dovuto a un possesso poco chiaro, con un overhead molto limitato rispetto a un garbage collector.
unique_ptr |
shared_ptr |
|
|---|---|---|
| proprietà | un solo proprietario | più proprietari |
| copia | disabilitata (costruttore e assegnamento di copia cancellati) | ammessa |
| spostamento | ammesso (std::move, ritorno da funzione) |
ammesso |
| meccanismo | nessun contatore: costo quasi uguale a un puntatore | reference counting: ogni copia aumenta il contatore condiviso; costo maggiore |
| alla distruzione | dealloca subito l'oggetto puntato (se non vuoto) | decrementa il contatore: dealloca l'oggetto solo quando l'ultimo shared_ptr viene distrutto |
| operazioni | reset(), release() (rinuncia al possesso), get() |
use_count(), reset() |
#include <iostream>
#include <memory>
#include <utility>
using namespace std;
struct Risorsa {
int id;
explicit Risorsa(int i) : id{i} { cout << " Risorsa(" << id << ") creata\n"; }
~Risorsa() { cout << " Risorsa(" << id << ") distrutta\n"; }
};
unique_ptr<Risorsa> crea(int id) { return unique_ptr<Risorsa>(new Risorsa{id}); } // ritorno per move
int main()
{
cout << "unique_ptr: un solo proprietario\n";
{
unique_ptr<Risorsa> u = crea(1);
// unique_ptr<Risorsa> copia = u; // errore di compilazione: copia disabilitata
unique_ptr<Risorsa> v = std::move(u); // la proprieta' passa a v
cout << " u vuoto: " << (u == nullptr) << ", v->id = " << v->id << '\n';
} // v esce dallo scope: Risorsa(1) distrutta
cout << "shared_ptr: piu' proprietari, conteggio dei riferimenti\n";
{
shared_ptr<Risorsa> a = make_shared<Risorsa>(2);
{
shared_ptr<Risorsa> b = a; // copia ammessa: contatore a 2
cout << " use_count = " << a.use_count() << '\n';
} // b muore: contatore a 1, la Risorsa resta
cout << " use_count = " << a.use_count() << ", la Risorsa esiste ancora\n";
} // a muore: contatore a 0, Risorsa(2) distrutta
cout << "fine\n";
}Uscita eseguita:
unique_ptr: un solo proprietario
Risorsa(1) creata
u vuoto: 1, v->id = 1
Risorsa(1) distrutta
shared_ptr: piu' proprietari, conteggio dei riferimenti
Risorsa(2) creata
use_count = 2
use_count = 1, la Risorsa esiste ancora
Risorsa(2) distrutta
finePer l'unique_ptr la proprietà passa da u a v con lo spostamento e Risorsa(1) è distrutta quando esce v. Per lo shared_ptr la Risorsa sopravvive all'uscita di b (il contatore scende da 2 a 1) ed è distrutta solo all'uscita di a. Debolezza dell'unique_ptr: due unique_ptr costruiti con lo stesso puntatore grezzo compilano ma causano doppia delete; si evita con make_unique. Dettagli: C++ - smart pointerGli smart pointer (classi template di
Domanda aperta 2 (4 punti): function object e predicato
Testo. Descrivere il concetto di function object, come viene implementato e riportare il codice con un esempio di utilizzo. Specificare la relazione fra function object e predicato e riportare un esempio di codice in cui viene utilizzato in tal modo.
Risposta. Un function object (o functor) è un oggetto di una classe che ridefinisce l'operatore di chiamata operator(), perciò si usa con la sintassi di una funzione (f(3)). Rispetto a una funzione può portare con sé dati (membri), per esempio una soglia salvata dal costruttore. Un predicato è una funzione (o un oggetto richiamabile) che restituisce bool: un function object il cui operator() restituisce bool è quindi un predicato, e si può passare agli algoritmi (find_if, count_if, sort come criterio, ...), dove viene copiato e chiamato internamente (pred(*first)). Se è piccolo e operator() è inline, è più efficiente di una chiamata a funzione tramite puntatore.
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
// Function object: una classe con operator(). La soglia e' un dato membro.
class Maggiore_di {
int soglia_;
public:
explicit Maggiore_di(int s) : soglia_{s} {}
bool operator()(int x) const { return x > soglia_; } // predicato: restituisce bool
};
// Predicato "normale" (funzione) per confronto
bool pari(int x) { return x % 2 == 0; }
int main()
{
Maggiore_di piu_di_10{10};
cout << piu_di_10(7) << piu_di_10(15) << '\n'; // si usa come una funzione: 0 poi 1
vector<int> v{3, 14, 8, 21, 6};
auto p = find_if(v.begin(), v.end(), Maggiore_di{10}); // function object come predicato
cout << "primo > 10: " << *p << '\n'; // 14
cout << "quanti > 5: " << count_if(v.begin(), v.end(), Maggiore_di{5}) << '\n'; // 14, 8, 21, 6 = 4
cout << "quanti pari: " << count_if(v.begin(), v.end(), pari) << '\n'; // 14, 8, 6 = 3
int soglia = 7; // la lambda e' un function object anonimo
cout << "lambda: " << count_if(v.begin(), v.end(), [soglia](int x) { return x > soglia; }) << '\n'; // 14, 8, 21 = 3
}Uscita eseguita: 01, primo > 10: 14, quanti > 5: 4, quanti pari: 3, lambda: 3. Il function object Maggiore_di è creato nell'argomento (Maggiore_di{10}), passato a find_if per copia, e dentro find_if l'espressione pred(*first) diventa una chiamata a operator(). Il passaggio della soglia con una variabile globale sarebbe la soluzione da evitare. La lambda è un function object anonimo definito nel punto d'uso (STL - algoritmi, predicati e lambdaLa STL ha circa 80 algoritmi generici (header
Domanda aperta 3 (4 punti): aritmetica dei puntatori e copy_string
Testo. Descrivere che cosa si intende per aritmetica dei puntatori, specificando per quali operatori e operandi è definita. Implementare la funzione copy_string (specificando valore di ritorno e parametri) che accetta come argomento una stringa stile C e la copia su un buffer allocato dinamicamente. La copia deve essere effettuata usando l'aritmetica dei puntatori sia per la lettura sia per la scrittura. Il buffer è poi restituito dalla funzione.
Risposta.
Aritmetica dei puntatori. È l'insieme delle operazioni aritmetiche definite sui puntatori ad elementi di uno stesso array; le unità sono elementi, non byte: p + n avanza di n * sizeof(T) byte.
| operazione | operandi | significato |
|---|---|---|
p + n, n + p, p - n |
puntatore e intero | puntatore all'elemento n posizioni dopo/prima |
++p, --p, p++, p--, p += n, p -= n |
puntatore (e intero) | spostamento di p |
p - q |
due puntatori dello stesso array | numero di elementi tra i due (std::ptrdiff_t) |
p < q, p == q, ... |
due puntatori dello stesso array | confronto di posizione |
*p, p[n] (equivale a *(p + n)) |
puntatore | accesso all'elemento |
Non sono definiti: somma di due puntatori, prodotto, divisione; né l'uscita dai limiti dell'array (si può arrivare al più a "uno dopo l'ultimo"); void* non supporta l'aritmetica (non si conosce sizeof).
La funzione. Valore di ritorno char* (il buffer, di cui il chiamante diventa proprietario e deve fare delete[]); parametro const char* (la stringa non viene modificata). Passi: (1) con un puntatore che avanza fino a '\0' e la differenza tra puntatori si ricava la lunghezza; (2) si alloca lunghezza + 1 byte (il terminatore); (3) due puntatori, uno di lettura e uno di scrittura, avanzano insieme copiando; (4) si scrive il '\0' finale.
#include <cstddef>
#include <iostream>
// Copia una stringa stile C su un buffer allocato dinamicamente, usando SOLO aritmetica dei puntatori
// (niente indici [], niente strlen/strcpy). Il chiamante deve fare delete[] sul risultato.
char* copy_string(const char* s)
{
const char* end = s;
while (*end != '\0') ++end; // end avanza di un char alla volta fino al terminatore
std::ptrdiff_t length = end - s; // differenza tra puntatori = numero di elementi
char* buffer = new char[length + 1]; // + 1 per il terminatore
const char* src = s; // puntatore di lettura
char* dst = buffer; // puntatore di scrittura
while (*src != '\0') {
*dst = *src;
++dst;
++src;
}
*dst = '\0'; // la copia deve terminare con '\0'
return buffer;
}
int main()
{
const char* originale = "ciao mondo";
char* copia = copy_string(originale);
std::cout << copia << '\n';
copia[0] = 'C'; // e' una copia: l'originale non cambia
std::cout << copia << ' ' << originale << '\n';
delete[] copia;
char* vuota = copy_string(""); // stringa vuota: un solo '\0'
std::cout << "vuota: [" << vuota << "] primo byte " << static_cast<int>(vuota[0]) << '\n';
delete[] vuota;
// aritmetica dei puntatori su un array
int a[] = {10, 20, 30, 40};
int* p = a; // punta ad a[0]
int* q = p + 3; // punta ad a[3]: avanza di 3 ELEMENTI (3 * sizeof(int) byte)
std::cout << *(p + 1) << ' ' << *q << ' ' << (q - p) << '\n'; // 20 40 3
++p; // ora p punta ad a[1]
std::cout << *p << ' ' << (q > p) << '\n'; // 20 1
}Uscita eseguita: ciao mondo, Ciao mondo ciao mondo (la copia è indipendente), vuota: [] primo byte 0, 20 40 3, 20 1 (aritmetica su un array di quattro int: q - p vale 3 elementi).
Versione ripasso
- Quiz (risposte V/F): 1) linking F; compilazione F; accesso a memoria deallocata V; solo con virtuali pure F. 2) per valore F; con puntatore V; errore con
const&V; mai modificabile F. 3)&&= lvalue reference F; argomentoconsto no F; sposta invalidando la sorgente V; usato automaticamente V. 4) conversione sicura F; errore di compilazione F; non sicura solo su ARM F; sicura solo a 64 bit F. 5) downcasting = derivata in base F (è l'upcasting); unico cast verificato V;dynamic_castV; puntatori e reference V. 6) set di valori V; struct con membri privati F; valore numerico V; scope conenum classV. 7) definizione V; dichiarazione V; stack V; free store F. 8) definita inBF; override inDV; impedisce oggettiBV; impedisce derivate daDF. 9) scope globale F; inizializzata a ogni chiamata F; nel record di attivazione F; mantiene il valore V. 10) gestita dal compilatore V; provoca il linking F; in un header V; errore di esecuzione F. - Smart pointer: detiene un puntatore e lo dealloca all'uscita dallo scope (RAII): niente leak e dangling, overhead minimo.
unique_ptr: un solo proprietario, copia disabilitata, move ammesso, alla distruzione dealloca subito.shared_ptr: più proprietari, copia ammessa, reference counting (use_count), alla distruzione decrementa e dealloca con l'ultimo. Debolezza diunique_ptr: stesso puntatore grezzo in due →make_unique. Vedi C++ - smart pointerGli smart pointer (classi template di) sono oggetti che detengono un puntatore e lo deallocano automaticamente all'uscita dallo scope (RAII), eliminando memory leak e dangling pointer con un overhead minimo. unique_ptr ha un solo proprietario: non si copia (costruttore e assegnamento di copia cancellati) ma si sposta (std::move, ritorno da funzione), ha reset(), release(), get(), * e ->; due unique_ptr costruiti con lo stesso puntatore grezzo sono un errore (doppia delete) che il compilatore non vede, e si evita con make_unique. shared_ptr ha più proprietari: si copia, tiene un contatore di riferimenti (use_count) e libera l'oggetto quando l'ultimo shared_ptr viene distrutto, a un costo maggiore; weak_ptr osserva senza possedere, per rompere i cicli. auto_ptr è deprecato (rimosso in C++17).C++ - smart pointer → . - Function object: classe con
operator()(anche con dati membro, la soglia); un function object che restituisceboolè un predicato e si passa afind_if,count_if,sort. Esempio:Maggiore_di piu_di_10{10}; find_if(v.begin(), v.end(), Maggiore_di{10});(14);count_if(..., Maggiore_di{5})= 4. - Aritmetica dei puntatori: su elementi dello stesso array, in unità di elementi (
p + n=n * sizeof(T)byte):p ± n,++,--,+=,-=,p - q(numero di elementi), confronti,*p,p[n]; non somma di puntatori; non oltre "uno dopo l'ultimo".
#include <cstddef>
#include <iostream>
// Copia una stringa stile C su un buffer allocato dinamicamente, usando SOLO aritmetica dei puntatori
// (niente indici [], niente strlen/strcpy). Il chiamante deve fare delete[] sul risultato.
char* copy_string(const char* s)
{
const char* end = s;
while (*end != '\0') ++end; // end avanza di un char alla volta fino al terminatore
std::ptrdiff_t length = end - s; // differenza tra puntatori = numero di elementi
char* buffer = new char[length + 1]; // + 1 per il terminatore
const char* src = s; // puntatore di lettura
char* dst = buffer; // puntatore di scrittura
while (*src != '\0') {
*dst = *src;
++dst;
++src;
}
*dst = '\0'; // la copia deve terminare con '\0'
return buffer;
}
int main()
{
const char* originale = "ciao mondo";
char* copia = copy_string(originale);
std::cout << copia << '\n';
copia[0] = 'C'; // e' una copia: l'originale non cambia
std::cout << copia << ' ' << originale << '\n';
delete[] copia;
char* vuota = copy_string(""); // stringa vuota: un solo '\0'
std::cout << "vuota: [" << vuota << "] primo byte " << static_cast<int>(vuota[0]) << '\n';
delete[] vuota;
// aritmetica dei puntatori su un array
int a[] = {10, 20, 30, 40};
int* p = a; // punta ad a[0]
int* q = p + 3; // punta ad a[3]: avanza di 3 ELEMENTI (3 * sizeof(int) byte)
std::cout << *(p + 1) << ' ' << *q << ' ' << (q - p) << '\n'; // 20 40 3
++p; // ora p punta ad a[1]
std::cout << *p << ' ' << (q > p) << '\n'; // 20 1
}copy_stringrestituiscechar*(il chiamante fadelete[]) e prendeconst char*: lunghezza con un puntatore che avanza fino a'\0', allocalunghezza + 1, copia con due puntatori, scrive'\0'.