لماذا لا يزال C++ يخيف حتى كبار المطورين؟
\nC++ هي لغة تمنحك تحكمًا كاملاً في الذاكرة، ولكن فقط حتى تنسى وضع فاصلة منقوطة أو تستدعي دالة افتراضية من المُنشئ عن طريق الخطأ. تشتهر مقابلات C++ بقسوتها: قد يُسأل المرء عن الحقول البتية، وعن بنية الجدول الافتراضي، وعن تفاصيل std::move. إذا كنت تستعد لمقابلة تقنية، فهذه المقالة هي قائمة التحقق الخاصة بك. سنستعرض أكثر المواضيع غدرًا التي ستظهر بشكل شبه مؤكد في المقابلة، وسنوضح كيفية الإجابة عليها لتجنب الفشل.
في بوابة التعليم PythonLib، نناقش غالبًا كيفية الانتقال من Python إلى C++ والعكس. لكن اليوم هو C++ الخالص. لا حلول وسط.
\n\n1. الدوال الافتراضية وجداول VTable: كيف تتجنب الفخ
\nالسؤال: "ماذا يحدث إذا استدعيت دالة افتراضية من مُنشئ الفئة الأساسية؟" هو سؤال كلاسيكي. 70% من المرشحين يجيبون بشكل خاطئ. دعنا نفهم الأمر.
\n\nلماذا هذا خطير؟
\nعند استدعاء دالة افتراضية في مُنشئ الفئة الأساسية، لم يتم إنشاء كائن الفئة المشتقة بعد. يشير الجدول الافتراضي (VTable) في هذه اللحظة إلى دوال الفئة الأساسية، وليس الفئة المشتقة. ونتيجة لذلك، لا يعمل تعدد الأشكال.
\n\n#include <iostream>\n\nclass Base {\npublic:\n Base() { callFoo(); } // استدعاء في المُنشئ\n virtual void foo() { std::cout << "Base::foo\\n"; }\n void callFoo() { foo(); }\n};\n\nclass Derived : public Base {\npublic:\n Derived() {}\n virtual void foo() override { std::cout << "Derived::foo\\n"; }\n};\n\nint main() {\n Derived d; // سيُطبع: Base::foo، وليس Derived::foo\n return 0;\n}\n\nالخلاصة: لا تستدعِ أبدًا الدوال الافتراضية في المُنشئات والمُدمّرات. إذا سألك المحاور، اشرح بثقة آلية VTable.
\n\n2. المؤشرات الذكية: RAII وإدارة الذاكرة
\nلا تخلو أي مقابلة C++ جادة من هذا الموضوع. الأسئلة حول std::unique_ptr و std::shared_ptr و std::weak_ptr هي الأساس. لكن غالبًا ما يُسأل عن المراجع الدائرية و std::enable_shared_from_this.
كيف تتجنب تسرب الذاكرة مع shared_ptr؟
\nتخيل أن لديك كائنين يشير كل منهما إلى الآخر عبر shared_ptr. لن يصبح عداد المراجع صفرًا أبدًا - ستتسرب الذاكرة. الحل: يجب أن يكون أحد المؤشرات من نوع weak_ptr.
#include <memory>\n#include <iostream>\n\nstruct Node {\n std::shared_ptr<Node> next;\n std::weak_ptr<Node> prev; // مرجع ضعيف لتجنب الدورة\n ~Node() { std::cout << "Node destroyed\\n"; }\n};\n\nint main() {\n auto a = std::make_shared<Node>();\n auto b = std::make_shared<Node>();\n a->next = b;\n b->prev = a; // weak_ptr لا يزيد العداد\n // عند الخروج من النطاق، سيتم حذف كلا العقدتين بشكل صحيح\n return 0;\n}\n\nفي المقابلة، تأكد من ذكر مفهوم RAII (اكتساب الموارد هو التهيئة) - وهي فلسفة C++ التي تربط عمر المورد بعمر الكائن.
\n\n3. دلالات النقل و std::move: سحر أم خدعة؟
\nسؤال نموذجي: "ماذا يفعل std::move؟" الإجابة "ينقل الكائن" غير صحيحة. std::move فقط يحول النوع إلى مرجع rvalue. النقل الفعلي يحدث في مُنشئ النقل أو عامل الإسناد بالنقل.
مثال للمقابلة
\n#include <string>\n#include <vector>\n#include <iostream>\n\nclass MyString {\n std::string data;\npublic:\n MyString(const std::string& s) : data(s) {}\n // مُنشئ النقل\n MyString(MyString&& other) noexcept : data(std::move(other.data)) {\n std::cout << "Move constructor c