Sequence Containers: vector, deque, list, array
Εξερευνήστε τα δοχεία ακολουθίας στο STL — διάνυσμα, deque, list, forward_list και πίνακας. Μάθετε τους συμβιβασμούς απόδοσης, τις διατάξεις μνήμης και τους κανόνες ακύρωσης επαναλήπτη.
Τι είναι τα δοχεία ακολουθίας;
Τα δοχεία ακολουθίας αποθηκεύουν στοιχεία με αυστηρή γραμμική σειρά. Το STL παρέχει πέντε δοχεία ακολουθίας, το καθένα βελτιστοποιημένο για διαφορετικά μοτίβα πρόσβασης και μετάλλαξης:
- std::vector — δυναμικός πίνακας, συνεχόμενη μνήμη
- std::deque — διπλή ουρά, γρήγορη εισαγωγή και στα δύο άκρα
- std::list — λίστα διπλά συνδεδεμένη
- std::forward_list — λίστα μεμονωμένα
- std::array — πίνακας σταθερού μεγέθους, εκχωρημένος σε στοίβα
Η επιλογή του σωστού κοντέινερ είναι μία από τις πιο σημαντικές αποφάσεις απόδοσης που θα λάβετε στο C++. Οι βασικοί παράγοντες είναι μοτίβο πρόσβασης, μοτίβο εισαγωγής/διαγραφής και τοποθεσία προσωρινής μνήμης.
std::vector — Η προεπιλεγμένη επιλογή
Τοstd::vectorείναι το πιο συχνά χρησιμοποιούμενο δοχείο. Αποθηκεύει στοιχεία σε ένα ενιαίο συνεχόμενο μπλοκ μνήμης, παρέχοντάς του εξαιρετική απόδοση κρυφής μνήμης και τυχαία πρόσβαση O(1). Η προσθήκη στοιχείων μεpush_backαποσβένεται O(1) επειδή το διάνυσμα διπλασιάζει τη χωρητικότητά του όταν είναι γεμάτο.
#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είναι μια λίστα με μονή σύνδεση που χρησιμοποιεί λιγότερη μνήμη ανά κόμβο, αλλά υποστηρίζει μόνο διέλευση προς τα εμπρός.
#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].
#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ενεργοποιήσει την ανακατανομή, οι κρυφοί επαναλήπτες του βρόχου γίνονται κρέμονται.
Εάν γνωρίζετε (ή μπορείτε να υπολογίσετε) πόσα στοιχεία θα περιέχει έναvector, καλέστε τοreserve()πριν το συμπληρώσετε. Αυτό εκχωρεί εκ των προτέρων τη μνήμη και αποφεύγει τις δαπανηρές ανακατανομές:
- Τοreserve(n)αλλάζει χωρητικότητα αλλά όχι μέγεθος — δεν προστίθενται στοιχεία.
- Τοresize(n)αλλάζει μέγεθος — στην πραγματικότητα κατασκευάζει στοιχεία από προεπιλογή.
- Τοshrink_to_fit()είναι ένα μη δεσμευτικό αίτημα για μείωση της χωρητικότητας ώστε να ταιριάζει με το μέγεθος.
Για hot loop που δημιουργούν διανύσματα, μια κλήσηreserve()μπορεί να μειώσει δραματικά την επιβάρυνση της κατανομής. Μετρήστε με μεγάλα σύνολα δεδομένων για να δείτε τη διαφορά.
- Το
std::vectorείναι η προεπιλεγμένη επιλογή — συνεχόμενη μνήμη σημαίνει επανάληψη φιλική στην κρυφή μνήμη και τυχαία πρόσβαση O(1) - Χρησιμοποιήστε το
reserve()για να εκχωρήσετε εκ των προτέρων διανυσματική μνήμη όταν είναι γνωστό το κατά προσέγγιση μέγεθος - Το
std::dequeπαρέχει O(1) push/pop τόσο μπροστά όσο και πίσω, αλλά δεν είναι συνεχόμενο - Το
std::listπροσφέρει εισαγωγή/διαγραφή O(1) με επαναλήπτη και δεν ακυρώνει ποτέ άλλους επαναλήπτες - Το
std::arrayείναι ένα περιτύλιγμα μηδενικής επιβάρυνσης γύρω από συστοιχίες C με πλήρη συμβατότητα STL - Οι κανόνες ακύρωσης του Iterator διαφέρουν ανά κοντέινερ — γνωρίζετε τους για να αποφύγετε κρέμονται σφάλματα επαναλήπτη
Κουίζ — Δοκιμάστε τις γνώσεις σας
(15 XP)1. Ποιο κοντέινερ ακολουθίας παρέχει O(1) αποσβεσμένο`push_back`και διάταξη συνεχούς μνήμης;
2. Τι συμβαίνει σε όλους τους επαναλήπτες διανυσμάτων όταν ένα`push_back`ενεργοποιεί την ανακατανομή;
3. Ποια είναι η διαφορά μεταξύ`reserve(n)`και`resize(n)`σε ένα διάνυσμα;