سؤال

لدي شيطان من وقت فهم المراجع. النظر في الرمز التالي:

class Animal
{
public:
    virtual void makeSound() {cout << "rawr" << endl;}
};

class Dog : public Animal
{
public:
    virtual void makeSound() {cout << "bark" << endl;}
};

Animal* pFunc()
{
    return new Dog();
}

Animal& rFunc()
{
    return *(new Dog());
}

Animal vFunc()
{
    return Dog();
}

int main()
{
    Animal* p = pFunc();
    p->makeSound();

    Animal& r1 = rFunc();
    r1.makeSound();

    Animal r2 = rFunc();
    r2.makeSound();

    Animal v = vFunc();
    v.makeSound();
}

والنتائج هي: "Bark Bark Rawr Rawr".

في طريقة تفكير Java ، (التي أوضحت على ما يبدو تفسد تصورتي لـ C ++) ، ستكون النتائج هي "لحاء لحاء اللحاء". أنا أفهم من بلدي السؤال السابق أن هذا الاختلاف يرجع إلى التقطيع ولدي الآن فهم جيد لماهية التقطيع.

ولكن دعنا نقول أنني أريد وظيفة تُرجع قيمة حيوانية هي حقًا كلب.

  1. هل أفهم بشكل صحيح أن الأقرب الذي يمكنني الحصول عليه هو المرجعي?
  2. علاوة على ذلك ، هل يتعين على الواجهة التي تستخدم واجهة RFUNC أن ترى أن المرجع الذي تم إرجاعه هو تعيين حيوان و؟ (أو تخصيص الإشارة إلى حيوان عن قصد ، من خلال التقطيع ، يتجاهل تعدد الأشكال.)
  3. كيف من المفترض أن أعيد إشارة إلى كائن تم إنشاؤه حديثًا دون القيام بالشيء الغبي الذي فعلته أعلاه في RFUNC؟ (على الأقل سمعت هذا غبي.)

تحديث: نظرًا لأن الجميع يتفقون حتى الآن على أنه لا يشرح ذلك ، فإن هذا يطرح أسئلة أخرى ذات صلة:

إذا قمت بإعداد مؤشر ، فكيف يمكنني التواصل مع المبرمج بأن المؤشر ليس له حذف إذا كان هذا هو الحال؟ أو بدلاً من ذلك ، كيف يمكنني توصيل أن المؤشر يخضع للحذف في أي وقت (من نفس الخيط ولكن وظيفة مختلفة) حتى لا ينبغي أن تخزن وظيفة الاتصال ، إذا كان هذا هو الحال. هل الطريقة الوحيدة لتوصيل هذا من خلال التعليقات؟ هذا يبدو قذرة.

ملاحظة: كل هذا يؤدي إلى فكرة عن مفهوم shared_pimpl المشترك الذي كنت أعمل عليه. آمل أن أتعلم ما يكفي لنشر شيء ما عن ذلك في غضون يومين.

هل كانت مفيدة؟

المحلول

1) إذا كنت تقوم بإنشاء كائنات جديدة ، فأنت لا ترغب أبدًا في إرجاع مرجع (انظر تعليقك الخاص على #3) std::shared_ptr أو std::auto_ptr). (يمكنك أيضًا العودة بالنسخ ، لكن هذا لا يتوافق مع استخدام new المشغل أو العامل؛ كما أنه غير متوافق قليلاً مع تعدد الأشكال.)

2) rFunc مجرد خطأ. لا تفعل ذلك. إذا كنت تستخدم new لإنشاء الكائن ، ثم إرجاعه من خلال مؤشر (ملفوف اختياريا).

3) ليس من المفترض أن. هذا ما هي المؤشرات ل.


تعديل (الرد على التحديث الخاص بك :) من الصعب تصوير السيناريو الذي تصفه. هل سيكون من الأكثر دقة القول أن المؤشر الذي تم إرجاعه قد يكون غير صالح بمجرد قيام المتصل بإجراء مكالمة إلى طريقة أخرى (محددة)؟

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

