Χρήση νέας διαγραφής για την υλοποίηση πινάκων.

Πρόγραμμα Kerish Doctor. 26.04.2019
Επισκόπηση προγράμματος Η έκδοση υπολογιστή του Microsoft Excel Viewer θα επιτρέψει...

Chercher Οικιακές συσκευές, του οποίου η τιμή είναι η διεύθυνση του πρώτου στοιχείου του πίνακα (&arr). Επομένως, το όνομα του πίνακα μπορεί να είναι ένας αρχικοποιητής δείκτη στον οποίο θα ισχύουν όλοι οι κανόνες αριθμητικής διεύθυνσης που σχετίζονται με δείκτες. Παράδειγμα προγράμματος:
Πρόγραμμα 11.1

#συμπεριλαμβάνω χρησιμοποιώντας namespace std? int main() ( const int k = 10; int arr[k]; int *p = arr; // ο δείκτης δείχνει στο πρώτο στοιχείο του πίνακα για (int i = 0; i< 10; i++){ *p = i; p++; // указатель указывает на επόμενο στοιχείο) p = arr; // επιστρέψτε έναν δείκτη στο πρώτο στοιχείο για (int i = 0; i< 10; i++){ cout << *p++ << " "; } cout << endl; // аналогично: for (int i = 0; i < 10; i++){ cout << *(arr + i) << " "; } cout << endl; p = arr; // выводим адреса элементов: for (int i = 0; i < 10; i++){ cout << "arr[" << i << "] => " << p++ << endl; } return 0; }

Έξοδος προγράμματος:

0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 arr => 0xbffc8f00 arr => 0xbffc8f04 arr => 0xbffc8f08 arr => 0xbffc8fff>0xf 4 arr => 0xbffc8f18 arr = > 0xbffc8f1c arr => 0xbffc8f20 arr => 0xbffc8f24

Η έκφραση arr[i] – η πρόσβαση σε ένα στοιχείο με ευρετήριο αντιστοιχεί στην έκφραση *(arr + i) , η οποία ονομάζεται μετατόπιση δείκτη(γραμμή 22). Αυτή η έκφραση δείχνει πιο καθαρά πώς η C++ λειτουργεί πραγματικά με στοιχεία πίνακα. Μεταβλητή μετρητή i υποδεικνύει πόσα στοιχεία πρέπει να αντισταθμιστούν από το πρώτο στοιχείο. Η γραμμή 17 εκτυπώνει την τιμή του στοιχείου πίνακα μετά την αποαναφορά του δείκτη.

Τι σημαίνει η έκφραση *p++; Ο τελεστής * έχει χαμηλότερη προτεραιότητα, ενώ η προσαύξηση του postfix είναι συσχετιστική από αριστερά προς τα δεξιά. Επομένως, σε αυτή τη σύνθετη έκφραση, θα εκτελεστεί πρώτα η έμμεση διευθυνσιοδότηση (πρόσβαση στην τιμή ενός στοιχείου πίνακα) και μετά θα αυξηθεί ο δείκτης. Διαφορετικά, αυτή η έκφραση θα μπορούσε να αναπαρασταθεί ως εξής: cout Σημείωμα. Ο τελεστής sizeof() που εφαρμόζεται σε ένα όνομα πίνακα θα επιστρέψει το μέγεθος ολόκληρου του πίνακα (όχι του πρώτου στοιχείου).
Σημείωμα. Ο τελεστής διεύθυνσης (&) για στοιχεία πίνακα χρησιμοποιείται με τον ίδιο τρόπο όπως και για κανονικές μεταβλητές (τα στοιχεία του πίνακα μερικές φορές ονομάζονται μεταβλητές με ευρετήριο). Για παράδειγμα, &arr . Επομένως, μπορείτε πάντα να λάβετε έναν δείκτη σε οποιοδήποτε στοιχείο του πίνακα. Ωστόσο, η λειτουργία &arr (όπου arr είναι το όνομα του πίνακα) θα επιστρέψει τη διεύθυνση ολόκληρου του πίνακα και τέτοια, για παράδειγμα, μια πράξη (&arr + 1) θα σημαίνει ένα βήμα μεγέθους πίνακα, δηλαδή τη λήψη ενός δείκτη στο στοιχείο δίπλα στο τελευταίο.

Οφέλη από τη χρήση δεικτών κατά την εργασία με στοιχεία πίνακα

Ας δούμε δύο παραδείγματα προγραμμάτων που οδηγούν στο ίδιο αποτέλεσμα: νέες τιμές από 0 έως 1999999 εκχωρούνται σε στοιχεία πίνακα και εξάγονται.
Πρόγραμμα 11.2

#συμπεριλαμβάνω χρησιμοποιώντας namespace std? int main() ( const int n = 2000000; int mass[n] (); for (int i = 0; i< n; i++) { mass[i] = i; cout << mass[i]; } return 0; }

Πρόγραμμα 11.3

#συμπεριλαμβάνω χρησιμοποιώντας namespace std? int main() ( const int n = 2000000; int mass[n] (); int *p = mass; for (int i = 0; i< n; i++) { *p = i; cout << *p++; } return 0; }

Το πρόγραμμα 11.3 θα τρέχει πιο γρήγορα από το πρόγραμμα 11.2 (καθώς ο αριθμός των στοιχείων αυξάνεται, το πρόγραμμα 11.3 θα γίνεται πιο αποτελεσματικό)! Ο λόγος είναι ότι το Πρόγραμμα 11.2 υπολογίζει εκ νέου τη θέση (διεύθυνση) του τρέχοντος στοιχείου πίνακα σε σχέση με το πρώτο κάθε φορά (11.2, γραμμές 12 και 13). Στο Πρόγραμμα 11.3, η πρόσβαση στη διεύθυνση του πρώτου στοιχείου γίνεται μία φορά όταν αρχικοποιείται ο δείκτης (11.3, γραμμή 11).

Συστοιχία εκτός ορίων

Ας σημειώσουμε μια άλλη σημαντική πτυχή της εργασίας με πίνακες C στη C++. Δεν διατίθεται σε C++ παρακολούθηση της συμμόρφωσης με τα όρια της συστοιχίας C. Οτι. Η ευθύνη για την παρατήρηση του τρόπου επεξεργασίας των στοιχείων εντός των ορίων του πίνακα ανήκει εξ ολοκλήρου στον προγραμματιστή του αλγορίθμου. Ας δούμε ένα παράδειγμα.
Πρόγραμμα 11.4

#συμπεριλαμβάνω #συμπεριλαμβάνω #συμπεριλαμβάνω χρησιμοποιώντας namespace std? int main() ( int mas; default_random_engine rnd(time(0)); uniform_int_distribution < 10; i++) mas[i] = d(rnd); cout << "Элементы массива:" << endl; for (int i = 0; i < 10; i++) cout << mas[i] << endl; return 0; }

Το πρόγραμμα θα βγάζει κάτι σαν αυτό:

Στοιχεία πίνακα: 21 58 38 91 23 5 38 -1219324996 -1074960992 0

Παρουσιάστηκε σκόπιμα σφάλμα στο πρόγραμμα 11.4. Αλλά ο μεταγλωττιστής δεν θα αναφέρει σφάλμα: ο πίνακας έχει δηλωμένα πέντε στοιχεία, αλλά οι βρόχοι υποθέτουν ότι υπάρχουν 10 στοιχεία! Ως αποτέλεσμα, μόνο πέντε στοιχεία θα αρχικοποιηθούν σωστά (είναι δυνατή περαιτέρω καταστροφή δεδομένων) και θα εξάγονται μαζί με τα «σκουπίδια». Η C++ παρέχει τη δυνατότητα ελέγχου των ορίων χρησιμοποιώντας τις συναρτήσεις της βιβλιοθήκης begin() και end() (πρέπει να συμπεριλάβετε το αρχείο κεφαλίδας iterator). Τροποποίηση προγράμματος 11.4
Πρόγραμμα 11.5

#συμπεριλαμβάνω #συμπεριλαμβάνω #συμπεριλαμβάνω #συμπεριλαμβάνω χρησιμοποιώντας namespace std? int main() ( int mas; int *first = start(mas); int *last = end(mas); default_random_engine rnd(time(0)); uniform_int_distribution d(10, 99);<< "Элементы массива:" << endl; while(first != last) { cout << *first++ << " "; } return 0; }

while(first != last) ( *first = d(rnd); first++; ) first = start(mas);
cout

Οι συναρτήσεις begin() και end() επιστρέφουν. Θα καλύψουμε την έννοια των επαναλήψεων αργότερα, αλλά προς το παρόν θα πούμε ότι συμπεριφέρονται σαν δείκτες που δείχνουν προς το πρώτο στοιχείο (πρώτο) και το στοιχείο που ακολουθεί το τελευταίο (τελευταίο). Στο πρόγραμμα 11.5, για συμπαγή και ευκολία, αντικαταστήσαμε τον βρόχο for με βρόχο while (αφού δεν χρειαζόμαστε πλέον μετρητή εδώ - χρησιμοποιούμε αριθμητική δείκτη). Έχοντας δύο δείκτες, μπορούμε εύκολα να διατυπώσουμε μια συνθήκη για την έξοδο από τον βρόχο, αφού σε κάθε βήμα του βρόχου ο πρώτος δείκτης αυξάνεται.

Πριν εξοικειωθείτε με τους δείκτες, ήξερες τον μόνο τρόπο εγγραφής μεταβλητών δεδομένων στη μνήμη μέσω μεταβλητών. Μια μεταβλητή είναι μια ονομαζόμενη περιοχή μνήμης. Τα μπλοκ μνήμης για τις αντίστοιχες μεταβλητές εκχωρούνται κατά την έναρξη του προγράμματος και χρησιμοποιούνται μέχρι να τερματιστεί. Χρησιμοποιώντας δείκτες, μπορείτε να δημιουργήσετε μπλοκ μνήμης χωρίς όνομα συγκεκριμένου τύπου και μεγέθους (και επίσης να τα ελευθερώσετε) ενώ εκτελείται το ίδιο το πρόγραμμα. Αυτό δείχνει ένα αξιοσημείωτο χαρακτηριστικό των δεικτών, το οποίο αποκαλύπτεται πλήρως στον αντικειμενοστραφή προγραμματισμό κατά τη δημιουργία κλάσεων.
Η δυναμική εκχώρηση μνήμης γίνεται χρησιμοποιώντας τη νέα λειτουργία. Σύνταξη:

data_type *pointer_name = new data_type;

Για παράδειγμα:

Int *a = new int; // Δήλωση δείκτη τύπου int int *b = new int(5); // Αρχικοποίηση του δείκτη

Η δεξιά πλευρά της έκφρασης λέει ότι το new ζητά ένα μπλοκ μνήμης για την αποθήκευση δεδομένων τύπου int. Εάν βρεθεί η μνήμη, η διεύθυνση επιστρέφεται και εκχωρείται σε μια μεταβλητή δείκτη τύπου int. Τώρα μπορείτε να έχετε πρόσβαση μόνο στη δυναμικά δημιουργημένη μνήμη χρησιμοποιώντας δείκτες! Ένα παράδειγμα εργασίας με δυναμική μνήμη εμφανίζεται στο Πρόγραμμα 3.
Πρόγραμμα 11.6

#συμπεριλαμβάνω χρησιμοποιώντας namespace std? int main() ( int *a = new int(5); int *b = new int(4); int *c = new int; *c = *a + *b; cout<< *c << endl; delete a; delete b; delete c; return 0; }

Μετά την εργασία με την εκχωρημένη μνήμη, πρέπει να ελευθερωθεί (να επιστραφεί, να γίνει διαθέσιμη για άλλα δεδομένα) χρησιμοποιώντας τη λειτουργία διαγραφής. Ο έλεγχος της κατανάλωσης μνήμης είναι μια σημαντική πτυχή της ανάπτυξης εφαρμογών. Τα σφάλματα όπου η μνήμη δεν ελευθερώνεται έχει ως αποτέλεσμα " διαρροές μνήμης", το οποίο με τη σειρά του μπορεί να προκαλέσει διακοπή λειτουργίας του προγράμματος. Η λειτουργία διαγραφής μπορεί να εφαρμοστεί σε έναν μηδενικό δείκτη (nullptr) ή σε έναν που δημιουργήθηκε με νέο (δηλαδή, το νέο και το delete χρησιμοποιούνται σε ζεύγη).

Δυναμικοί πίνακες

Δυναμική συστοιχίαείναι ένας πίνακας του οποίου το μέγεθος καθορίζεται κατά την εκτέλεση του προγράμματος. Αυστηρά μιλώντας, ένας πίνακας C δεν είναι δυναμικός στη C++. Δηλαδή, μπορείτε να προσδιορίσετε μόνο το μέγεθος του πίνακα και η αλλαγή του μεγέθους του πίνακα ενώ το πρόγραμμα εκτελείται είναι ακόμα αδύνατη. Για να αποκτήσετε έναν πίνακα του απαιτούμενου μεγέθους, πρέπει να εκχωρήσετε μνήμη για έναν νέο πίνακα και να αντιγράψετε δεδομένα από τον αρχικό σε αυτόν και, στη συνέχεια, να ελευθερώσετε τη μνήμη που είχε εκχωρηθεί προηγουμένως για τον αρχικό πίνακα. Ο πραγματικός τύπος δυναμικού πίνακα στη C++ είναι ο , τον οποίο θα εξετάσουμε αργότερα. Για την εκχώρηση μνήμης για έναν πίνακα, χρησιμοποιείται η νέα λειτουργία. Η σύνταξη για την εκχώρηση μνήμης για έναν πίνακα είναι:
δείκτης = νέος τύπος[μέγεθος] . Για παράδειγμα:

