تخطَّ إلى المحتوى
ARAB CODEمجتمع المطورين العرب

الذاكرة في C++: المكدس والكومة والمؤشرات الذكية وRAII

درسنُشر ٢٤ أكتوبر ٢٠٢٦ · 0 رد · 1 مشاهدة#c++#ذاكرة#raii#مؤشرات-ذكية
قبل ساعتين

مقدمة

C++ تمنحك تحكماً كاملاً في الذاكرة، وهذا مصدر قوتها وأخطائها الشهيرة: تسريب الذاكرة، والمؤشرات المعلقة، والتحرير المزدوج. الخبر الجيد أن C++ الحديثة (منذ C++11) توفر أدوات تجعل هذه الأخطاء نادرة إن استخدمتها بشكل صحيح.

أهداف التعلم

  • فهم عمر الكائنات على المكدس والكومة.
  • تطبيق مبدأ RAII.
  • استخدام std::unique_ptr وstd::shared_ptr بدل new/delete.
  • الاعتماد على الحاويات القياسية مثل std::vector وstd::string.

1. المكدس: العمر مرتبط بالنطاق

#include <string>
void greet() {
    std::string name = "سارة";   // يُنشأ هنا
    // ...
}                                // يُدمَّر تلقائياً عند الخروج من النطاق

الكائنات على المكدس تُحرر تلقائياً بترتيب عكسي لإنشائها. سريعة وآمنة، لكن حجمها محدود وعمرها لا يتجاوز النطاق.

2. الكومة والمشكلة القديمة

int* scores = new int[100];
// ... إن حدث استثناء أو return مبكر هنا، لن يصل البرنامج إلى السطر التالي
delete[] scores;   // نسيانه = تسريب، تكراره = سلوك غير معرّف

الإدارة اليدوية هشة: كل مسار خروج من الدالة يجب أن يتذكر التحرير.

3. RAII: اربط المورد بعمر كائن

RAII (Resource Acquisition Is Initialization) يعني: الكائن يحجز المورد في المُنشئ ويحرره في المُدمِّر. بما أن المُدمِّر يُستدعى تلقائياً عند الخروج من النطاق — حتى مع الاستثناءات — لا ينسى أحد التحرير.

std::vector وstd::string وstd::fstream كلها تطبق RAII:

#include <vector>
#include <numeric>

double average(int n) {
    std::vector<int> scores(n, 0);   // ذاكرة على الكومة، يديرها vector
    // ... ملء القيم
    if (scores.empty()) return 0.0;  // خروج مبكر آمن: vector يحرر ذاكرته
    return std::accumulate(scores.begin(), scores.end(), 0.0) / scores.size();
}

4. المؤشرات الذكية

#include <memory>

struct Session { std::string user; };

std::unique_ptr<Session> open(std::string user) {
    return std::make_unique<Session>(Session{std::move(user)});
}

int main() {
    auto s = open("ahmed");          // مالك وحيد
    auto t = std::move(s);           // نقل الملكية؛ s أصبح فارغاً
    // لا delete: تحرَّر عند انتهاء نطاق t
}
  • std::unique_ptr: مالك وحيد، بلا تكلفة إضافية تقريباً. الاختيار الافتراضي.
  • std::shared_ptr: ملكية مشتركة بعداد مراجع. استخدمه فقط عندما تكون الملكية مشتركة فعلاً.
  • std::weak_ptr: مرجع لا يملك، لكسر دوائر shared_ptr.

واستخدم دائماً std::make_unique وstd::make_shared بدل new.

5. المؤشرات والمراجع الخام

المؤشر الخام (T*) أو المرجع (T&) مناسبان للاستخدام دون ملكية: دالة تقرأ كائناً لا تملكه. القاعدة: الملكية بالمؤشر الذكي أو القيمة، والاستعارة بالمرجع.

void print(const Session& s) { std::cout << s.user << '\n'; }   // لا يملك

احذر المرجع المعلق: لا تعِد مرجعاً أو مؤشراً إلى متغير محلي.

const std::string& bad() { std::string s = "x"; return s; }   // معلّق! s دُمّر

6. أدوات تكشف الأخطاء

g++ -std=c++20 -g -fsanitize=address,undefined main.cpp -o main && ./main

AddressSanitizer يكشف التسريب، والكتابة خارج الحدود، واستخدام الذاكرة بعد تحريرها، مع رقم السطر.

تمرين عملي

  1. اكتب صنفاً FileReader يفتح ملفاً في المُنشئ ويغلقه في المُدمِّر، واطبع رسالة في كل منهما، ثم ارمِ استثناء داخل النطاق ولاحظ أن الإغلاق حدث.
  2. حوّل كوداً قديماً يستخدم new[]/delete[] إلى std::vector.
  3. شغّل برنامجاً فيه تسريب متعمد مع -fsanitize=address واقرأ التقرير.

أخطاء شائعة

  • new وdelete يدوياً في كود حديث.
  • shared_ptr في كل مكان «احتياطاً» (تكلفة وغموض في الملكية).
  • إعادة مراجع لمتغيرات محلية.
  • الخلط بين delete وdelete[].

الخلاصة

اجعل كل مورد مملوكاً لكائن يحرره تلقائياً (RAII). استخدم الحاويات القياسية وunique_ptr افتراضياً، وshared_ptr عند الحاجة الحقيقية فقط، وشغّل المُعقِّمات (Sanitizers) أثناء التطوير.

مصادر مقترحة

  • C++ Core Guidelines (قسم Resource management).
  • موقع cppreference.com.
  • كتاب A Tour of C++ لـ Bjarne Stroustrup.

الرد على الموضوع

سجّل الدخول للمشاركة في النقاش.