نصائح أخرى

للإجابة على الجزء الثاني من سؤالك ("كيف يمكنني توصيل أن المؤشر يخضع للحذف في أي وقت") -

هذه ممارسة خطيرة ، ولها تفاصيل دقيقة ستحتاج إلى مراعاتها. إنه مفعم بالحيوية في الطبيعة.

إذا كان يمكن حذف المؤشر في أي وقت من الأوقات ، فليس من الآمن أبدًا استخدامه من سياق آخر ، لأنه حتى لو كنت تحقق "هل ما زلت صالحًا؟" في كل مرة ، قد يتم حذفه قليلاً بعد الشيك ، ولكن قبل استخدامه.

هناك طريقة آمنة للقيام بهذه الأشياء هي مفهوم "المؤشر الضعيف" - هل يتم تخزين الكائن كمؤشر مشترك (مستوى واحد من عدم التوجيه ، يمكن إصداره في أي وقت) ، وأن تكون القيمة المرتجعة مؤشرًا ضعيفًا - شيء يجب عليك أن استفسار قبل أن تتمكن من الاستخدام ، ويجب إصداره بعد استخدامه. وبهذه الطريقة ، لا يزال الكائن صالحًا ، يمكنك استخدامه.

رمز الزائفة (استنادًا إلى المؤشرات الضعيفة والمشتركة التي تم اختراعها ، أنا لا أستخدم Boost ...) -

weak< Animal > animalWeak = getAnimalThatMayDisappear();
// ...
{
    shared< Animal > animal = animalWeak.getShared();
    if ( animal )
    {
        // 'animal' is still valid, use it.
        // ...
    }
    else
    {
        // 'animal' is not valid, can't use it. It points to NULL.
        // Now what?
    }
}
// And at this point the shared pointer of 'animal' is implicitly released.

ولكن هذا معقد ومعرض للخطأ ، ومن المحتمل أن يجعل حياتك أكثر صعوبة. أوصي بالذهاب لتصميمات أبسط إن أمكن.

