ما هو Object Slicing في C++: شرح كامل مع كود
\n\nتخيل الموقف: لقد كتبت تسلسلاً هرمياً جميلاً للفئات، وتستخدم تعدد الأشكال (polymorphism)، وكل شيء يعمل بشكل مثالي... ثم يظهر خطأ لا يمكنك الإمساك به. البيانات تختفي ببساطة. الكائن يبدو وكأنه "ينحف" أمام عينيك. هل هذا مألوف؟ مرحباً بك في عالم object slicing (سَبر الكائنات) — أحد أكثر الفخاخ غدراً في لغة C++.
\n\nفي هذه المقالة، سنشرح ما هو object slicing، ولماذا يحدث، وكيفية اكتشافه، والأهم من ذلك، كيفية التعامل معه. ستتعلم ليس فقط النظرية، بل سترى أيضاً أمثلة حية من الكود ستساعدك على تجنب هذه المشكلة في مشاريعك.
\n\nكيف يعمل الوراثة ولماذا يحدث السَبر
\n\nأولاً، دعنا نتذكر كيف تدير C++ الذاكرة عند استخدام الوراثة. عندما تنشئ كائناً من فئة مشتقة، فإنه يحتوي على نسخة كاملة من الفئة الأساسية بالإضافة إلى الحقول والطرق الخاصة به.
\n\nclass Animal {\npublic:\n std::string name;\n int age;\n Animal(const std::string& n, int a) : name(n), age(a) {}\n virtual void sound() const { std::cout << "Some sound\\n"; }\n};\n\nclass Dog : public Animal {\npublic:\n std::string breed;\n Dog(const std::string& n, int a, const std::string& b) \n : Animal(n, a), breed(b) {}\n void sound() const override { std::cout << "Woof!\\n"; }\n};\n\nكائن Dog يستهلك ذاكرة أكبر من Animal. وهنا يكمن جذر المشكلة.
ماذا يحدث عند Object Slicing
\n\nObject slicing (سَبر الكائن) هو فقدان البيانات الذي يحدث عندما يتم تعيين كائن من فئة مشتقة لمتغير من الفئة الأساسية بالقيمة (by value). يقوم المترجم "بقص" (سَبر) الجزء الخاص بالفئة المشتقة، تاركاً فقط الجزء الأساسي.
\n\nمثال كلاسيكي:
\n\nDog myDog("Rex", 3, "Labrador");\nAnimal animal = myDog; // ها هو السَبر!\n\nstd::cout << animal.name << ", " << animal.age << std::endl;\n// animal.breed — غير متاح!\n// animal.sound() ستطبع "Some sound"، وليس "Woof!"\n\nماذا حدث هنا؟ لقد أنشأنا كلباً كاملاً باسم وعمر وسلالة. ولكن عند التعيين animal = myDog، قام المترجم بنسخ الجزء الخاص بـ Animal فقط. اختفى حقل breed دون أثر. والدالة الافتراضية؟ لقد تم "سَبرها" أيضاً — الآن يتم استدعاء طريقة الفئة الأساسية.
لماذا هذا خطير جداً؟
\n\nالمشكلة هي أن object slicing ينتهك مبدأ تعدد الأشكال. أنت تعتقد أنك تتعامل مع كلب، ولكن في الواقع أنت تتعامل مع حيوان مجرد. هذا يؤدي إلى:
\n- \n
- فقدان البيانات (سلالة الكلب، لون السيارة، طراز الهاتف — كلها تختفي) \n
- عمل غير صحيح للدوال الافتراضية \n
- أخطاء يصعب اكتشافها لا يتم التقاطها في مرحلة الترجمة \n
يكون السَبر خبيثاً بشكل خاص في الحاويات. انظر إلى هذا الكود:
\n\nstd::vector<Animal> zoo;\nzoo.push_back(Dog("Rex", 3, "Labrador"));\nzoo.push_back(Cat("Whiskers", 2, "Persian"));\n\nfor (const auto& animal : zoo) {\n animal.sound(); // دائماً "Some sound"!\n}\n\nتتوقع سماع نباح ومواء، لكنك تحصل فقط على أصوات مجردة. الحاوية std::vector<Animal> تخزن كائنات من النوع Animal فقط، وتقوم بسَبر كل ما هو إضافي.
تحليل تفصيلي: كيف يعمل السَبر بالضبط
\n\nلفهم الآلية، دعنا ننظر إلى أحجام الكائنات:
\n\nstd::cout << "Size of Animal: " << sizeof(Animal) << std::endl;\nstd::cout << "Size of Dog: " << sizeof(Dog) << std::endl;\n\n// الإخراج (يعتمد على المنصة):\n// Size of Animal: 40\n// Size of Dog: 56\n\nعندما تكتب Animal animal = dog، يقوم المترجم باستدعاء المنشئ الناسخ (copy constructor) للفئة الأساسية، والذي ينسخ فقط الأعضاء الموجودين في الفئة الأساسية، متجاهلاً تماماً الأعضاء الإضافية من الفئة المشتقة.