Riepilogo
Un std::array esige di sapere in compilazione quanti elementi avrà; la maggior parte dei problemi, invece, non lo dice finché i dati non arrivano. Questa lezione presenta std::vector, la sequenza che cresce secondo i dati: come si crea, come le si aggiungono elementi con push_back e come si leggono con essa serie di lunghezza sconosciuta. Seguono le tre scansioni canoniche di ogni programmazione con collezioni (accumulare, filtrare e trasformare), con un ciclo e con gli algoritmi della libreria; il passaggio di un vettore a una funzione, e perché restituire un vettore nuovo costa poco. Chiude l’interno del vettore: la sua dimensione e la sua capacità, il blocco che viene sostituito da un altro di dimensione doppia quando si riempie, e il riferimento a un elemento che per questo può restare puntato su memoria liberata.
Obiettivi di apprendimento
- Creare un
std::vector, aggiungervi elementi conpush_backe scorrerlo, leggendo con esso una serie di dati di lunghezza sconosciuta. - Scrivere funzioni che ricevano un vettore per riferimento costante per leggerlo e per riferimento per modificarlo, e che restituiscano un vettore nuovo quando il risultato è una sequenza.
- Implementare scansioni di accumulazione, di filtraggio e di trasformazione, riconoscere in un problema lo schema che gli corrisponde e abbreviarlo, quando la libreria lo consente, con
std::ranges::count,std::ranges::maxostd::ranges::fold_left. - Spiegare con un esempio la differenza tra la dimensione e la capacità di un vettore, e perché un riferimento a un elemento può essere invalidato quando se ne aggiunge un altro.
La sequenza che cresce
Creare e riempire un vettore
La lezione Array ha presentato la sequenza indicizzata: elementi dello stesso tipo, uno dopo l’altro, numerati a partire da zero. La sua forma moderna di dimensione fissa, std::array<int, 7>, porta la dimensione nel tipo, e il compilatore deve conoscerla. Ora, chi scrive un programma che calcola la media dei voti di un corso non sa quanti studenti avrà. Ne segue la necessità di una sequenza la cui dimensione sia un dato in più, che cambi durante l’esecuzione: è il vettore, std::vector<T>, dell’header <vector>, dove T è il tipo degli elementi.
#include <print>#include <vector>int main(){ std::vector<int> squares{}; std::println("Inizio: dimensione {}, vuoto: {}", squares.size(), squares.empty()); for (int k{1}; k <= 5; ++k) { squares.push_back(k * k); } std::println("{}", squares); std::println("Dimensione {}, primo {}, ultimo {}", squares.size(), squares[0], squares.back()); squares[1] = 0; squares.pop_back(); std::println("{}", squares);}Inizio: dimensione 0, vuoto: true[1, 4, 9, 16, 25]Dimensione 5, primo 1, ultimo 25[1, 0, 9, 16]La riga 6 crea un vettore vuoto di interi: a differenza dell’array, non occorre annunciare quanti elementi avrà. size() restituisce quanti ne ha, e empty() dice se non ne ha nessuno. Il ciclo delle righe da 8 a 10 aggiunge cinque quadrati con push_back, che colloca il suo argomento alla fine e aumenta la dimensione di uno. La riga 11 scrive il vettore intero con la formattazione dei range che la lezione precedente ha usato con gli array. L’accesso per indice è quello di sempre, e squares.back() abbrevia squares[squares.size() - 1]. La riga 13 assegna un elemento esistente, e la 14 toglie l’ultimo con pop_back. Le operazioni di questa lezione sono, in sintesi, le seguenti.
v.push_back(x)aggiungexalla fine;v.pop_back()toglie l’ultimo (con il vettore vuoto, è comportamento indefinito).v.size()è la dimensione, di tipostd::size_t, l’intero senza segno delle dimensioni che ha presentato la lezione Array;v.empty()equivale av.size() == 0.v[i]ev.at(i)danno l’elemento di indicei, che deve soddisfarei < v.size();v.front()ev.back()danno il primo e l’ultimo, ed esigono che il vettore non sia vuoto.v.resize(n)lascia il vettore connelementi: toglie quelli che avanzano alla fine o aggiunge alla fine elementi inizializzati come con le graffe vuote (zeri, per i numeri).
Come negli array, v[i] non garantisce alcun controllo, e v.at(i) sì: con un indice 7 e cinque elementi, ferma il programma con il messaggio vector::_M_range_check: __n (which is 7) >= this->size() (which is 5), che nomina l’indice e la dimensione. Il meccanismo che produce quell’arresto, un’eccezione, si studia in Programmazione in C++ II.
Graffe e parentesi tonde
Il corso inizializza tutto con le graffe, e il vettore è il primo tipo del corso in cui sostituire le graffe con le parentesi tonde cambia il risultato. Con le graffe, i valori scritti sono gli elementi; con le tonde, il primo numero è la quantità di elementi e il secondo, se c’è, il valore di ciascuno.
#include <print>#include <vector>int main(){ const std::vector<int> a{5}; const std::vector<int> b(5); const std::vector<int> c{3, 7}; const std::vector<int> d(3, 7); std::println("{} {} {} {}", a, b, c, d);}[5] [0, 0, 0, 0, 0] [3, 7] [7, 7, 7]a ha un elemento, il 5; b, cinque zeri; c, il 3 e il 7; d, tre sette. La lista tra graffe dice, quindi, «questi sono gli elementi», e le parentesi tonde sono riservate a chiedere n copie di uno stesso valore, come un vettore di contatori a zero.
L’ultimo di un vettore vuoto
Un vettore vuoto è un caso legittimo (l’input senza dati, il filtro che nessuno supera), e ogni formula con la sua dimensione va rivista in quel caso; la più insidiosa è v.size() - 1. Poiché v.size() è un intero senza segno, sottrargli 1 quando vale 0 non dà , ma fa il giro, come si è visto nella lezione Gli interi e la loro rappresentazione, fino al massimo valore di std::size_t.
#include <cstddef>#include <print>#include <vector>int main(){ const std::vector<int> values{}; std::println("values.size() - 1 = {}", values.size() - 1); // Limite errato: con il vettore vuoto, la condizione è vera. for (std::size_t i{0}; i <= values.size() - 1; ++i) { std::println("{}", values[i]); }}values.size() - 1 = 18446744073709551615/usr/include/c++/15/bits/stl_vector.h:1282: constexpr std::vector< <template-parameter-1-1>, <template-parameter-1-2> >::const_reference std::vector< <template-parameter-1-1>, <template-parameter-1-2> >::operator[](size_type) const [with _Tp = int; _Alloc = std::allocator<int>; const_reference = const int&; size_type = long unsigned int]: Assertion '__n < this->size()' failed.Il sistema ha arrestato il programma con il segnale 6.
La prima riga dell’output è , cioè , il massimo std::size_t di questa piattaforma. Ne segue che la condizione del ciclo, 0 <= 18446744073709551615, è vera, e che la prima iterazione legge values[0] da un vettore che non ha l’elemento 0: comportamento indefinito. Ciò che si osserva dipende dalle opzioni, come negli array: compilata senza ottimizzazione, la libreria di GCC 15 controlla l’indice di operator[] e ferma il programma con un’asserzione che dice __n < this->size(); con l’ottimizzazione quel controllo scompare, e il ciclo, che non si ferma in tempo, tenta di leggere dove non c’è alcun elemento (verificato su Linux con -O2: poiché il vettore vuoto non ha un blocco, il programma termina con un errore di segmentazione, senza un messaggio che ne spieghi la causa). La correzione consiste nello scrivere il limite senza sottrazione, i < values.size(), che con il vettore vuoto è falso fin dall’inizio; e, dove la sottrazione sembri necessaria, nel chiedere prima values.empty().
Caricamento del contenuto…