«لماذا يجب على مبرمجي C++ تقليل استخدام 'new'؟»: تحليل السؤال الشهير
إذا كنت قد ألقيت نظرة على Stack Overflow يومًا ما بحثًا عن إجابات حول C++، فمن المحتمل أنك رأيت هذا السؤال. لقد حصل على آلاف الأصوات وعشرات الآلاف من المشاهدات. وليس من دون سبب. بالنسبة للمبتدئين، يبدو الموضوع غير بديهي: «كيف ذلك؟ في Java أو C# أكتب باستمرار new، وهذا طبيعي. لماذا هو فجأة سيء في C++؟»
اليوم، لن نترجم الإجابة من المنتدى الإنجليزي فقط. سنقوم بتفصيلها، لماذا تقليل استخدام new في C++ ليس مجرد نزوة للخبراء، بل مسألة أداء وأمان ومنطق سليم. ونعم، سنكتب كودًا، وننظر إلى لغة التجميع (تقريبًا) ونحسب النانو ثانية.
أين تذهب الذاكرة: المكدس مقابل الكومة
قبل أن نتحدث عن new، دعنا نتذكر الأساسيات. في C++، هناك مكانان رئيسيان لتخزين البيانات:
- المكدس (stack) — صغير، سريع، يُدار تلقائيًا. بمجرد أن يخرج المتغير عن النطاق، يتم تدميره. فوريًا.
- الكومة (heap) — كبير، بطيء، يتطلب إدارة يدوية. للحصول على ذاكرة هناك، تحتاج إلى استدعاء
new(أوmalloc). لتحريرها —delete.
كل استدعاء لـ new ليس مجرد «أعطني ذاكرة». إنه استدعاء نظام معقد، بحث عن كتلة حرة، تحديث للهياكل الإدارية. إنه أبطأ بمئات المرات من التخصيص على المكدس.
مثال رقم 1: مقارنة السرعة
#include <chrono>#include <iostream>#include <vector>
struct Point { double x, y, z;};
int main() { const int N = 1000000;
// تخصيص على المكدس (مصفوفة على المكدس) auto start = std::chrono::high_resolution_clock::now(); Point points_stack[1000]; // 1000 كائن على المكدس auto end = std::chrono::high_resolution_clock::now(); std::cout << "Stack time: " << std::chrono::duration_cast<std::chrono::nanoseconds>(end - start).count() << " ns";
// تخصيص ديناميكي عبر new start = std::chrono::high_resolution_clock::now(); std::vector<Point*> points_heap; for (int i = 0; i < 1000; ++i) { points_heap.push_back(new Point()); } end = std::chrono::high_resolution_clock::now(); std::cout << "Heap (new) time: " << std::chrono::duration_cast<std::chrono::nanoseconds>(end - start).count() << " ns";
// التنظيف بعد أنفسنا (إلزامي!) for (auto p : points_heap) delete p;
return 0;}النتيجة (على جهاز نموذجي):
Stack time: 0 nsHeap (new) time: 45200 nsالفرق عشرات الآلاف من المرات. بالطبع، هذا اختبار اصطناعي، لكنه يوضح بوضوح: كل new إضافي يعني ميكروثانية تُفقد من ميزانية أدائك.
المشكلة الرئيسية في new: التسريبات والتجزئة
إذا كانت المسألة تتعلق بالسرعة فقط، لكان الأمر نصف المشكلة. لكن new يجلب معه مجموعة كاملة من المشاكل:
1. تسريبات الذاكرة (memory leaks)
هل نسيت استدعاء delete؟ تهانينا، لديك تسرب. البرنامج يعمل لمدة ساعة، يوم، أسبوع — ثم ينهار فجأة مع std::bad_alloc. كلاسيكي من هذا النوع. خطيرة بشكل خاص في العمليات الطويلة (الخوادم، الألعاب، واجهات المستخدم الرسومية).
2. المؤشرات المعلقة (dangling pointers)
لقد حذفت الكائن، لكن المؤشر بقي. أي استخدام له — سلوك غير محدد (UB). قد يعمل البرنامج، قد ينهار، قد يستبدل موسيقاك بموسيقى تكنو. UB شر.
3. تجزئة الكومة
عمليات new/delete المتكررة تجزئ الكومة إلى قطع صغيرة. حتى لو كانت الذاكرة كافية، يصبح تخصيص كتلة كبيرة مستحيلًا. هذا يسمى التجزئة الخارجية