Salta al contenuto
Note per Studenti Esercizio - Quiz e domande su smart pointer, function object e aritmetica dei puntatori (terzo appello 2023-24, teoria)

Esercizio - Quiz e domande su smart pointer, function object e aritmetica dei puntatori (terzo appello 2023-24, teoria)

Esame
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 ) 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 →, STL - algoritmi, predicati e lambdaLa STL ha circa 80 algoritmi generici (header e ) che lavorano su sequenze [first, last) di qualunque contenitore tramite iteratori: find/find_if, count/count_if, sort, copy/copy_if/unique_copy, merge, equal_range, equal, accumulate, inner_product. find cerca un valore e restituisce l'iteratore alla prima occorrenza o end(); find_if verifica un predicato (funzione che restituisce bool) passato come puntatore a funzione, function object (classe con operator()) o lambda expression. Una variabile globale per passare la soglia al predicato è da evitare; il function object la memorizza in un dato membro. Una lambda cattura -> tipo { corpo } definisce la funzione sul posto e può catturare le variabili locali per valore ([=], [x]) o per riferimento ([&], [&x]). sort(b, e) ordina con < e sort(b, e, cmp) con un criterio, solo su iteratori random access; copy richiede spazio a destinazione.STL - algoritmi, predicati e lambda →, C++ - puntatori, array e castun puntatore è una variabile che contiene un indirizzo di memoria e sa il tipo dell'oggetto puntato (T*); si ottiene l'indirizzo con & e si accede al dato con * (dereference, un lvalue), nullptr indica nessun oggetto e due puntatori a tipi diversi sono tipi diversi; void* è un puntatore a memoria grezza (accetta qualsiasi puntatore ma non si dereferenzia) e i cast (static_cast, reinterpret_cast, const_cast) servono per interfacciarsi con hardware e codice non modificabile; il puntatore, a differenza della reference, si può spostare, può essere nullptr e va dereferenziato, e assegnarlo copia l'indirizzo (shallow copy) mentre assegnare una reference copia l'oggetto; un array C è una sequenza contigua di elementi dello stesso tipo, con dimensione costante, senza controllo dei limiti, che passato a una funzione decade in un puntatore al primo elemento; l'aritmetica dei puntatori somma o sottrae interi spostando di n elementi (non di n byte) e la differenza fra puntatori è in elementi; gli errori classici sono nullptr, puntatori non inizializzati, accessi fuori dai limiti e puntatori a oggetti usciti dallo scope.C++ - puntatori, array e cast →. La parte di programmazione dello stesso appello è Esercizio - Classe Image con buffer dinamico (terzo appello 2023-24, programmazione).


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++:

affermazione risposta motivo
la notazione && è chiamata anche lvalue reference F si chiama rvalue reference (& è lvalue reference)
l'argomento può essere const o non const a seconda dell'utilizzo F per spostare bisogna modificare la sorgente (azzerarla): l'argomento è T&& non const
è usato per spostare i dati, invalidando la sorgente V la sorgente resta valida ma svuotata (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 →)
se definito, è usato automaticamente dal compilatore quando opportuno V per i temporanei e per std::move

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()
a3t/smart_due.cppcpp
#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
fine

Per 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 ) 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 →.

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.

a3t/function_object.cppcpp
#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 e ) che lavorano su sequenze [first, last) di qualunque contenitore tramite iteratori: find/find_if, count/count_if, sort, copy/copy_if/unique_copy, merge, equal_range, equal, accumulate, inner_product. find cerca un valore e restituisce l'iteratore alla prima occorrenza o end(); find_if verifica un predicato (funzione che restituisce bool) passato come puntatore a funzione, function object (classe con operator()) o lambda expression. Una variabile globale per passare la soglia al predicato è da evitare; il function object la memorizza in un dato membro. Una lambda cattura -> tipo { corpo } definisce la funzione sul posto e può catturare le variabili locali per valore ([=], [x]) o per riferimento ([&], [&x]). sort(b, e) ordina con < e sort(b, e, cmp) con un criterio, solo su iteratori random access; copy richiede spazio a destinazione.STL - algoritmi, predicati e lambda →).

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.

a3t/copy_string.cppcpp
#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

a3t/copy_string.cppcpp
#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_string restituisce char* (il chiamante fa delete[]) e prende const char*: lunghezza con un puntatore che avanza fino a '\0', alloca lunghezza + 1, copia con due puntatori, scrive '\0'.

Teoria collegata