سؤال

قرأت عن مشكلة التقطيع في لغة 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++، أو حتى على جميع مستويات التحسين، مع جميع خيارات المترجم وما إلى ذلك.
مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top