Salta al contenuto
Note per Studenti Esercizio - Spostare i valori di una list in un array dinamico senza delete (quarto appello 2021-22, programmazione)

Esercizio - Spostare i valori di una list in un array dinamico senza delete (quarto appello 2021-22, programmazione)

Esame

Prova di programmazione (60 minuti) del quarto appello 2021-22 (12 settembre 2022) del corso di Laboratorio di Programmazione, Ing. Informatica UniPD. 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 - 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) →, C++ - eccezioni e gestione delle risorse (RAII)Un errore che non si può gestire dove avviene si segnala con throw e si gestisce con try/catch più in alto; la libreria standard ha la gerarchia std::exception (logic_error e runtime_error con le loro derivate) e le eccezioni proprie si derivano pubblicamente da una di esse; si cattura per riferimento. Le eccezioni mettono in crisi il codice che acquisisce risorse (memoria con new, file, lock, socket): se una chiamata lancia prima della delete, la risorsa si perde. La soluzione è RAII (Resource Acquisition Is Initialization): acquisire la risorsa nel costruttore di un oggetto locale e rilasciarla nel distruttore, perché all'uscita da uno scope, anche per eccezione (stack unwinding), i distruttori degli oggetti locali vengono chiamati. Garanzie di una funzione: basic (nessuna perdita, stato valido), strong (commit or rollback: se fallisce lo stato osservabile è quello di prima) e no-throw (non lancia).C++ - eccezioni e gestione delle risorse (RAII) →. Una variante con std::array e copia è nel terzo appello 2022-23: Esercizio - Copiare da array a array dinamico senza delete (terzo appello 2022-23, programmazione).

Testo. Progettare un programma che: 1. crei una standard list di valori interi e la popoli usando 20 valori a piacere; 2. allochi dinamicamente un array stile C di dimensione 20; 3. scorra tale lista e sposti nell'array i valori inferiori a una soglia costante definita opportunamente; 4. esca senza usare esplicitamente delete/delete[] nel codice dell'esercizio e senza causare memory leak.

Ragionamento

  1. Lista: std::list<int> inizializzata con la lista di 20 valori ({83, 12, ...}).
  2. Array dinamico senza delete: il vincolo 4 dice di usare RAII. L'array è new int[20], quindi la responsabilità di liberarlo va a un oggetto con distruttore: std::unique_ptr<int[]> (la specializzazione per array chiama delete[] da sola; senza [] nel tipo, unique_ptr<int> farebbe una delete sbagliata sull'array). L'alternativa make_unique<int[]>(20) è equivalente e più sicura. Un std::vector<int> sarebbe ancora più semplice, ma il testo vuole un array stile C allocato dinamicamente.
  3. "Sposti": i valori vanno tolti dalla lista e messi nell'array: si copia *it in arr[usati++] e si cancella l'elemento dalla lista con erase.
  4. Attenzione all'iteratore: erase(it) invalida it e restituisce l'iteratore all'elemento successivo. Quindi il ciclo non incrementa nel for ma dentro: it = lista.erase(it) se si è spostato, ++it altrimenti. (Con list, erase invalida solo l'iteratore dell'elemento cancellato; con vector invaliderebbe tutti quelli successivi.)
  5. Quanti elementi: non si sa in anticipo quanti valori siano sotto soglia (da 0 a 20): un contatore usati tiene il numero di elementi validi dell'array; l'array ha sempre 20 posti, e al massimo se ne usano 20, quindi non c'è overflow.
  6. Uscita: lista e arr sono oggetti locali: i loro distruttori liberano tutto; nessuna delete nel codice.
list_to_array.cppcpp
#include <iostream>
#include <list>
#include <memory>
using namespace std;

int main()
{
    constexpr int kN = 20;                          // numero di valori
    constexpr int kSoglia = 50;                     // soglia costante

    // 1. lista di 20 interi "a piacere"
    list<int> lista {83, 12, 47, 95, 3, 60, 28, 71, 50, 19, 66, 5, 90, 34, 77, 41, 8, 58, 25, 99};

    // 2. array C di dimensione 20 allocato dinamicamente, gestito da uno unique_ptr<int[]>:
    //    il suo distruttore fa delete[] da solo, quindi nel codice non compare nessuna delete
    unique_ptr<int[]> arr(new int[kN]);
    int usati = 0;                                  // quanti elementi dell'array sono stati scritti

    // 3. si scorre la lista e si SPOSTANO nell'array i valori inferiori alla soglia
    for (auto it = lista.begin(); it != lista.end(); ) {
        if (*it < kSoglia) {
            arr[usati++] = *it;                     // copiato nell'array...
            it = lista.erase(it);                   // ...e tolto dalla lista (erase restituisce l'iteratore successivo)
        }
        else
            ++it;
    }

    cout << "nell'array (" << usati << " valori < " << kSoglia << "):";
    for (int i = 0; i < usati; ++i) cout << ' ' << arr[i];
    cout << "\nnella lista (" << lista.size() << " valori >= " << kSoglia << "):";
    for (int x : lista) cout << ' ' << x;
    cout << '\n';
    // 4. si esce: lista e arr sono oggetti con distruttore, quindi liberano la memoria da soli
    return 0;
}
nell'array (10 valori < 50): 12 47 3 28 19 5 34 41 8 25
nella lista (10 valori >= 50): 83 95 60 71 50 66 90 77 58 99

Controllo dell'uscita: i valori sotto 50 (12, 47, 3, 28, 19, 5, 34, 41, 8, 25) sono finiti nell'array nell'ordine di scorrimento (dieci); i dieci rimasti nella lista (83, 95, 60, 71, 50, 66, 90, 77, 58, 99) sono quelli ≥50\ge 50 (il 5050 resta: la condizione è strettamente minore).

Versione ripasso

  • Passi: list<int> con 20 valori; array C dinamico di 20 int gestito da unique_ptr<int[]> (RAII: libera con delete[] da solo, nessuna delete nel codice); scorrere la lista e spostare i valori << soglia (copia in arr[usati++] + erase dalla lista); uscire.
  • Iteratore: erase(it) invalida it e restituisce il successivo: it = lista.erase(it) se si sposta, ++it altrimenti (l'incremento sta nel corpo, non nel for).
cpp
constexpr int kN = 20, kSoglia = 50;
list<int> lista {83, 12, 47, 95, 3, 60, 28, 71, 50, 19, 66, 5, 90, 34, 77, 41, 8, 58, 25, 99};
unique_ptr<int[]> arr(new int[kN]);
int usati = 0;
for (auto it = lista.begin(); it != lista.end(); ) {
    if (*it < kSoglia) { arr[usati++] = *it; it = lista.erase(it); }
    else ++it;
}
  • Uscita: array con 10 valori <50<50 (12 47 3 28 19 5 34 41 8 25); lista con 10 valori ≥50\ge 50 (83 95 60 71 50 66 90 77 58 99).
  • Errori tipici: unique_ptr<int> al posto di unique_ptr<int[]> (delete sull'array); erase dentro un for con ++it (iteratore invalidato); non contare gli elementi usati; new int[20] con delete[] esplicito (vietato dal testo); soglia nel ciclo come numero magico.

Esercizi su questo argomento

Teoria collegata