رفتن به محتوا

Sequence Containers: vector, deque, list, array

کانتینرهای ترتیبی در STL را بررسی کنید — vector، deque، list، forward_list و array. تفاوت‌های عملکرد، چیدمان حافظه و قوانین نامعتبرسازی تکرارگر را بیاموزید.

کانتینرهای ترتیبی چیستند؟

کانتینرهای ترتیبی (Sequence Containers) عناصر را به ترتیب خطی دقیق ذخیره می‌کنند. STL پنج کانتینر ترتیبی ارائه می‌دهد که هر کدام برای الگوهای مختلف دسترسی و تغییر بهینه شده‌اند:

- std::vector — آرایه پویا، حافظه پیوسته
- std::deque — صف دوسره، درج سریع در هر دو انتها
- std::list — لیست پیوندی دوطرفه
- std::forward_list — لیست پیوندی یک‌طرفه
- std::array — آرایه با اندازه ثابت، تخصیص روی پشته

انتخاب کانتینر مناسب یکی از تأثیرگذارترین تصمیمات عملکردی است که در ++C خواهید گرفت. عوامل کلیدی الگوی دسترسی، الگوی درج/حذف و موقعیت‌مندی حافظه نهان (cache locality) هستند.

std::vector — انتخاب پیش‌فرض

std::vector پرکاربردترین کانتینر است. عناصر را در یک بلوک پیوسته حافظه ذخیره می‌کند و عملکرد عالی حافظه نهان و دسترسی تصادفی O(1) را فراهم می‌کند. افزودن عناصر با push_back به صورت سرشکن O(1) است زیرا vector هنگام پر شدن ظرفیت خود را دو برابر می‌کند.

vector_basics.cpp
#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 و forward_list

std::deque درج و حذف O(1) در هر دو انتهای ابتدا و انتها را فراهم می‌کند، اما از چیدمان حافظه بخش‌بندی شده (آرایه‌ای از بلوک‌های با اندازه ثابت) به جای یک تخصیص پیوسته استفاده می‌کند. std::list یک لیست پیوندی دوطرفه است که درج/حذف O(1) را در هر نقطه با داشتن تکرارگر ارائه می‌دهد، به بهای ضعف موقعیت‌مندی حافظه نهان. std::forward_list یک لیست پیوندی یک‌طرفه است که حافظه کمتری در هر گره مصرف می‌کند اما فقط از پیمایش رو به جلو پشتیبانی می‌کند.

deque_list.cpp
#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 — اندازه ثابت، سربار صفر

std::array یک آرایه به سبک C را در یک کلاس مناسب پوشش می‌دهد و .size()، .at()، تکرارگرها و سازگاری با تمام الگوریتم‌های STL را فراهم می‌کند — با سربار صفر در مقایسه با T[N] خام.

std_array.cpp
#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 استفاده کنید (پیش‌فرض) وقتی به دسترسی تصادفی، پیمایش سازگار با حافظه نهان، یا push/pop از انتها نیاز دارید. بخش عمده کدها باید از vector استفاده کنند.

از std::deque استفاده کنید وقتی به درج/حذف مکرر در هر دو انتهای ابتدا و انتها نیاز دارید (مثلاً پنجره لغزان، صف BFS).

از std::list استفاده کنید وقتی به درج/حذف O(1) در میانه با داشتن تکرارگر نیاز دارید، یا وقتی به splice() برای انتقال زیرلیست‌ها بین لیست‌ها بدون کپی نیاز دارید.

از std::forward_list استفاده کنید وقتی حافظه هر گره مهم است و فقط به پیمایش رو به جلو نیاز دارید.

از std::array استفاده کنید وقتی اندازه در زمان کامپایل مشخص است و هرگز تغییر نخواهد کرد.

نکته عملکردی: به دلیل اثرات حافظه نهان CPU، std::vector اغلب حتی برای درج‌های میانی روی سخت‌افزار مدرن از std::list بهتر عمل می‌کند، تا اندازه‌های به طور شگفت‌انگیز بزرگ (هزاران عنصر). همیشه قبل از انتخاب لیست پیوندی اندازه‌گیری کنید.