Int n = 10; int *arr = νέος int[n];

Η μνήμη ελευθερώνεται χρησιμοποιώντας τον τελεστή διαγραφής:

Διαγραφή arr.

Σε αυτήν την περίπτωση, το μέγεθος του πίνακα δεν καθορίζεται.
Παράδειγμα προγράμματος. Συμπληρώστε τον δυναμικό ακέραιο πίνακα arr1 με τυχαίους αριθμούς. Εμφάνιση πίνακα πηγών. Ξαναγράψτε όλα τα στοιχεία με περιττούς αριθμούς ακολουθίας (1, 3, ...) σε έναν νέο δυναμικό πίνακα ακέραιων αριθμών arr2. Εκτυπώστε τα περιεχόμενα του πίνακα arr2.
Πρόγραμμα 11.7

#συμπεριλαμβάνω #συμπεριλαμβάνω #συμπεριλαμβάνω χρησιμοποιώντας namespace std? int main() ( int n; cout<< "n = "; cin >>n; int *arr1 = new int[n];< n; i++) { arr1[i] = d(rnd); cout << arr1[i] << " "; } cout << endl; int *arr2 = new int; for (int i = 0; i < n / 2; i++) { arr2[i] = arr1; cout << arr2[i] << " "; } delete arr1; delete arr2; return 0; } n = 10 73 94 17 52 11 76 22 70 57 68 94 52 76 70 68

default_random_engine rnd(time(0)); uniform_int_distribution d(10, 99);

