متى تحتاج إلى المدمرات الافتراضية في C++: تحليل في 2025

مدرب بايثون أونلاين للمبتدئين

تعلم Python بسهولة دون إرهاق نظري. حل مهام عملية مع التحقق التلقائي واكتب الكود مباشرة في المتصفح.

ابدأ الدورة

المدمرات الافتراضية في C++: لماذا لا تزال ذات صلة (ومتى يمكنك تجاهلها)

\n\n

إذا كنت قد كتبت كودًا بلغة C++ وواجهت الوراثة، فمن المحتمل أنك رأيت هذا السؤال على Stack Overflow مع عشرات الآلاف من المشاهدات: «متى تستخدم المدمرات الافتراضية؟». في عام 2025، لم يفقد الموضوع حدته - لا تزال تسريبات الذاكرة بسبب إغفال virtual تحصد ضحاياها في بيئات الإنتاج. ولكن هناك جانب آخر: الإضافة العشوائية لـ virtual في كل مكان ليست أفضل ممارسة أيضًا.

\n\n

دعنا نفهم الأمر مرة واحدة وإلى الأبد: متى يكون المدمر الافتراضي إلزاميًا، ومتى يكون مجرد لمسة جيدة، ومتى يكون وجوده علامة على بنية سيئة. هيا بنا.

\n\n

إجابة مختصرة (للمستعجلين)

\n\n

المدمر الافتراضي مطلوب فقط عندما تخطط لحذف كائن من فئة مشتقة عبر مؤشر إلى الفئة الأساسية. إذا لم يكن هناك مثل هذا السيناريو في الكود - فلا حاجة لـ virtual. ولكن هناك تفاصيل دقيقة سنناقشها أدناه.

\n\n

لماذا هذه مشكلة أصلاً؟ تشريح التسرب

\n\n

دعنا نلقي نظرة على النمط المعاكس الكلاسيكي الذي يكسر كود المبتدئين (وليس فقط).

\n\n
#include <iostream>\n\nclass Base {\npublic:\n    ~Base() { \n        std::cout << "Base destructor\\n"; \n    }\n};\n\nclass Derived : public Base {\nprivate:\n    int* big_data = new int[1000]; // موارد تحتاج إلى تحرير\npublic:\n    Derived() { std::cout << "Derived created\\n"; }\n    ~Derived() {\n        delete[] big_data;\n        std::cout << "Derived destructor\\n";\n    }\n};\n\nint main() {\n    Base* ptr = new Derived();\n    delete ptr;  // عفوًا! سيتم استدعاء ~Base() فقط\n    return 0;\n}
\n\n

المخرجات:

\n
Derived created\nBase destructor
\n\n

المصفوفة big_data تسربت. لم يتم استدعاء مدمر Derived لأن المترجم، عند رؤية النوع Base*، استدعى مدمر هذه الفئة بالضبط. وهو ليس افتراضيًا.

\n\n

الحل - إضافة virtual قبل مدمر Base:

\n\n
class Base {\npublic:\n    virtual ~Base() = default;  // أو جسم فارغ\n};
\n\n

الآن عند delete ptr، سيتم تنفيذ ~Derived() أولاً، ثم ~Base(). تم تحرير الموارد، لا تسرب للذاكرة، والمحلل الثابت سعيد.

\n\n

متى يكون المدمر الافتراضي إلزاميًا (قاعدة حديدية)

\n\n

هناك حالة واحدة غير مشروطة: إذا كانت الفئة مخصصة للاستخدام متعدد الأشكال عبر مؤشر أو مرجع إلى الفئة الأساسية، وسيتم حذف كائنات الفئات المشتقة عبر هذا المؤشر.

\n\n

عمليًا، هذا يعني:

\n
    \n
  • تحتوي الفئة على دالة افتراضية واحدة على الأقل (نقية أو غير نقية)
  • \n
  • تخطط لاستخدام Base* أو Base& للعمل مع Derived
  • \n
  • يتم إنشاء الكائنات في الكومة (new) وحذفها عبر delete
  • \n
\n\n

مثال - التسلسل الهرمي الكلاسيكي للأشكال:

\n\n
class Shape {\npublic:\n    virtual double area() const = 0;\n    virtual ~Shape() = default;  // إلزامي للفئة المجردة\n};\n\nclass Circle : public Shape {\n    double radius;\npublic:\n    explicit Circle(double r) : radius(r) {}\n    double area() const override { return 3.14159 * radius * radius; }\n};\n\nint main() {\n    std::vector<std::unique_ptr<Shape>> shapes;\n    shapes.push_back(std::make_unique<Circle>(5.0));\n    // unique_ptr سيهتم بـ delete بنفسه، لكن مدمر Shape يجب أن يكون virtual\n    return 0;\n}
\n\n

لاحظ: حتى إذا كنت تستخدم std::unique_ptr (وهو النهج الموصى به في 2025)، فإن المدمر الافتراضي لا يزال مطلوبًا. unique_ptr يستدعي delete عبر مؤشر إلى الفئة الأساسية -

المدونات

توصيات الكتب