سؤال

المراجع في C ++ تحيرني. قون

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

لذلك ، الكل في الكل ، إليك خيارات القيام بذلك التي وجدتها:

  • يمكن أن يكون نوع إرجاع الوظيفة إما الفئة نفسها (MyClass fun() { ... }) أو إشارة إلى الفصل (MyClass& fun() { ... }).
  • يمكن للدالة إما بناء المتغير في خط العودة (return MyClass(a,b,c);) أو إرجاع متغير موجود (MyClass x(a,b,c); return x;).
  • يمكن أن يحتوي الكود الذي يتلقى المتغير أيضًا على متغير من كلا النوعين: (MyClass x = fun(); أو MyClass& x = fun();)
  • يمكن للرمز الذي يتلقى المتغير إما إنشاء متغير جديد أثناء الطيران (MyClass x = fun();) أو تعيينه إلى متغير موجود (MyClass x; x = fun();)

وبعض الأفكار حول ذلك:

  • يبدو أنها فكرة سيئة أن يكون لديك نوع العودة MyClass& لأن ذلك يؤدي دائمًا إلى تدمير المتغير قبل إرجاعه.
  • يبدو أن مُنشئ النسخ يشارك فقط عندما أعيد متغيرًا موجودًا. عند إرجاع متغير تم بناؤه في خط العودة ، لا يتم استدعاؤه أبدًا.
  • عندما أقوم بتعيين النتيجة إلى متغير موجود ، يبدأ المدمر دائمًا قبل إرجاع القيمة. أيضًا ، لا يتم استدعاء أي منشئ للنسخ ، ولكن المتغير الهدف لا يتلقى قيم الأعضاء للكائن الذي تم إرجاعه من الوظيفة.

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

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

المحلول

أفضل طريقة لفهم النسخ في C ++ غالبًا ما لا تحاول إنتاج مثال اصطناعي وأدواته - يُسمح للمترجم بإزالة وإضافة مكالمات منشئ النسخ ، أكثر أو أقل كما يراها مناسبة.

خلاصة القول - إذا كنت بحاجة إلى إرجاع قيمة ، وإرجاع قيمة ولا تقلق بشأن أي "نفقات".

نصائح أخرى

اقتراحات للقراءة: فعالة C ++ بقلم سكوت مايرز. تجد شرحًا جيدًا حول هذا الموضوع (وأكثر من ذلك بكثير) هناك.

باختصار ، إذا عدت بالقيمة ، فسيشارك مُنشئ النسخ والمدمر افتراضيًا (ما لم يحسنها المترجم بعيدًا - هذا ما يحدث في بعض حالاتك).

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

الطريقة الكنسية لبناء كائن في وظيفة وإعادتها هي بالقيمة ، مثل:

MyClass fun() {
    return MyClass(a, b, c);
}

MyClass x = fun();

إذا كنت تستخدم هذا ، فلا داعي للقلق بشأن مشكلات الملكية ، والمراجع المتدلية وما إلى ذلك ، ومن المرجح أن يقوم المترجم بتحسين مكالمات / Destructor الإضافية ، لذلك لا داعي للقلق بشأن الأداء أيضًا.

من الممكن العودة بالرجوع إليها كائن تم إنشاؤه بواسطة new (أي على الكومة) - لن يتم تدمير هذا الكائن عند العودة من الوظيفة. ومع ذلك ، عليك أن تدمرها بشكل صريح في وقت لاحق عن طريق الاتصال delete.

من الممكن تقنيًا أيضًا تخزين كائن يتم إرجاعه بواسطة القيمة في مرجع ، مثل:

MyClass& x = fun();

ومع ذلك ، AFAIK لا يوجد الكثير من نقطة في القيام بذلك. خاصة وأن المرء يمكن للمرء أن ينقل هذه الإشارة بسهولة إلى أجزاء أخرى من البرنامج التي تقع خارج النطاق الحالي ؛ ومع ذلك ، فإن الكائن المشار إليه x هو كائن محلي سيتم تدميره بمجرد مغادرة النطاق الحالي. لذلك يمكن أن يؤدي هذا النمط إلى حشرات سيئة.

أقرأ عن RVO و NRVO (في كلمة واحدة ، هذين الاثنين من أجل تحسين قيمة الإرجاع و RVO ، وهما تقنيات التحسين التي يستخدمها المترجم للقيام بما تحاول تحقيقه)

ستجد الكثير من الموضوعات هنا على Stackoverflow

إذا قمت بإنشاء كائن مثل هذا:

MyClass foo(a, b, c);

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

لذلك إذا كنت ترغب في إرجاع كائن إلى المتصل ، فأنت فقط الخيارات هي:

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

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

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

يسمح المعيار "Copy Elision" ، مما يعني أن مُنشئ النسخ لا يحتاج إلى استدعاء عند إرجاع كائن. يأتي هذا في نموذجين: تحسين قيمة إرجاع الاسم (NRVO) وتحسين قيمة الإرجاع المجهول (عادةً فقط RVO).

مما تقول المحتمل مترجم أقدم إلى حد ما. معظم المترجمين الحاليين ينفذ كلاهما. يعني DTOR غير المطابق في هذه الحالة أنه ربما يكون شيئًا مثل GCC 3.4 أو ما يق بالطبع ، من الممكن أيضًا أن تكون أجهزتك غير صحيحة تمامًا ، لذا فإن CTOR لم يتم استخدامه للأداة ، ويتم استدعاء DTOR المطابقة لهذا الكائن.

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

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

إرجاع مرجع بدلاً من كائن عن طريق القيمة يحفظ نسخ الكائن الذي قد يكون مهمًا.

المراجع أكثر أمانًا من المؤشرات لأنها تتمتع بسمات مختلفة ، ولكن وراء الكواليس فهي مؤشرات.

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

void initFoo(Foo& foo) 
{
  foo.setN(3);
  foo.setBar("bar");
  // ... etc ...
}

int main() 
{
  Foo foo;
  initFoo(foo);

  return 0;
}

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

ولكن بعد ذلك فكر في سبب محاولة تجنب البناء في المقام الأول. هل "حساب" بناء النسخ يؤثر حقًا على برنامجك ، أم أن هذه حالة من الأمثل المبكرة؟

أنت عالق مع:

1) إعادة مؤشر

myClass* func () {// بعض stuf إرجاع myclass جديد (a ، b ، c) ؛ }

2) إرجاع نسخة من الكائن myClass func () {return myClass (a ، b ، c) ؛ }

إرجاع مرجع غير صالح لأن الكائن يجب تدميره بعد الخروج من نطاق FUNC ، إلا إذا كانت الوظيفة عضوًا في الفصل والمرجع من متغير هو عضو في الفصل.

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

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