για (int i = 0; i

Γνωρίζουμε ότι στη C++, ένας δισδιάστατος πίνακας είναι ένας πίνακας πινάκων. Επομένως, για να δημιουργηθεί ένας δισδιάστατος δυναμικός πίνακας, είναι απαραίτητο να εκχωρηθεί μνήμη σε βρόχο για κάθε εισερχόμενο πίνακα, έχοντας προηγουμένως καθορίσει τον αριθμό των πινάκων που θα δημιουργηθούν. Για το σκοπό αυτό χρησιμοποιείται
δείκτη σε δείκτη
, με άλλα λόγια, μια περιγραφή μιας σειράς δεικτών:

#συμπεριλαμβάνω #συμπεριλαμβάνω #συμπεριλαμβάνω #συμπεριλαμβάνω Int **arr = new int *[m]; όπου m είναι ο αριθμός τέτοιων πινάκων (σειρές ενός δισδιάστατου πίνακα).<< "Введите количество строк:" << endl; cout << "m = "; cin >Παράδειγμα εργασίας. Συμπληρώστε με τυχαίους αριθμούς και εξάγετε τα στοιχεία ενός δισδιάστατου δυναμικού πίνακα.<< "введите количество столбцов:" << endl; cout << "n = "; cin >Πρόγραμμα 11.8< m; i++) { arr[i] = new int[n]; for (int j = 0; j < n; j++) { arr[i][j] = d(rnd); } } // вывод массива: for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { cout << arr[i][j] << setw(3); } cout << "\n"; } // освобождение памяти выделенной для каждой // строки: for (int i = 0; i < m; i++) delete arr[i]; // освобождение памяти выделенной под массив: delete arr; return 0; } Введите количество строк: m = 5 введите количество столбцов: n = 10 66 99 17 47 90 70 74 37 97 39 28 67 60 15 76 64 42 65 87 75 17 38 40 81 66 36 15 67 82 48 73 10 47 42 47 90 64 22 79 61 13 98 28 25 13 94 41 98 21 28

χρησιμοποιώντας namespace std? int main() ( int n, m; default_random_engine rnd(time(0)); uniform_int_distribution
  1. d(10, 99);
  2. cout
  3. > m;
  4. cout
  5. >n;
  6. int **arr = new int *[m];
// γεμίζοντας τον πίνακα: για (int i = 0; i
Σχολική εργασία στο σπίτι

Χρησιμοποιώντας δυναμικούς πίνακες, λύστε το ακόλουθο πρόβλημα: Δίνεται ένας ακέραιος πίνακας Α μεγέθους N. Ξαναγράψτε όλους τους ζυγούς αριθμούς από τον αρχικό πίνακα (με την ίδια σειρά) σε έναν νέο ακέραιο πίνακα Β και εκτυπώστε το μέγεθος του προκύπτοντος πίνακα Β και τα περιεχόμενά του.

Σχολικό βιβλίο

§62 (10) §40 (11)

Λογοτεχνία
  1. Lafore R. Αντικειμενοστραφής προγραμματισμός στη C++ (4η έκδοση). Πέτρος: 2004
  2. Πράτα, Στέφανος. Γλώσσα προγραμματισμού C++. Διαλέξεις και ασκήσεις, 6η έκδ.: Μετάφρ. από τα αγγλικά - M.: LLC «I.D. William», 2012
  3. Lippman B. Stanley, Josie Lajoie, Barbara E. Mu. Γλώσσα προγραμματισμού C++. Βασικό μάθημα. Εκδ. 5η. M: LLC "I.D. Williams", 2014
  4. Elline A. C++. Από lamer σε προγραμματιστή. Αγία Πετρούπολη: Πέτρος, 2015
  5. Shildt G. C++: Basic course, 3rd ed. Μ.: Williams, 2010

Η C++ υποστηρίζει τρεις κύριους τύπους εκπλήρωση (διανομή) μνήμη, δύο από τα οποία είμαστε ήδη εξοικειωμένοι:

Εκχώρηση στατικής μνήμηςισχύει για και μεταβλητές. Η μνήμη εκχωρείται μία φορά, όταν ξεκινά το πρόγραμμα, και διατηρείται σε όλο το πρόγραμμα.

Αυτόματη κατανομή μνήμηςκρατά για και . Η μνήμη εκχωρείται κατά την είσοδο στο μπλοκ στο οποίο βρίσκονται αυτές οι μεταβλητές και ελευθερώνεται κατά την έξοδο από αυτό.

είναι το θέμα αυτού του άρθρου.

Τόσο η στατική όσο και η αυτόματη εκχώρηση μνήμης έχουν δύο κοινά πράγματα:

Το μέγεθος της μεταβλητής/πίνακα πρέπει να είναι γνωστό κατά το χρόνο μεταγλώττισης.

Η μνήμη εκχωρείται και εκχωρείται αυτόματα (όταν δημιουργείται ή καταστρέφεται μια μεταβλητή).

Στις περισσότερες περιπτώσεις αυτό είναι εντάξει. Ωστόσο, όταν πρόκειται για εργασία με εξωτερική είσοδο, αυτοί οι περιορισμοί μπορεί να οδηγήσουν σε προβλήματα.

Για παράδειγμα, όταν χρησιμοποιείται για την αποθήκευση ενός ονόματος, δεν γνωρίζουμε εκ των προτέρων πόσος χρόνος θα χρειαστεί μέχρι να το εισαγάγει ο χρήστης. Ή όταν πρέπει να γράψουμε τον αριθμό των εγγραφών από το δίσκο σε μια μεταβλητή, αλλά δεν γνωρίζουμε εκ των προτέρων πόσες από αυτές τις εγγραφές υπάρχουν. Ή μπορούμε να δημιουργήσουμε ένα παιχνίδι με ασυνεπή αριθμό τεράτων (κατά τη διάρκεια του παιχνιδιού, μερικά τέρατα πεθαίνουν, άλλα γεννιούνται), προσπαθώντας έτσι να σκοτώσει τον παίκτη.

Εάν πρέπει να δηλώσουμε το μέγεθος όλων των μεταβλητών κατά τη στιγμή της μεταγλώττισης, το καλύτερο που μπορούμε να κάνουμε είναι να προσπαθήσουμε να μαντέψουμε το μέγιστο μέγεθός τους, ελπίζοντας ότι αυτό θα είναι αρκετό:

όνομα char; // ας ελπίσουμε ότι ο χρήστης θα εισαγάγει ένα όνομα με λιγότερους από 30 χαρακτήρες! Ρεκόρ ρεκόρ? // ας ελπίσουμε ότι ο αριθμός των εγγραφών δεν θα είναι μεγαλύτερος από 400! Τέρας τέρας? // Μέγιστη απόδοση πολυγώνου 30 τέρατα. // αυτή η τρισδιάστατη απόδοση καλύτερα να είναι λιγότερα από 40.000 πολύγωνα!

Αυτή είναι μια κακή λύση για τουλάχιστον τρεις λόγους:

Πρώτον, η μνήμη σπαταλιέται εάν οι μεταβλητές δεν χρησιμοποιούνται πραγματικά ή χρησιμοποιούνται αλλά όχι πλήρως. Για παράδειγμα, εάν διαθέσουμε 30 χαρακτήρες για κάθε όνομα, αλλά τα ονόματα καταλαμβάνουν 15 χαρακτήρες κατά μέσο όρο, τότε η κατανάλωση μνήμης θα είναι διπλάσια από αυτή που πραγματικά χρειάζεται. Ή σκεφτείτε έναν πίνακα απόδοσης: εάν χρησιμοποιεί μόνο 20.000 πολύγωνα, τότε η μνήμη με 20.000 πολύγωνα καταναλώνεται ουσιαστικά (δηλαδή δεν χρησιμοποιείται)!

Δεύτερον, η μνήμη για τις περισσότερες συνηθισμένες μεταβλητές (συμπεριλαμβανομένων των σταθερών συστοιχιών) εκχωρείται από μια ειδική δεξαμενή μνήμης - σωρός. Η ποσότητα της μνήμης στοίβας σε ένα πρόγραμμα είναι συνήθως μικρή - στο Visual Studio είναι 1 MB από προεπιλογή. Εάν υπερβείτε αυτόν τον αριθμό, τότε υπερχείλιση στοίβας, και το λειτουργικό σύστημα θα τερματίσει αυτόματα το πρόγραμμά σας.

Στο Visual Studio μπορείτε να το ελέγξετε εκτελώντας το ακόλουθο πρόγραμμα:

int main() (πίνακας int; // εκχώρηση 1 εκατομμυρίου ακέραιων τιμών)

Το όριο μνήμης 1 MB μπορεί να είναι προβληματικό για πολλά προγράμματα, ειδικά εκείνα που χρησιμοποιούν γραφικά.

Τρίτον, και το πιο σημαντικό, αυτό μπορεί να οδηγήσει σε τεχνητά όρια ή/και υπερχείλιση πίνακα. Τι συμβαίνει εάν ο χρήστης προσπαθήσει να διαβάσει 500 εγγραφές από το δίσκο, αλλά έχουμε εκχωρήσει μνήμη για 400 το πολύ; Είτε θα εμφανίσουμε ένα σφάλμα στον χρήστη ότι ο μέγιστος αριθμός εγγραφών είναι 400, είτε (στη χειρότερη περίπτωση) θα προκύψει υπερχείλιση πίνακα και μετά θα συμβεί κάτι πολύ κακό.

Ευτυχώς, αυτά τα προβλήματα επιλύονται εύκολα χρησιμοποιώντας δυναμική εκχώρηση μνήμης. Δυναμική κατανομή μνήμηςείναι ένας τρόπος εκτέλεσης προγραμμάτων να ζητούν μνήμη από το λειτουργικό σύστημα όταν χρειάζεται. Αυτή η μνήμη δεν εκχωρείται από την περιορισμένη μνήμη στοίβας του προγράμματος, αλλά από έναν πολύ μεγαλύτερο χώρο αποθήκευσης που διαχειρίζεται το λειτουργικό σύστημα - σωρός (πλήθος) . Στους σύγχρονους υπολογιστές, το μέγεθος του σωρού μπορεί να είναι gigabyte μνήμης.

Δυναμική κατανομή μεταβλητών

Για να εκχωρήσετε δυναμικά μνήμη για μία μεταβλητή, χρησιμοποιήστε τον τελεστή νέος:

νέα int? // εκχωρήστε δυναμικά μια ακέραια μεταβλητή και απορρίψτε αμέσως το αποτέλεσμα (καθώς δεν το αποθηκεύουμε πουθενά)

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

Δημιουργείται ένας δείκτης για πρόσβαση στην εκχωρημένη μνήμη:

int *ptr = new int; // εκχωρεί δυναμικά μια ακέραια μεταβλητή και εκχωρεί τη διεύθυνσή της στο ptr ώστε να έχουμε πρόσβαση αργότερα

Στη συνέχεια, μπορούμε να αποαναφέρουμε τον δείκτη για να λάβουμε την τιμή:

*ptr = 8; // εκχωρήστε την τιμή 8 στη μνήμη που εκχωρήθηκε πρόσφατα

Αυτή είναι μια περίπτωση όπου οι δείκτες είναι χρήσιμοι. Χωρίς έναν δείκτη στη διεύθυνση της νέας εκχωρημένης μνήμης, δεν θα είχαμε τρόπο πρόσβασης σε αυτήν.

Πώς λειτουργεί η δυναμική εκχώρηση μνήμης;

Ο υπολογιστής σας διαθέτει μνήμη (ίσως το μεγαλύτερο μέρος της) που είναι διαθέσιμη για χρήση από εφαρμογές. Όταν εκτελείτε μια εφαρμογή, το λειτουργικό σας σύστημα φορτώνει αυτήν την εφαρμογή σε κάποιο τμήμα αυτής της μνήμης. Και αυτή η μνήμη που χρησιμοποιείται από την εφαρμογή σας χωρίζεται σε διάφορα μέρη, καθένα από τα οποία εκτελεί μια συγκεκριμένη εργασία. Το ένα μέρος περιέχει τον κώδικά σας, το άλλο χρησιμοποιείται για την εκτέλεση κανονικών λειτουργιών (παρακολούθηση των συναρτήσεων που καλούνται, δημιουργία και καταστροφή καθολικών και τοπικών μεταβλητών κ.λπ.). Θα μιλήσουμε για αυτό αργότερα. Ωστόσο, το μεγαλύτερο μέρος της διαθέσιμης μνήμης βρίσκεται απλώς εκεί, περιμένοντας αιτήματα κατανομής από προγράμματα.

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

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

Εκκίνηση δυναμικά κατανεμημένων μεταβλητών

Όταν εκχωρείτε δυναμικά μια μεταβλητή, μπορείτε επίσης να την αρχικοποιήσετε μέσω ή ομοιόμορφης αρχικοποίησης (στην C++11):

int *ptr1 = new int (7); // χρήση άμεσης προετοιμασίας int *ptr2 = new int ( 8 ); // χρήση ομοιόμορφης αρχικοποίησης

Αφαίρεση μεταβλητών

Όταν όλα όσα χρειάζονταν έχουν ήδη γίνει με μια δυναμικά εκχωρημένη μεταβλητή, πρέπει να πείτε ρητά στη C++ να ελευθερώσει αυτή τη μνήμη. Για μεμονωμένες μεταβλητές αυτό γίνεται χρησιμοποιώντας τον τελεστή διαγράφω:

// υποθέστε ότι το ptr είχε εκχωρηθεί προηγουμένως χρησιμοποιώντας τον τελεστή new delete ptr; // επιστρέψτε τη μνήμη που δείχνει το ptr πίσω στο λειτουργικό σύστημα ptr = 0; // κάνει το ptr μηδενικό δείκτη (χρησιμοποιήστε nullptr αντί για 0 ​​στη C++11)

Τι σημαίνει "διαγραφή μνήμης";

Ο τελεστής διαγραφής στην πραγματικότητα δεν διαγράφει τίποτα. Απλώς επιστρέφει τη μνήμη που είχε εκχωρηθεί προηγουμένως πίσω στο λειτουργικό σύστημα. Το λειτουργικό σύστημα μπορεί στη συνέχεια να εκχωρήσει εκ νέου αυτήν τη μνήμη σε άλλη εφαρμογή (ή ξανά στην ίδια εφαρμογή).

Αν και μπορεί να φαίνεται ότι διαγράφουμε μεταβλητός, αλλά αυτό δεν είναι αλήθεια! Μια μεταβλητή δείκτη εξακολουθεί να έχει το ίδιο εύρος με πριν και μπορεί να της εκχωρηθεί μια νέα τιμή όπως κάθε άλλη μεταβλητή.

Σημειώστε ότι η διαγραφή ενός δείκτη που δεν οδηγεί σε δυναμικά εκχωρημένη μνήμη μπορεί να προκαλέσει προβλήματα.

Κρεμαστά σημάδια

Η C++ δεν παρέχει καμία εγγύηση για το τι θα συμβεί με τα περιεχόμενα της ελευθερωμένης μνήμης ή με την τιμή του δείκτη που διαγράφεται. Στις περισσότερες περιπτώσεις, η μνήμη που επιστρέφεται στο λειτουργικό σύστημα θα περιέχει τις ίδιες τιμές που είχε πριν απελευθέρωση, και ο δείκτης θα εξακολουθεί να δείχνει στη μνήμη, μόνο που έχει ήδη ελευθερωθεί (διαγραφεί).

Καλείται ένας δείκτης που δείχνει την ελευθερωμένη μνήμη κρεμαστό σημάδι. Η απόκλιση αναφοράς ή η αφαίρεση ενός κρεμασμένου δείκτη θα παράγει απροσδόκητα αποτελέσματα. Σκεφτείτε το ακόλουθο πρόγραμμα:

#συμπεριλαμβάνω int main() ( int *ptr = new int; *ptr = 8; // τοποθετήστε την τιμή στην εκχωρημένη θέση μνήμης διαγράψτε ptr; // επιστρέψτε τη μνήμη πίσω στο λειτουργικό σύστημα. Το ptr είναι τώρα ένας κρεμασμένος δείκτης std:: cout<< *ptr; // разыменование висячого указателя приведет к неожиданным результатам delete ptr; // попытка освободить память снова приведет к неожиданным результатам также return 0; }

#συμπεριλαμβάνω

int main()

int * ptr = new int ; // εκχωρεί δυναμικά μια ακέραια μεταβλητή

* ptr = 8 ; // τοποθετήστε την τιμή στο εκχωρημένο κελί μνήμης

διαγραφή ptr ; // επιστροφή της μνήμης στο λειτουργικό σύστημα. Το ptr είναι πλέον ένας κρεμασμένος δείκτης

std::cout<< * ptr ; // Η αποαναφορά ενός κρεμασμένου δείκτη θα παράγει απροσδόκητα αποτελέσματα

διαγραφή ptr ; //προσπαθώντας να ελευθερώσετε ξανά τη μνήμη θα παράγει επίσης απροσδόκητα αποτελέσματα

επιστροφή 0 ;

Στο παραπάνω πρόγραμμα, η τιμή 8, η οποία είχε εκχωρηθεί προηγουμένως στην εκχωρημένη μνήμη, μπορεί ή δεν μπορεί να συνεχίσει να υπάρχει μετά την απελευθέρωσή της. Είναι επίσης πιθανό η ελευθερωμένη μνήμη να έχει ήδη εκχωρηθεί σε άλλη εφαρμογή (ή για δική του χρήση του λειτουργικού συστήματος) και η προσπάθεια πρόσβασης σε αυτήν θα έχει ως αποτέλεσμα το λειτουργικό σύστημα να τερματίσει αυτόματα το πρόγραμμά σας.

Η διαδικασία απελευθέρωσης μνήμης μπορεί επίσης να οδηγήσει στη δημιουργία διάφοροικρεμαστά σημάδια. Εξετάστε το ακόλουθο παράδειγμα:

#συμπεριλαμβάνω int main() ( int *ptr = νέο int; // εκχωρεί δυναμικά μια ακέραια μεταβλητή int *otherPtr = ptr; // otherPtr δείχνει τώρα την ίδια εκχωρημένη μνήμη με το ptr delete ptr; // επιστροφή μνήμης πίσω στο λειτουργικό σύστημα . Το ptr και το otherPtr είναι πλέον κρεμασμένοι δείκτες.

#συμπεριλαμβάνω

int main()

int * ptr = new int ; // εκχωρεί δυναμικά μια ακέραια μεταβλητή

int * otherPtr = ptr ; // otherPtr δείχνει τώρα την ίδια εκχωρημένη μνήμη με το ptr

διαγραφή ptr ; // επιστροφή της μνήμης στο λειτουργικό σύστημα. Το ptr και το otherPtr είναι πλέον κρέμονται δείκτες

ptr = 0 ; Το // ptr είναι τώρα nullptr

// ωστόσο το otherPtr εξακολουθεί να είναι ένας κρεμασμένος δείκτης!

επιστροφή 0 ;

Αρχικά, προσπαθήστε να αποφύγετε καταστάσεις όπου πολλοί δείκτες δείχνουν προς το ίδιο τμήμα της εκχωρημένης μνήμης. Εάν αυτό δεν είναι δυνατό, τότε διευκρινίστε ποιος δείκτης από όλους "κατέχει" τη μνήμη (και είναι υπεύθυνος για τη διαγραφή της) και ποιοι δείκτες απλώς έχουν πρόσβαση σε αυτήν.

Δεύτερον, όταν διαγράφετε έναν δείκτη και αν δεν βγαίνει αμέσως μετά τη διαγραφή, τότε πρέπει να γίνει μηδενικός, π.χ. ορίστε την τιμή σε 0 (ή σε C++11). Με τον όρο "εξόδου από το πεδίο εφαρμογής αμέσως μετά τη διαγραφή" εννοούμε ότι διαγράφετε τον δείκτη στο τέλος του μπλοκ στο οποίο δηλώνεται.

Κανόνας: Ορίστε τους διαγραμμένους δείκτες σε 0 (ή nullptr στη C++11), εκτός εάν βγουν εκτός πεδίου εφαρμογής αμέσως μετά τη διαγραφή τους.

Χειριστής νέος

Όταν ζητάτε μνήμη από το λειτουργικό σύστημα, σε σπάνιες περιπτώσεις μπορεί να μην είναι διαθέσιμη (δηλαδή να μην είναι διαθέσιμη).

Από προεπιλογή, εάν το νέο δεν λειτουργεί, η μνήμη δεν εκχωρείται, τότε δημιουργείται μια εξαίρεση bad_alloc. Εάν αυτή η εξαίρεση δεν αντιμετωπιστεί σωστά (κάτι που θα γίνει, καθώς δεν έχουμε καλύψει ακόμα τις εξαιρέσεις και τον χειρισμό τους), τότε το πρόγραμμα απλώς θα τερματιστεί (crash) με ένα μη χειριζόμενο σφάλμα εξαίρεσης.

Σε πολλές περιπτώσεις, η διαδικασία δημιουργίας εξαίρεσης με τον νέο τελεστή (καθώς και η κατάρρευση του προγράμματος) είναι ανεπιθύμητη, επομένως υπάρχει μια εναλλακτική μορφή νέου που επιστρέφει έναν μηδενικό δείκτη εάν δεν μπορεί να εκχωρηθεί μνήμη. Απλώς πρέπει να προσθέσετε τη σταθερά std::nothrow μεταξύ της νέας λέξης-κλειδιού και του τύπου κατανομής δεδομένων:

int *value = new (std::nothrow) int; // ο δείκτης τιμής θα γίνει μηδενικός εάν η δυναμική κατανομή μιας ακέραιας μεταβλητής αποτύχει

Στο παραπάνω παράδειγμα, εάν το new δεν επιστρέψει δείκτη με δυναμικά εκχωρημένη μνήμη, τότε θα επιστραφεί ένας μηδενικός δείκτης.

Επίσης, δεν συνιστάται η απόκλιση αναφοράς, καθώς αυτό θα οδηγήσει σε απροσδόκητα αποτελέσματα (πιθανότατα σφάλμα προγράμματος). Επομένως, η καλύτερη πρακτική είναι να ελέγχετε όλα τα αιτήματα εκχώρησης μνήμης για να βεβαιωθείτε ότι τα αιτήματα είναι επιτυχημένα και ότι η μνήμη έχει εκχωρηθεί.

int *value = new (std::nothrow) int; // αίτημα για εκχώρηση δυναμικής μνήμης για μια ακέραια τιμή εάν η (!value) // χειριστεί την περίπτωση όταν η νέα επιστρέφει null (δηλαδή η μνήμη δεν έχει εκχωρηθεί) ( // handle this case std::cout<< "Could not allocate memory"; }

Δεδομένου ότι η μη εκχώρηση μνήμης από τον νέο χειριστή είναι εξαιρετικά σπάνια, οι προγραμματιστές συνήθως ξεχνούν να εκτελέσουν αυτόν τον έλεγχο!

Μηδενικοί δείκτες και δυναμική εκχώρηση μνήμης

Οι μηδενικοί δείκτες (δείκτες με την τιμή 0 ή nullptr) είναι ιδιαίτερα χρήσιμοι κατά τη διαδικασία δυναμικής εκχώρησης μνήμης. Η παρουσία τους φαίνεται να λέει: «Δεν έχει εκχωρηθεί μνήμη σε αυτόν τον δείκτη». Και αυτό με τη σειρά του μπορεί να χρησιμοποιηθεί για την εκτέλεση εκχώρησης μνήμης υπό όρους:

// εάν στο ptr δεν έχει εκχωρηθεί ακόμη μνήμη, εκχωρήστε την εάν (!ptr) ptr = new int;

Η αφαίρεση του μηδενικού δείκτη δεν έχει κανένα αποτέλεσμα. Άρα δεν είναι απαραίτητα τα εξής:

if (ptr) delete ptr;

if(ptr)

διαγραφή ptr ;

Αντίθετα, μπορείτε απλά να γράψετε:

διαγραφή ptr ;

Εάν το ptr δεν είναι null, τότε η δυναμικά εκχωρημένη μεταβλητή θα διαγραφεί. Εάν η τιμή του δείκτη είναι μηδενική, τότε δεν θα συμβεί τίποτα.

Διαρροή μνήμης

Η δυναμικά εκχωρημένη μνήμη δεν έχει εμβέλεια. Δηλαδή, παραμένει εκχωρημένο έως ότου ελευθερωθεί ρητά ή μέχρι να βγει το πρόγραμμά σας (και το λειτουργικό σύστημα να διαγράψει όλα τα buffer μνήμης από μόνο του). Ωστόσο, οι δείκτες που χρησιμοποιούνται για την αποθήκευση διευθύνσεων μνήμης που έχουν εκχωρηθεί δυναμικά ακολουθούν τους κανόνες εύρους των κανονικών μεταβλητών. Αυτή η ασυμφωνία μπορεί να προκαλέσει ενδιαφέρουσα συμπεριφορά.

Εξετάστε την ακόλουθη συνάρτηση:

void doSomething() ( int *ptr = new int; )

  • Φροντιστήριο

Γειά σου! Παρακάτω θα μιλήσουμε για γνωστούς χειριστές νέοςΚαι διαγράφω, ή μάλλον για όσα δεν γράφονται στα βιβλία (τουλάχιστον σε βιβλία για αρχάριους).
Μου ώθησε να γράψω αυτό το άρθρο από μια κοινή παρανόηση σχετικά με νέοςΚαι διαγράφω, που βλέπω συνέχεια στα φόρουμ και μάλιστα (!!!) σε κάποια βιβλία.
Ξέρουμε όλοι τι είναι πραγματικά; νέοςΚαι διαγράφω? Ή απλώς νομίζουμε ότι ξέρουμε;
Αυτό το άρθρο θα σας βοηθήσει να το καταλάβετε (καλά, όσοι γνωρίζουν μπορούν να κάνουν κριτική :))

Σημείωμα: παρακάτω θα μιλήσουμε αποκλειστικά για τον νέο τελεστή, για άλλες μορφές του νέου τελεστή και για όλες τις μορφές του τελεστή διαγραφής, όλα όσα γράφονται παρακάτω ισχύουν επίσης και ισχύουν κατ' αναλογία.

Λοιπόν, ας ξεκινήσουμε με αυτό που συνήθως γράφουν σε βιβλία για αρχάριους όταν περιγράφουν νέος(το κείμενο βγήκε από τον αέρα, αλλά είναι πέρα ​​για πέρα ​​αληθινό):

Χειριστής νέοςεκχωρεί μνήμη μεγαλύτερη ή ίση με το απαιτούμενο μέγεθος και, σε αντίθεση με τις συναρτήσεις της γλώσσας C, καλεί τον κατασκευαστή για τα αντικείμενα για τα οποία έχει εκχωρηθεί η μνήμη... μπορείτε να υπερφορτώσετε (κάπου γράφουν για υλοποίηση) τον τελεστή νέοςγια να ταιριάζει στις ανάγκες σας.

Και για παράδειγμα, δείχνουν μια πρωτόγονη υπερφόρτωση (υλοποίηση) του νέου χειριστή, το πρωτότυπο του οποίου μοιάζει με αυτό
void* operator new (std::size_t size) throw (std::bad_alloc);

Τι θέλετε να προσέξετε:
1. Δεν μοιράζονται πουθενά νέος λέξη-κλειδίΓλώσσα και τελεστής C++ νέος, παντού γίνεται λόγος για μια οντότητα.
2. Παντού το γράφουν αυτό νέοςκαλεί τον(ους) κατασκευαστή(ες) στο(α) αντικείμενο(α).
Τόσο η πρώτη όσο και η δεύτερη είναι κοινές παρανοήσεις.

Αλλά ας μην βασιζόμαστε σε βιβλία για αρχάριους, ας στραφούμε στο Πρότυπο, δηλαδή στην ενότητα 5.3.4 και 18.6.1, όπου το θέμα αυτού του άρθρου αποκαλύπτεται πραγματικά (ή μάλλον, αποκαλύπτεται ελαφρώς).

5.3.4
Η new-expression επιχειρεί να δημιουργήσει ένα αντικείμενο του type-id (8.1) ή new-type-id στο οποίο εφαρμόζεται. /*δεν μας ενδιαφέρει περαιτέρω*/
18.6.1
void* operator new(std::size_t size) throw(std::bad_alloc);
Εφέ: Η συνάρτηση εκχώρησης που καλείται από μια νέα έκφραση (5.3.4) για να εκχωρήσει byte μεγέθους
αποθήκευση κατάλληλα ευθυγραμμισμένη για να αντιπροσωπεύει οποιοδήποτε αντικείμενο αυτού του μεγέθους /*δεν μας ενδιαφέρει περαιτέρω*/

Εδώ το βλέπουμε ήδη στην πρώτη περίπτωση νέοςαναφέρεται ως έκφραση, και στο δεύτερο δηλώνεται ως χειριστής.Και αυτά είναι πραγματικά 2 διαφορετικές οντότητες!
Ας προσπαθήσουμε να καταλάβουμε γιατί συμβαίνει αυτό, για αυτό θα χρειαστούμε λίστες συναρμολόγησης που λαμβάνονται μετά τη μεταγλώττιση του κώδικα χρησιμοποιώντας νέος.Λοιπόν, τώρα ας μιλήσουμε για όλα με τη σειρά.

νέα-έκφρασηείναι ένας χειριστής γλώσσας, όπως αν, ενώκαι τα λοιπά. (Αν και αν, ενώκαι τα λοιπά. εξακολουθούν να αναφέρονται ως δήλωση, αλλά ας απορρίψουμε τους στίχους) Δηλαδή. συναντώντας το στη λίστα, ο μεταγλωττιστής δημιουργεί συγκεκριμένο κώδικα που αντιστοιχεί σε αυτόν τον τελεστή. Επίσης νέος- αυτό είναι ένα από λέξεις-κλειδιάγλώσσα C++, η οποία επιβεβαιώνει για άλλη μια φορά την κοινότητά της με αν"αμί, για" ami, κλπ. ΕΝΑ χειριστής new()με τη σειρά του, είναι απλώς μια συνάρτηση γλώσσας C++ με το ίδιο όνομα, η συμπεριφορά της οποίας μπορεί να παρακαμφθεί. ΣΠΟΥΔΑΙΟΣ - χειριστής new() ΔΕΝκαλεί τον(ους) κατασκευαστή(ες) για το(τα) αντικείμενο(α) για τα οποία έχει εκχωρηθεί η μνήμη. Απλώς εκχωρεί μνήμη του απαιτούμενου μεγέθους και τέλος. Η διαφορά του από τις συναρτήσεις C είναι ότι μπορεί να δημιουργήσει μια εξαίρεση και να επαναπροσδιοριστεί, καθώς και να δημιουργήσει έναν τελεστή για μια ξεχωριστή κλάση, επαναπροσδιορίζοντάς τον έτσι μόνο για αυτήν την κλάση (θυμηθείτε τα υπόλοιπα μόνοι σας :)).
Αλλά νέα-έκφρασηΑπλώς καλεί τους κατασκευαστές του αντικειμένου. Αν και θα ήταν πιο σωστό να πούμε ότι επίσης δεν καλεί τίποτα απλά, όταν το συναντά, ο μεταγλωττιστής δημιουργεί κώδικα για την κλήση του κατασκευαστή.

Για να συμπληρώσετε την εικόνα, λάβετε υπόψη το ακόλουθο παράδειγμα:

#συμπεριλαμβάνω class Foo ( public: Foo() ( std::cout<< "Foo()" << std::endl; } }; int main () { Foo *bar = new Foo; }

Μετά την εκτέλεση αυτού του κώδικα, το "Foo()" θα εκτυπωθεί, όπως αναμένεται. Ας καταλάβουμε γιατί, για αυτό πρέπει να κοιτάξετε τον assembler, τον οποίο σχολίασα λίγο για ευκολία.
(Ο κώδικας προέρχεται από τον μεταγλωττιστή cl που χρησιμοποιείται στο MSVS 2012, αν και χρησιμοποιώ κυρίως gcc, αλλά αυτό είναι εκτός θέματος)
/Foo *bar = νέο Foo; ώθηση 1 ; μέγεθος σε byte για τον τελεστή κλήσης αντικειμένου Foo new (02013D4h) ; κλήση χειριστή new pop ecx mov dword ptr ,eax ; γράψτε τον δείκτη που επέστρεψε από νέο στη γραμμή και dword ptr ,0 cmp dword ptr ,0 ; ελέγχουμε αν το 0 έχει εγγραφεί στο bar je main+69h (0204990h); αν είναι 0, τότε φεύγουμε από εδώ (ίσως ακόμη και από κύριο ή σε κάποιο είδος χειριστή, σε αυτήν την περίπτωση δεν έχει σημασία) mov ecx,dword ptr ; βάλτε έναν δείκτη στην εκχωρημένη μνήμη στο ecx (το MSVS το περνάει πάντα στο ecx(rcx)) καλέστε Foo::Foo (02011DBh) ; και καλέστε τον κατασκευαστή. κανένα άλλο ενδιαφέρον
Για όσους δεν κατάλαβαν τίποτα, εδώ είναι ένα (σχεδόν) ανάλογο αυτού που συνέβη σε ψευδοκώδικα τύπου C (δηλαδή, δεν χρειάζεται να προσπαθήσετε να το μεταγλωττίσετε :))
Foo *bar = νέος χειριστής (1); // όπου 1 είναι η απαιτούμενη γραμμή μεγέθους->Foo(); // καλέστε τον κατασκευαστή

