التقطيع في C++ حيث أكون مخطئا؟
-
21-12-2019 - |
سؤال
قرأت عن مشكلة التقطيع في لغة C++ وحاولت بعض الأمثلة (أنا من خلفية Java).لسوء الحظ، لا أفهم بعض السلوكيات.حاليًا، أنا عالق في هذا المثال (مثال بديل من الإصدار الثالث من Efficent C++).هل يمكن لأحد أن يساعدني على فهم ذلك؟
صفي البسيط لأحد الوالدين:
class Parent
{
public:
Parent(int type) { _type = type; }
virtual std::string getName() { return "Parent"; }
int getType() { return _type; }
private:
int _type;
};
صفي البسيط للطفل:
class Child : public Parent
{
public:
Child(void) : Parent(2) {};
virtual std::string getName() { return "Child"; }
std::string extraString() { return "Child extra string"; }
};
الرئيسي:
void printNames(Parent p)
{
std::cout << "Name: " << p.getName() << std::endl;
if (p.getType() == 2)
{
Child & c = static_cast<Child&>(p);
std::cout << "Extra: " << c.extraString() << std::endl;
std::cout << "Name after cast: " << c.getName() << std::endl;
}
}
int main()
{
Parent p(1);
Child c;
printNames(p);
printNames(c);
}
بعد الإعدام أحصل على:
اسم:الأبوين
اسم:الأبوين
إضافي:سلسلة اضافية للطفل
الاسم بعد الإلقاء:الأبوين
أفهم السطرين الأولين، لأنه سبب "التقطيع".لكنني لا أفهم لماذا يمكنني تحويل الطفل إلى أحد الوالدين عبر قالب ثابت.مكتوب في الكتاب أنه بعد التقطيع، سيتم تقطيع جميع المعلومات المتخصصة.لذلك أعتقد أنني لا أستطيع الإدلاء ص داخل ج, ، لأنه ليس لدي معلومات (في بداية الدالة printNames، يتم إنشاء كائن أصلي جديد بدون المعلومات الإضافية).
بالإضافة إلى ذلك، لماذا أحصل على "الاسم بعد طاقم التمثيل:"الوالد" إذا كان الإرسال ناجحًا بدلاً من "الاسم بعد الإرسال:طفل"؟
المحلول
النتيجة الخاصة بك هي ضربة غير عادية من الحظ السيئ.هذه هي النتيجة التي أحصل عليها:
Name: Parent
Name: Parent
Extra: Child extra string
bash: line 8: 6391 Segmentation fault (core dumped) ./a.out
إليك ما يحدث في التعليمات البرمجية الخاصة بك:
عندما تمر c
ل printNames
, ، أ تحويل يحدث.على وجه الخصوص، نظرًا لأن التمرير من حيث القيمة، يمكنك استدعاء مُنشئ النسخ الخاص به Parent
, ، والذي تم الإعلان عنه ضمنيًا والذي سيبدو رمزه كما يلي:
Parent(Parent const& other) : _type{other._type} {}
وبعبارة أخرى، يمكنك نسخ _type
متغير من c
ولا شيء آخر.لديك الآن جديد كائن من النوع Parent
(كلا النوعين الثابت والديناميكي Parent
) و c
لا يتم تمريرها في الواقع printNames
على الاطلاق.
داخل الدالة، تقوم بعد ذلك بالتحويل بقوة p
إلى أ Child&
.هذا الممثل لا يمكن أن ينجح لأنه p
ببساطة ليس أ Child
, ، أو قابل للتحويل إلى واحد، ولكن C++ لا يمنحك أي تشخيص لذلك (وهو في الواقع عار، حيث يمكن للمترجم أن يثبت بشكل تافه أن طاقم الممثلين خاطئ).
نحن الآن في أرض السلوك غير المحدد، والآن يُسمح لكل شيء أن يحدث.عمليا منذ ذلك الحين Child::extraString
لا يصل أبدا this
(إما ضمنيًا أو صراحةً)، ينجح استدعاء تلك الوظيفة.يتم إجراء المكالمة على كائن غير قانوني ولكن بما أنه لم يتم لمس الكائن مطلقًا، فهذا يعمل (لكنه لا يزال غير قانوني!).
المكالمة التالية إلى Child::getName
, ، هي مكالمة افتراضية وبالتالي تحتاج إلى الوصول إليها بشكل صريح this
(في معظم التطبيقات، يصل إلى مؤشر جدول الطريقة الافتراضية).ومرة أخرى، لأن الرمز هو UB على أي حال، يمكن أن يحدث أي شيء.لقد كنت "محظوظًا" وقد حصل الكود للتو على مؤشر جدول الطريقة الافتراضية للفئة الأصلية.مع المترجم الخاص بي، من الواضح أن هذا الوصول فشل.
نصائح أخرى
هذا الرمز مروع.ماذا يحدث:
printNames(c)
شرائحc
, ، بناء نسخة محليةp
منParent
كائن مضمن في المتصلc
الكائن، ثم الإعدادp
مؤشر إلىParent
جدول الإرسال الظاهري.لأن أعضاء البيانات
Parent
تم نسخها منc
, ، نوع منp
هو 2 وif
يتم إدخال الفرعChild & c = static_cast<Child&>(p);
يخبر المترجم بشكل فعال "ثق بي (أنا مبرمج) وأنا أعلم ذلكp
هو في الواقع أChild
الكائن الذي أود الإشارة إليه"، ولكن هذه كذبة صارخةp
هو في الواقع أParent
الكائن المنسوخ منChild
c
- يقع على عاتقك كمبرمج عدم مطالبة المترجم بالقيام بذلك إذا لم تكن متأكدًا من صحته
c.extraString()
تم العثور عليه بشكل ثابت (في وقت الترجمة) بواسطة المترجم لأنه يعرفc
هوChild
(أو نوع مشتق آخر، ولكنc.extraString
ليس كذلكvirtual
بحيث يمكن حلها بشكل ثابت.إنه سلوك غير محدد للقيام بذلك علىParent
كائن، ولكن ربما بسببextraString
لا يحاول الوصول إلى أي بيانات لا يقوم بها سوى أChild
الكائن، فإنه يعمل ظاهريًا "بشكل جيد" بالنسبة لكc.getName()
يكونvirtual
, ، لذلك يستخدم المترجم جدول الإرسال الافتراضي للكائن - لأن الكائن هو في الحقيقة ملفParent
يتم حله ديناميكيًا (في وقت التشغيل) إلى ملفParent::getName
وظيفة وتنتج المخرجات المرتبطة بها- إن تنفيذ الإرسال الظاهري هو تطبيق محدد، وقد لا يحدث سلوكك غير المحدد ليعمل بهذه الطريقة في جميع تطبيقات C++، أو حتى على جميع مستويات التحسين، مع جميع خيارات المترجم وما إلى ذلك.