من أجل تجنب التقطيع ، يجب عليك العودة أو النزول مؤشر إلى الكائن. (لاحظ أن المرجع هو في الأساس "مؤشر غير مرغوب فيه بشكل دائم".

Animal r2 = rFunc();
r2.makeSound();

هنا ، يتم إنشاء R2 (باستخدام CTOR الذي تم إنشاؤه بواسطة برنامج التحويل البرمجي) لكنه يترك أجزاء الكلاب. إذا قمت بذلك مثل هذا ، فلن تحدث التقطيع:

Animal& r2 = rFunc();

ومع ذلك ، شرائح وظيفة VFUNC () داخل الطريقة نفسها.

سأذكر أيضًا هذه الوظيفة:

Animal& rFunc()
{
    return *(new Dog());
}

إنه غريب وغير آمن. أنت تقوم بإنشاء إشارة إلى متغير مؤقت لم يكشف عن اسمه (الكلب غير المرغوب فيه). من الأنسب لإعادة المؤشر. عادة ما يتم استخدام المراجع العائدة لإعادة متغيرات الأعضاء وهلم جرا.

إذا قمت بإعداد مؤشر ، فكيف يمكنني التواصل مع المبرمج بأن المؤشر ليس له حذف إذا كان هذا هو الحال؟ أو بدلاً من ذلك ، كيف يمكنني توصيل أن المؤشر يخضع للحذف في أي وقت (من نفس الخيط ولكن وظيفة مختلفة) حتى لا ينبغي أن تخزن وظيفة الاتصال ، إذا كان هذا هو الحال.

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

خلاف ذلك ، حاول استخدام مؤشر ذكي مع دلالات مناسبة. لا أحد عاقل سيعتقد ذلك من أي وقت مضى delete &*some_boost_shared_ptr; فكرة جيدة.

ولكن دعنا نقول أنني أريد وظيفة تُرجع قيمة حيوانية هي حقًا كلب.

  1. هل أفهم بشكل صحيح أن الأقرب الذي يمكنني الحصول عليه هو مرجع؟

نعم ، أنت محق. لكنني أعتقد أن المشكلة ليست كثيرًا لدرجة أنك لا تفهم المراجع ، لكنك لا تفهم الأنواع المختلفة من المتغيرات في C ++ أو كيف new يعمل في C ++. في C ++ ، يمكن أن تكون المتغيرات بيانات بدائية (int ، تعويم ، مزدوج ، إلخ) ، كائن ، أو مؤشر/مرجع إلى كائن بدائي و/أو كائن. في Java ، يمكن أن تكون المتغيرات بدائية أو إشارة إلى كائن.

في C ++ ، عندما تعلن متغيرًا ، يتم تخصيص الذاكرة الفعلية وترتبط بالمتغير. في Java ، يتعين عليك إنشاء كائنات صريحة باستخدام كائن جديد وتصنيفًا جديدًا إلى متغير. النقطة الرئيسية هنا هي أنه في C ++ ، فإن الكائن والمتغير الذي تستخدمه للوصول ليسوا هو نفس الشيء عندما يكون المتغير مؤشرًا أو مرجعًا. Animal a; يعني شيئًا مختلفًا عن Animal *a; وهو ما يعني شيئًا مختلفًا عن Animal &a;. لا يوجد أي من هذه الأنواع المتوافقة ، وهي غير قابلة للتبديل.

عندما تكتب ، Animal a1 في C ++. جديد Animal تم إنشاء الكائن. لذلك ، عندما تكتب Animal a2 = a1;, ، ينتهي بك المطاف بمتغيرين (a1 و a2) و اثنان Animal الكائنات في موقع مختلف في الذاكرة. كلا الكائنين لهما نفس القيمة ، ولكن يمكنك تغيير قيمهما بشكل مستقل إذا كنت تريد. في Java ، إذا قمت بكتابة نفس الرمز الدقيق ، فسوف ينتهي بك الأمر مع متغيرين ، ولكن كائن واحد فقط. طالما أنك لم تعيد تعيين أي من المتغيرات ، سيكون لديهم دائمًا نفس القيمة.

  1. علاوة على ذلك ، هل يتعين على الواجهة التي تستخدم واجهة RFUNC أن ترى أن المرجع الذي تم إرجاعه هو تعيين حيوان و؟ (أو تخصيص الإشارة إلى حيوان عن قصد ، من خلال التقطيع ، يتجاهل تعدد الأشكال.)

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

لذا ، نعم ، يتعين على المستخدم rFunc(), ، إذا قاموا بتعيين قيمة الإرجاع إلى أي شيء ، لتعيينها إلى مرجع. يمكنك أن ترى لماذا. إذا قمت بتعيين المرجع الذي تم إرجاعه بواسطة rFunc() إلى متغير أعلن أنه Animal animal_variable, ، ينتهي بك المطاف مع واحد Animal متغير ، واحد Animal كائن وواحد Dog هدف. ال Animal كائن مرتبط animal_variable هو ، قدر الإمكان ، نسخة من Dog كائن تم إرجاعه بالرجوع إليه rFunc(). لكن لا يمكنك الحصول على سلوك متعدد الأشكال من animal_variable لأن هذا المتغير لا يرتبط بـ Dog هدف. ال Dog الكائن الذي تم إرجاعه بالرجوع لا يزال موجودًا لأنك أنشأته باستخدام new, ، لكنه لم يعد متاحًا-تم تسريبه.

  1. كيف من المفترض أن أعيد إشارة إلى كائن تم إنشاؤه حديثًا دون القيام بالشيء الغبي الذي فعلته أعلاه في RFUNC؟ (على الأقل سمعت هذا غبي.)

المشكلة هي أنه يمكنك إنشاء كائن بثلاث طرق.

{ // the following expressions evaluate to ...  
 Animal local;  
 // an object that will be destroyed when control exits this block  
 Animal();  
 // an unamed object that will be destroyed immediately if not bound to a reference  
 new Animal();  
 // an unamed Animal *pointer* that can't be deleted unless it is assigned to a Animal pointer variable.  
 {  
  // doing other stuff
 }  
} // <- local destroyed

الجميع new هل في C ++ هو إنشاء كائنات في الذاكرة حيث لن يتم تدميرها حتى تقول ذلك. ولكن ، من أجل تدميره ، عليك أن تتذكر المكان الذي تم إنشاؤه فيه في الذاكرة. أنت تفعل ذلك عن طريق إنشاء متغير مؤشر ،Animal *AnimalPointer;, وتعيين المؤشر الذي تم إرجاعه بواسطة new Animal() لذلك ،AnimalPointer = new Animal();. لتدمير Animal كائن عند الانتهاء من ذلك ، عليك الكتابة delete AnimalPointer;.

النقطة 1: لا تستخدم المراجع. استخدم المؤشرات.

النقطة 2: الشيء الذي لديك أعلاه يسمى تصنيفًا وهو مخطط تصنيف هرمي. التصنيفات هي النموذج النموذج من النوع الذي لا يناسبه تمامًا للنمذجة الموجهة للكائن. يعمل مثالك التافهة فقط لأن حيوانك الأساسي يفترض أن جميع الحيوانات تحدث ضجيجًا ، ولا يمكنها فعل أي شيء آخر مثير للاهتمام.

إذا حاولت تنفيذ علاقة ، مثل

حيوان منطقي افتراضي :: يأكل (حيوان *آخر) = 0 ؛

ستجد أنه لا يمكنك فعل ذلك. الشيء هو: الكلب ليس نوعًا فرعيًا من تجريد الحيوانات. بيت القصيد من التصنيف هو أن فئات كل مستوى من القسم لها خصائص جديدة مثيرة للاهتمام.

على سبيل المثال: تحتوي الفقاريات على العمود الفقري ويمكننا أن نسأل ما إذا كان مصنوعًا من CartileDge أو عظم .. لا يمكننا حتى طرح سؤال اللافقاريات هذا.

لفهم تمامًا ، يجب أن ترى أنه لا يمكنك صنع كائن كلب. بعد كل شيء ، إنه تجريد ، أليس كذلك؟ لأنه ، هناك Kelpies و Collies ، ويجب أن يكون الكلب الفردي من بعض الأنواع .. يمكن أن يكون مخطط التصنيف عميقًا كما تريد ، لكنه لا يمكن أن يدعم أي أفراد ملموسة. فيدو هو غير مجسد, ، هذا مجرد علامة تصنيفه.

(أتجاهل مشاكلك في الذاكرة الديناميكية في المراجع التي تسبب تسريبات الذاكرة ...)

تختفي مشاكل الانقسام عندما يكون الحيوان فئة قاعدة مجردة. هذا يعني أنه يحتوي على طريقة افتراضية واحدة على الأقل ولا يمكن إنشاء مثيل لها مباشرة. يصبح ما يلي خطأ في المترجم:

Animal a = rFunc();   // a cannot be directly instantiated
                      // spliting prevented by compiler!

لكن المترجم يسمح:

Animal* a = pFunc();  // polymorphism maintained!
Animal& a = rFunc();  // polymorphism maintained!

وبالتالي فإن المترجم ينقذ اليوم!

إذا كنت ترغب في إرجاع نوع متعدد الأشكال من طريقة ولا تريد تعيينه على الكومة ، فيمكنك التفكير في جعله حقلًا في فئة هذه الطريقة وجعل الوظيفة لإعادة مؤشر إلى أي فئة أساسية تريدها.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top