Ο παραπάνω κωδικός επιβεβαιώνει όλα όσα γράφτηκαν παραπάνω, δηλαδή:
1. χειριστής (γλώσσα) νέοςΚαι χειριστής new()- ΔΕΝ είναι το ίδιο πράγμα.
2. χειριστής new()ΔΕΝ καλεί κατασκευαστή(ες)
3. η κλήση προς τον(τους) κατασκευαστή(ες) δημιουργείται από τον μεταγλωττιστή όταν συναντάται στον κώδικα λέξη κλειδί "νέο"

Κατώτατη γραμμή: Ελπίζω ότι αυτό το άρθρο σας βοήθησε να κατανοήσετε τη διαφορά μεταξύ νέα-έκφρασηΚαι χειριστής new()ή ακόμα και να ανακαλύψετε ότι (αυτή η διαφορά) υπάρχει καθόλου, αν κάποιος δεν το γνώριζε.

P.S. χειριστής διαγράφωΚαι χειριστής delete()έχουν παρόμοια διαφορά, οπότε στην αρχή του άρθρου είπα ότι δεν θα το περιγράψω. Νομίζω ότι τώρα καταλαβαίνετε γιατί η περιγραφή του δεν έχει νόημα και μπορείτε να ελέγξετε ανεξάρτητα την εγκυρότητα όσων γράφτηκαν παραπάνω διαγράφω.

Εκσυγχρονίζω:
Habrazhitel με παρατσούκλι χιμΣε προσωπική αλληλογραφία πρότεινε τον ακόλουθο κώδικα, ο οποίος καταδεικνύει ξεκάθαρα την ουσία των όσων γράφτηκαν παραπάνω.
#συμπεριλαμβάνω class Test ( public: Test() ( std::cout<< "Test::Test()" << std::endl; } void* operator new (std::size_t size) throw (std::bad_alloc) { std::cout << "Test::operator new(" << size << ")" << std::endl; return::operator new(size); } }; int main() { Test *t = new Test(); void *p = Test::operator new(100); // 100 для различия в выводе }
Αυτός ο κώδικας θα δώσει τα ακόλουθα
Test::operator new(1) Test::Test() Test::operator new(100)
που είναι αναμενόμενο.

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

Αλλά για τα δυναμικά αντικείμενα δεν υπάρχουν τέτοιοι κανόνες. Πρέπει πάντα να διαγράφονται ρητά (η ρητή διαγραφή μπορεί να κρύβεται στα βάθη των χρηστικών τάξεων και λειτουργιών). Ακολουθεί μια μικρή απεικόνιση για καλύτερη κατανόηση:
struct A ( std::string str; // Αυτόματο αντικείμενο, διαγράφεται σιωπηρά στον καταστροφέα του A (που δημιουργείται // αυτόματα). Το ίδιο το buffer συμβολοσειράς είναι ένα δυναμικό αντικείμενο (*), θα διαγραφεί ρητά // ο καταστροφέας του std::string, ο οποίος θα καλείται σιωπηρά στον καταστροφέα του A // (*) Εκτός εάν η συμβολοσειρά είναι πολύ μικρή, τότε το Small String Optimization θα λειτουργήσει και το δυναμικό // buffer δεν θα εκχωρηθεί καθόλου. void foo() ( std::vector v; // Αυτόματο αντικείμενο, σιωπηρά διαγράφεται κατά την έξοδο από τη συνάρτηση. v.push_back(10); // Τα περιεχόμενα του διανύσματος - ένα δυναμικό αντικείμενο (πίνακας) - θα διαγραφούν ρητά στον καταστροφέα // του διανύσματος, ο οποίος θα κληθεί σιωπηρά κατά την έξοδο από τη συνάρτηση.
Α α; // Αυτόματο αντικείμενο της κλάσης Α, που διαγράφεται σιωπηρά κατά την έξοδο από τη συνάρτηση.

Η ιδιότητα δυναμική/αυτόματη ανήκει συγκεκριμένα στο αντικείμενο, και όχι στον τύπο, γιατί αντικείμενα του ίδιου τύπου μπορεί να είναι είτε δυναμικά είτε αυτόματα). Στο παραπάνω παράδειγμα, τα αντικείμενα a και *pa είναι και τα δύο τύπου Α, αλλά το πρώτο είναι αυτόματο και το δεύτερο είναι δυναμικό.

Τα δυναμικά αντικείμενα στη C++ δημιουργούνται χρησιμοποιώντας νέα και διαγράφονται με τη χρήση του delete . Από εδώ προέρχονται όλα τα προβλήματα: κανείς δεν είπε ότι αυτές οι κατασκευές πρέπει να χρησιμοποιηθούν απευθείας! Αυτές είναι κλήσεις χαμηλού επιπέδου, είναι κάπως κάτω από την κουκούλα. Και δεν χρειάζεται να σέρνεστε κάτω από την κουκούλα, εκτός εάν είναι απαραίτητο.

Θα μιλήσουμε λίγο αργότερα για το γιατί μπορεί να χρειαστούν καθόλου δυναμικά αντικείμενα.

* Υπάρχουν τεχνικές για τον περιορισμό της δυναμικής/αυτόματης ιδιότητας σε επίπεδο τύπου. Για παράδειγμα, ιδιώτες κατασκευαστές.

Ποιο είναι το πρόβλημα με τη νέα και τη διαγραφή;

