Perché vector invece di un array grezzo
Un array grezzo ha una dimensione fissa stabilita in fase di compilazione e si dimentica quanto è lungo nel momento in cui lo passi a una funzione. std::vector risolve entrambi i problemi: è un array ridimensionabile che tiene traccia della propria lunghezza, cresce quando serve e libera da solo la propria memoria. Nel C++ moderno vector è il contenitore di default: usa un array grezzo solo quando hai un motivo preciso per non usarlo.
Includi <vector>, poi dichiaralo con il tipo degli elementi tra parentesi angolari:
scores.size() ti dice sempre la lunghezza attuale: nessun int n separato da tenere allineato e nessun trucco con sizeof. {90, 75, 100, 60} è un inizializzatore con graffe; il vector capisce da solo che gli servono quattro posti.
Creare e inizializzare un vector
Ci sono diversi modi per costruirne uno, a seconda di cosa sai in partenza:
Attenzione alla trappola tonde contro graffe: vector<int> tens(5, 10) crea cinque copie di 10, mentre vector<int> tens{5, 10} crea un vector di due elementi che contiene 5 e 10. Le parentesi tonde significano "dimensione e valore di riempimento"; le graffe significano "proprio questi elementi".
Aggiungere e rimuovere elementi
Il senso di un vector è proprio che cresce. push_back aggiunge in fondo e pop_back rimuove dal fondo:
back() restituisce l'ultimo elemento e front() il primo, in modo più pulito di v[v.size() - 1] e v[0]. Da C++11 puoi anche usare emplace_back(args...) per costruire un elemento sul posto, evitando una copia temporanea per i tipi più pesanti.
Un errore comune da principianti è chiamare front() o back() su un vector vuoto. È undefined behavior, non un errore segnalato: proteggiti sempre prima con if (!v.empty()).
Leggere gli elementi: [] e at()
Accedi a un vector con [] esattamente come a un array. Ma [] non fa alcun controllo sui limiti: un indice fuori intervallo è undefined behavior, che può leggere spazzatura in silenzio o crashare più tardi in un punto che confonde:
vector<int> v = {1, 2, 3};
cout << v[10]; // UNDEFINED BEHAVIOR: nessun controllo, nessun errore
Quando vuoi sicurezza, usa at(). Controlla l'indice e lancia std::out_of_range in caso di accesso errato, una delle eccezioni della libreria standard che puoi catturare, così ottieni un errore chiaro invece di una corruzione:
Regola pratica: usa [] nei cicli stretti in cui hai già dimostrato che l'indice è valido, e at() ai confini in cui potrebbe infilarsi un input sbagliato.
Scorrere un vector
Il modo più pulito per scorrere un vector è il ciclo for basato su intervallo. Prendi gli elementi come const auto& per leggerli senza copiarli, oppure come auto& per modificarli sul posto:
Se ti serve davvero l'indice (per confrontare elementi vicini, ad esempio), usa un classico ciclo con contatore, ma nota che size() restituisce un tipo senza segno (size_t). Confrontarlo con un int i con segno può generare avvisi del compilatore e ritorni a capo sorprendenti, quindi quando puoi preferisci size_t i o un ciclo basato su intervallo:
for (size_t i = 0; i < v.size(); i++) { // size_t, non int
cout << v[i];
}
size, capacity e reserve
Un vector tiene due numeri: size() (quanti elementi contiene) e capacity() (quanti ne può contenere prima di dover crescere). Quando un push_back supera la capacità, il vector alloca un blocco più grande, ci copia ogni elemento e libera il vecchio blocco. Ecco perché una serie di push_back costa poco in media, ma ogni singola riallocazione non è gratis:
Se sai più o meno quanti elementi aggiungerai, chiama prima reserve() per evitare le riallocazioni ripetute. Nota che reserve() cambia la capacità, non la dimensione: il vector ha ancora zero elementi finché non li aggiungi.
Questa riallocazione è anche la fonte del bug più subdolo dei vector. Dato che la crescita sposta lo spazio in memoria, qualsiasi puntatore, riferimento o iteratore che hai salvato nel vector diventa pendente dopo un push_back che rialloca:
vector<int> v = {1, 2, 3};
int& first = v[0]; // riferimento dentro il vector
v.push_back(4); // può riallocare...
cout << first; // PENDENTE: può puntare a memoria liberata
Lo stesso vale per gli iteratori: non fare push_back o erase mentre iteri con un iteratore salvato. Se devi rimuovere elementi durante un ciclo, usa il valore restituito da erase, oppure l'idioma erase-remove con std::remove.
Prossimo: map
Un vector è perfetto quando cerchi le cose per posizione: elemento 0, elemento 1 e così via. Ma spesso vuoi invece cercarle tramite una chiave: un nome utente, l'ID di un prodotto, una parola. A questo serve std::map. Adesso vedremo map, il contenitore chiave-valore del C++, compreso come inserire, cercare e scorrere gli elementi, e l'insidia di [] che crea un valore predefinito e che fa inciampare quasi tutti.
Domande frequenti
Cos'è un vector in C++?
Un std::vector è un array dinamico (ridimensionabile) della libreria standard del C++. A differenza di un array grezzo, conosce la propria dimensione, cresce in automatico quando aggiungi elementi con push_back e libera la memoria al posto tuo. Includi <vector> e scrivi vector<int> v; per crearne uno.
Qual è la differenza tra [] e at() su un vector C++?
v[i] non fa alcun controllo sui limiti: un indice fuori intervallo è undefined behavior (crash o corruzione silenziosa). v.at(i) controlla l'indice e lancia std::out_of_range se non è valido. Usa [] nei cicli critici in cui hai già verificato l'indice, e at() quando vuoi un errore sicuro e facile da diagnosticare.
push_back invalida puntatori e riferimenti a un vector C++?
Sì, può farlo. Quando un vector esaurisce la capacità, push_back rialloca lo spazio in un nuovo blocco, invalidando ogni puntatore, riferimento e iteratore ai vecchi elementi. Non tenere un riferimento a un elemento attraverso un push_back, e se puoi chiama reserve() in anticipo per evitare riallocazioni a sorpresa.