Μετάβαση στο περιεχόμενο
Topos Uranos

Περίληψη

Ένα std::array απαιτεί να είναι γνωστό κατά τη μεταγλώττιση πόσα στοιχεία θα έχει· τα περισσότερα προβλήματα, αντίθετα, δεν το λένε παρά μόνο όταν φτάσουν τα δεδομένα. Το μάθημα αυτό παρουσιάζει το std::vector, την ακολουθία που μεγαλώνει ανάλογα με τα δεδομένα: πώς δημιουργείται, πώς του προστίθενται στοιχεία με τη push_back και πώς διαβάζονται με αυτό σειρές άγνωστου μήκους. Ακολουθούν οι τρεις κανονικές διασχίσεις κάθε προγραμματισμού με συλλογές (συσσώρευση, φιλτράρισμα και μετασχηματισμός), με βρόχο και με τους αλγορίθμους της βιβλιοθήκης· η μεταβίβαση ενός διανύσματος σε μια συνάρτηση, και γιατί η επιστροφή ενός νέου διανύσματος είναι φθηνή. Κλείνει με το εσωτερικό του διανύσματος: το μέγεθος και τη χωρητικότητά του, το μπλοκ μνήμης που αντικαθίσταται από άλλο διπλάσιο όταν γεμίσει, και την αναφορά σε ένα στοιχείο που, γι’ αυτόν τον λόγο, μπορεί να μείνει να δείχνει σε μνήμη που έχει αποδεσμευτεί.

Μαθησιακοί στόχοι

  1. Να δημιουργούμε ένα std::vector, να του προσθέτουμε στοιχεία με τη push_back και να το διατρέχουμε, διαβάζοντας με αυτό μια σειρά δεδομένων άγνωστου μήκους.
  2. Να γράφουμε συναρτήσεις που δέχονται ένα διάνυσμα με σταθερή αναφορά για να το διαβάσουν και με αναφορά για να το τροποποιήσουν, και που επιστρέφουν ένα νέο διάνυσμα όταν το αποτέλεσμα είναι ακολουθία.
  3. Να υλοποιούμε διασχίσεις συσσώρευσης, φιλτραρίσματος και μετασχηματισμού, να αναγνωρίζουμε σε ένα πρόβλημα το μοτίβο που του αντιστοιχεί και να το συντομεύουμε, όταν η βιβλιοθήκη το επιτρέπει, με τις std::ranges::count, std::ranges::max ή std::ranges::fold_left.
  4. Να εξηγούμε με ένα παράδειγμα τη διαφορά ανάμεσα στο μέγεθος και στη χωρητικότητα ενός διανύσματος, και γιατί μια αναφορά σε ένα στοιχείο μπορεί να ακυρωθεί όταν προστεθεί ένα άλλο.

Η ακολουθία που μεγαλώνει

Δημιουργία και γέμισμα ενός διανύσματος

Το μάθημα Πίνακες σταθερού μεγέθους παρουσίασε την ακολουθία με δείκτες: στοιχεία του ίδιου τύπου, το ένα μετά το άλλο, αριθμημένα από το μηδέν. Η σύγχρονη μορφή της με σταθερό μέγεθος, std::array<int, 7>, φέρει το μέγεθος μέσα στον τύπο, και ο μεταγλωττιστής πρέπει να το γνωρίζει. Ωστόσο, όποιος γράφει ένα πρόγραμμα που βγάζει τον μέσο όρο των βαθμών μιας τάξης δεν ξέρει πόσους μαθητές θα έχει. Από αυτό έπεται η ανάγκη για μια ακολουθία της οποίας το μέγεθος είναι ένα ακόμη δεδομένο, που αλλάζει κατά την εκτέλεση: είναι το διάνυσμα, std::vector<T>, της κεφαλίδας <vector>, όπου T είναι ο τύπος των στοιχείων.