Από την εφεύρεσή τους, οι χειριστές new και delete έχουν χρησιμοποιηθεί υπερβολικά. Τα μεγαλύτερα προβλήματα σχετίζονται με τον τελεστή διαγραφής:
  • Μπορείτε να ξεχάσετε εντελώς την κλήση διαγραφής (διαρροή μνήμης).
  • Μπορείτε να ξεχάσετε να καλέσετε τη διαγραφή σε περίπτωση εξαίρεσης ή πρόωρης επιστροφής από μια συνάρτηση (επίσης διαρροή μνήμης).
  • Μπορείτε να καλέσετε τη διαγραφή δύο φορές (διπλή διαγραφή, διπλή διαγραφή).
  • Είναι δυνατό να καλέσετε τη λανθασμένη μορφή του χειριστή: διαγραφή αντί για διαγραφή ή αντίστροφα (απροσδιόριστη συμπεριφορά).
  • Μπορείτε να χρησιμοποιήσετε το αντικείμενο μετά την κλήση διαγραφής (κρεμασμένος δείκτης).
Όλες αυτές οι καταστάσεις οδηγούν, στην καλύτερη περίπτωση, σε σφάλματα προγράμματος, και στη χειρότερη, σε διαρροές μνήμης και ρινικούς δαίμονες.

Επομένως, οι άνθρωποι έχουν καταλάβει εδώ και καιρό να κρύβουν τον τελεστή διαγραφής στα βάθη των κοντέινερ και των έξυπνων δεικτών, αφαιρώντας τον έτσι από τον κωδικό πελάτη. Ωστόσο, υπάρχουν επίσης προβλήματα που σχετίζονται με τον νέο χειριστή, αλλά οι λύσεις για αυτούς δεν εμφανίστηκαν αμέσως και, στην πραγματικότητα, πολλοί προγραμματιστές εξακολουθούν να ντρέπονται να χρησιμοποιήσουν αυτές τις λύσεις. Θα μιλήσουμε περισσότερο για αυτό όταν φτάσουμε να φτιάξουμε λειτουργίες.

Τώρα ας περάσουμε στις περιπτώσεις χρήσης για νέα και διαγραφή . Να σας υπενθυμίσω ότι θα εξετάσουμε διάφορα σενάρια και θα δείξουμε συστηματικά ότι στα περισσότερα από αυτά ο κώδικας θα είναι καλύτερος εάν εγκαταλείψουμε τη χρήση του new και διαγράψουμε.

Ας ξεκινήσουμε με κάτι απλό - με δυναμικούς πίνακες.

Δυναμικοί πίνακες

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

Για να εκχωρηθούν δυναμικοί πίνακες σε χαμηλό επίπεδο, η C++ παρέχει μια διανυσματική μορφή των τελεστών new και delete: new και delete . Για παράδειγμα, εξετάστε κάποια συνάρτηση που λειτουργεί με ένα εξωτερικό buffer:
void DoWork(int* buffer, size_t bufSize);
Παρόμοιες λειτουργίες βρίσκονται συχνά σε βιβλιοθήκες με API σε καθαρό C. Παρακάτω είναι ένα παράδειγμα για το πώς μπορεί να μοιάζει ο κώδικας που τον χρησιμοποιεί. Αυτός είναι κακός κώδικας γιατί... χρησιμοποιεί ρητά το delete , και έχουμε ήδη περιγράψει τα προβλήματα που σχετίζονται με αυτό παραπάνω.
void Call(size_t n) (int* p = new int[n]; DoWork(p, n); delete p; // Bad! )
Όλα είναι απλά εδώ και οι περισσότεροι γνωρίζουν ότι για τέτοιους σκοπούς στη C++ θα πρέπει να χρησιμοποιήσετε το τυπικό κοντέινερ std::vector. Θα εκχωρήσει μνήμη στον κατασκευαστή και θα την ελευθερώσει στον καταστροφέα. Επιπλέον, μπορεί ακόμα να αλλάξει το μέγεθός του κατά τη διάρκεια της ζωής του, αλλά για εμάς αυτό δεν έχει σημασία τώρα. Χρησιμοποιώντας ένα διάνυσμα, ο κώδικας θα μοιάζει με αυτό:
void Call(size_t n) ( std:: vector v(n); // Καλύτερα. DoWork(v.data(), v.size()); )
Έτσι, λύνουμε όλα τα προβλήματα που σχετίζονται με την κλήση διαγραφής , και επιπλέον, αντί για το απρόσωπο ζεύγος δείκτη + αριθμός, έχουμε ένα ρητό κοντέινερ με μια βολική διεπαφή.

Ταυτόχρονα, κανένα νέο και διαγραφή .Δεν θα μπω σε περισσότερες λεπτομέρειες για αυτό το σενάριο. Από την εμπειρία μου, οι περισσότεροι προγραμματιστές γνωρίζουν ήδη τι πρέπει να κάνουν σε αυτήν την περίπτωση και γιατί.

* Στη C++, μια τέτοια διεπαφή θα πρέπει να υλοποιηθεί χρησιμοποιώντας τον τύπο span . Παρέχει μια ενοποιημένη διεπαφή συμβατή με STL για την πρόσβαση σε συνεχείς ακολουθίες στοιχείων χωρίς να επηρεάζει τη διάρκεια ζωής τους με οποιονδήποτε τρόπο (μη ιδιόκτητη σημασιολογία).

** Εφόσον οι προγραμματιστές της C++ διαβάζουν αυτό το άρθρο, είμαι σίγουρος ότι κάποιος θα σκεφτεί, "Χα! Το std::vector αποθηκεύει έως και τρεις (!) δείκτες, όταν το παλιό καλό int* είναι, εξ ορισμού, μόνο ένας δείκτης. Υπάρχει υπερβολική χρήση της μνήμης και αρκετές οδηγίες μηχανήματος για την προετοιμασία τους! Αυτό είναι απαράδεκτο! Ο Myers σχολίασε εξαιρετικά αυτήν την ιδιότητα των προγραμματιστών C++ στην έκθεσή του Γιατί η C++ πλέει όταν βυθίστηκε το Vasa. Αν αυτό είναι για εσάς πραγματικάπρόβλημα, μπορώ να προτείνω το std::unique_ptr , και στο μέλλον το πρότυπο μπορεί να μας δώσει dynarray.

Δυναμικά αντικείμενα

Τα δυναμικά αντικείμενα χρησιμοποιούνται συνήθως όταν είναι αδύνατο να δεσμευτεί η διάρκεια ζωής ενός αντικειμένου σε ένα συγκεκριμένο πεδίο. Εάν μπορεί να γίνει αυτό, θα πρέπει πιθανώς να χρησιμοποιήσετε αυτόματη μνήμη (δείτε γιατί δεν πρέπει να κάνετε κατάχρηση δυναμικών αντικειμένων). Αλλά αυτό είναι το θέμα ενός ξεχωριστού άρθρου.

Όταν δημιουργείται ένα δυναμικό αντικείμενο, κάποιος πρέπει να το διαγράψει και οι τύποι αντικειμένων μπορούν να χωριστούν σε δύο ομάδες: σε αυτά που σε καμία περίπτωση δεν γνωρίζουν τη διαδικασία της διαγραφής τους και σε αυτά που υποψιάζονται κάτι. Θα πούμε ότι τα πρώτα έχουν ένα τυπικό μοντέλο διαχείρισης μνήμης και τα δεύτερα έχουν ένα μη τυπικό.

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

Οι τύποι με ένα μη τυπικό μοντέλο διαχείρισης μνήμης περιλαμβάνουν, για παράδειγμα, αντικείμενα Qt. Εδώ, κάθε αντικείμενο έχει έναν γονέα που είναι υπεύθυνος για τη διαγραφή του. Και το αντικείμενο το γνωρίζει αυτό, γιατί κληρονομεί από την κλάση QObject. Αυτό περιλαμβάνει επίσης τύπους με πλήθος αναφοράς, για παράδειγμα, αυτούς που έχουν σχεδιαστεί για να λειτουργούν με boost::intrusive_ptr .

Με άλλα λόγια, ένας τύπος με τυπικό μοντέλο διαχείρισης μνήμης δεν παρέχει πρόσθετους μηχανισμούς για τη διαχείριση της διάρκειας ζωής του. Αυτό θα πρέπει να αντιμετωπιστεί εξ ολοκλήρου από την πλευρά του χρήστη. Αλλά ο τύπος με ένα μη τυποποιημένο μοντέλο παρέχει τέτοιους μηχανισμούς. Για παράδειγμα, το QObject έχει τις μεθόδους setParent() και Children() και περιέχει μια λίστα παιδιών και ο τύπος boost::intrusive_ptr βασίζεται στις συναρτήσεις intrusive_ptr_add_ref και intrusive_ptr_release και περιέχει έναν μετρητή αναφοράς.

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

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

* Μερικές εξαιρέσεις: ιδίωμα pimpl; ένα πολύ μεγάλο αντικείμενο (για παράδειγμα, ένα buffer μνήμης).

** Η εξαίρεση είναι std::locale::facet (δείτε παρακάτω).

Δυναμικά αντικείμενα με τυπική διαχείριση μνήμης

Αυτά συναντώνται συχνότερα στην πράξη. Και είναι αυτοί που θα πρέπει να προσπαθήσουν να χρησιμοποιήσουν στη σύγχρονη C++, επειδή οι τυπικές προσεγγίσεις, που χρησιμοποιούνται ιδίως σε έξυπνους δείκτες, λειτουργούν μαζί τους.

Στην πραγματικότητα, οι έξυπνοι δείκτες, ναι, είναι η απάντηση. Θα πρέπει να έχουν τον έλεγχο της διάρκειας ζωής των δυναμικών αντικειμένων. Υπάρχουν δύο από αυτά στη C++: std::shared_ptr και std::unique_ptr. Δεν θα επισημάνουμε εδώ το std::weak_ptr, γιατί είναι απλώς ένας βοηθός για το std::shared_ptr σε ορισμένες περιπτώσεις χρήσης.

Όσο για το std::auto_ptr, αφαιρέθηκε επίσημα από τη C++ ξεκινώντας από τη C++17. Αναπαύσου εν ειρήνη!

Δεν θα σταθώ εδώ στο σχεδιασμό και τη χρήση έξυπνων δεικτών, γιατί... αυτό ξεφεύγει από το πεδίο εφαρμογής του άρθρου. Επιτρέψτε μου να σας υπενθυμίσω αμέσως ότι συνοδεύονται από τις υπέροχες συναρτήσεις std::make_shared και std::make_unique, και θα πρέπει να χρησιμοποιηθούν για τη δημιουργία έξυπνων δεικτών.

Εκείνοι. αντί για αυτό:
std::unique_ptr μπισκότο (νέο μπισκότο (ζύμη, ζάχαρη, κανέλα));
πρέπει να γραφτεί ως εξής:
auto cookie = std::make_unique (ζύμη, ζάχαρη, κανέλα).
Τα πλεονεκτήματα των συναρτήσεων make σε σχέση με τη ρητή δημιουργία έξυπνων δεικτών περιγράφονται όμορφα από τον Herb Sutter στο GotW #89 και από τον Scott Myers στο Effective Modern C++, Item 21. Δεν θα επαναλάβω τον εαυτό μου, αλλά θα κάνω μια σύντομη λίστα σημείων εδώ:

  • Και για τις δύο λειτουργίες make:
    • Ασφάλεια όσον αφορά τις εξαιρέσεις.
    • Δεν υπάρχει όνομα διπλότυπου τύπου.
  • Για std::make_shared:
    • Κέρδος στην παραγωγικότητα, γιατί το μπλοκ ελέγχου εκχωρείται δίπλα στο ίδιο το αντικείμενο, γεγονός που μειώνει τον αριθμό των κλήσεων προς τη διαχείριση μνήμης και αυξάνει την εντοπιότητα των δεδομένων. Βελτιστοποίηση.
Οι λειτουργίες Make έχουν επίσης ορισμένους περιορισμούς, οι οποίοι περιγράφονται λεπτομερώς στις ίδιες πηγές:
  • Και για τις δύο λειτουργίες make:
    • Δεν μπορείς να περάσεις το δικό σου deleter . Αυτό είναι πολύ λογικό, γιατί εσωτερικά, κάντε συναρτήσεις, εξ ορισμού, χρησιμοποιήστε το τυπικό νέο .
    • Δεν μπορείτε να χρησιμοποιήσετε τον αρχικοποιητή με αγκύλες, ούτε όλες τις άλλες όμορφες δυνατότητες που σχετίζονται με την τέλεια προώθηση (δείτε το Effective Modern C++, Item 30).
  • Για std::make_shared:
    • Πιθανή κατανάλωση μνήμης για μεγάλα αντικείμενα με αδύναμες αναφορές μεγάλης διάρκειας (std::weak_pointer).
    • Προβλήματα με τους νέους τελεστές και τους τελεστές διαγραφής που παρακάμπτονται σε επίπεδο κλάσης.
    • Πιθανή ψευδής κοινή χρήση μεταξύ ενός αντικειμένου και ενός μπλοκ ελέγχου (δείτε ερώτηση στο StackOverflow).
Στην πράξη, αυτοί οι περιορισμοί είναι σπάνιοι και δεν μειώνουν τα πλεονεκτήματα. Αποδεικνύεται ότι οι έξυπνοι δείκτες έκρυψαν την κλήση για διαγραφή από εμάς και οι λειτουργίες δημιουργίας έκρυψαν την κλήση προς νέο από εμάς. Ως αποτέλεσμα, έχουμε πιο αξιόπιστο κώδικα, ο οποίος δεν περιέχει ούτε νέο ούτε διαγραφή .

Παρεμπιπτόντως, η δομή των συναρτήσεων make συζητείται σοβαρά στις αναφορές του από τον Stefan Lavavey (γνωστός και ως STL). Ακολουθεί μια εύγλωττη διαφάνεια από την έκθεσή του Don't Help the Compiler:

Δυναμικά αντικείμενα με μη τυπική διαχείριση μνήμης

Εκτός από την τυπική προσέγγιση διαχείρισης μνήμης μέσω έξυπνων δεικτών, υπάρχουν και άλλα μοντέλα. Για παράδειγμα, μέτρηση αναφοράς και σχέσεις γονέα σε παιδιά.

Δυναμικά αντικείμενα με μέτρηση αναφοράς


Μια πολύ κοινή τεχνική που χρησιμοποιείται σε πολλές βιβλιοθήκες. Ας πάρουμε ως παράδειγμα τη βιβλιοθήκη OpenSceneGraph. Είναι μια ανοιχτή μηχανή 3D cross-platform γραμμένη σε C++ και OpenGL.

Οι περισσότερες κλάσεις σε αυτό κληρονομούν από την κλάση osg::Referenced, η οποία εκτελεί εσωτερικά την καταμέτρηση αναφορών. Η μέθοδος ref() αυξάνει τον μετρητή, η μέθοδος unref() μειώνει τον μετρητή και διαγράφει το αντικείμενο όταν ο μετρητής φτάσει στο μηδέν.

Το κιτ περιλαμβάνει επίσης έναν έξυπνο δείκτη osg::ref_ptr , που καλεί τη μέθοδο T::ref() στο αποθηκευμένο αντικείμενο στον κατασκευαστή του και τη μέθοδο T::unref() στον καταστροφέα του. Η ίδια προσέγγιση χρησιμοποιείται στο boost::intrusive_ptr, μόνο που υπάρχουν εξωτερικές συναρτήσεις αντί για τις μεθόδους ref() και unref().

Ας δούμε ένα κομμάτι κώδικα που δίνεται στον επίσημο OpenSceneGraph 3.0: Οδηγός για αρχάριους:
osg::ref_ptr vertices = new osg::Vec3Array; // ... osg::ref_ptr normals = new osg::Vec3Array; // ... osg::ref_ptr geom = new osg::Geometry; geom->setVertexArray(vertices.get()); γεωμ->
Πολύ γνωστές κατασκευές όπως το osg::ref_ptr p = νέο T . Με τον ίδιο ακριβώς τρόπο που χρησιμοποιούνται οι συναρτήσεις std::make_unique και std::make_shared για τη δημιουργία των κλάσεων std::unique_ptr και std::shared_ptr, μπορούμε να γράψουμε τη συνάρτηση osg::make_ref για να δημιουργήσουμε την κλάση osg::ref_ptr . Αυτό γίνεται πολύ απλά, κατ' αναλογία με τη συνάρτηση std::make_unique:
χώρος ονομάτων osg (πρότυπο osg::ref_ptr make_ref(Args&&... args) ( return new T(std::forward (args)...);
) )
Ας ξαναγράψουμε αυτό το κομμάτι κώδικα οπλισμένο με τη νέα μας λειτουργία: auto vertices = osg::make_ref () // ... auto normals = osg::make_ref () // ... auto geom = osg::make_ref
() geom->setVertexArray(vertices.get()); geom->setNormalArray(normals.get()); //...

Οι αλλαγές είναι ασήμαντες και μπορούν εύκολα να γίνουν αυτόματα. Με αυτόν τον απλό τρόπο, έχουμε ασφάλεια εξαίρεσης, χωρίς διπλότυπο όνομα τύπου και εξαιρετική συμμόρφωση με το τυπικό στυλ. Η κλήση διαγραφής ήταν ήδη κρυμμένη στη μέθοδο osg::Referenced::unref() και τώρα έχουμε κρύψει τη νέα κλήση στη συνάρτηση osg::make_ref.

* Τεχνικά, σε αυτό το τμήμα δεν υπάρχουν καταστάσεις που να είναι μη ασφαλείς όσον αφορά τις εξαιρέσεις, αλλά σε πιο σύνθετες διαμορφώσεις θα μπορούσαν να υπάρχουν κάποιες.

Δυναμικά αντικείμενα για διαλόγους χωρίς μοντέλο στο MFC


Ας δούμε ένα συγκεκριμένο παράδειγμα για τη βιβλιοθήκη MFC. Αυτό είναι ένα περιτύλιγμα κλάσεων C++ πάνω από το API των Windows. Χρησιμοποιείται για την απλοποίηση της ανάπτυξης GUI στα Windows.

Μια ενδιαφέρουσα τεχνική που η Microsoft συνιστά επίσημα τη χρήση για τη δημιουργία διαλόγων χωρίς μοντέλα. Επειδή Ο διάλογος είναι άμετρος, δεν είναι απολύτως σαφές ποιος είναι υπεύθυνος για τη διαγραφή του. Προτείνεται να διαγραφεί μόνο του στη μέθοδο CDialog::PostNcDestroy(). Αυτή η μέθοδος καλείται μετά την επεξεργασία του μηνύματος WM_NCDESTROY, το τελευταίο μήνυμα που έλαβε το παράθυρο στον κύκλο ζωής του.

Στο παρακάτω παράδειγμα, δημιουργείται ένα παράθυρο διαλόγου όταν γίνεται κλικ σε ένα κουμπί στη μέθοδο CMainFrame::OnBnClickedCreate() και διαγράφεται στη μέθοδο CMyDialog::PostNcDestroy() που έχει παρακαμφθεί.
void CMainFrame::OnBnClickedCreate() ( auto* pDialog = new CMyDialog(this); pDialog->ShowWindow(SW_SHOW); ) class CMyDialog: public CDialog ( public: CMyDialog(CWnd* pParent) (pDialog)(Δημιουργία)Y_IDO; προστατευμένο: void PostNcDestroy() παράκαμψη ( CDialog::PostNcDestroy(); διαγράψτε αυτό; ) );
Εδώ δεν έχουμε κρυφή ούτε τη νέα ούτε την κλήση διαγραφής. Υπάρχουν πολλοί τρόποι για να πυροβολήσετε τον εαυτό σας στο πόδι. Εκτός από τα συνηθισμένα προβλήματα με τους δείκτες, μπορείτε να ξεχάσετε να παρακάμψετε τη μέθοδο PostNcDestroy() στο διάλογό σας, με αποτέλεσμα να υπάρχει διαρροή μνήμης. Όταν δείτε την κλήση σε νέο , μπορεί να θέλετε να καλέσετε τη διαγραφή σε μια συγκεκριμένη στιγμή, κάτι που θα έχει ως αποτέλεσμα διπλή διαγραφή. Μπορείτε να δημιουργήσετε κατά λάθος ένα αντικείμενο διαλόγου στην αυτόματη μνήμη, και πάλι έχουμε μια διπλή διαγραφή.

Ας προσπαθήσουμε να αποκρύψουμε τις κλήσεις για νέα και να διαγράψουμε μέσα στην ενδιάμεση κλάση CModelessDialog και στο εργοστάσιο CreateModelessDialog, το οποίο θα είναι υπεύθυνο για τους διαλόγους χωρίς mode στην εφαρμογή μας:
class CModelessDialog: public CDialog ( public: CModelessDialog(UINT nIDTemplate, CWnd* pParent) ( Create(nIDTemplate, pParent); ) protected: void PostNcDestroy() override ( CDialog::PostNcDestroy(); διαγράψτε αυτό; )); // Εργοστάσιο για τη δημιουργία προτύπου τροπικών διαλόγων Παράγωγο* CreateModelessDialog(Args&&... args) ( // Αντί για static_assert στο σώμα της συνάρτησης, μπορούμε να χρησιμοποιήσουμε το std::enable_if στην κεφαλίδα του, το οποίο θα μας επιτρέψει να χρησιμοποιήσουμε το SFINAE. // Αλλά επειδή άλλες υπερφορτώσεις αυτής της συνάρτησης είναι απίθανο να αναμένεται, Φαίνεται λογικό να χρησιμοποιηθεί μια πιο απλή και πιο οπτική λύση static_assert(std::is_base_of) ::value, "Το CreateModelessDialog θα πρέπει να κληθεί για τους απογόνους του CModelessDialog"); auto* pDialog = new Παράγωγο(std::forward
(args)...);
pDialog->ShowWindow(SW_SHOW); επιστροφή pDialog; )
Η ίδια η κλάση παρακάμπτει τη μέθοδο PostNcDestroy(), στην οποία αποκρύψαμε το delete , και για τη δημιουργία κλάσεων απόγονων, χρησιμοποιείται το εργοστάσιο στο οποίο αποκρύψαμε το νέο. Η δημιουργία και ο ορισμός μιας κλάσης καταγωγής μοιάζει τώρα με αυτό:

