المدمرات الافتراضية في C++: لماذا لا تزال ذات صلة (ومتى يمكنك تجاهلها)
\n\nإذا كنت قد كتبت كودًا بلغة C++ وواجهت الوراثة، فمن المحتمل أنك رأيت هذا السؤال على Stack Overflow مع عشرات الآلاف من المشاهدات: «متى تستخدم المدمرات الافتراضية؟». في عام 2025، لم يفقد الموضوع حدته - لا تزال تسريبات الذاكرة بسبب إغفال virtual تحصد ضحاياها في بيئات الإنتاج. ولكن هناك جانب آخر: الإضافة العشوائية لـ virtual في كل مكان ليست أفضل ممارسة أيضًا.
دعنا نفهم الأمر مرة واحدة وإلى الأبد: متى يكون المدمر الافتراضي إلزاميًا، ومتى يكون مجرد لمسة جيدة، ومتى يكون وجوده علامة على بنية سيئة. هيا بنا.
\n\nإجابة مختصرة (للمستعجلين)
\n\nالمدمر الافتراضي مطلوب فقط عندما تخطط لحذف كائن من فئة مشتقة عبر مؤشر إلى الفئة الأساسية. إذا لم يكن هناك مثل هذا السيناريو في الكود - فلا حاجة لـ virtual. ولكن هناك تفاصيل دقيقة سنناقشها أدناه.
لماذا هذه مشكلة أصلاً؟ تشريح التسرب
\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المخرجات:
\nDerived created\nBase destructor\n\nالمصفوفة big_data تسربت. لم يتم استدعاء مدمر Derived لأن المترجم، عند رؤية النوع Base*، استدعى مدمر هذه الفئة بالضبط. وهو ليس افتراضيًا.
الحل - إضافة virtual قبل مدمر Base:
class Base {\npublic:\n virtual ~Base() = default; // أو جسم فارغ\n};\n\nالآن عند delete ptr، سيتم تنفيذ ~Derived() أولاً، ثم ~Base(). تم تحرير الموارد، لا تسرب للذاكرة، والمحلل الثابت سعيد.
متى يكون المدمر الافتراضي إلزاميًا (قاعدة حديدية)
\n\nهناك حالة واحدة غير مشروطة: إذا كانت الفئة مخصصة للاستخدام متعدد الأشكال عبر مؤشر أو مرجع إلى الفئة الأساسية، وسيتم حذف كائنات الفئات المشتقة عبر هذا المؤشر.
\n\nعمليًا، هذا يعني:
\n- \n
- تحتوي الفئة على دالة افتراضية واحدة على الأقل (نقية أو غير نقية) \n
- تخطط لاستخدام
Base*أوBase&للعمل معDerived\n - يتم إنشاء الكائنات في الكومة (
new) وحذفها عبرdelete\n
مثال - التسلسل الهرمي الكلاسيكي للأشكال:
\n\nclass 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 عبر مؤشر إلى الفئة الأساسية -