دورة حياة الكائن و 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اختبر فهمك