void CMainFrame::OnBnClickedCreate() ( CreateModelessDialog (αυτό); ) class CMyDialog: public CModelessDialog ( public: CMyDialog(CWnd* pParent) : CModelessDialog(IDD_MY_DIALOG, pParent) () );

Φυσικά, δεν έχουμε λύσει όλα τα προβλήματα με αυτόν τον τρόπο. Για παράδειγμα, ένα αντικείμενο μπορεί ακόμα να εκχωρηθεί στη στοίβα και να διαγραφεί διπλά. Μπορείτε να αποτρέψετε την εκχώρηση ενός αντικειμένου στη στοίβα μόνο τροποποιώντας την ίδια την κλάση αντικειμένου, για παράδειγμα προσθέτοντας έναν ιδιωτικό κατασκευαστή. Αλλά δεν υπάρχει τρόπος να το κάνουμε αυτό από τη βασική κλάση CModelessDialog. Μπορείτε, φυσικά, να αποκρύψετε εντελώς την κλάση CMyDialog και να κάνετε την εργοστασιακή όχι πρότυπο, αλλά πιο κλασική, αποδεχόμενοι ένα συγκεκριμένο αναγνωριστικό κλάσης. Αλλά όλα αυτά ξεφεύγουν από το πεδίο εφαρμογής του άρθρου.



Τέλος πάντων, διευκολύναμε τη δημιουργία ενός διαλόγου από τον κώδικα πελάτη και τη σύνταξη μιας νέας κλάσης διαλόγου.

Και ταυτόχρονα, αφαιρέσαμε νέες κλήσεις και διαγράψαμε από τον κωδικό πελάτη.

Δυναμικά αντικείμενα με σχέση γονέα-παιδιού

Εμφανίζονται αρκετά συχνά, ειδικά σε βιβλιοθήκες για ανάπτυξη GUI. Ως παράδειγμα, εξετάστε την Qt, μια πολύ γνωστή βιβλιοθήκη για ανάπτυξη εφαρμογών και διεπαφής χρήστη.

Οι περισσότερες κλάσεις κληρονομούν από το QObject. Αποθηκεύει μια λίστα με παιδιά και τα διαγράφει όταν διαγράφεται μόνο του. Αποθηκεύει έναν δείκτη στον γονέα (μπορεί να είναι μηδενικός) και μπορεί να αλλάξει τον γονέα κατά τη διάρκεια της ζωής του.


Ένα εξαιρετικό παράδειγμα μιας κατάστασης όπου η απαλλαγή από νέα και η διαγραφή δεν θα λειτουργήσει τόσο εύκολα. Η βιβλιοθήκη σχεδιάστηκε με τέτοιο τρόπο ώστε αυτοί οι χειριστές να μπορούν και πρέπει να χρησιμοποιούνται σε πολλές περιπτώσεις. Πρότεινα ένα περιτύλιγμα για τη δημιουργία αντικειμένων με μη μηδενικό γονέα, αλλά η ιδέα δεν λειτούργησε (δείτε τη συζήτηση στη λίστα αλληλογραφίας Qt).

Η ίδια η τοπική ρύθμιση είναι υπεύθυνη για τη διαγραφή όψεων όταν ο αριθμός αναφοράς φτάσει στο μηδέν, αλλά ο χρήστης πρέπει να δημιουργήσει όψεις χρησιμοποιώντας τον νέο τελεστή (δείτε την ενότητα Σημειώσεις στην περιγραφή του κατασκευαστή std::locale):
std::locale default; std::locale myLocale(προεπιλογή,νέο std::codecvt_utf8 );
Αυτός ο μηχανισμός εφαρμόστηκε ακόμη και πριν από την εισαγωγή των τυπικών έξυπνων δεικτών και ξεχωρίζει από τους γενικούς κανόνες χρήσης κλάσεων στην τυπική βιβλιοθήκη.

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

Σύναψη

Έτσι, πρώτα εξετάσαμε σενάρια όπως η δημιουργία δυναμικών πινάκων και δυναμικών αντικειμένων με τυπική διαχείριση μνήμης. Αντί για νέο και διαγραφή, χρησιμοποιήσαμε τυπικά κοντέινερ και δημιουργήσαμε λειτουργίες και αποκτήσαμε απλούστερο και πιο αξιόπιστο κώδικα.

Στη συνέχεια, εξετάσαμε μια σειρά από παραδείγματα μη τυπικής διαχείρισης μνήμης και είδαμε πώς θα μπορούσαμε να βελτιώσουμε τον κώδικα αφαιρώντας νέο και διαγράφοντας σε κατάλληλα περιτυλίγματα. Βρήκαμε επίσης ένα παράδειγμα όπου αυτή η προσέγγιση δεν λειτουργεί.

Ωστόσο, στις περισσότερες περιπτώσεις αυτή η σύσταση παράγει εξαιρετικά αποτελέσματα και μπορεί να χρησιμοποιηθεί ως η προεπιλεγμένη αρχή. Τώρα μπορούμε να θεωρήσουμε ότι εάν ο κώδικας χρησιμοποιεί νέο ή delete , αυτή είναι μια ειδική περίπτωση που απαιτεί ιδιαίτερη προσοχή. Εάν βλέπετε αυτές τις κλήσεις στον κωδικό πελάτη, σκεφτείτε εάν είναι πραγματικά δικαιολογημένες.

  • Αποφύγετε τη χρήση νέων και διαγράψτε στον κώδικά σας. Σκεφτείτε τις ως χειροκίνητες λειτουργίες διαχείρισης σωρού χαμηλού επιπέδου.
  • Χρησιμοποιήστε τυπικά κοντέινερ για δυναμικές δομές δεδομένων.
  • Χρησιμοποιήστε τις συναρτήσεις make για να δημιουργήσετε δυναμικά αντικείμενα όποτε είναι δυνατόν.
  • Δημιουργήστε περιτυλίγματα για αντικείμενα με μη τυπικό μοντέλο μνήμης.

