Kodax

مشروع عملي - إدارة الذاكرة — المؤشرات والذاكرة

مشروع: قائمة مترابطة (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 يحرران كل شيء
}
اختبر فهمك