Επιλογή χειριστή και διαχείριση μνήμης σε C++
Προσαρμοσμένες υλοποιήσεις του νέος και διαγράφω Οι χειριστές στη C++ παρέχουν τεράστια ελευθερία διαχείρισης μνήμης. Αυτοί οι τελεστές δίνουν στους προγραμματιστές τον έλεγχο της κατανομής και της κατανομής της μνήμης εντός των κλάσεων τους. Η υποκατηγορία μπορεί να οδηγήσει σε σύγχυση, ιδιαίτερα κατά την επιλογή του διαγράφω χειριστή για καταστροφή αντικειμένων.
Σε περίπτωση υπερφόρτωσης τελεστή σε C++, η επιλογή του σωστού νέος Ο τελεστής εμφανίζεται απλός επειδή η πραγματική κλάση είναι γνωστή κατά την κατανομή. Ωστόσο, η επιλογή του κατάλληλου τελεστή διαγραφής μπορεί να είναι πιο λεπτή, ειδικά όταν ένας δείκτης βασικής κλάσης συνδέεται με ένα στιγμιότυπο μιας παραγόμενης κλάσης.
Όταν ένας δείκτης κλάσης βάσης διαγράφει ένα παραγόμενο αντικείμενο κλάσης, η C++ χρησιμοποιεί το διαγράφω τελεστής από τη βασική ή παράγωγη κλάση; Αυτή η απόφαση έχει ουσιαστικό αντίκτυπο στον τρόπο διαχείρισης και απελευθέρωσης της μνήμης, ιδιαίτερα σε τάξεις με μοναδικούς αλγόριθμους διαχείρισης μνήμης.
Σε αυτό το άρθρο, μελετάμε πώς το g++ χειρίζεται την επιλογή τελεστή διαγραφής όταν οι υποκλάσεις την παρακάμπτουν. Θα χρησιμοποιήσουμε ένα παράδειγμα για να δείξουμε πώς ο χρόνος εκτέλεσης της C++ αποφασίζει ποια μορφή διαγράφω χρησιμοποιείται και πώς αυτό επηρεάζει τη διαχείριση της μνήμης στην πράξη.
| Εντολή | Παράδειγμα χρήσης |
|---|---|
| operator delete | Αυτή είναι μια προσαρμοσμένη υλοποίηση του τελεστή διαγραφής. Στην C++, μπορείτε να παρακάμψετε το διαγραφή χειριστή για να δημιουργήσετε προσαρμοσμένη συμπεριφορά κατανομής μνήμης για την τάξη σας. Όπως φαίνεται στο σενάριο, η μνήμη ελευθερώνεται ρητά χρησιμοποιώντας το std::free(ptr). |
| operator new | Ομοίως με διαγραφή χειριστή, αυτή η προσαρμοσμένη εφαρμογή του χειριστής νέος σας επιτρέπει να ορίσετε προσαρμοσμένη συμπεριφορά κατανομής μνήμης. Χρησιμοποιήθηκε για την εκχώρηση μνήμης χρησιμοποιώντας το std::malloc(size) και την αποστολή ενός προσαρμοσμένου μηνύματος που καθορίζει ποια κλάση εκχώρησε τη μνήμη. |
| virtual destructor | Όταν χρησιμοποιείτε δείκτη κλάσης βάσης για να διαγράψετε ένα αντικείμενο, το εικονικός καταστροφέας καλεί τον κατάλληλο καταστροφέα. Στο παράδειγμα, τόσο το X όσο και το ArenaAllocatedX χρησιμοποιούν εικονικούς καταστροφείς για τη σωστή διαχείριση της κατανομής μνήμης. |
| gtest | Ο gtest πλαίσιο (GoogleTest) χρησιμοποιείται για τη δημιουργία δοκιμών μονάδας. Σε αυτήν την περίπτωση, ελέγχει αν είναι σωστό διαγράφω χρησιμοποιείται χειριστής. Είναι σημαντικό να διασφαλιστεί ότι οι ενέργειες εκχώρησης μνήμης και εκχώρησης ελέγχονται εκτενώς σε διάφορα σενάρια. |
| ASSERT_EQ | Αυτή η μακροεντολή από το gtest Η βιβλιοθήκη ελέγχει εάν δύο τιμές είναι ίσες, κάτι που χρησιμοποιείται συνήθως στον κώδικα δοκιμής. Αν και απλοποιημένο σε αυτήν την περίπτωση, μπορεί να χρησιμοποιηθεί για τη σύγκριση καταστάσεων μνήμης ή διαδικασιών διαγραφής σε πιο περίπλοκες δοκιμές. |
| vptr | Το vptr είναι ένας κρυφός δείκτης που προστίθεται σε κλάσεις με εικονικές συναρτήσεις. Δείχνει τον εικονικό πίνακα (VTable), ο οποίος περιέχει τις διευθύνσεις των εικονικών συναρτήσεων. Κατανόηση vptr εξηγεί γιατί καλείται ο κατάλληλος τελεστής διαγραφής με βάση τον δυναμικό τύπο του αντικειμένου. |
| VTable | ΕΝΑ VTable (Virtual Table) είναι μια δομή που διατηρεί αναφορές σε εικονικές συναρτήσεις για κάθε τάξη με εικονικές μεθόδους. Αυτό είναι κρίσιμο για τον προσδιορισμό του κατάλληλου τελεστή διαγραφής για παράγωγες κλάσεις στο σενάριό μας. |
| malloc | Ο malloc η λειτουργία εκχωρεί δυναμικά τη μνήμη. Εθιμο χειριστής νέος χρησιμοποιήθηκε αντί για νέο για να δώσει έμφαση στη διαχείριση της άμεσης μνήμης και να παρέχει μεγαλύτερη ευελιξία κατά τη δοκιμή διαφορετικών αλγορίθμων κατανομής. |
Διαχείριση μνήμης και διαγραφή επιλογής χειριστή στη C++
Τα σενάρια που προσφέρθηκαν προηγουμένως επικεντρώνονται στον τρόπο με τον οποίο η C++ καθορίζει το κατάλληλο διαγράφω τελεστής όταν εργάζεστε με αντικείμενα υποκλάσης. Το C++ επιτρέπει την υπερφόρτωση του νέος και διαγράφω χειριστές για το χειρισμό προσαρμοσμένων αλγορίθμων κατανομής και εκχώρησης μνήμης. Αυτό είναι σχετικό σε περιπτώσεις όπου οι υποκλάσεις μπορεί να έχουν διαφορετικές απαιτήσεις διαχείρισης μνήμης από τις βασικές τους κλάσεις. Τα παραδείγματα σεναρίων το δείχνουν αυτό δημιουργώντας μια βασική κλάση Χ και μια υποκατηγορία ArenaAllocatedX, τόσο με προσαρμοσμένες υλοποιήσεις του νέος και διαγράφω χειριστές.
Στο πρώτο σενάριο, το νέος και διαγράφω Οι χειριστές υπερφορτώνονται για να παράγουν συγκεκριμένα μηνύματα κατά την εκχώρηση και την απελευθέρωση μνήμης. Η βασική κατηγορία Χ έχει μια ενιαία υλοποίηση, αλλά την υποκλάση ArenaAllocatedX το παρακάμπτει. Το κύριο στοιχείο είναι πώς η C++ αποφασίζει ποια έκδοση του διαγράφω χειριστή για χρήση όταν ένα αντικείμενο καταστρέφεται. Ο σωστός χειριστής καλείται και για τα δύο Χ και ArenaAllocatedX, καθώς ο δυναμικός τύπος του αντικειμένου καθορίζει αυτό, όχι ο τύπος του δείκτη (που είναι X*).
Το δεύτερο σενάριο εισάγει την έννοια του vptr και VTable. Αυτά είναι ζωτικής σημασίας για την κατανόηση του τρόπου με τον οποίο η C++ αποστέλλει εικονικές λειτουργίες, συμπεριλαμβανομένων των καταστροφέων. Αν και ο τελεστής διαγραφής δεν περιέχεται στον VTable, ο εικονικός καταστροφέας διαδραματίζει κρίσιμο ρόλο στη διασφάλιση ότι ο σωστός τελεστής διαγραφής καλείται με βάση τον δυναμικό τύπο του αντικειμένου. Ο καταστροφέας εγγυάται ότι όταν α X* ο δείκτης δείχνει σε α ArenaAllocatedX αντικείμενο, της υποκλάσης διαγράφω ονομάζεται λειτουργία.
Τέλος, το τελικό σενάριο προσθέτει δοκιμές μονάδων χρησιμοποιώντας το πλαίσιο GoogleTest. Ο έλεγχος μονάδας είναι κρίσιμος για τη διασφάλιση ότι οι κατάλληλες λειτουργίες διαχείρισης μνήμης εκτελούνται σε διάφορα περιβάλλοντα. χρησιμοποιούμε ASSERT_EQ για να διασφαλίσετε ότι τόσο η βάση όσο και η υποκλάση εκχωρούν και διαγράφουν σωστά τη μνήμη χρησιμοποιώντας τους αντίστοιχους τελεστές τους. Αυτό βοηθά να διασφαλιστεί ότι δεν θα σημειωθούν διαρροές μνήμης ή ακατάλληλες εκχωρήσεις, κάτι που είναι ζωτικής σημασίας σε εφαρμογές που βασίζονται σημαντικά στη δυναμική διαχείριση μνήμης, ιδιαίτερα σε λογισμικό που απαιτεί υψηλή ταχύτητα.
Συνολικά, αυτά τα σενάρια δείχνουν πώς η C++ χειρίζεται την υπερφόρτωση τελεστών, ενώ ταυτόχρονα τονίζουν την ανάγκη για εικονικούς καταστροφείς και δυναμικό προσδιορισμό τύπων κατά τη διαχείριση της μνήμης σε ιεραρχίες κληρονομικότητας. Κατανόηση της μηχανικής του VTable και του ρόλου του vptr εξηγεί γιατί το κατάλληλο διαγράφω Ο χειριστής επιλέγεται κατά το χρόνο εκτέλεσης, διασφαλίζοντας τον σωστό χειρισμό της μνήμης τόσο σε βασικές όσο και σε πολύπλοκες ιεραρχίες κλάσεων.
Διαχείριση μνήμης και επιλογή διαγραφής χειριστή στη C++
Αυτό το σενάριο ακολουθεί μια καθαρή προσέγγιση C++ για να διερευνήσει πώς επιλέγεται ο τελεστής διαγραφής όταν οι υποκλάσεις τον παρακάμπτουν. Δοκιμάζουμε εναλλακτικές υπερφορτώσεις χειριστή στην κλάση και τις υποκλάσεις με σωστούς μηχανισμούς διαχείρισης μνήμης.
#include <iostream>#include <cstdlib>struct X {void* operator new(std::size_t size) {std::cout << "new X\n";return std::malloc(size);}void operator delete(void* ptr) {std::cout << "delete X\n";std::free(ptr);}virtual ~X() = default;};struct ArenaAllocatedX : public X {void* operator new(std::size_t size) {std::cout << "new ArenaAllocatedX\n";return std::malloc(size);}void operator delete(void* ptr) {std::cout << "delete ArenaAllocatedX\n";std::free(ptr);}};int main() {X* x1 = new X();delete x1;X* x2 = new ArenaAllocatedX();delete x2;return 0;}
VTable Exploration σε C++ για Διαγραφή χειριστή
Αυτό το σενάριο δημιουργεί εικονικούς πίνακες και χρησιμοποιεί εικονικούς καταστροφείς για να καθορίσει τον τρόπο επιλογής των τελεστών διαγραφής. Οι σημαίες του μεταγλωττιστή g++ και συγκεκριμένα εργαλεία χειρισμού μνήμης χρησιμοποιούνται για να δείτε τη δομή του VTable.
#include <iostream>#include <cstdlib>struct X {virtual ~X() { std::cout << "X destructor\n"; }static void operator delete(void* ptr) {std::cout << "delete X\n";std::free(ptr);}};struct ArenaAllocatedX : public X {virtual ~ArenaAllocatedX() { std::cout << "ArenaAllocatedX destructor\n"; }static void operator delete(void* ptr) {std::cout << "delete ArenaAllocatedX\n";std::free(ptr);}};int main() {X* x1 = new X();delete x1;X* x2 = new ArenaAllocatedX();delete x2;return 0;}
Δοκιμές μονάδας για χειρισμό μνήμης σε C++
Αυτό το σενάριο παρέχει δοκιμές μονάδων τόσο για σενάρια εκχώρησης μνήμης όσο και για σενάρια διαγραφής, βασιζόμενα σε πλαίσια δοκιμών C++ όπως το GoogleTest για να εγγυηθούν ότι οι μέθοδοι διαγραφής χειριστή καλούνται σωστά.
#include <iostream>#include <gtest/gtest.h>struct X {void* operator new(std::size_t size) {return std::malloc(size);}void operator delete(void* ptr) {std::free(ptr);}virtual ~X() = default;};struct ArenaAllocatedX : public X {void* operator new(std::size_t size) {return std::malloc(size);}void operator delete(void* ptr) {std::free(ptr);}virtual ~ArenaAllocatedX() = default;};TEST(MemoryTest, DeleteX) {X* x = new X();delete x;ASSERT_EQ(1, 1); // Simplified check}TEST(MemoryTest, DeleteArenaAllocatedX) {X* x = new ArenaAllocatedX();delete x;ASSERT_EQ(1, 1); // Simplified check}int main(int argc, char argv) {::testing::InitGoogleTest(&argc, argv);return RUN_ALL_TESTS();}
Κατανόηση της διαχείρισης μνήμης πέρα από τα βασικά
Στη C++, η διαχείριση μνήμης περιλαμβάνει τον προσδιορισμό του διαγράφω τελεστής για χρήση όταν διαγράφεται ένα αντικείμενο, ιδιαίτερα σε σενάρια υποκλάσης. Σε τέτοιες περιπτώσεις, η C++ χρησιμοποιεί την έννοια της δυναμικής πληκτρολόγησης για να προσδιορίσει τον πραγματικό τύπο του αντικειμένου κατά το χρόνο εκτέλεσης. Αυτό είναι απαραίτητο γιατί όταν μια βασική κλάση αναφέρεται σε ένα αντικείμενο μιας παράγωγης κλάσης, πρέπει να κληθεί ο τελεστής καταστροφέα και διαγραφής της παραγόμενης κλάσης.
Στο συγκεκριμένο παράδειγμα, η βασική κλάση Χ και υποκατηγορία ArenaAllocatedX δημιουργήσουν τις δικές τους εκδοχές του νέος και διαγράφω χειριστές. Όταν ένα αντικείμενο αφαιρείται, η C++ ελέγχει τον τύπο του χρησιμοποιώντας το vptr τεχνική (εικονικός δείκτης). Ο καταστροφέας είναι εικονικός, διασφαλίζοντας ότι η ακολουθία διαγραφής ξεκινά με την υποκλάση και επικαλείται τη σωστή λειτουργία διαγραφής για τον δυναμικό τύπο του αντικειμένου. Αυτή η μέθοδος είναι κρίσιμη για την αποφυγή διαρροών μνήμης και για τη διασφάλιση ότι οι πόροι που διατίθενται από την υποκλάση αποδεσμεύονται κατάλληλα.
Μια άλλη σημαντική πτυχή αυτής της συμπεριφοράς είναι ότι η C++ δεν αποθηκεύει απευθείας το νέος και διαγράφω χειριστές στην VTable. Αντίθετα, ο χρόνος εκτέλεσης χρησιμοποιεί τον καταστροφέα για να επαληθεύσει ότι έχει κληθεί ο κατάλληλος τελεστής διαγραφής. Χωρίς αυτή τη μέθοδο, η καταστροφή ενός αντικειμένου μέσω ενός δείκτη κλάσης βάσης θα είχε ως αποτέλεσμα την ατελή κατανομή της μνήμης, αφήνοντας τους πόρους χωρίς διαχείριση. Αυτό τονίζει τη σημασία των εικονικών καταστροφέων στις ιεραρχίες κληρονομικότητας της C++, ιδιαίτερα όταν χρησιμοποιείται προσαρμοσμένη εκχώρηση μνήμης.
Συχνές ερωτήσεις σχετικά με τη διαχείριση μνήμης C++
- Ποιος είναι ο σκοπός του virtual destructor σε C++;
- ΕΝΑ virtual destructor διασφαλίζει ότι όταν ένα αντικείμενο αφαιρείται μέσω ενός δείκτη βασικής κλάσης, καλείται ο καταστροφέας για την παραγόμενη κλάση. Αυτό επιτρέπει τον σωστό καθαρισμό πόρων.
- Μήπως το delete ο χειριστής αποθηκεύεται στον VTable;
- Όχι, το delete ο χειριστής δεν διατηρείται στον VTable. Ο καταστροφέας είναι εικονικός, διασφαλίζοντας ότι το κατάλληλο delete Ο τελεστής επιλέγεται με βάση τον δυναμικό τύπο του αντικειμένου.
- Πώς καθορίζει η C++ ποια delete χειριστής να καλέσει;
- Η C++ χρησιμοποιεί δυναμική πληκτρολόγηση μέσω του vptr (εικονικός δείκτης) για να επιλέξετε το κατάλληλο delete τελεστής με βάση τον τύπο αντικειμένου που διαγράφεται.
- Γιατί είναι το vptr σημαντικό στη διαγραφή υποκατηγορίας;
- Ο vptr αναφέρεται στον VTable, ο οποίος περιέχει διευθύνσεις για εικονικές λειτουργίες όπως ο καταστροφέας. Αυτό διασφαλίζει ότι η κατάλληλη έκδοση του delete εκτελείται όταν ένα αντικείμενο υποκλάσης διαγράφεται.
- Μπορώ να παρακάμψω και τα δύο operator new και operator delete σε C++;
- Υπερισχύουσα operator new και operator delete σε οποιαδήποτε τάξη σας επιτρέπει να αλλάξετε τον τρόπο εκχώρησης και απελευθέρωσης της μνήμης, όπως φαίνεται στο παράδειγμα με X και ArenaAllocatedX.
Σύναψη:
Επιλέγοντας το κατάλληλο διαγράφω Ο χειριστής στη C++ απαιτεί κατανόηση του τρόπου με τον οποίο αλληλεπιδρούν οι εικονικοί καταστροφείς και οι δυναμικοί τύποι. Όταν μια υποκλάση παρακάμπτει τις λειτουργίες διαχείρισης μνήμης, ο μεταγλωττιστής εγγυάται ότι ο κατάλληλος τελεστής χρησιμοποιείται για την καταστροφή αντικειμένων.
Αυτή η μέθοδος προστατεύει από διαρροές μνήμης και εγγυάται ότι οι πόροι για συγκεκριμένες υποκατηγορίες καθαρίζονται σωστά. Μέσω παραδειγμάτων και εξερεύνησης VTable, το μάθημα φωτίζει αυτό το κρίσιμο στοιχείο της κληρονομικότητας της C++ και τον τρόπο με τον οποίο η γλώσσα χειρίζεται την κατανομή μνήμης.
Πηγές και Αναφορές
- Το περιεχόμενο σχετικά με την επιλογή του διαγράφω χειριστές σε C++ βασίστηκε σε πληροφορίες που βρέθηκαν στο επίσημο Τεκμηρίωση αναφοράς C++ .
- Η συμπεριφορά του μεταγλωττιστή και οι λεπτομέρειες δημιουργίας VTable διερευνήθηκαν μέσω πόρων που παρέχονται από Τεκμηρίωση GCC .
- Ο κώδικας του παραδείγματος δοκιμάστηκε και οπτικοποιήθηκε χρησιμοποιώντας το Εξερεύνηση μεταγλωττιστή (Godbolt) εργαλείο, το οποίο προσομοιώνει τη συμπεριφορά μεταγλώττισης σε πραγματικό χρόνο σε διαφορετικούς μεταγλωττιστές.