Εργασία με τη μνήμη χρησιμοποιώντας νέα και διαγραφή. §11 Πίνακες και δείκτες

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

Chercher Οικιακές συσκευέςκαι ελευθερώνοντας τη μνήμη, χρησιμοποιούνται οι συναρτήσεις malloc() και free(). Ωστόσο, η C++ περιέχει δύο τελεστές που εκτελούν την κατανομή και την κατανομή μνήμης πιο αποτελεσματικά και απλά. Αυτοί οι τελεστές είναι νέοι και διαγράφονται. Τους γενικό σχήμαέχει τη μορφή:

μεταβλητή δείκτη = new variable_type;

διαγραφή μεταβλητής δείκτη.

Εδώ, το pointer-variable είναι ένας δείκτης τύπου variable-type. Ο νέος χειριστής εκχωρεί μνήμη για να αποθηκεύσει μια τιμή τύπου variable_type και επιστρέφει τη διεύθυνσή του. Οποιοσδήποτε τύπος δεδομένων μπορεί να τοποθετηθεί με νέα. Ο τελεστής διαγραφής ελευθερώνει τη μνήμη που δείχνει το pointer_variable.

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

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

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

Παρακάτω είναι ένα απλό παράδειγμα χρήσης των τελεστών new και delete. Σημειώστε τη χρήση ενός μπλοκ try/catch για την παρακολούθηση σφαλμάτων εκχώρησης μνήμης.

#συμπεριλαμβάνω
#συμπεριλαμβάνω
int main()
{
int *p;
δοκίμασε (
p = new int; // εκχώρηση μνήμης για int
) πιάσε (xalloc xa) (
cout<< "Allocation failure.\n";
επιστροφή 1;
}
*p = 20; // εκχωρώντας την τιμή 20 σε αυτήν τη θέση μνήμης
cout<< *р; // демонстрация работы путем вывода значения
διαγραφή p; // απελευθέρωση μνήμης
επιστροφή 0;
}

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

Όπως σημειώθηκε, μπορείτε να αρχικοποιήσετε τη μνήμη χρησιμοποιώντας τον νέο τελεστή. Για να το κάνετε αυτό, πρέπει να καθορίσετε την τιμή αρχικοποίησης σε παρενθέσεις μετά το όνομα τύπου. Για παράδειγμα, στο ακόλουθο παράδειγμα, η μνήμη που δείχνει το p έχει αρχικοποιηθεί σε 99:

#συμπεριλαμβάνω
#συμπεριλαμβάνω
int main()
{
int *p;
δοκίμασε (
p = new int(99); // αρχικοποίηση 99ο
) πιάσε (xalloc xa) (
cout<< "Allocation failure.\n";
επιστροφή 1;
}
cout<< *p;
διαγραφή p;
επιστροφή 0;
}

Μπορείτε να χρησιμοποιήσετε το νέο για να εκχωρήσετε πίνακες. Η γενική μορφή για έναν μονοδιάστατο πίνακα είναι:

variable_pointer = new variable_type [μέγεθος];

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

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

διαγραφή μεταβλητής δείκτη.

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

Το παρακάτω πρόγραμμα εκχωρεί μνήμη για έναν πίνακα 10 float στοιχείων. Στα στοιχεία του πίνακα εκχωρούνται τιμές από 100 έως 109 και, στη συνέχεια, τα περιεχόμενα του πίνακα εκτυπώνονται στην οθόνη:

#συμπεριλαμβάνω
#συμπεριλαμβάνω
int main()
{
float *p;
int i?
δοκίμασε (
p = νέο float ; // λήψη του δέκατου στοιχείου του πίνακα
) catch(xalloc xa) (
cout<< "Allocation failure.\n";
επιστροφή 1;
}
// εκχώρηση τιμών από 100 έως 109
για (i=0; i<10; i + +) p[i] = 100.00 + i;
// εξάγει τα περιεχόμενα του πίνακα
για (i=0; i<10; i++) cout << p[i] << " ";
διαγραφή p; // διαγραφή ολόκληρου του πίνακα
επιστροφή 0;
}

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;Εκ πρώτης όψεως, όλα είναι εντάξει - η χρήση του νέου αντιστοιχεί στη χρήση του 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);

Ο νέος τελεστής σάς επιτρέπει να εκχωρήσετε μνήμη για πίνακες. Επιστρέφει

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

int *p=new int[k]; // το σφάλμα δεν μπορεί να μετατραπεί από "int (*)" σε "int *"

int (*p)=new int[k]; // δεξιά

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

int *a = new int (10234);

Αυτή η επιλογή δεν μπορεί να χρησιμοποιηθεί για την προετοιμασία πινάκων. Ωστόσο

Στη θέση της τιμής αρχικοποίησης μπορείτε να τοποθετήσετε μια λίστα διαχωρισμένη με κόμμα

Οι τιμές μεταβιβάστηκαν στον κατασκευαστή κατά την εκχώρηση μνήμης για τον πίνακα (μάζα

siv νέα αντικείμενα που καθορίζονται από τον χρήστη). Μνήμη για μια σειρά αντικειμένων