دام

هر کانتینر قوانین متفاوتی دارد برای زمانی که تکرارگرها، اشاره‌گرها و ارجاعات نامعتبر می‌شوند:

vector — درج‌ای که بازتخصیص حافظه ایجاد کند تمام تکرارگرها را نامعتبر می‌کند. درج بدون بازتخصیص، تکرارگرها را در نقطه درج یا پس از آن نامعتبر می‌کند. حذف، تکرارگرها را در نقطه حذف یا پس از آن نامعتبر می‌کند.

deque — درج در ابتدا یا انتها تمام تکرارگرها را نامعتبر می‌کند اما ارجاعات/اشاره‌گرها به عناصر موجود را خیر. درج در میانه همه چیز را نامعتبر می‌کند. حذف در ابتدا/انتها فقط تکرارگر عنصر حذف‌شده را نامعتبر می‌کند.

list / forward_list — درج و حذف هرگز تکرارگرها، ارجاعات یا اشاره‌گرها به عناصر دیگر را نامعتبر نمی‌کند. فقط تکرارگر عنصر حذف‌شده نامعتبر می‌شود.

array — تکرارگرها هرگز نامعتبر نمی‌شوند (آرایه نمی‌تواند اندازه‌اش را تغییر دهد).

یک خطای رایج تغییر vector داخل یک حلقه for مبتنی بر بازه است. اگر push_back باعث بازتخصیص شود، تکرارگرهای پنهان حلقه آویزان (dangling) می‌شوند.

بهترین شیوه

اگر تعداد عناصری که vector نگه خواهد داشت را می‌دانید (یا می‌توانید تخمین بزنید)، قبل از پر کردن آن reserve() را فراخوانی کنید. این کار حافظه را از پیش تخصیص می‌دهد و از بازتخصیص‌های پرهزینه جلوگیری می‌کند:

- reserve(n) ظرفیت را تغییر می‌دهد اما اندازه را خیر — هیچ عنصری اضافه نمی‌شود.
- resize(n) اندازه را تغییر می‌دهد — در واقع عناصر را به صورت پیش‌فرض می‌سازد.
- shrink_to_fit() یک درخواست غیرالزام‌آور برای کاهش ظرفیت به اندازه واقعی است.

برای حلقه‌های حساس که vectorها را می‌سازند، یک فراخوانی reserve() می‌تواند سربار تخصیص را به طور چشمگیری کاهش دهد. با مجموعه‌داده‌های بزرگ اندازه‌گیری کنید تا تفاوت را ببینید.

نکات کلیدی
  • std::vector انتخاب پیش‌فرض است — حافظه پیوسته به معنای پیمایش سازگار با حافظه نهان و دسترسی تصادفی O(1) است
  • از reserve() برای تخصیص پیشینی حافظه vector استفاده کنید وقتی اندازه تقریبی مشخص است
  • std::deque عملیات push/pop با O(1) در هر دو انتها را فراهم می‌کند، اما پیوسته نیست
  • std::list درج/حذف O(1) با تکرارگر ارائه می‌دهد و هرگز تکرارگرهای دیگر را نامعتبر نمی‌کند
  • std::array یک پوشش بدون سربار روی آرایه‌های C با سازگاری کامل STL است
  • قوانین نامعتبرسازی تکرارگر برای هر کانتینر متفاوت است — آن‌ها را بشناسید تا از باگ‌های تکرارگر آویزان جلوگیری کنید

آزمون — دانش خود را بسنجید

(15 امتیاز)

1. کدام کانتینر ترتیبی `push_back` سرشکن O(1) و چیدمان حافظه پیوسته ارائه می‌دهد؟

2. وقتی `push_back` باعث بازتخصیص حافظه شود، چه اتفاقی برای تمام تکرارگرهای vector می‌افتد؟

3. تفاوت بین `reserve(n)` و `resize(n)` روی vector چیست؟