Από τον συγγραφέα

Προσωπικά, έχω αντιμετωπίσει πολλές περιπτώσεις διαρροών μνήμης και σφαλμάτων λόγω υπερβολικής χρήσης του new και του delete . Ναι, το μεγαλύτερο μέρος αυτού του κώδικα γράφτηκε πριν από πολλά χρόνια, αλλά μετά οι νέοι προγραμματιστές αρχίζουν να δουλεύουν με αυτόν και πιστεύουν ότι έτσι πρέπει να γράφεται.

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

Πριν από λίγο περισσότερο από ένα χρόνο έκανα μια παρουσίαση σχετικά με αυτό το θέμα στο συνέδριο C++ Ρωσία. Μετά την ομιλία μου, το κοινό χωρίστηκε σε δύο ομάδες: σε αυτούς για τους οποίους όλα ήταν προφανή και σε αυτούς που έκαναν μια υπέροχη ανακάλυψη για τον εαυτό τους. Πιστεύω ότι τα συνέδρια τείνουν να παρακολουθούνται από πιο έμπειρους προγραμματιστές, οπότε ακόμα κι αν υπήρχαν πολλοί άνθρωποι που ήταν νέοι σε αυτές τις πληροφορίες, ελπίζω αυτό το άρθρο να είναι χρήσιμο στην κοινότητα.

ΥΓΣτη διαδικασία συζήτησης του άρθρου, οι συνάδελφοί μου και εγώ είχαμε μια ολόκληρη συζήτηση σχετικά με το ποιο είναι το σωστό: «Myers» ή «Meyers». Από τη μια πλευρά, το "Meyers" ακούγεται πιο οικείο στα αυτιά των Ρώσων, και εμείς οι ίδιοι φαίνεται να μιλούσαμε πάντα με αυτόν τον τρόπο. Από την άλλη πλευρά, το "Myers" χρησιμοποιείται στο wiki. Αν κοιτάξετε τα τοπικά βιβλία, τότε γενικά υπάρχουν πολλά πράγματα: σε αυτές τις δύο επιλογές προστίθεται και το "Meyers". Σε συνέδρια διαφορετικός Ανθρωποι εκπροσωπώτο με διαφορετικούς τρόπους. Τελικά εμείς κατάφερε να μάθει, ότι αυτοαποκαλείται «Myers», κάτι που αποφάσισαν.

Εδαφος διά παιγνίδι γκολφ

  1. Herb Sutter GotW #89 Λύση: Έξυπνοι δείκτες.
  2. Σκοτ Μάγιερς Αποτελεσματική σύγχρονη C++, Στοιχείο 21, σελ. 139.
  3. Stephan T. Lavavej, Μην βοηθάτε τον μεταγλωττιστή.
  4. Bjarne Stroustrup, Η γλώσσα προγραμματισμού C++, 11.2.1, σελ. 281.
  5. Πέντε δημοφιλείς μύθοι για τη C++., Μέρος 2
  6. Μιχαήλ Ματρόσοφ, C++ χωρίς νέα και διαγραφή.

Ετικέτες:

Προσθήκη ετικετών

Σχόλια 134

15.8. Νέοι χειριστές και διαγραφή

Από προεπιλογή, η εκχώρηση ενός αντικειμένου κλάσης από ένα σωρό και η απελευθέρωση της μνήμης που καταλάμβανε πραγματοποιείται χρησιμοποιώντας τους καθολικούς τελεστές new() και delete() που ορίζονται στην τυπική βιβλιοθήκη C++. (Συζητήσαμε αυτούς τους τελεστές στην Ενότητα 8.4.) Αλλά μια κλάση μπορεί να εφαρμόσει τη δική της στρατηγική διαχείρισης μνήμης παρέχοντας τελεστές μελών με το ίδιο όνομα. Εάν ορίζονται σε μια κλάση, καλούνται αντί για καθολικούς τελεστές για να εκχωρήσουν και να ελευθερώσουν μνήμη για αντικείμενα αυτής της κλάσης.

Ας ορίσουμε τους τελεστές new() και delete() στην κλάση Screen.

Ο τελεστής μέλος new() πρέπει να επιστρέψει μια τιμή τύπου void* και να λάβει ως πρώτη παράμετρό του μια τιμή τύπου size_t, όπου size_t είναι το typedef που ορίζεται στο αρχείο κεφαλίδας συστήματος. Ιδού η ανακοίνωσή του:

void *operator new(size_t);

Όταν η new() χρησιμοποιείται για τη δημιουργία ενός αντικειμένου ενός τύπου κλάσης, ο μεταγλωττιστής ελέγχει εάν ένας τέτοιος τελεστής ορίζεται σε αυτήν την κλάση. Εάν ναι, τότε καλείται να εκχωρήσει μνήμη για το αντικείμενο διαφορετικά, καλείται ο καθολικός τελεστής new(). Για παράδειγμα, η ακόλουθη οδηγία

Οθόνη *ps = νέα οθόνη;

δημιουργεί ένα αντικείμενο Screen στο σωρό και αφού αυτή η κλάση έχει έναν τελεστή new(), καλείται. Η παράμετρος size_t του χειριστή αρχικοποιείται αυτόματα σε μια τιμή ίση με το μέγεθος της οθόνης σε byte.

Η προσθήκη ή η αφαίρεση της νέας() σε μια κλάση δεν έχει καμία επίδραση στον κώδικα χρήστη. Η κλήση προς το νέο φαίνεται η ίδια τόσο για τον παγκόσμιο χειριστή όσο και για τον χειριστή μέλους. Εάν η κλάση Screen δεν είχε τη δική της new(), τότε η κλήση θα παρέμενε σωστή, θα κληθεί μόνο ο καθολικός τελεστής αντί για τον τελεστή μέλους.

Χρησιμοποιώντας τον τελεστή ανάλυσης καθολικού εύρους, μπορείτε να καλέσετε την global new() ακόμα κι αν η κλάση Screen ορίζει τη δική της έκδοση:

Οθόνη *ps = ::new Screen;

void operator delete(void *);

Όταν ο τελεστής διαγραφής είναι δείκτης σε ένα αντικείμενο ενός τύπου κλάσης, ο μεταγλωττιστής ελέγχει εάν ο τελεστής delete() ορίζεται σε αυτήν την κλάση. Εάν ναι, τότε καλείται να ελευθερωθεί η μνήμη, διαφορετικά καλείται η καθολική έκδοση του χειριστή. Επόμενες οδηγίες

Απελευθερώνει τη μνήμη που καταλαμβάνεται από το αντικείμενο Screen που δείχνει το ps. Εφόσον το Screen έχει τελεστή μέλους delete(), αυτό χρησιμοποιείται. Η παράμετρος χειριστή τύπου void* αρχικοποιείται αυτόματα στην τιμή ps. Η προσθήκη της delete() σε ή η αφαίρεσή της από μια κλάση δεν έχει καμία επίδραση στον κώδικα χρήστη. Η κλήση για διαγραφή φαίνεται ίδια τόσο για τον παγκόσμιο χειριστή όσο και για τον χειριστή μέλους. Εάν η κλάση Screen δεν είχε τον δικό της τελεστή delete(), τότε η κλήση θα παρέμενε σωστή, θα κληθεί μόνο ο καθολικός τελεστής αντί για τον τελεστή μέλους.

Χρησιμοποιώντας τον τελεστή ανάλυσης καθολικού εύρους, μπορείτε να καλέσετε την καθολική delete() ακόμα κι αν η οθόνη έχει ορίσει τη δική της έκδοση:

Γενικά, ο τελεστής delete() που χρησιμοποιείται πρέπει να ταιριάζει με τον τελεστή new() με τον οποίο εκχωρήθηκε η μνήμη. Για παράδειγμα, εάν το ps δείχνει σε μια περιοχή μνήμης που έχει εκχωρηθεί από την καθολική new(), τότε η καθολική delete() θα πρέπει να χρησιμοποιηθεί για να την ελευθερώσει.

Ο τελεστής delete() που ορίζεται για έναν τύπο κλάσης μπορεί να λάβει δύο παραμέτρους αντί για μία. Η πρώτη παράμετρος πρέπει να είναι ακόμα τύπου void* και η δεύτερη πρέπει να είναι προκαθορισμένου τύπου size_t (μην ξεχάσετε να συμπεριλάβετε το αρχείο κεφαλίδας):

// αντικαθιστά

// void operator delete(void *);

Εάν υπάρχει η δεύτερη παράμετρος, ο μεταγλωττιστής την αρχικοποιεί αυτόματα με μια τιμή ίση με το μέγεθος σε byte του αντικειμένου που απευθύνεται από την πρώτη παράμετρο. (Αυτή η επιλογή είναι σημαντική σε μια ιεραρχία κλάσης, όπου ο τελεστής delete() μπορεί να κληρονομηθεί από μια παράγωγη κλάση. Η κληρονομικότητα συζητείται με περισσότερες λεπτομέρειες στο Κεφάλαιο 17.)

Ας δούμε την υλοποίηση των τελεστών new() και delete() στην κλάση Screen με περισσότερες λεπτομέρειες. Η στρατηγική μας για την κατανομή μνήμης θα βασίζεται σε μια συνδεδεμένη λίστα αντικειμένων οθόνης, ξεκινώντας από το μέλος του freeStore. Κάθε φορά που καλείται ο τελεστής μέλους new(), επιστρέφεται το επόμενο αντικείμενο στη λίστα. Όταν καλείται η delete(), το αντικείμενο επιστρέφει στη λίστα. Εάν, κατά τη δημιουργία ενός νέου αντικειμένου, η λίστα που απευθύνεται στο freeStore είναι κενή, τότε ο καθολικός τελεστής new() καλείται να αποκτήσει ένα μπλοκ μνήμης επαρκούς για την αποθήκευση αντικειμένων screenChunk της κλάσης Screen.

Τόσο το screenChunk όσο και το freeStore ενδιαφέρουν μόνο το Screen, επομένως θα τα κάνουμε ιδιωτικά μέλη. Επιπλέον, για όλα τα δημιουργημένα αντικείμενα της κλάσης μας, οι τιμές αυτών των μελών πρέπει να είναι οι ίδιες και επομένως πρέπει να δηλώνονται στατικά. Για να υποστηρίξουμε τη δομή της συνδεδεμένης λίστας των αντικειμένων οθόνης, χρειαζόμαστε ένα τρίτο επόμενο μέλος:

void *operator new(size_t);

void operator delete(void *, size_t);

στατική οθόνη *freeStore;

static const int screenChunk;

Εδώ είναι μια πιθανή υλοποίηση του τελεστή new() για την κλάση Screen:

#include "Screen.h"

#include cstddef

// αρχικοποιούνται τα στατικά μέλη

// στα αρχεία προέλευσης του προγράμματος, όχι στα αρχεία κεφαλίδας

Οθόνη *Screen::freeStore = 0;

const int Screen::screenChunk = 24;

void *Screen::operator new(size_t size)

