C++: المؤشرات الذكية المتقدمة
آخر تحديث: 2026-08-26
في الدرس 45، تعلمنا عن دلالات النقل.
الآن، سنتعمق في المؤشرات الذكية — الأداة الأساسية لإدارة الذاكرة في C++ الحديثة.
مع المؤشرات الذكية، لن تقلق بشأن تسرب الذاكرة بعد الآن.
1. نظرة عامة على المؤشرات الذكية
(1) 1.1 ما هي المؤشرات الذكية؟
المؤشرات الذكية هي أصناف قوالب تدير الذاكرة الديناميكية، وتحرر الذاكرة تلقائيًا (RAII).
ثلاثة أنواع من المؤشرات الذكية:
| المؤشر الذكي | الوظيفة | حالة الاستخدام |
|---|---|---|
unique_ptr |
الملكية الحصرية | الملكية الفردية لكائن |
shared_ptr |
الملكية المشتركة | أماكن متعددة تحتاج الوصول لنفس الكائن |
weak_ptr |
مرجع ضعيف | كسر المراجع الدائرية |
2. unique_ptr
(1) 2.1 الاستخدام الأساسي
unique_ptr هو مؤشر ذكي بـ ملكية حصرية — لا يمكن نسخه، فقط نقله.
مثال: الاستخدام الأساسي لـ unique_ptr (الصعوبة ⭐)
▶ مثال 2: مثال على الكود (الصعوبة ⭐)
#include <iostream>
#include <memory>
int main() {
std::unique_ptr<int> p1(new int(42));
std::cout << *p1 << std::endl; // Output: 42
// std::unique_ptr<int> p2 = p1; // ❌ خطأ! لا يمكن النسخ
std::unique_ptr<int> p2 = std::move(p1); // ✅ يمكن النقل
std::cout << *p2 << std::endl; // Output: 42
return 0;
} // p2 يحرر الذاكرة تلقائيًا
الخرج:
(خرج البرنامج)
(2) 2.2 المحذف المخصص
مثال: استخدام unique_ptr لإدارة ملف (الصعوبة ⭐⭐)
#include <iostream>
#include <memory>
#include <cstdio>
// محذف مخصص: إغلاق الملف
struct FileDeleter {
void operator()(FILE* fp) const {
if (fp) {
fclose(fp);
std::cout << "File closed" << std::endl;
}
}
};
int main() {
std::unique_ptr<FILE, FileDeleter> file(fopen("test.txt", "w"));
// لا حاجة لإغلاق الملف يدويًا، unique_ptr يستدعي FileDeleter تلقائيًا
return 0;
}
3. shared_ptr
(1) 3.1 الاستخدام الأساسي
shared_ptr هو مؤشر ذكي بـ ملكية مشتركة، يدير الذاكرة بـ عدد المراجع.
مثال: الاستخدام الأساسي لـ shared_ptr (الصعوبة ⭐)
#include <iostream>
#include <memory>
int main() {
std::shared_ptr<int> p1 = std::make_shared<int>(42);
std::cout << "Reference count: " << p1.use_count() << std::endl; // 1
{
std::shared_ptr<int> p2 = p1; // نسخ، عدد المراجع +1
std::cout << "Reference count: " << p1.use_count() << std::endl; // 2
} // p2 يتدمر، عدد المراجع -1
std::cout << "Reference count: " << p1.use_count() << std::endl; // 1
return 0;
}
(2) 3.2 make_shared مقابل new
موصى به: استخدم std::make_shared بدلاً من new.
| المقارنة | new |
make_shared |
|---|---|---|
| أمان الاستثناءات | قد يتسرب | آمن |
| الأداء | تخصيصان | تخصيص واحد |
| وضوح الكود | مطوّل | موجز |
// موصى به
auto p1 = std::make_shared<int>(42);
// غير موصى به
std::shared_ptr<int> p2(new int(42));
4. weak_ptr
(1) 4.1 لماذا نحتاج weak_ptr؟
المشكلة: shared_ptr قد يسبب مراجع دائرية، مما يؤدي إلى تسرب الذاكرة.
مثال: مرجع دائري (الصعوبة ⭐⭐⭐)
#include <iostream>
#include <memory>
struct Node {
std::shared_ptr<Node> next; // مرجع دائري!
~Node() { std::cout << "Node destroyed" << std::endl; }
};
int main() {
auto n1 = std::make_shared<Node>();
auto n2 = std::make_shared<Node>();
n1->next = n2; // مرجع دائري
n2->next = n1;
return 0;
} // ❌ n1 وn2 لن يتم تحريرهما (تسرب ذاكرة)
(2) 4.2 كسر المراجع الدائرية باستخدام weak_ptr
الحل: استبدل أحد shared_ptr بـ weak_ptr.
struct Node {
std::weak_ptr<Node> next; // استخدام weak_ptr، لا يزيد عدد المراجع
~Node() { std::cout << "Node destroyed" << std::endl; }
};
int main() {
auto n1 = std::make_shared<Node>();
auto n2 = std::make_shared<Node>();
n1->next = n2; // لا يزيد عدد المراجع
n2->next = n1;
return 0;
} // ✅ تم التحرير بشكل صحيح
5. دليل اختيار المؤشرات الذكية
(1) 5.1 كيف تختار؟
| السيناريو | التوصية |
|---|---|
| ملكية حصرية | unique_ptr |
| ملكية مشتركة | shared_ptr |
| مراقب | weak_ptr أو مؤشر خام |
| مصفوفات | unique_ptr<T> |
(2) 5.2 ما لا يجب فعله
| الخطأ | التوضيح |
|---|---|
| لا تهيئ عدة shared_ptr من نفس المؤشر الخام | سيسبب تحريرًا مزدوجًا |
| لا تخلط المؤشرات الخام والمؤشرات الذكية | يكسر RAII |
| لا تستدعِ delete يدويًا | دع المؤشرات الذكية تدير ذلك |
6. تمرين: إدارة الموارد بالمؤشرات الذكية
▶ مثال 1: استخدام المؤشرات الذكية لإدارة اتصال قاعدة بيانات (الصعوبة ⭐⭐⭐)
#include <iostream>
#include <memory>
// محاكاة اتصال قاعدة بيانات
class DatabaseConnection {
public:
DatabaseConnection() {
std::cout << "Connecting to database" << std::endl;
}
~DatabaseConnection() {
std::cout << "Disconnected" << std::endl;
}
void query(const std::string& sql) {
std::cout << "Executing SQL: " << sql << std::endl;
}
};
int main() {
// استخدام unique_ptr لإدارة المورد الحصري
std::unique_ptrDatabaseConnection conn(new DatabaseConnection());
conn->query("SELECT * FROM users");
return 0;
} // يقطع الاتصال تلقائيًا
الخرج:
Connecting to database
Disconnected
Executing SQL:
❓ أسئلة شائعة
unique_ptr أسرع (لا يوجد عبء عد المراجع). يُفضّل unique_ptr.س: هل shared_ptr آمن للخيوط؟ ج: - تعديلات عدد المراجع آمنة للخيوط - لكن الكائن المُشار إليه ليس آمنًا للخيوط (يحتاج حماية mutex)
▶ مثال 3: الملكية المشتركة مع shared_ptr (الصعوبة ⭐)
#include <iostream>
#include <memory>
class Resource {
public:
Resource() { std::cout << "Construct" << std::endl; }
~Resource() { std::cout << "Destruct" << std::endl; }
};
int main() {
{
std::shared_ptr<Resource> p1 = std::make_shared<Resource>();
std::shared_ptr<Resource> p2 = p1; // ملكية مشتركة
std::cout << "Reference count: " << p1.use_count() << std::endl;
}
// يغادر النطاق، يتدمر تلقائيًا
std::cout << "Program ended" << std::endl;
return 0;
}
الخرج:
Construct
Destruct
Reference count:
Program ended
shared_ptr يشارك الملكية ويحرر تلقائيًا عندما يصل عدد المراجع إلى الصفر. استخدام make_shared أكثر أمانًا للإنشاء.
| النقطة الرئيسية | الملخص |
|---|---|
| unique_ptr | ملكية حصرية، لا يمكن نسخه |
| shared_ptr | ملكية مشتركة، عد المراجع |
| weak_ptr | مرجع ضعيف، يكسر المراجع الدائرية |
| make_shared | الطريقة الموصى بها، آمنة للاستثناءات |
| مبدأ الاختيار | يُفضّل unique_ptr، استخدم shared_ptr عند الحاجة |
📖 ملخص
- shared_ptr: ملكية مشتركة، يُدار بعدد المراجع
- unique_ptr: ملكية حصرية، غير قابل للنسخ
- weak_ptr: مرجع ضعيف، لا يزيد عدد المراجع
- make_shared/make_unique: طرق الإنشاء الموصى بها
📝 تمارين
-
أساسي (الصعوبة ⭐): استخدم
unique_ptrلإدارةintمُخصص ديناميكيًا، أُنشئ بـmake_unique. حاول نسخunique_ptr(سينفشل التجميع)، ثم استخدمmoveبدلاً من ذلك. -
متوسط (الصعوبة ⭐⭐): استخدم
shared_ptrلتنفيذ كائنات متعددة تشترك في نفس المورد. أنشئ اثنين منshared_ptrيشيران إلى نفس الكائن، واخرجuse_count()لمراقبة تغيرات عدد المراجع. -
متقدم (الصعوبة ⭐⭐⭐): استخدم
weak_ptrلحل مشكلة المرجع الدائري فيshared_ptr. أنشئ صنفين A وB يحملانshared_ptrلبعضهما، ولاحظ تسرب الذاكرة. ثم انتقل إلىweak_ptrوتحقق من التحرير الصحيح.
- unique_ptr له ملكية حصرية، غير قابل للنسخ لكن قابل للنقل
- shared_ptr له ملكية مشتركة، يُدار بعدد المراجع
- weak_ptr مع shared_ptr يتجنب المراجع الدائرية
- make_unique/make_shared طرق آمنة للاستثناءات للإنشاء
- المحذفات المخصصة تتعامل مع تحرير الموارد الخاصة
الدرس التالي: مقدمة في برمجة القوالب الفوقية (#47)