Πρόγραμμα σε C++
#include <print>#include <vector>​int main(){    std::vector<int> squares{};    std::println("Στην αρχή: μέγεθος {}, κενό: {}", squares.size(), squares.empty());    for (int k{1}; k <= 5; ++k) {        squares.push_back(k * k);    }    std::println("{}", squares);    std::println("Μέγεθος {}, πρώτο {}, τελευταίο {}", squares.size(), squares[0], squares.back());    squares[1] = 0;    squares.pop_back();    std::println("{}", squares);}
Έξοδος
Στην αρχή: μέγεθος 0, κενό: true[1, 4, 9, 16, 25]Μέγεθος 5, πρώτο 1, τελευταίο 25[1, 0, 9, 16]

Η γραμμή 6 δημιουργεί ένα κενό διάνυσμα ακεραίων: σε αντίθεση με τον πίνακα, δεν χρειάζεται να δηλωθεί πόσα στοιχεία θα έχει. Η size() επιστρέφει πόσα έχει, και η empty() λέει αν δεν έχει κανένα. Ο βρόχος των γραμμών 8 έως 10 προσθέτει πέντε τετράγωνα με τη push_back, που τοποθετεί το όρισμά της στο τέλος και αυξάνει το μέγεθος κατά ένα. Η γραμμή 11 γράφει ολόκληρο το διάνυσμα με τη μορφοποίηση των ranges που το μάθημα που προηγήθηκε χρησιμοποίησε με τους πίνακες. Η πρόσβαση με δείκτη είναι η γνωστή, και το squares.back() συντομεύει το squares[squares.size() - 1]. Η γραμμή 13 αναθέτει τιμή σε ένα υπάρχον στοιχείο, και η 14 αφαιρεί το τελευταίο με τη pop_back. Οι πράξεις αυτού του μαθήματος είναι, συνοπτικά, οι εξής.

  • Το v.push_back(x) προσθέτει το x στο τέλος· το v.pop_back() αφαιρεί το τελευταίο (με κενό διάνυσμα, είναι απροσδιόριστη συμπεριφορά).
  • Το v.size() είναι το μέγεθος, τύπου std::size_t, ο μη προσημασμένος ακέραιος των μεγεθών που παρουσίασε το μάθημα Πίνακες σταθερού μεγέθους· το v.empty() ισοδυναμεί με v.size() == 0.
  • Τα v[i] και v.at(i) δίνουν το στοιχείο με δείκτη i, που πρέπει να ικανοποιεί το i < v.size()· τα v.front() και v.back() δίνουν το πρώτο και το τελευταίο, και απαιτούν το διάνυσμα να μην είναι κενό.
  • Το v.resize(n) αφήνει το διάνυσμα με n στοιχεία: αφαιρεί όσα περισσεύουν στο τέλος ή προσθέτει στο τέλος στοιχεία αρχικοποιημένα όπως με κενά άγκιστρα (μηδενικά, για τους αριθμούς).

Όπως στους πίνακες, το v[i] δεν εγγυάται κανέναν έλεγχο, ενώ το v.at(i) τον εγγυάται: με δείκτη 7 και πέντε στοιχεία, σταματά το πρόγραμμα με το μήνυμα vector::_M_range_check: __n (which is 7) >= this->size() (which is 5), που ονομάζει τον δείκτη και το μέγεθος. Ο μηχανισμός που προκαλεί αυτή τη διακοπή, μια εξαίρεση, μελετάται στον Προγραμματισμό σε C++ II.

Άγκιστρα και παρενθέσεις

Ο κύκλος αρχικοποιεί τα πάντα με άγκιστρα, και το διάνυσμα είναι ο πρώτος τύπος του κύκλου στον οποίο η αντικατάσταση των αγκίστρων με παρενθέσεις αλλάζει το αποτέλεσμα. Με άγκιστρα, οι τιμές που γράφονται είναι τα στοιχεία· με παρενθέσεις, ο πρώτος αριθμός είναι το πλήθος των στοιχείων και ο δεύτερος, αν υπάρχει, η τιμή καθενός.

Πρόγραμμα σε C++
#include <print>#include <vector>​int main(){    const std::vector<int> a{5};    const std::vector<int> b(5);    const std::vector<int> c{3, 7};    const std::vector<int> d(3, 7);    std::println("{} {} {} {}", a, b, c, d);}
Έξοδος
[5] [0, 0, 0, 0, 0] [3, 7] [7, 7, 7]

