مشروع عملي - إدارة الذاكرة — المؤشرات والذاكرة
مشروع: قائمة مترابطة (Linked List) بإدارة ذاكرة آمنة
هذا المشروع يجمع كل مفاهيم الدورة في هيكل بيانات حقيقي يعتمد على المؤشرات: القائمة المترابطة المفردة.
الفكرة
كل عقدة تحتوي قيمة ومؤشراً للعقدة التالية. القائمة تحفظ مؤشراً لأول عقدة (head). آخر عقدة يشير next فيها إلى nullptr.
head → [10 | •] → [20 | •] → [30 | nullptr]لماذا قائمة مترابطة؟
- إضافة وحذف من البداية بسرعة ثابتة دون تحريك العناصر (بعكس المصفوفة).
- لكن الوصول للعنصر رقم i بطيء: يجب المرور من البداية.
- في التطبيق العملي std::vector أسرع في أغلب الحالات بسبب ترتيب الذاكرة، لكن القائمة المترابطة أفضل تمرين لفهم المؤشرات، وأساس هياكل أعقد (الأشجار، الرسوم البيانية).
المرحلة 1: بمؤشرات خام
- push_front: عقدة جديدة بـ new، تشير لـ head القديم، ثم تصبح head.
- print: مؤشر متحرك من head حتى nullptr.
- remove(value): البحث مع تتبع العقدة السابقة، ثم ربط السابقة بالتالية، ثم delete.
- الهادم (destructor): يمر على كل العقد ويحررها. بدونه تسرب كامل.
المرحلة 2: بـ unique_ptr
next يصبح unique_ptr<Node>: كل عقدة تملك التالية. عند حذف عقدة أو تدمير القائمة تتحرر السلسلة تلقائياً، فلا يوجد delete في الكود إطلاقاً. هذا هو الأسلوب الحديث.
نقاط دقيقة
- عند الحذف يجب تحديث head إذا كانت العقدة المحذوفة هي الأولى.
- الهادم بالمؤشرات الخام يجب أن يحفظ next قبل delete العقدة الحالية، وإلا استخدمت ذاكرة محررة.
- القائمة بمؤشرات خام يجب ألا تُنسخ بالنسخ الافتراضي (سيحرر النسختان نفس العقد مرتين). الحل الكامل هو "قاعدة الثلاثة/الخمسة" التي ستتعلمها في دورة البرمجة الكائنية؛ في المثال منعنا النسخ صراحة بـ = delete.
تحقق من عملك
ترجم بـ -fsanitize=address وشغّل: إذا لم تظهر أي رسالة، فلا تسرب ولا وصول خاطئ.
أفكار للتوسعة
push_back (مع مؤشر tail)، reverse (عكس الاتجاهات)، size، أو قائمة مترابطة مزدوجة.
#include <iostream>
#include <memory>
using namespace std;
// ===== المرحلة 1: مؤشرات خام =====
struct RawNode {
int value;
RawNode* next;
};
struct RawList {
RawNode* head = nullptr;
RawList() = default;
RawList(const RawList&) = delete; // منع النسخ: كان سيحرر العقد مرتين
RawList& operator=(const RawList&) = delete;
void pushFront(int value) {
head = new RawNode{value, head};
}
bool remove(int value) {
RawNode* prev = nullptr;
RawNode* cur = head;
while (cur != nullptr && cur->value != value) {
prev = cur;
cur = cur->next;
}
if (cur == nullptr) return false; // غير موجود
if (prev == nullptr) head = cur->next; // كانت الأولى
else prev->next = cur->next;
delete cur;
return true;
}
void print() const {
for (RawNode* n = head; n != nullptr; n = n->next) cout << n->value << " -> ";
cout << "nullptr" << endl;
}
~RawList() { // بدونه: تسرب كامل
RawNode* cur = head;
while (cur != nullptr) {
RawNode* next = cur->next; // احفظ التالي قبل الحذف
delete cur;
cur = next;
}
}
};
// ===== المرحلة 2: unique_ptr — لا delete إطلاقاً =====
struct Node {
int value;
unique_ptr<Node> next; // كل عقدة تملك التالية
};
struct SafeList {
unique_ptr<Node> head;
void pushFront(int value) {
head = make_unique<Node>(Node{value, std::move(head)});
}
bool remove(int value) {
unique_ptr<Node>* link = &head; // مؤشر إلى الرابط الذي يشير للعقدة الحالية
while (*link && (*link)->value != value) link = &(*link)->next;
if (!*link) return false;
*link = std::move((*link)->next); // العقدة القديمة تتحرر تلقائياً
return true;
}
void print() const {
for (Node* n = head.get(); n != nullptr; n = n->next.get()) cout << n->value << " -> ";
cout << "nullptr" << endl;
}
};
int main() {
RawList raw;
for (int v : {30, 20, 10}) raw.pushFront(v);
raw.print(); // 10 -> 20 -> 30 -> nullptr
raw.remove(20);
raw.remove(10);
raw.print(); // 30 -> nullptr
SafeList safe;
for (int v : {3, 2, 1}) safe.pushFront(v);
safe.print(); // 1 -> 2 -> 3 -> nullptr
cout << boolalpha << "removed 2: " << safe.remove(2) << ", removed 9: " << safe.remove(9) << endl;
safe.print(); // 1 -> 3 -> nullptr
return 0; // الهادم و unique_ptr يحرران كل شيء
}
اختبر فهمك