Sequence Containers: vector, deque, list, array
Explore the sequence containers in the STL — vector, deque, list, forward_list, and array. Learn their performance trade-offs, memory layouts, and iterator invalidation rules.
What Are Sequence Containers?
Sequence containers store elements in a strict linear order. The STL provides five sequence containers, each optimized for different access and mutation patterns:
- std::vector — dynamic array, contiguous memory
- std::deque — double-ended queue, fast insertion at both ends
- std::list — doubly-linked list
- std::forward_list — singly-linked list
- std::array — fixed-size array, stack-allocated
Choosing the right container is one of the most impactful performance decisions you will make in C++. The key factors are access pattern, insertion/deletion pattern, and cache locality.
std::vector — The Default Choice
std::vectoreste cel mai des folosit container. Stochează elemente într-un singur bloc de memorie contiguu, oferindu-i performanțe excelente în cache și acces aleatoriu O(1). Adăugarea elementelor cupush_backeste amortizată O(1) deoarece vectorul își dublează capacitatea când este plin.
#include <iostream>
#include <vector>
int main() {
std::vector<int> nums; // empty vector
nums.reserve(100); // pre-allocate for 100 elements (no reallocation)
for (int i = 0; i < 10; ++i) {
nums.push_back(i * i); // amortized O(1)
}
// Size vs capacity
std::cout << "Size: " << nums.size() << '\n'; // 10
std::cout << "Capacity: " << nums.capacity() << '\n'; // >= 100
// Random access — O(1)
std::cout << "Element 3: " << nums[3] << '\n'; // 9
std::cout << "Element 3: " << nums.at(3) << '\n'; // 9 (bounds-checked)
// Erase-remove idiom (pre-C++20)
nums.erase(std::remove_if(nums.begin(), nums.end(),
[](int x) { return x > 50; }),
nums.end());
// C++20: std::erase_if (cleaner)
std::erase_if(nums, [](int x) { return x > 30; });
// Range-based for loop
for (int n : nums) {
std::cout << n << ' ';
}
std::cout << '\n';
}deque, list și forward_list
std::dequeoferă inserarea și ștergerea O(1) atât în față, cât și în spate, dar utilizează un aspect de memorie segmentat (matrice de blocuri de dimensiuni fixe) mai degrabă decât o singură alocare contigue.std::listeste o listă dublu legată care oferă inserarea/ștergerea O(1) oriunde având în vedere un iterator, cu prețul unei localități slabe de cache.std::forward_listeste o listă cu legături unice care utilizează mai puțină memorie pe nod, dar acceptă doar traversarea înainte.
#include <deque>
#include <list>
#include <forward_list>
#include <iostream>
int main() {
// deque — O(1) push_front and push_back
std::deque<int> dq;
dq.push_front(1); // O(1) — vector cannot do this efficiently
dq.push_back(2); // O(1)
dq.push_front(0); // O(1)
// dq: {0, 1, 2}
// list — O(1) insert/erase at any position with iterator
std::list<std::string> names = {"Alice", "Charlie", "Eve"};
auto it = std::next(names.begin()); // points to "Charlie"
names.insert(it, "Bob"); // O(1) insert before "Charlie"
names.erase(it); // O(1) erase "Charlie"
// names: {"Alice", "Bob", "Eve"}
// list has splice — O(1) move of entire sublists
std::list<std::string> more = {"Frank", "Grace"};
names.splice(names.end(), more); // moves all of 'more' into 'names'
// names: {"Alice", "Bob", "Eve", "Frank", "Grace"}
// more is now empty
// forward_list — singly linked, no size(), push_front only
std::forward_list<int> fl = {3, 1, 4, 1, 5};
fl.push_front(0);
fl.sort(); // member sort — O(n log n), stable
fl.unique(); // remove consecutive duplicates
// fl: {0, 1, 3, 4, 5}
for (int v : fl) std::cout << v << ' ';
std::cout << '\n';
}std::array — Dimensiune fixă, Zero Overhead
std::arrayîmpachetează o matrice în stil C într-o clasă adecvată, oferind.size(),.at(), iteratoare și compatibilitate cu toți algoritmii STL - cu zero overhead în comparație cu unT[N]brut.
#include <array>
#include <algorithm>
#include <iostream>
int main() {
std::array<int, 5> arr = {5, 2, 8, 1, 9};
std::sort(arr.begin(), arr.end()); // works with STL algorithms
std::cout << "Size: " << arr.size() << '\n'; // constexpr — known at compile time
std::cout << "Front: " << arr.front() << '\n'; // 1
std::cout << "Back: " << arr.back() << '\n'; // 9
// Bounds-checked access
try {
arr.at(10); // throws std::out_of_range
} catch (const std::out_of_range& e) {
std::cout << "Out of range: " << e.what() << '\n';
}
// Compile-time size via std::tuple_size
constexpr std::size_t n = std::tuple_size_v<decltype(arr)>; // 5
static_assert(n == 5);
}Alegerea containerului de secvență potrivită
Utilizați acest ghid de decizie:
Utilizațistd::vector (prestabilit) atunci când aveți nevoie de acces aleatoriu, de iterare cache-friendly sau de push/pop în spate. Marea majoritate a codului ar trebui să foloseascăvector.
Folosițistd::deque atunci când aveți nevoie de inserare/demontare frecventă atât în față, cât și în spate (de exemplu, o fereastră glisantă, coadă BFS).
Folosițistd::list când aveți nevoie de inserarea/eliminarea O(1) în mijloc, având în vedere un iterator, sau când aveți nevoie desplice()pentru a muta sublistele între liste fără a le copia.
Folosițistd::forward_list când memoria per nod contează și aveți nevoie doar de traversare înainte.
Folosițistd::array când dimensiunea este cunoscută în momentul compilării și nu se va schimba niciodată.
Notă de performanță: Datorită efectelor cache-ului CPU,std::vectordepășește adeseastd::listchiar și pentru inserții medii pe hardware modern, până la dimensiuni surprinzător de mari (mii de elemente). Măsurați întotdeauna înainte de a alege o listă legată.
Fiecare container are reguli diferite pentru momentul în care iteratorii, indicatorii și referințele devin invalide:
vector — Inserarea care declanșează realocare invalidează toți iteratorii. Inserarea fără realocare invalidează iteratorii la sau după punctul de inserare. Ștergerea invalidează iteratorii la sau după punctul de ștergere.
deque — Inserarea în față sau în spate invalidează toți iteratorii, dar nu și referințele/pointerii la elemente existente. Inserarea la mijloc invalidează totul. Ștergerea din față/spate invalidează doar iteratorul elementului șters.
list/forward_list — Inserarea și ștergerea niciodată nu invalidează iteratorii, referințele sau indicatorii către alte elemente. Numai iteratorul elementului șters este invalidat.
array — Iteratorii nu sunt niciodată invalidați (matricea nu poate schimba dimensiunea).
O eroare comună este modificarea unuivectorîn interiorul unei bucle for bazate pe interval. Dacă unpush_backdeclanșează realocare, iteratoarele ascunse ale buclei devin atârnate.
Dacă știți (sau puteți estima) câte elemente va conține unvector, apelațireserve()înainte de a-l completa. Acest lucru prealocează memoria și evită realocările costisitoare:
-reserve(n)modifică capacitate, dar nu dimensiune — nu sunt adăugate elemente.
-resize(n)modifică dimensiunea - de fapt, construiește elemente implicite.
-shrink_to_fit()este o solicitare neobligatorie pentru a reduce capacitatea pentru a se potrivi cu dimensiunea.
Pentru buclele fierbinți care construiesc vectori, un singur apelreserve()poate reduce dramatic supraîncărcarea de alocare. Măsurați cu seturi mari de date pentru a vedea diferența.
std::vectoreste alegerea implicită - memoria contiguă înseamnă iterație cache-friendly și acces aleatoriu O(1)- Utilizați
reserve()pentru a pre-aloca memoria vectorială atunci când dimensiunea aproximativă este cunoscută std::dequeoferă O(1) push/pop atât în față, cât și în spate, dar nu este contiguustd::listoferă O(1) inserare/ștergere cu un iterator și nu invalidează niciodată alte iteratoarestd::arrayis a zero-overhead wrapper around C arrays with full STL compatibility- Regulile de invalidare a iteratorului diferă în funcție de container - cunoașteți-le pentru a evita erorile iteratoarelor suspendate
Test — Verifică-ți cunoștințele
(15 XP)1. Ce container de secvență oferă`push_back`amortizat O(1) și aspectul memoriei adiacente?
2. Ce se întâmplă cu toți iteratoarele vectoriale când un`push_back`declanșează realocare?
3. Care este diferența dintre`reserve(n)`și`resize(n)`pe un vector?