Kodax

دورة حياة الكائن و RAII — OOP في C++

دورة حياة الكائن و RAII

أهم فكرة تميّز C++ عن لغات مثل Python و Java: المدمّر يُستدعى في لحظة محددة ومعروفة — عند انتهاء عمر الكائن. لا يوجد جامع قمامة ينتظر. هذا يتيح النمط الأقوى في اللغة: RAII.

متى ينتهي عمر الكائن؟

| نوع الكائن | يُبنى | يُهدم |

|---|---|---|

| محلي (على الـ Stack) | عند تعريفه | عند نهاية الكتلة } التي عُرّف فيها |

| عضو في كائن آخر | قبل جسم مُنشئ الحاوي | بعد جسم مدمّر الحاوي |

| عنصر في vector | عند الإضافة | عند الحذف أو تدمير الـ vector |

| على الـ Heap (new) | عند new | عند delete فقط (أو عبر مؤشر ذكي) |

| عام أو static | قبل main (أو عند أول استخدام) | بعد انتهاء main |

ترتيب الهدم عكس ترتيب البناء

#include <iostream>
#include <string>
using namespace std;

struct Noisy {
    string n;
    explicit Noisy(string s) : n(s) { cout << "بناء " << n << '\n'; }
    ~Noisy() { cout << "هدم " << n << '\n'; }
};

int main() {
    Noisy a("a");
    {
        Noisy b("b");
        Noisy c("c");
    }                         // هدم c ثم هدم b
    Noisy d("d");
}                             // هدم d ثم هدم a

كالأطباق المكدسة: آخر ما وُضع أول ما يُرفع. هذا منطقي: c قد يعتمد على b، فيجب أن يُهدم قبله.

ترتيب بناء الأعضاء

الأعضاء تُبنى بترتيب تصريحها في الصنف دائماً — وليس بترتيب قائمة التهيئة:

class Buffer {
    int size;
    int* data;        // مُصرَّح بعد size ← يُبنى بعده
public:
    explicit Buffer(int n) : size(n), data(new int[n]) {}   // آمن: size جاهز قبل data
    ~Buffer() { delete[] data; }
    Buffer(const Buffer&) = delete;
    Buffer& operator=(const Buffer&) = delete;
};

لو صُرّح عن data قبل size واستخدمت size لتهيئة data، لقرأت قيمة غير مهيأة. المترجم ينبّهك مع -Wall (تحذير -Wreorder).

RAII: Resource Acquisition Is Initialization

الاسم غريب لكن الفكرة بسيطة:

> اربط كل مورد (ذاكرة، ملف، قفل، اتصال) بـكائن: احصل عليه في المُنشئ وحرّره في المدمّر.

النتيجة: المورد يُحرر تلقائياً ودائماً عند خروج الكائن من النطاق — سواء انتهت الدالة طبيعياً، أو بـ return مبكر، أو برمي استثناء.

بدون RAII

#include <cstdio>

bool saveReport(bool fail) {
    std::FILE* f = std::fopen("report.txt", "w");
    if (!f) return false;
    if (fail) return false;          // ❌ نسينا fclose ← تسرب
    std::fputs("ok", f);
    std::fclose(f);
    return true;
}

كل مسار خروج يجب أن يتذكر التحرير. مع عشرة مسارات وثلاثة موارد يصبح هذا مستحيلاً.

مع RAII

#include <fstream>

bool saveReport2(bool fail) {
    std::ofstream f("report.txt");   // المُنشئ يفتح الملف
    if (!f) return false;
    if (fail) return false;          // ✅ المدمّر يغلق الملف
    f << "ok";
    return true;
}                                    // ✅ وهنا أيضاً

RAII في كل مكان في المكتبة القياسية

| المورد | الصنف |

|---|---|

| ذاكرة ديناميكية | unique_ptr، shared_ptr، vector، string |

| ملفات | ifstream، ofstream |

| أقفال الخيوط | lock_guard، unique_lock |

| الخيوط (C++20) | jthread (ينتظر الخيط تلقائياً) |

لهذا نادراً ما تكتب delete في C++ الحديثة: تختار صنفاً يطبق RAII.

قواعد عند كتابة صنف RAII

1. احصل على المورد في المُنشئ؛ إن فشل، لا تُنشئ كائناً نصف جاهز (ارمِ استثناء — في دورة المتقدم).

2. حرّر في المدمّر، ولا ترمِ استثناءات من المدمّر.

3. امنع النسخ (= delete) أو عرّفه بشكل صحيح — موضوع الدرس القادم "النسخ والنقل".

تمرين

اكتب صنف ScopedTimer يسجّل الوقت في مُنشئه (std::chrono::steady_clock::now()) ويطبع المدة المنقضية في مدمّره. ضعه في أول دالة بطيئة وشاهد كيف يقيس زمنها مهما كانت طريقة خروجها.

#include <iostream>
#include <string>
using namespace std;

// يطبع عند البناء والهدم لنرى دورة الحياة
class Tracer {
    string name;
public:
    explicit Tracer(string n) : name(move(n)) { cout << "  + " << name << "\n"; }
    ~Tracer() { cout << "  - " << name << "\n"; }
};

class Car {
    Tracer engine{"المحرك"};   // يُبنى أولاً (ترتيب التصريح)
    Tracer wheels{"العجلات"};  // ثم هذا
public:
    Car() { cout << "  السيارة جاهزة\n"; }
    ~Car() { cout << "  تفكيك السيارة\n"; }   // جسم المدمّر قبل هدم الأعضاء
};

// RAII: الكائن يحصل على المورد في المُنشئ ويحرره في المدمّر
class Connection {
    string server;
public:
    explicit Connection(string s) : server(move(s)) { cout << "فتح الاتصال بـ " << server << "\n"; }
    ~Connection() { cout << "إغلاق الاتصال بـ " << server << "\n"; }
    Connection(const Connection&) = delete;             // مورد لا يُنسخ
    Connection& operator=(const Connection&) = delete;
    void send(const string& msg) const { cout << "إرسال: " << msg << "\n"; }
};

bool process(int value) {
    Connection conn("db.local");
    if (value < 0) {
        cout << "قيمة سالبة: خروج مبكر\n";
        return false;          // المدمّر يُستدعى هنا...
    }
    conn.send(to_string(value));
    return true;
}                              // ...وهنا

int main() {
    cout << "1) ترتيب البناء والهدم:\n";
    {
        Car c;
    }

    cout << "\n2) نطاقات متداخلة:\n";
    Tracer a("a");
    {
        Tracer b("b");
        Tracer c("c");
    }                          // - c ثم - b

    cout << "\n3) RAII مع الخروج المبكر:\n";
    process(5);
    process(-1);

    cout << "\nنهاية main\n";
    return 0;
}                              // - a
اختبر فهمك