Sequence Containers: vector, deque, list, array
استكشف حاويات التسلسل في STL — المتجه، وdeque، والقائمة، وward_list، والمصفوفة. تعرف على مقايضات الأداء وتخطيطات الذاكرة وقواعد إبطال المكرر.
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::vectoris the most commonly used container. It stores elements in a single contiguous block of memory, giving it excellent cache performance and O(1) random access. Appending elements withpush_backis amortized O(1) because the vector doubles its capacity when full.
#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, and forward_list
std::dequeprovides O(1) insertion and deletion at both the front and back, but uses a segmented memory layout (array of fixed-size blocks) rather than a single contiguous allocation.std::listis a doubly-linked list offering O(1) insertion/deletion anywhere given an iterator, at the cost of poor cache locality.std::forward_listis a singly-linked list that uses less memory per node but only supports forward traversal.
#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 — Fixed-Size, Zero Overhead
يقومstd::arrayبتغليف مصفوفة نمط C في فئة مناسبة، مما يوفر.size()و.at()والمكررات والتوافق مع جميع خوارزميات STL - مع صفر حمل مقارنةً بـT[N]الخام.
#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);
}اختيار حاوية التسلسل الصحيحة
استخدم دليل القرار هذا:
استخدمstd::vector (الافتراضي) عندما تحتاج إلى وصول عشوائي، أو تكرار صديق لذاكرة التخزين المؤقت، أو الضغط/البوب في الخلف. يجب أن تستخدم الغالبية العظمى من التعليمات البرمجيةvector.
استخدمstd::deque عندما تحتاج إلى إدخال/إزالة متكررة في كل من الأمام والخلف (على سبيل المثال، نافذة منزلقة، قائمة انتظار BFS).
استخدمstd::list عندما تحتاج إلى إدراج/إزالة O(1) في المنتصف باستخدام مُكرِّر، أو عندما تحتاج إلىsplice()لنقل القوائم الفرعية بين القوائم دون نسخ.
استخدمstd::forward_list عندما تكون الذاكرة لكل عقدة مهمة وتحتاج فقط إلى الاجتياز للأمام.
استخدمstd::array عندما يكون الحجم معروفًا في وقت الترجمة ولن يتغير أبدًا.
ملاحظة الأداء: نظرًا لتأثيرات ذاكرة التخزين المؤقت لوحدة المعالجة المركزية، غالبًا ما يتفوقstd::vectorعلىstd::listحتى بالنسبة لعمليات الإدراج المتوسطة على الأجهزة الحديثة، حتى أحجام كبيرة بشكل مدهش (آلاف العناصر). قم دائمًا بالقياس قبل اختيار قائمة مرتبطة.
تحتوي كل حاوية على قواعد مختلفة عندما تصبح التكرارات والمؤشرات والمراجع غير صالحة:
vector — يؤدي الإدخال الذي يؤدي إلى إعادة التخصيص إلى إبطال جميع التكرارات. يؤدي الإدراج بدون إعادة التخصيص إلى إبطال المكررات عند نقطة الإدراج أو بعدها. يؤدي المحو إلى إبطال المكررات عند نقطة المحو أو بعدها.
deque — يؤدي الإدخال في الأمام أو الخلف إلى إبطال جميع التكرارات ولكن ليس المراجع/المؤشرات إلى العناصر الموجودة. الإدراج في الوسط يبطل كل شيء. يؤدي المحو في الأمام/الخلف إلى إبطال مكرر العنصر الذي تم مسحه فقط.
list/forward_list — يؤدي الإدراج والمسح أبدًا إلى إبطال المكررات أو المراجع أو المؤشرات إلى عناصر أخرى. يتم إبطال مكرر العنصر الذي تم مسحه فقط.
array — لا يتم إبطال المكررات أبدًا (لا يمكن للمصفوفة تغيير الحجم).
هناك خطأ شائع وهو تعديلvectorداخل حلقة for المستندة إلى النطاق. إذا أدىpush_backإلى إعادة التخصيص، فستصبح مكررات الحلقة المخفية متدلية.
إذا كنت تعرف (أو تستطيع تقدير) عدد العناصر التي سيحتفظ بهاvector، فاتصل بـreserve()قبل ملئه. يؤدي هذا إلى تخصيص الذاكرة مسبقًا وتجنب عمليات إعادة التخصيص المكلفة:
- يغيرreserve(n)السعة ولكن ليس الحجم — لا تتم إضافة أي عناصر.
- يقومresize(n)بتغيير الحجم — فهو في الواقع يقوم ببناء العناصر بشكل افتراضي.
-shrink_to_fit()هو طلب غير ملزم لتقليل السعة لتتناسب مع الحجم.
بالنسبة للحلقات الفعالة التي تبني المتجهات، يمكن لاستدعاءreserve()واحد أن يقلل من حمل التخصيص بشكل كبير. قم بالقياس باستخدام مجموعات بيانات كبيرة لمعرفة الفرق.
std::vectorهو الخيار الافتراضي - الذاكرة المتجاورة تعني التكرار الصديق لذاكرة التخزين المؤقت والوصول العشوائي O(1)- استخدم
reserve()لتخصيص ذاكرة المتجهات مسبقًا عندما يكون الحجم التقريبي معروفًا - يوفر
std::dequeدفع/انبثاق O(1) في كل من الأمام والخلف، ولكنه ليس متجاورًا - يقدم
std::listإدراج/مسح O(1) باستخدام مكرر ولا يبطل مطلقًا المكررات الأخرى std::arrayعبارة عن غلاف صفري حول صفائف C مع توافق كامل مع STL- تختلف قواعد إبطال المكرر لكل حاوية - تعرف عليها لتجنب أخطاء التكرار المتدلية
اختبار — اختبر معرفتك
(15 نقطة خبرة)1. ما هي حاوية التسلسل التي توفر O(1)`push_back`المطفأ وتخطيط الذاكرة المتجاورة؟
2. ماذا يحدث لجميع مكررات المتجهات عندما يقوم`push_back`بتشغيل إعادة التخصيص؟
3. ما الفرق بين`reserve(n)`و`resize(n)`على المتجه؟