Οι έξυπνοι δείκτες είναι ένα σύγχρονο χαρακτηριστικό της C++ που έχει σχεδιαστεί για να παρέχει αυτόματη διαχείριση μνήμης, βοηθώντας στην αποφυγή διαρροών μνήμης και εκκρεμών δεικτών, που συνήθως συνδέονται με τους ακατέργαστους δείκτες. Λειτουργούν ως περιτυλίγματα γύρω από ακατέργαστους δείκτες, προσθέτοντας επιπλέον λειτουργικότητα, όπως η αυτόματη αποδέσμευση μνήμης όταν ο δείκτης δεν χρειάζεται πλέον.
Οι έξυπνοι δείκτες υλοποιούνται συνήθως ως πρότυπα κλάσεων στην πρότυπη βιβλιοθήκη της C++.
Οι δύο πιο συχνά χρησιμοποιούμενοι έξυπνοι δείκτες είναι οι std::unique_ptr και std::shared_ptr.
Ο std::unique_ptr είναι ένας έξυπνος δείκτης που κατέχει το αντικείμενο αποκλειστικά.
Διασφαλίζει ότι ανά πάσα στιγμή μόνο ένα αντικείμενο std::unique_ptr κατέχει τον πόρο.
Όταν ο std::unique_ptr που κατέχει τον πόρο καταστραφεί ή επαναφερθεί, καταστρέφει αυτόματα τα αντικείμενα και απελευθερώνει τη μνήμη του.
#include <memory>
// Declaring and defining a unique pointer
auto rightful_king_of_england = std::make_unique<std::string>("Excalibur");
// Unique pointers cannot be copied or assigned
auto mordred = rightful_king_of_england; // Error: Cannot copy a unique_ptr
std::make_unique()
Όταν δημιουργείς έναν std::unique_ptr, είναι προτιμότερο να χρησιμοποιείς τον std::make_unique() αντί να χρησιμοποιείς απευθείας τον new για να δεσμεύσεις μνήμη.
Ο std::make_unique() προσφέρει αρκετά πλεονεκτήματα:
std::make_unique() εγγυάται ασφάλεια έναντι εξαιρέσεων.
Αν πεταχτεί μια εξαίρεση κατά την κατασκευή του αντικειμένου, η μνήμη θα αποδεσμευτεί αυτόματα, αποτρέποντας διαρροές μνήμης.std::make_unique() κάνει τον κώδικα πιο σαφή και πιο συνοπτικό.
Εξαλείφει την ανάγκη να προσδιορίσεις ρητά τον τύπο που δεσμεύεται, καθώς τα ορίσματα του προτύπου συνάγονται αυτόματα.std::make_unique() πιο αποτελεσματικά από ό,τι με τη χειροκίνητη δέσμευση μνήμης με τον new, κάτι που μπορεί να οδηγήσει σε καλύτερη απόδοση.std::unique_ptr κατασκευάζεται χειροκίνητα.
Αυτό θα οδηγούσε σε απροσδιόριστη συμπεριφορά, όταν ο std::unique_ptr προσπαθεί να τον διαγράψει στο τέλος της εμβέλειάς του.Ο std::shared_ptr είναι ένας έξυπνος δείκτης που επιτρέπει σε πολλά αντικείμενα std::shared_ptr να μοιράζονται την ιδιοκτησία του ίδιου πόρου.
Παρακολουθεί πόσοι κοινόχρηστοι δείκτες αναφέρονται στον πόρο και αποδεσμεύει τη μνήμη μόνο όταν ο τελευταίος κοινόχρηστος δείκτης που κατέχει τον πόρο βγει από την εμβέλεια ή επαναφερθεί.
// Declaring and defining a shared pointer to a dynamically allocated string
auto martian_congressional_republic = std::make_shared<std::string>("protomolecule");
// Creating more shared pointer that shares ownership
auto outer_planets_alliance = martian_congressional_republic;
auto united_nations = martian_congressional_republic;
Στην C++17 και σε παλαιότερες εκδόσεις, η χρήση του std::shared_ptr με πίνακες μέσω του std::make_shared<T[]> δεν υποστηρίζεται άμεσα.
Αν και είναι δυνατό να δεσμεύσεις πίνακες με το std::make_shared<T[]>, η δημιουργία κοινόχρηστων δεικτών απευθείας από αυτούς μπορεί να οδηγήσει σε απροσδιόριστη συμπεριφορά, λόγω διαφορών στη διαχείριση μνήμης ανάμεσα σε μεμονωμένα αντικείμενα και πίνακες.
Αντ' αυτού, σκέψου να χρησιμοποιήσεις std::vector ή προσαρμοσμένες συναρτήσεις διαγραφής για να διαχειριστείς αποτελεσματικά πίνακες με κοινόχρηστους δείκτες.
Να διασφαλίζεις πάντα τη συμβατότητα με τον μεταγλωττιστή και την υλοποίηση της πρότυπης βιβλιοθήκης σου όταν έχεις να κάνεις με δεσμεύσεις πινάκων και κοινόχρηστους δείκτες στην C++17.
std::make_shared()
Όπως και ο std::make_unique(), ο std::make_shared() προσφέρει οφέλη όπως καλύτερη αποδοτικότητα μνήμης, ασφάλεια έναντι εξαιρέσεων και αναγνωσιμότητα.
Συνδυάζει τη δέσμευση μνήμης για το μπλοκ ελέγχου και το διαχειριζόμενο αντικείμενο σε μία μόνο λειτουργία, βελτιώνοντας την αποδοτικότητα και μειώνοντας τον κίνδυνο διαρροών μνήμης.
Επιπλέον, η αυτόματη συναγωγή των ορισμάτων του προτύπου απλοποιεί τον κώδικα και βελτιώνει την αναγνωσιμότητα.
Η χρήση του std::make_shared() οδηγεί σε καθαρότερο, ασφαλέστερο και πιο αποδοτικό κώδικα όταν δουλεύεις με αντικείμενα std::shared_ptr στην C++.
Ο std::weak_ptr είναι μια συνοδευτική κλάση του std::shared_ptr που παρέχει μια μη κατέχουσα, "αδύναμη" αναφορά σε ένα αντικείμενο που διαχειρίζεται ένας κοινόχρηστος δείκτης.
Επιτρέπει την πρόσβαση στον πόρο χωρίς να επηρεάζει τη διάρκεια ζωής του.
Οι αδύναμοι δείκτες είναι χρήσιμοι σε σενάρια όπου πρέπει να σπάσουν κυκλικές αναφορές για να αποτραπούν διαρροές μνήμης.
// Creating a shared pointer
auto your_account = std::make_shared<std::string>("secret_subscription_password");
// Creating a shared pointer that shares ownership
auto your_flatmates_account = your_account;
// Creating a weak pointer from the shared pointer
auto your_flatmates_boyfriends_account = your_flatmates_account;
// if your_account and your_flatmates_account are deleted, there is no more reference to the shared pointer.
// your_flatmates_boyfriends_account will be a null pointer and cannot use the associated object any longer.
std::weak_ptr και η κυκλική ιδιοκτησίαΟ std::weak_ptr σχεδιάστηκε για να αντιμετωπίσει το πρόβλημα της κυκλικής ιδιοκτησίας, γνωστό και ως κυκλικές αναφορές, που μπορεί να προκύψει όταν χρησιμοποιείς std::shared_ptr.
Σε ένα σενάριο κυκλικής ιδιοκτησίας, δύο ή περισσότερα αντικείμενα std::shared_ptr αναφέρονται το ένα στο άλλο, δημιουργώντας έναν κύκλο όπου κανένα από τα αντικείμενα δεν μπορεί να διαγραφεί, επειδή έχουν ισχυρές αναφορές μεταξύ τους, με αποτέλεσμα διαρροές μνήμης.
Ο std::weak_ptr δίνει μια λύση σε αυτό το πρόβλημα, επιτρέποντας αδύναμες αναφορές σε κοινόχρηστα αντικείμενα χωρίς να συνεισφέρει στον μετρητή αναφορών τους.
Αυτό σημαίνει ότι μπορεί να παρατηρεί και να προσπελαύνει το κοινόχρηστο αντικείμενο, αλλά δεν εμποδίζει τη διαγραφή του.
Αν όλες οι ισχυρές αναφορές στο κοινόχρηστο αντικείμενο απελευθερωθούν, το αντικείμενο καταστρέφεται και οι αδύναμοι δείκτες που το παρατηρούν επαναφέρονται αυτόματα σε nullptr.
Ορίστε ένα σύντομο παράδειγμα που δείχνει τη χρήση του std::weak_ptr για να σπάσει η κυκλική ιδιοκτησία:
struct Node {
Node() { std::cout << "Node constructed\n"; }
~Node() { std::cout << "Node destructed\n"; }
std::weak_ptr<Node> next;
};
// Creating two nodes
auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();
// Creating a cycle by making each node point to the other
node1->next = node2;
node2->next = node1;
// Release strong references to nodes
node1.reset();
node2.reset();
// Nodes are automatically destructed due to weak references
Το "Node constructed" εκτυπώνεται δύο φορές και ακολουθείται από δύο γραμμές με "Node destructed".
Όταν το next δεν είναι αδύναμος δείκτης αλλά std::shared_ptr, δημιουργείται μια κατάσταση κυκλικής ιδιοκτησίας, που οδηγεί σε διαρροές μνήμης, επειδή και οι δύο κόμβοι θα κρατούν ισχυρές αναφορές ο ένας στον άλλο, εμποδίζοντας την καταστροφή τους.
struct Node {
Node() { std::cout << "Node constructed\n"; }
~Node() { std::cout << "Node destructed\n"; }
std::shared_ptr<Node> next; // Change to shared_ptr
};
Σε αυτό το σενάριο, τόσο το node1 όσο και το node2 κρατούν ισχυρές αναφορές το ένα στο άλλο μέσω std::shared_ptr, σχηματίζοντας έναν κύκλο.
Όταν το πρόγραμμα τερματίζει και προσπαθεί να απελευθερώσει τις ισχυρές αναφορές με το reset(), οι μετρητές αναφορών και των δύο κόμβων παραμένουν μη μηδενικοί, επειδή κάθε κόμβος κρατά μια ισχυρή αναφορά στον άλλο.
Επομένως, οι κόμβοι δεν καταστρέφονται, με αποτέλεσμα διαρροές μνήμης.
Το "Node constructed" εκτυπώνεται δύο φορές, αλλά δεν ακολουθείται από καμία γραμμή με "Node destructed".
std::weak_ptr
Οι εκκρεμείς δείκτες προκύπτουν όταν ένας δείκτης αναφέρεται σε ένα αντικείμενο που έχει διαγραφεί, οδηγώντας σε απροσδιόριστη συμπεριφορά όταν αποαναφέρεται ο δείκτης.
Ο std::weak_ptr βοηθά να αποφύγεις τους εκκρεμείς δείκτες, παρέχοντας μια μη κατέχουσα, αδύναμη αναφορά σε ένα αντικείμενο που διαχειρίζεται ο std::shared_ptr.
Να πώς μπορείς να αποφύγεις τους εκκρεμείς δείκτες με τον std::weak_ptr:
Δημιούργησε έναν std::weak_ptr: Αντί να κρατάς απευθείας έναν std::shared_ptr, δημιούργησε έναν std::weak_ptr προς το κοινόχρηστο αντικείμενο.
Έλεγξε την εγκυρότητα: Πριν χρησιμοποιήσεις τον std::weak_ptr, έλεγξε την εγκυρότητά του με τη συνάρτηση expired().
Η συνάρτηση αυτή επιστρέφει true αν το σχετιζόμενο κοινόχρηστο αντικείμενο έχει διαγραφεί.
Κλείδωσε τον std::weak_ptr: Για να προσπελάσεις με ασφάλεια το κοινόχρηστο αντικείμενο, χρησιμοποίησε τη συνάρτηση lock(), η οποία επιστρέφει έναν std::shared_ptr που δείχνει στο ίδιο αντικείμενο, αν αυτό είναι ακόμα έγκυρο.
Αν το αντικείμενο έχει διαγραφεί, η lock() επιστρέφει έναν κενό std::shared_ptr.
Ορίστε ένα παράδειγμα κώδικα που δείχνει τη χρήση του std::weak_ptr για να αποφύγεις τους εκκρεμείς δείκτες:
auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();
// Create weak pointers
node1->next = node2;
node2->next = node1;
// Check validity and lock
if (auto lockedNode2 = node1->next.lock()) {
// Use lockedNode2 safely
} else {
// Handle case where node2 has been deleted
}
Σε αυτό το παράδειγμα, τα node1->next και node2->next είναι αντικείμενα std::weak_ptr.
Πριν από την πρόσβαση στο κοινόχρηστο αντικείμενο, ελέγχεται η εγκυρότητα με τη συνάρτηση expired().
Στη συνέχεια, χρησιμοποιείται η lock() για την ασφαλή πρόσβαση στο κοινόχρηστο αντικείμενο.
Αυτή η προσέγγιση διασφαλίζει ότι αποφεύγονται οι εκκρεμείς δείκτες όταν δουλεύεις με τον std::weak_ptr.
Χρησιμοποίησε έξυπνους δείκτες από προεπιλογή: std::unique_ptr για αποκλειστική ιδιοκτησία και std::shared_ptr για κοινόχρηστη ιδιοκτησία.
Κράτησε τους ακατέργαστους δείκτες για μη κατέχουσες αναφορές ή όταν συνεργάζεσαι με παλιό κώδικα.
Στις περισσότερες περιπτώσεις, ο std::unique_ptr αρκεί για την αποκλειστική ιδιοκτησία, καθώς προσφέρει ελαφριά διαχείριση μνήμης χωρίς το βάρος της μέτρησης αναφορών.
Ο std::shared_ptr πρέπει να χρησιμοποιείται με φειδώ, καθώς προσθέτει βάρος και πολυπλοκότητα, εκτός αν χρειάζεται πραγματικά κοινόχρηστη ιδιοκτησία.
Ο std::weak_ptr είναι εξειδικευμένος για το σπάσιμο κυκλικών εξαρτήσεων ή την παρατήρηση κοινόχρηστων αντικειμένων, αλλά δεν χρησιμοποιείται συχνά.