لماذا المؤشر وليس الكائن؟ C++ تحت الغطاء للمبتدئين

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

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

ابدأ الدورة

مقدمة: السؤال الأبدي للمبتدئ

\n\n

إذا كنت قد ألقيت نظرة على Stack Overflow في قسم C++، فمن المحتمل أنك رأيت سؤالاً حصد آلاف المشاهدات: «لماذا يجب أن أستخدم المؤشر بدلاً من الكائن نفسه؟». وهذا ليس مجرد فضول عابر. كل من يبدأ في تعلم C++ بعد Python أو JavaScript يواجه هذه المعضلة. يبدو أن الكائن مريح: أنشأته، استدعيت دالته، نسيت أمره. فلماذا كل هذه النجوم والأسهم وnew؟

\n\n

في الحقيقة، يكمن الجواب في كيفية إدارة C++ للذاكرة تحت الغطاء. في هذه المقالة، لن نقدم تعريفاً جافاً فحسب، بل سنحلل سيناريوهات حقيقية من حياة المبرمج، حيث يصبح المؤشر ليس مجرد خيار مريح، بل الخيار الصحيح الوحيد. هل أنت مستعد للنظر في محرك الكود الخاص بك؟ هيا بنا!

\n\n

الكائن مقابل المؤشر: ماذا يقول المترجم؟

\n\n

دعنا نصل إلى صلب الموضوع مباشرة. تخيل أنك تكتب صنف Monster للعبة RPG الخاصة بك.

\n\n
class Monster {\npublic:\n    Monster() { std::cout << "تم إنشاء الوحش!\\n"; }\n    ~Monster() { std::cout << "تم تدمير الوحش!\\n"; }\n    void roar() { std::cout << "RAAAWR!\\n"; }\n};
\n\n

الآن، طريقتان لاستخدام هذا الصنف:

\n\n

1. كائن على المكدس (إدارة تلقائية)

\n
void spawnOnStack() {\n    Monster m;  // الكائن يعيش هنا\n    m.roar();\n}  // هنا يتم تدمير m تلقائياً
\n\n

2. مؤشر إلى الكومة (إدارة يدوية)

\n
void spawnOnHeap() {\n    Monster* m = new Monster(); // الكائن في الكومة\n    m->roar();\n    delete m;  // يجب حذفه بأنفسنا!\n}
\n\n

يبدو أن الخيار الأول أبسط وأكثر أماناً. فلماذا الثاني؟ تحت الغطاء يكمن هنا الاختلاف الرئيسي: المكدس (stack) والكومة (heap).

\n\n
    \n
  • المكدس — صغير، سريع، لكن الكائن يعيش فقط داخل الكتلة {}.
  • \n
  • الكومة — كبيرة، أبطأ، لكن الكائن يعيش حتى تقوم بحذفه.
  • \n
\n\n

عندما يكون حجم الكائن ضخماً (مثل نسيج 4K)، فإن تخزينه على المكدس يعني المخاطرة بفيضان المكدس. هنا يأتي دور المؤشرات.

\n\n

أفضل 5 أسباب لاستخدام المؤشر (بدلاً من الكائن)

\n\n

الآن بعد أن فهمنا الفرق الأساسي، دعنا نحلل المواقف المحددة حيث يكون المؤشر ليس ترفاً، بل ضرورة.

\n\n

1. تعدد الأشكال: عندما لا يكون النوع معروفاً فوراً

\n

هذا هو السبب الأكثر شيوعاً. لنفترض أن لديك صنفاً أساسياً Character وأصنافاً فرعية Warrior وMage. إذا أنشأت كائناً على المكدس، يجب عليك تحديد النوع بدقة:

\n\n
Warrior w;  // كل شيء جيد، لكنه ليس متعدد الأشكال
\n\n

ماذا لو تم اختيار نوع الشخصية أثناء اللعبة؟ فقط المؤشر يعطي المرونة:

\n\n
Character* hero;\nif (choice == "warrior")\n    hero = new Warrior();\nelse\n    hero = new Mage();\n\nhero->attack(); // سيتم استدعاء دالة الصنف الفرعي!\ndelete hero;
\n\n

تحت الغطاء: يستخدم المترجم جدول الدوال الافتراضية (vtable). يخزن المؤشر العنوان الذي يقع فيه الكائن، وعند استدعاء الدالة ينظر إلى vtable. مع الكائن على المكدس، هذه الحيلة لا تنجح — النوع مثبت بشكل صارم.

\n\n

2. الإسناد إلى دالة دون نسخ

\n

انظر إلى هذا الكود:

\n\n
void process(Monster m) { // إسناد بالقيمة!\n    m.roar();\n}\n\nMonster bigBoss;\nprocess(bigBoss); // نسخ!
\n\n

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

\n\n
void process(Monster* m)                

المدونات

توصيات الكتب