C++ / المتوسط / الدرس 7
RAII والأعمار والمؤشرات الذكية
اربط تحرير الموارد بعمر الكائن، وميّز المقابض المالكة من المستعيرين، واختر الملكية المنفردة أو المشتركة وفق حاجة التصميم.
في هذه الصفحة
أهداف الدرس
- شرح التنظيف الحتمي عند مغادرة النطاق وترتيب الإتلاف العكسي.
- نقل ملكية unique_ptr دون نسخه.
- تمييز دورات shared_ptr ودور weak_ptr.
المورد يحتاج إلى مالك
قد يكون المورد ذاكرة مخصصة أو ملفًا مفتوحًا أو قفلًا ممسوكًا أو قدرة أخرى مكتسبة. تضع تقنية RAII، أي اكتساب المورد أثناء التهيئة، الملكية في كائن يحرر مدمّره تلك القدرة. يركز الاسم على الإنشاء، لكن فائدته التنظيف الموثوق عند الخروج العادي من النطاق وفك المكدس. يعبّر المؤشر الخام عادة عن وصول دون ملكية في التطبيقات الحديثة. قبل اختيار نوع مؤشر اسأل هل يلزم التخصيص الديناميكي أصلًا؛ فكثيرًا ما تعبّر قيمة محلية أو حاوية قياسية عن أبسط عمر.
التنظيف يتبع بنية النطاق
تُدمّر الكائنات التلقائية بعكس ترتيب اكتمال إنشائها عند مغادرة نطاقها. تظهر النطاقات المتداخلة حدود الأعمار بوضوح. يمثّل مثال التتبع الاكتساب والتحرير برسائل بدل حجز مورد خارجي حقيقي. يُدمّر B قبل A لأن نطاقه الداخلي ينتهي أولًا. يجب ألا يسمح المدمّر بخروج استثناء أثناء فك المكدس، إذ قد ينهي البرنامج. للملفات الحقيقية استخدم تدفق ملف مالكًا، وتعامل مع فشل التفريغ أو الإغلاق الصريح حيث يحتاج التطبيق إلى رصده.
#include <iostream>
#include <string>
#include <utility>
struct Trace {
std::string name;
explicit Trace(std::string value) : name(std::move(value)) {
std::cout << "acquire " << name << "\n";
}
~Trace() { std::cout << "release " << name << "\n"; }
};
int main() {
Trace outer{"A"};
{ Trace inner{"B"}; std::cout << "work\n"; }
}
acquire A
acquire B
work
release B
release A
تبيّن الرسائل ترتيب النطاق فقط. ينبغي أن تكون مدمّرات الموارد الحقيقية غير رامِية؛ إذ يمكن ضبط تدفقات الإخراج لترمي استثناءات.
استخدام unique_ptr لمالك ديناميكي واحد
تملك std::unique_ptr<T> كائنًا ديناميكيًا واحدًا افتراضيًا وتحذفه عند إتلاف المقبض المالك. تنشئ make_unique الكائن ومالكه معًا. يُمنع نسخ المقبض لأنه يكرر ملكية حصرية. ينقل النقل الملكية ويترك unique_ptr المصدر فارغًا؛ وهذا ضمان محدد أقوى من قاعدة الحالة العامة بعد النقل. تعيد get مؤشرًا مستعارًا، وتتخلى release عن الملكية دون إتلاف الكائن ونادرًا ما تلزم. تجنب delete على مؤشر مستعار أو إنشاء مالك مستقل ثانٍ منه.
#include <iostream>
#include <memory>
#include <utility>
int main() {
auto first = std::make_unique<int>(42);
auto second = std::move(first);
std::cout << std::boolalpha << (first == nullptr) << " " << *second << "\n";
}
true 42
يملك second العدد نفسه. يُضمن فراغ first ولا يُفك مرجعه، ويحرر second الذاكرة عند الخروج من النطاق.
مراجعة الملكية عند كل حد
ميّز لكل معامل بين مراقبة مورد وتعديله مؤقتًا واستلام ملكيته. يستعير T& عادة كائنًا قائمًا، ويستقبل unique_ptr<T> بالقيمة الملكية صراحة. قد تنقل إعادة unique_ptr نتيجة متعددة الأشكال ديناميكيًا، بينما تكون القيمة العادية أبسط لكثير من الكائنات. وثّق هل يمكن أن يغادر المستعير الاستدعاء. لا يُستنتج التنظيف من الأسماء وحدها؛ افحص أنواع المقابض ودوال الحذف والنطاق. اختبر العودة المبكرة ومسارات الاستثناء مع النجاح، فهي ما يفوته التنظيف اليدوي غالبًا.
الأخطاء الشائعة وإصلاحها
مالكان يحذفان الكائن نفسه.
- لماذا يحدث؟
- أُنشئ مؤشّران ذكيان مستقلان من مؤشر خام واحد.
- كيف تصلحه؟
- انقل المالك المنفرد القائم أو انسخ المالك المشترك القائم.
- كيف تتجنب تكراره؟
- أنشئ الملكية مرة واحدة وأبقِ المؤشرات الخام للمراقبة.
كائن مشترك لا يُدمّر مطلقًا.
- لماذا يحدث؟
- روابط الملكية تشكّل دورة في العد المرجعي.
- كيف تصلحه؟
- اجعل رابطًا خلفيًا مناسبًا ضعيفًا وافحص نتيجة lock.
- كيف تتجنب تكراره؟
- ارسم شبكة الملكية منفصلة عن شبكة الوصول.
فك مرجع unique_ptr منقول منه.
- لماذا يحدث؟
- افتُرض أن المصدر يحتفظ بالملكية.
- كيف تصلحه؟
- استخدم المالك الوجهة أو هيّئ المصدر بقيمة جديدة.
- كيف تتجنب تكراره؟
- عامل نقل الملكية كانتقال حالة ظاهر.
اختبر فهمك
1. يجب أن تستهلك دالة عنصرًا مخصصًا ديناميكيًا وتحتفظ به بعد العودة. اختر بين Widget& وWidget* وstd::unique_ptr<Widget> بالقيمة.
اعرض الحل المشروح
استخدم unique_ptr<Widget> بالقيمة ليكون انتقال الملكية صريحًا. يمرّر المستدعي std::move(owner)، وتنقل الدالة معاملها إلى مالكها المحتفَظ به. يستعير المرجع أو المؤشر الخام فقط ما لم يوجد عقد استثنائي منفصل. افحص المالك إذا كان null غير صالح، أو وثّق حالة المقبض الفارغ.
2. عقدتان تملكان بعضهما بـshared_ptr. فسّر عدم كفاية إسقاط المقبضين الخارجيين وكيف يغيّر رابط خلفي ضعيف الإتلاف.
اعرض الحل المشروح
تبقى لكل عقدة إشارة مالكة من الأخرى فلا يصل عدّ أي منهما إلى صفر. استبدل الرابط الخلفي غير المالك منطقيًا بـweak_ptr. عند انتهاء الملكية الخارجية للأولى يستطيع إتلافها تحرير رابطها المالك وإتلاف الثانية أيضًا. استخدم lock قبل مراقبة الهدف الضعيف لاحتمال انتهاء عمره.
أهم ما تعلمته
- اربط التنظيف بأعمار الكائنات المالكة.
- فضّل القيم ثم الملكية المنفردة ثم المشاركة المبررة.
- يجب ألا يتجاوز المستعير عمر مورده.