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

Δυναμική μνήμη: νέα, διαγραφή και οι κίνδυνοι τους

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

Δυναμική κατανομή με νέα και διαγραφή

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

Τοnew T(args)εκχωρεί μνήμη για έναT, καλεί τον κατασκευαστή του και επιστρέφει έναT*. Τοdelete pκαλεί τον καταστροφέα και ελευθερώνει τη μνήμη. Για πίνακες:new T[n]καιdelete[] p. Η αναντιστοιχίαnewμεdelete[]ήnew[]μεdeleteείναι απροσδιόριστη συμπεριφορά — ο μεταγλωττιστής δεν θα το καταλάβει.

νέο, διαγραφή και παραλλαγές συστοιχίας τους

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

new_delete.cpp
#include <iostream>
#include <cstring>

struct Widget {
    int id;
    Widget(int i) : id(i) { std::cout << "Widget " << id << " created\n"; }
    ~Widget() { std::cout << "Widget " << id << " destroyed\n"; }
};

int main() {
    // Single object
    Widget* w = new Widget(1);
    std::cout << "Using widget " << w->id << '\n';
    delete w;  // destructor called, memory freed

    // Array
    Widget* arr = new Widget[3]{{10}, {20}, {30}};
    for (int i = 0; i < 3; ++i)
        std::cout << arr[i].id << ' ';
    std::cout << '\n';
    delete[] arr;  // MUST use delete[] for new[]

    // UNDEFINED BEHAVIOR — don't do these:
    // delete arr;      // wrong: used delete instead of delete[]
    // delete[] w;      // wrong: used delete[] instead of delete
}

Placement new

Η Τοποθέτηση νέα κατασκευάζει ένα αντικείμενο σε μια συγκεκριμένη διεύθυνση μνήμης χωρίς εκχώρηση. Χρησιμοποιείται από κατανεμητές, ομάδες μνήμης και κοντέινερ όπως τοstd::vectorεσωτερικά. Πρέπει να καλέσετε ρητά τον καταστροφέα — ΤοdeleteΔΕΝ πρέπει να χρησιμοποιείται σε αντικείμενα τοποθέτησης-νέα.

placement_new.cpp
#include <iostream>
#include <new>       // required for placement new
#include <cstddef>   // std::byte
#include <memory>    // std::align

struct Sensor {
    int id;
    double reading;
    Sensor(int i, double r) : id(i), reading(r) {
        std::cout << "Sensor " << id << " init\n";
    }
    ~Sensor() { std::cout << "Sensor " << id << " shutdown\n"; }
};

int main() {
    // Pre-allocate raw storage (aligned)
    alignas(Sensor) std::byte buffer[sizeof(Sensor)];

    // Construct in pre-allocated memory
    Sensor* s = new (buffer) Sensor(42, 3.14);
    std::cout << "Reading: " << s->reading << '\n';

    // Must destroy explicitly — do NOT use delete
    s->~Sensor();  // explicit destructor call
    // delete s;    // WRONG — memory was not allocated by new
}

The Three Deadly Memory Bugs

Το Rawnew/deleteσας εκθέτει σε τρεις κατηγορίες σφαλμάτων:

1. Διαρροή μνήμης — εκχώρηση μεnewαλλά ποτέ κλήσηdelete. Η μνήμη δεν επιστρέφεται ποτέ στο λειτουργικό σύστημα (μέχρι την έξοδο της διαδικασίας). Σε έναν μακροχρόνιο διακομιστή, αυτό τελικά καταναλώνει όλη τη διαθέσιμη μνήμη RAM.

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

3. Χρήση-Μετά-Δωρεάν — πρόσβαση στη μνήμη μετά τοdelete. Η μνήμη μπορεί να έχει επαναχρησιμοποιηθεί για διαφορετική εκχώρηση, επομένως θα μπορούσατε να διαβάζετε ή να γράφετε τα δεδομένα ενός άλλου αντικειμένου. Αυτό είναι ένα θέμα ευπάθειας ασφαλείας (σφάλμα κατηγορίας CVE).

Παγίδα

Σκεφτείτε:f(new A(), new B()). Ο μεταγλωττιστής μπορεί να αξιολογήσει ορίσματα με οποιαδήποτε σειρά και μπορεί να τα παρεμβάλει:

1. Εκχωρήστε μνήμη για το A
2. Εκχωρήστε μνήμη για το B
3. Κατασκευάστε το Α
4. Κατασκευάστε το Β

Εάν το βήμα 4 ρίξει, το A διαρρέει — κανείς δεν καλεί τοdeleteσε αυτό. Ακόμη και τοf(std::shared_ptr(new A()), std::shared_ptr(new B()))είχε αυτό το πρόβλημα πριν από το C++17 (που εγγυάται ότι κάθε όρισμα αξιολογείται πλήρως πριν από το επόμενο). Η επιδιόρθωση: χρησιμοποιείτε πάνταstd::make_uniqueήstd::make_shared — εκτελούν την κατανομή και την κατασκευή ως μια ενιαία, ασφαλής για εξαιρέσεις λειτουργία.