αν (!freeStore) (

// η συνδεδεμένη λίστα είναι κενή: λήψη νέου μπλοκ

// Ο παγκόσμιος τελεστής new καλείται

size_t chunk = screenChunk * μέγεθος;

reinterpret_cast Screen* (νέο char[ κομμάτι ]);

// συμπεριλάβετε το ληφθέν μπλοκ στη λίστα

p != &freeStore[ screenChunk - 1 ];

freeStore = freeStore-next;

Και εδώ είναι η υλοποίηση του τελεστή delete():

void Screen::διαγραφή χειριστή(void *p, size_t)

// εισάγετε το "απομακρυσμένο" αντικείμενο πίσω,

// στη δωρεάν λίστα

(static_cast Screen* (p))-next = freeStore;

freeStore = static_cast Screen* (p);

Ο τελεστής new() μπορεί να δηλωθεί σε μια κλάση χωρίς την αντίστοιχη delete(). Σε αυτήν την περίπτωση, τα αντικείμενα απελευθερώνονται χρησιμοποιώντας τον καθολικό τελεστή με το ίδιο όνομα. Επιτρέπεται επίσης η δήλωση του τελεστή delete() χωρίς new(): τα αντικείμενα θα δημιουργηθούν χρησιμοποιώντας τον καθολικό τελεστή με το ίδιο όνομα. Ωστόσο, συνήθως αυτοί οι τελεστές υλοποιούνται ταυτόχρονα, όπως στο παραπάνω παράδειγμα, αφού ο προγραμματιστής κλάσης συνήθως χρειάζεται και τα δύο.

Είναι στατικά μέλη της κλάσης, ακόμα κι αν ο προγραμματιστής δεν τα δηλώνει ρητά ως τέτοια, και υπόκεινται στους συνήθεις περιορισμούς για τέτοιες συναρτήσεις μέλους: δεν περνούν από αυτόν τον δείκτη και επομένως μπορούν να έχουν πρόσβαση μόνο στα στατικά μέλη απευθείας. (Δείτε τη συζήτηση των στατικών συναρτήσεων μέλους στην Ενότητα 13.5.) Ο λόγος που αυτοί οι τελεστές γίνονται στατικοί είναι ότι καλούνται είτε πριν κατασκευαστεί το αντικείμενο κλάσης (new()) είτε αφού καταστραφεί (delete()).

Εκχώρηση μνήμης χρησιμοποιώντας τον τελεστή new(), για παράδειγμα:

Screen *ptr = new Screen(10, 20);

// Ψευκωδικός σε C++

ptr = Screen::operator new(sizeof(Screen));

Screen::Screen(ptr, 10, 20);

Με άλλα λόγια, ο τελεστής new() της κλάσης καλείται πρώτα να εκχωρήσει μνήμη για το αντικείμενο και στη συνέχεια το αντικείμενο αρχικοποιείται από τον κατασκευαστή. Εάν η new() αποτύχει, δημιουργείται μια εξαίρεση του τύπου bad_alloc και ο κατασκευαστής δεν καλείται.

Ελευθέρωση μνήμης χρησιμοποιώντας τον τελεστή delete(), για παράδειγμα:

ισοδυναμεί με τη διαδοχική εκτέλεση των ακόλουθων εντολών:

// Ψευκωδικός σε C++

Οθόνη::~Οθόνη(ptr);

Οθόνη::διαγραφή χειριστή(ptr, sizeof(*ptr));

Έτσι, όταν ένα αντικείμενο καταστρέφεται, πρώτα καλείται ο καταστροφέας κλάσης και στη συνέχεια ο τελεστής delete() που ορίζεται στην κλάση καλείται για να ελευθερώσει τη μνήμη. Αν το ptr είναι 0, τότε δεν καλείται ούτε ο καταστροφέας ούτε η delete().

15.8.1. Νέοι χειριστές και διαγραφή

Ο τελεστής new(), που ορίστηκε στην προηγούμενη υποενότητα, καλείται μόνο όταν εκχωρείται μνήμη για ένα μεμονωμένο αντικείμενο. Έτσι, σε αυτήν την εντολή η new() της κλάσης Screen ονομάζεται:

Screen *ps = new Screen(24, 80);

ενώ κάτω από τον καθολικό τελεστή new() καλείται να εκχωρήσει μνήμη από το σωρό για μια σειρά αντικειμένων τύπου Screen:

// ονομάζεται Screen::operator new()

Οθόνη *psa = νέα οθόνη;

Η κλάση μπορεί επίσης να δηλώσει τελεστές new() και delete() για εργασία με πίνακες.

Ο τελεστής μέλος new() πρέπει να επιστρέψει μια τιμή τύπου void* και να λάβει μια τιμή τύπου size_t ως πρώτη παράμετρό του. Ιδού η ανακοίνωσή του για το Screen:

void *operator new(size_t);

Όταν χρησιμοποιείται new για τη δημιουργία μιας συστοιχίας αντικειμένων ενός τύπου κλάσης, ο μεταγλωττιστής ελέγχει αν η κλάση έχει ορίσει έναν τελεστή new(). Εάν ναι, τότε καλείται να εκχωρήσει μνήμη για τον πίνακα, διαφορετικά, καλείται global new(). Η ακόλουθη πρόταση δημιουργεί έναν πίνακα δέκα αντικειμένων οθόνης στο σωρό:

Οθόνη *ps = νέα οθόνη;

Αυτή η κλάση έχει τον τελεστή new() και γι' αυτό καλείται να εκχωρήσει μνήμη. Η παράμετρός του size_t αρχικοποιείται αυτόματα στην ποσότητα της μνήμης, σε byte, που απαιτείται για τη συγκράτηση δέκα αντικειμένων οθόνης.

Ακόμα κι αν μια κλάση έχει έναν τελεστή μέλους new(), ο προγραμματιστής μπορεί να καλέσει την global new() για να δημιουργήσει έναν πίνακα χρησιμοποιώντας τον τελεστή ανάλυσης καθολικού εύρους:

Οθόνη *ps = ::new Screen;

Ο τελεστής delete(), που είναι μέλος της κλάσης, πρέπει να είναι τύπου void και να παίρνει void* ως πρώτη του παράμετρο. Δείτε πώς φαίνεται η διαφήμισή του στην οθόνη:

void operator delete(void *);

Για να διαγράψετε έναν πίνακα αντικειμένων κλάσης, το delete πρέπει να καλείται ως εξής:

Όταν ο τελεστής της διαγραφής είναι ένας δείκτης σε ένα αντικείμενο ενός τύπου κλάσης, ο μεταγλωττιστής ελέγχει εάν ο τελεστής delete() ορίζεται σε αυτήν την κλάση. Αν ναι, τότε καλείται να ελευθερώσει τη μνήμη, αλλιώς καλείται η καθολική έκδοση. Μια παράμετρος τύπου void* αρχικοποιείται αυτόματα στην τιμή της διεύθυνσης της αρχής της περιοχής μνήμης στην οποία βρίσκεται ο πίνακας.

Ακόμα κι αν μια κλάση έχει έναν τελεστή μέλους delete(), ο προγραμματιστής μπορεί να καλέσει την global delete() χρησιμοποιώντας τον τελεστή ανάλυσης καθολικού εύρους:

Η προσθήκη ή η αφαίρεση τελεστών new() ή delete() σε μια κλάση δεν επηρεάζει τον κωδικό χρήστη: οι κλήσεις τόσο σε γενικούς όσο και σε τελεστές μελών φαίνονται ίδιες.

Όταν δημιουργείται ένας πίνακας, πρώτα καλείται η new() για να εκχωρήσει την απαραίτητη μνήμη και, στη συνέχεια, κάθε στοιχείο αρχικοποιείται χρησιμοποιώντας έναν προεπιλεγμένο κατασκευαστή. Εάν μια κλάση έχει τουλάχιστον έναν κατασκευαστή, αλλά όχι προεπιλεγμένο κατασκευαστή, τότε η κλήση του τελεστή new() θεωρείται σφάλμα. Δεν υπάρχει σύνταξη για τον καθορισμό αρχικοποιητών στοιχείων πίνακα ή ορισμάτων κατασκευής κλάσεων κατά τη δημιουργία ενός πίνακα με αυτόν τον τρόπο.

Όταν ένας πίνακας καταστρέφεται, ο καταστροφέας κλάσης καλείται πρώτα να καταστρέψει τα στοιχεία και στη συνέχεια ο τελεστής delete() καλείται να ελευθερώσει όλη τη μνήμη. Είναι σημαντικό να χρησιμοποιείτε τη σωστή σύνταξη. Εάν οι οδηγίες

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

Ο τελεστής μέλους delete() μπορεί να έχει δύο παραμέτρους αντί για μία και η δεύτερη πρέπει να είναι τύπου size_t:

// αντικαθιστά

// void operator delete(void*);

void operator delete(void*, size_t);

Εάν υπάρχει η δεύτερη παράμετρος, ο μεταγλωττιστής την αρχικοποιεί αυτόματα με μια τιμή ίση με την ποσότητα μνήμης που έχει εκχωρηθεί για τον πίνακα σε byte.

Από το βιβλίο The C++ Reference Guide συγγραφέας Stroustrap Bjarne

R.5.3.4 Η λειτουργία διαγραφής Η λειτουργία διαγραφής καταστρέφει ένα αντικείμενο που δημιουργήθηκε χρησιμοποιώντας new.deallocation-expression: ::opt delete cast-expression::opt delete cast-expression Το αποτέλεσμα είναι τύπου void. Ο τελεστής της διαγραφής πρέπει να είναι δείκτης, ο οποίος επιστρέφει νέος. Αποτέλεσμα χρήσης της λειτουργίας διαγραφής

Από το βιβλίο Microsoft Visual C++ and MFC. Προγραμματισμός για Windows 95 και Windows NT συγγραφέας Φρόλοφ Αλεξάντερ Βιατσεσλάβοβιτς

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

Από το βιβλίο Using C++ Effectively. 55 σίγουροι τρόποι για να βελτιώσετε τη δομή και τον κώδικα των προγραμμάτων σας από τον Meyers Scott

Κανόνας 16: Χρησιμοποιήστε τις ίδιες μορφές του new και διαγράψτε Τι συμβαίνει με το ακόλουθο κομμάτι;std::string *stringArray = new std::string;...διαγραφή stringArray;Εκ πρώτης όψεως, όλα είναι σε τέλεια σειρά - η χρήση του το new αντιστοιχεί στη χρήση του delete, αλλά εδώ κάτι δεν πάει καλά. Συμπεριφορά προγράμματος

Από το βιβλίο Windows Script Host για Windows 2000/XP συγγραφέας Ποπόφ Αντρέι Βλαντιμίροβιτς

Κεφάλαιο 8 Διαμόρφωση και διαγραφή νέων Στις μέρες μας, όταν τα υπολογιστικά περιβάλλοντα έχουν ενσωματωμένη υποστήριξη για τη συλλογή απορριμμάτων (όπως η Java και το .NET), η μη αυτόματη προσέγγιση της C++ στη διαχείριση της μνήμης μπορεί να φαίνεται λίγο ξεπερασμένη. Ωστόσο, πολλοί προγραμματιστές που δημιουργούν απαιτητικές

Από το βιβλίο Πρότυπα προγραμματισμού σε C++. 101 κανόνες και συστάσεις συγγραφέας Αλεξανδρέσκου Αντρέι

Μέθοδος διαγραφής Εάν η παράμετρος δύναμης είναι ψευδής ή δεν έχει καθοριστεί, τότε χρησιμοποιώντας τη μέθοδο Διαγραφής θα είναι αδύνατο να διαγράψετε έναν κατάλογο με χαρακτηριστικό μόνο για ανάγνωση. Η ρύθμιση της δύναμης σε true θα επιτρέψει τη διαγραφή τέτοιων καταλόγων Όταν χρησιμοποιείτε τη μέθοδο Delete, δεν έχει σημασία αν η καθορισμένη

Από το βιβλίο Flash Reference συγγραφέας Ομάδα συγγραφέων

Μέθοδος διαγραφής Εάν η παράμετρος δύναμης είναι ψευδής ή δεν έχει καθοριστεί, τότε χρησιμοποιώντας τη μέθοδο Διαγραφής θα είναι αδύνατο να διαγράψετε ένα αρχείο με χαρακτηριστικό μόνο για ανάγνωση. Η ρύθμιση της δύναμης σε true θα επιτρέψει την άμεση διαγραφή τέτοιων αρχείων. Σημείωση Μπορείτε να χρησιμοποιήσετε τη μέθοδο DeleteFile αντί για τη μέθοδο Delete.

Από το βιβλίο Firebird DATABASE DEVELOPER'S GUIDE από την Borri Helen

Σχετικοί και Λογικοί τελεστές Οι σχεσιακές τελεστές χρησιμοποιούνται για τη σύγκριση των τιμών δύο μεταβλητών. Αυτοί οι τελεστές, που περιγράφονται στον πίνακα. P2.11, μπορεί να επιστρέψει μόνο λογικές τιμές true ή false.Πίνακας P2.11. Σχετικοί τελεστές Κατάσταση χειριστή, όταν

Από το βιβλίο Linux and UNIX: shell programming. Οδηγός προγραμματιστή. από τον Tainsley David

45. new και delete θα πρέπει πάντα να σχεδιάζονται μαζί Περίληψη Κάθε υπερφόρτωση τελεστή void* new(parms) σε μια κλάση πρέπει να συνοδεύεται από αντίστοιχη υπερφόρτωση τελεστή κενού delete(void* , parms), όπου parms είναι μια λίστα πρόσθετων τύπων παραμέτρων (το πρώτο από τα οποία είναι πάντα std:: size_t). Ιδιο

Από το βιβλίο SQL Help του συγγραφέα

Διαγραφή - Διαγραφή αντικειμένου, στοιχείου πίνακα ή διαγραφής μεταβλητής (Χειριστής) Αυτός ο τελεστής χρησιμοποιείται για τη διαγραφή ενός αντικειμένου, ιδιότητας αντικειμένου, στοιχείου πίνακα ή μεταβλητών από ένα σενάριο: Διαγραφή αναγνωριστικού: Περιγραφή: Ο τελεστής διαγραφής καταστρέφει ένα αντικείμενο ή μεταβλητή, όνομα

Από το βιβλίο Understanding SQL από τον Gruber Martin

Δήλωση DELETE Η δήλωση DELETE χρησιμοποιείται για τη διαγραφή ολόκληρων σειρών από έναν πίνακα. Η SQL δεν επιτρέπει σε μία πρόταση DELETE να διαγράφει σειρές από περισσότερους από έναν πίνακες. Ένα αίτημα DELETE που τροποποιεί μόνο μία τρέχουσα σειρά του δρομέα ονομάζεται διαγραφή θέσης.

Από το βιβλίο του συγγραφέα

15.8. Οι τελεστές new και delete Από προεπιλογή, η εκχώρηση ενός αντικειμένου κλάσης από ένα σωρό και η απελευθέρωση της μνήμης που καταλαμβάνεται από αυτόν πραγματοποιείται χρησιμοποιώντας τους καθολικούς τελεστές new() και delete() που ορίζονται στην τυπική βιβλιοθήκη C++. (Εξετάσαμε αυτούς τους τελεστές στην Ενότητα 8.4.) Αλλά μια κλάση μπορεί να υλοποιήσει

Από το βιβλίο του συγγραφέα

15.8.1. Οι τελεστές new και delete Ο τελεστής new(), που ορίστηκε στην προηγούμενη υποενότητα, καλείται μόνο όταν εκχωρείται μνήμη για ένα μεμονωμένο αντικείμενο. Έτσι, σε αυτήν την εντολή, η new() της κλάσης Screen ονομάζεται: Screen::operator new()Screen *ps = new Screen(24, 80);



Συνιστούμε να διαβάσετε

Κορυφή