Το a έχει ένα στοιχείο, το 5· το b, πέντε μηδενικά· το c, το 3 και το 7· το d, τρία εφτάρια. Η λίστα μέσα σε άγκιστρα λέει επομένως «αυτά είναι τα στοιχεία», και οι παρενθέσεις κρατούνται για να ζητήσουμε n αντίγραφα της ίδιας τιμής, όπως ένα διάνυσμα μετρητών στο μηδέν.

Το τελευταίο ενός κενού διανύσματος

Ένα κενό διάνυσμα είναι νόμιμη περίπτωση (η είσοδος χωρίς δεδομένα, το φίλτρο που δεν περνά κανείς), και κάθε τύπος που περιέχει το μέγεθός του πρέπει να ελέγχεται σε αυτή την περίπτωση· ο πιο ύπουλος είναι ο v.size() - 1. Επειδή το v.size() είναι μη προσημασμένος ακέραιος, η αφαίρεση του 1 όταν ισούται με 0 δεν δίνει −1-1, αλλά κάνει αναδίπλωση, όπως είδαμε στο μάθημα Οι ακέραιοι και η αναπαράστασή τους, ως τη μεγαλύτερη τιμή του std::size_t.

Πρόγραμμα σε C++
#include <cstddef>#include <print>#include <vector>​int main(){    const std::vector<int> values{};    std::println("values.size() - 1 = {}", values.size() - 1);    // Λάθος όριο: με το κενό διάνυσμα, η συνθήκη είναι αληθής.    for (std::size_t i{0}; i <= values.size() - 1; ++i) {        std::println("{}", values[i]);    }}
Έξοδος
values.size() - 1 = 18446744073709551615/usr/include/c++/15/bits/stl_vector.h:1282: constexpr std::vector< <template-parameter-1-1>, <template-parameter-1-2> >::const_reference std::vector< <template-parameter-1-1>, <template-parameter-1-2> >::operator[](size_type) const [with _Tp = int; _Alloc = std::allocator<int>; const_reference = const int&; size_type = long unsigned int]: Assertion '__n < this->size()' failed.

Το σύστημα σταμάτησε το πρόγραμμα με το σήμα 6.

Η πρώτη γραμμή της εξόδου είναι 18 446 744 073 709 551 61518\,446\,744\,073\,709\,551\,615, δηλαδή 264−12^{64} - 1, ο μεγαλύτερος std::size_t αυτής της πλατφόρμας. Από αυτό έπεται ότι η συνθήκη του βρόχου, 0 <= 18446744073709551615, είναι αληθής, και ότι ο πρώτος γύρος διαβάζει το values[0] ενός διανύσματος που δεν έχει στοιχείο 0: απροσδιόριστη συμπεριφορά. Ό,τι παρατηρείται εξαρτάται από τις επιλογές, όπως στους πίνακες: μεταγλωττισμένη χωρίς βελτιστοποίηση, η βιβλιοθήκη του GCC 15 ελέγχει τον δείκτη του operator[] και σταματά το πρόγραμμα με έναν ισχυρισμό που λέει __n < this->size()· με βελτιστοποίηση αυτός ο έλεγχος εξαφανίζεται, και ο βρόχος, που δεν σταματά εγκαίρως, προσπαθεί να διαβάσει εκεί όπου δεν υπάρχει κανένα στοιχείο (επαληθευμένο σε Linux με -O2: επειδή το κενό διάνυσμα δεν έχει μπλοκ μνήμης, το πρόγραμμα τερματίζεται με σφάλμα κατάτμησης, χωρίς μήνυμα που να εξηγεί την αιτία). Η διόρθωση είναι να γραφτεί το όριο χωρίς αφαίρεση, i < values.size(), που με το κενό διάνυσμα είναι ψευδές από την αρχή· και όπου η αφαίρεση φαίνεται αναγκαία, να ρωτάμε πρώτα το values.empty().