لماذا قد تطلب ذاكرة أثناء التشغيل
حتى الآن، كل متغير أنشأته كان يعيش على المكدّس (stack): حجمه معروف وقت الترجمة ويُدمَّر تلقائيًا عند انتهاء نطاقه. هذا سريع وآمن، لكنه لا يتعامل مع الحالة التي لا تعرف فيها كمية الذاكرة المطلوبة إلا أثناء تشغيل البرنامج، مثل مخزن مؤقت يتحدد حجمه بإدخال المستخدم، أو بنية يجب أن تبقى حية بعد انتهاء الدالة التي أنشأتها، أو مخطط لا يُعرف شكله مسبقًا.
لهذه الحالات، تتيح لك C++ الحجز من الكومة (وتسمى أيضًا free store) باستخدام new، وإعادتها باستخدام delete. يحجز تعبير new كتلة، ويشغّل البانية (constructor)، ويعيد مؤشرًا إليها، مبنيًا مباشرةً على المؤشرات التي رأيتها في الصفحة السابقة.
المتغير p نفسه يعيش على المكدّس، فهو مجرد مؤشر. أما الـ int الذي يشير إليه فيعيش على الكومة ويبقى حيًّا حتى تحرّره باستخدام delete، مهما تعاقبت النطاقات.
المكدّس مقابل الكومة
هذا التمييز هو السبب الكامل لوجود new، لذا يستحق أن نجعله ملموسًا.
void demo() {
int a = 10; // على المكدّس - يختفي عندما ترجع demo()
int* b = new int(10); // 'b' على المكدّس، والـ int الذي يشير إليه على الكومة
} // 'a' يُدمَّر؛ والـ int على الكومة يُسرَّب - لا يُحرَّر أبدًا
الاختلافات الرئيسية:
- المكدّس - عمر تلقائي، سريع جدًا، حجم محدود (عادة بضعة ميغابايت)، يُحرَّر نيابةً عنك عند الخروج من النطاق.
- الكومة - عمر يدوي، أبطأ قليلًا، كبيرة، وتُحرَّر فقط عندما تستدعي
delete.
المقايضة هي مرونة مقابل مسؤولية: ذاكرة الكومة تعيش بالضبط المدة التي تريدها، لكنك تصبح المسؤول عن تذكّر تحريرها.
حجز المصفوفات باستخدام new[]
عندما تحتاج إلى كتلة يتحدد طولها أثناء التشغيل، استخدم صيغة المصفوفة new T[n]. تعيد مؤشرًا إلى العنصر الأول، وتحرّرها باستخدام delete[] المقابلة.
القاعدة صارمة ومن السهل الإخلال بها: الذاكرة الناتجة عن new تُحرَّر بـ delete، والذاكرة الناتجة عن new[] تُحرَّر بـ delete[]. خلطهما، باستخدام delete arr على شيء حُجز بـ new[]، هو سلوك غير معرّف، حتى لو بدا أنه يعمل على جهازك.
الأخطاء الكلاسيكية الثلاثة
تنطوي الإدارة اليدوية للذاكرة على مجموعة صغيرة من الأخطاء تمثّل معظم علل الكومة. تعلّم التعرّف على الثلاثة جميعًا.
1. تسريب الذاكرة - لا تستدعي delete أبدًا. تبقى الكتلة محجوزة إلى الأبد. غير ضار مرة واحدة، وقاتل داخل حلقة.
void leaky() {
int* p = new int(5);
// ... لا delete ...
} // p يختفي؛ والـ int على الكومة أصبح الآن غير قابل للوصول وغير محرَّر
2. المؤشر المعلّق - تستخدم الذاكرة بعد تحريرها. لا يزال المؤشر يحمل العنوان القديم، لكن تلك الذاكرة لم تعد ملكك.
3. التحرير المزدوج - تستدعي delete على الكتلة نفسها مرتين. هذا يفسد سجلات الكومة الداخلية وغالبًا ما يؤدي إلى انهيار.
int* p = new int(1);
delete p;
delete p; // تحرير مزدوج - سلوك غير معرّف، غالبًا انهيار
ضبط المؤشر على nullptr بعد حذفه يُبطل كلًّا من الاستخدام المعلّق والتحرير المزدوج: فك مرجعية nullptr ينهار فورًا (سهل التنقيح)، وdelete nullptr هو صراحةً عملية آمنة لا تفعل شيئًا.
دورة واقعية: احجز، استخدم، حرّر
بجمع كل ذلك، هذا هو شكل الإدارة اليدوية الصحيحة: احجز، استخدم، حرّر مرة واحدة بالضبط، ولا تلمس المؤشر بعد ذلك.
لاحظ أن delete u يقوم بأمرين لنوع الصنف (class): فهو يشغّل الهادمة (destructor) للكائن أولًا، ثم يحرّر الذاكرة الخام. هذا الترتيب يصبح مهمًا بمجرد أن تمتلك كائناتك مواردها الخاصة.
مأزق دقيق: إذا رُمي استثناء بين new وdelete، فلن يُنفَّذ delete أبدًا فتُسرّب الذاكرة. تغليف كل عملية حجز داخل try/catch للتعامل مع ذلك أمر ممل ومعرّض للخطأ، وهذا بالضبط هو ما تحلّه الصفحة التالية.
التالي: المؤشرات الذكية
لقد رأيت الآن التكلفة الكاملة لإدارة الذاكرة يدويًا: كل new هو وعد بـ delete لاحقًا، وأي تحرير واحد منسي أو مكرّر أو مبكّر هو سلوك غير معرّف. نادرًا ما تقدّم C++ الحديثة هذا الوعد يدويًا. تقدّم الصفحة التالية المؤشرات الذكية - std::unique_ptr وstd::shared_ptr - وهي كائنات تمتلك حجزًا على الكومة وتستدعي delete نيابةً عنك تلقائيًا عند خروجها من النطاق، فتحوّل الأخطاء الكلاسيكية الثلاثة إلى أمور يتولّاها المترجم وتقنية RAII بدلًا منك.
الأسئلة الشائعة
ما الفرق بين new و delete في C++؟
يحجز new ذاكرة على الكومة أثناء التشغيل ويعيد مؤشرًا إليها؛ بينما يحرّر delete الذاكرة التي حُجزت باستخدام new. يجب أن يقابل كل new تمامًا عملية delete واحدة، وإلا فستُسرّب الذاكرة. أما للمصفوفات فاستخدم new[] مع delete[].
ماذا يحدث إذا نسيت استدعاء delete في C++؟
يحدث تسريب للذاكرة: تبقى كتلة الكومة محجوزة طوال عمر برنامجك حتى لو لم يعد أي شيء يشير إليها. التسريب الواحد غالبًا غير ضار، لكن التسريبات داخل حلقة أو في خدمة طويلة التشغيل تتراكم حتى تنفد ذاكرة البرنامج فينهار.
هل ينبغي أن أستخدم new و delete مباشرة في C++ الحديثة؟
نادرًا. فضّل الحاويات مثل std::vector أو المؤشرات الذكية (std::unique_ptr، std::shared_ptr) التي تحرّر الذاكرة تلقائيًا. يستحق فهم new/delete الخام لأن المؤشرات الذكية تغلّفها، لكنها في الكود اليومي مصدر للتسريبات والمؤشرات المعلّقة.