الذاكرة الديناميكية: الجديدة والحذف ومخاطرها
تعرف على كيفية عمل التخصيص الديناميكي مع الجديد والحذف، وافهم مخاطر تسرب الذاكرة، والاستخدام المزدوج، والاستخدام بعد الاستخدام مجانًا، واكتشف لماذا يجب أن يحتوي C++ الحديث على صفر جديد/حذف أولي.
التخصيص الديناميكي مع الجديد والحذف
المكدس سريع ولكنه محدود الحجم والنطاق. عندما تحتاج إلى كائنات تتجاوز نطاق إنشائها، أو يكون حجمها غير معروف في وقت الترجمة، يمكنك تخصيصها في الكومة (متجر مجاني).
يخصصnew T(args)الذاكرة لـTواحد، ويستدعي المنشئ الخاص به، ويعيدT*. يستدعيdelete pالمدمر ويحرر الذاكرة. للمصفوفات:new T[n]وdelete[] p. عدم تطابقnewمعdelete[]أوnew[]معdeleteهو سلوك غير محدد - لن يتمكن المترجم من اكتشاف ذلك.
new, delete, and Their Array Variants
Here is the raw allocation API. Study it so you recognize it in legacy code, but never write this in new code — use smart pointers instead.
#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
}التنسيب جديد
الموضع الجديد ينشئ كائنًا على عنوان ذاكرة محدد دون تخصيصه. يتم استخدامه بواسطة المخصصات وتجمعات الذاكرة والحاويات مثلstd::vectorداخليًا. يجب عليك استدعاء المدمر بشكل صريح - يجب عدم استخدامdeleteعلى كائنات الموضع الجديد.
#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. لا يتم إرجاع الذاكرة أبدًا إلى نظام التشغيل (حتى خروج العملية). في الخادم الذي يعمل لفترة طويلة، يستهلك هذا في النهاية كل ذاكرة الوصول العشوائي المتوفرة.
2. Double Free - استدعاءdeleteعلى نفس المؤشر مرتين. يفسدdeleteالثاني هياكل البيانات الداخلية لمخصص الكومة، مما يؤدي غالبًا إلى حدوث أعطال في وقت لاحق في تعليمات برمجية غير ذات صلة. وهذا أمر صعب للغاية لتصحيح الأخطاء.
3. الاستخدام بعد الاستخدام — الوصول إلى الذاكرة بعدdelete. ربما تم إعادة استخدام الذاكرة لتخصيص مختلف، لذا قد تتمكن من قراءة أو كتابة بيانات كائن آخر. هذه ثغرة أمنية (خطأ من فئة CVE).
Consider:f(new A(), new B()). The compiler may evaluate arguments in any order and may interleave them:
1. Allocate memory for A
2. Allocate memory for B
3. Construct A
4. Construct B
If step 4 throws, A is leaked — nobody callsdeleteon it. Evenf(std::shared_ptr(new A()), std::shared_ptr(new B()))had this problem before C++17 (which guarantees that each argument is fully evaluated before the next). The fix: always usestd::make_uniqueorstd::make_shared — they perform allocation and construction as a single, exception-safe operation.
نظرة عامة على المخصصين المخصصين
يستخدمnew/deleteالافتراضي المُخصص العام (عادةًmalloc/free)، وهو للأغراض العامة ولكنه ليس الأمثل لجميع الأنماط. المخصصون المخصصون يقدمون ما يلي:
- مخصصات الساحة/الخطية — ارفع المؤشر، وحرر كل شيء مرة واحدة. رائعة لبيانات اللعبة لكل إطار.
- موزعات المجمعات — التخصيص المسبق للكتل ذات الحجم الثابت. O(1) تخصيص/مجاني، بدون تجزئة.
- مخصصات المكدس — تخصيص LIFO، سريع للغاية.
تقبل حاويات C++ معلمات قالب المخصص:std::vector. توفر مساحة الاسمstd::pmr(موارد الذاكرة متعددة الأشكال) في C++17 إطار عمل مخصص متعدد الأشكال لوقت التشغيل والذي يتجنب مشكلة معلمة القالب.
في C++ الحديث (C++14 والإصدارات الأحدث)، يجب أن يحتوي كود التطبيق المكتوب جيدًا على صفرnewوdelete:
- ملكية فردية: استخدمstd::make_unique
- الملكية المشتركة: استخدمstd::make_shared
- المصفوفات الديناميكية: استخدمstd::vector
- السلاسل: استخدمstd::string
- الكائنات الاختيارية: استخدمstd::optional
قد يظهرnew/deleteالخام في كود المكتبة (المخصصات، الحاويات المخصصة)، لكن كود التطبيق يجب ألا يحتاج إليها أبدًا. إذا رأيتnewفي مراجعة التعليمات البرمجية، فاسأل لماذا — هناك دائمًا بديل أكثر أمانًا.
- عدم تطابق
new/delete[]أوnew[]/deleteهو سلوك غير محدد - تسرب الذاكرة، والاستخدام المزدوج، والاستخدام بعد الاستخدام المجاني هي الأخطاء الثلاثة القاتلة في الذاكرة
- يمكن أن يتسرب
f(new A(), new B())في حالة رمي المُنشئ (ما قبل C++17 حتى مع أغلفة Shared_ptr) - ضع بنيات جديدة في عنوان معين - قم بتدميرها يدويًا، ولا تستخدم
deleteأبدًا - يجب أن يحتوي كود تطبيق C++ الحديث على صفر
new/delete- استخدم المؤشرات والحاويات الذكية
اختبار — اختبر معرفتك
(15 نقطة خبرة)1. ماذا يحدث إذا استخدمت`delete`على الذاكرة المخصصة لـ`new[]`؟
2. لماذا يمكن لـ`f(new A(), new B())`تسرب الذاكرة؟
3. كيف يجب عليك تدمير كائن تم إنشاؤه باستخدام موضع جديد؟