μπορεί να εκχωρηθεί μόνο εάν η αντίστοιχη κλάση έχει

υπάρχει ένας προεπιλεγμένος κατασκευαστής.

matr())(; // προεπιλεγμένος κατασκευαστής

matr(int i,float j): a(i),b(j) ()

( matr mt(3,.5);

matr *p1=new matr; // true p1 - δείκτης σε 2 αντικείμενα

matr *p2=new matr (2,3.4); // λάθος, η προετοιμασία δεν είναι δυνατή

matr *p3=new matr (2,3.4); // true p3 – αρχικοποιημένο αντικείμενο

( int i; // στοιχείο δεδομένων της κλάσης Α

A()() // κατασκευαστής της κλάσης Α

~A()() // καταστροφέας της κλάσης Α

( A *a,*b; // περιγραφή των δεικτών σε ένα αντικείμενο της κλάσης Α

float *c,*d; // περιγραφή δεικτών σε στοιχεία τύπου float

a=νέο A; // εκχώρηση μνήμης για ένα αντικείμενο της κλάσης Α

b=νέο A; // εκχώρηση μνήμης για έναν πίνακα αντικειμένων κλάσης Α

c=new float; // εκχώρηση μνήμης για ένα στοιχείο float

d=new float; // εκχώρηση μνήμης για μια σειρά από στοιχεία float

διαγραφή α? // απελευθέρωση μνήμης που καταλαμβάνεται από ένα αντικείμενο

διαγραφή β? // απελευθέρωση μνήμης που καταλαμβάνεται από μια σειρά αντικειμένων

διαγραφή γ? // απελευθέρωση της μνήμης ενός στοιχείου float

διαγραφή d? ) // απελευθέρωση της μνήμης μιας σειράς στοιχείων float

Οργάνωση εξωτερικής πρόσβασης σε τοπικά στοιχεία της τάξης (φίλος)

Έχουμε ήδη εξοικειωθεί με τον βασικό κανόνα του OOP - δεδομένα (εσωτερικά

μεταβλητές) του αντικειμένου προστατεύονται από εξωτερικές επιρροές και η πρόσβαση σε αυτές μπορεί να είναι

αποκτήστε μόνο χρησιμοποιώντας τις συναρτήσεις (μεθόδους) του αντικειμένου. Υπάρχουν όμως και τέτοιες περιπτώσεις

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

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

σε μια κλάση για να αποκτήσετε άμεση πρόσβαση σε εσωτερικές μεταβλητές. Ωστόσο, σε

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

η νέα δυνατότητα μπορεί να είναι περιττή. Ταυτόχρονα, μερικές φορές υπάρχει α

την ανάγκη οργάνωσης της άμεσης πρόσβασης σε εσωτερικά (τοπικά) δεδομένα

δύο διαφορετικά αντικείμενα από μια συνάρτηση. Ταυτόχρονα, στη C++ μια συνάρτηση δεν μπορεί

μπορεί να είναι συστατικό δύο διαφορετικών κλάσεων.

Για την υλοποίηση αυτού, ο προσδιοριστής φίλου εισήχθη στη C++. Αν κάποιοι

η συνάρτηση ορίζεται ως συνάρτηση φίλου για κάποια κλάση, τότε:

Δεν είναι στοιχείο συνάρτησης αυτής της κλάσης.

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

Παρακάτω είναι ένα παράδειγμα όπου μια εξωτερική συνάρτηση έχει πρόσβαση

δεδομένα εσωτερικής τάξης.

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

χρησιμοποιώντας namespace std?

kls(int i,int J) : i(I),j(J) () // κατασκευαστής

int max() (return i>j? i: j;) // συνάρτηση συνιστώσας της κλάσης kls

φίλος διπλή διασκέδαση (int, kls&); // δήλωση φίλου εξωτερικής λειτουργίας διασκέδαση

διπλή διασκέδαση(int i, kls &x) // εξωτερική συνάρτηση

(επιστροφή (διπλό)i/x.i;

cout<< obj.max() << endl;

Στη C(C++), υπάρχουν τρεις γνωστοί τρόποι μετάδοσης δεδομένων σε μια συνάρτηση: με τιμή

είναι δυνατό σε κάποιο υπάρχον αντικείμενο. Διακρίνονται οι παρακάτω χρόνοι:

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

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

οι παράμετροι μεταβιβάστηκαν σε αυτό, στη συνέχεια στη γλώσσα C πρέπει να δηλωθούν είτε

σε παγκόσμιο επίπεδο, ή η εργασία μαζί τους σε λειτουργίες πραγματοποιείται μέσω μεταβίβασης σε

Περιέχει δείκτες σε αυτές τις μεταβλητές. Στην C++, τα ορίσματα μπορούν να περάσουν σε μια συνάρτηση

ρούμι σημειώνεται &.

void fun1(int,int);

void fun2(int &,int &);

( int i=1,j=2; // i και j είναι τοπικές παράμετροι

cout<< "\n адрес переменных в main() i = "<<&i<<" j = "<<&j;

cout<< "\n i = "<Γιατί η 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. Είναι μια ανοιχτή cross-platform 3D μηχανή γραμμένη σε 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



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

Κορυφή