سؤال

سأبدأ بالقول ، استخدم المؤشرات الذكية ولن تضطر أبدًا للقلق بشأن هذا.

ما هي مشاكل الكود التالي؟

Foo * p = new Foo;
// (use p)
delete p;
p = NULL;

لقد أثار هذا من قبل إجابة وتعليقات لسؤال آخر. تعليق واحد من نيل بتروورث ولدت بضع فوتات:

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

هناك الكثير من الظروف التي لن تساعد فيها. لكن في تجربتي ، لا يمكن أن تؤذي. شخص ما ينيرني.

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

المحلول

ضبط مؤشر على 0 (وهو "فارغ" في C ++ القياسية ، فإن تعريف الفارغ من C يختلف إلى حد ما) يتجنب حوادث الحذف المزدوج.

النظر في ما يلي:

Foo* foo = 0; // Sets the pointer to 0 (C++ NULL)
delete foo; // Won't do anything

بينما:

Foo* foo = new Foo();
delete foo; // Deletes the object
delete foo; // Undefined behavior 

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

من الأفضل ألا يكون لديك حذف مزدوج الحذف ، من الواضح ، ولكن اعتمادًا على دلالات الملكية ودورات حياة الكائنات ، قد يكون من الصعب تحقيق ذلك في الممارسة العملية. أنا أفضل حذف مزدوج الحذف على UB.

أخيرًا ، جانبًا جانبيًا فيما يتعلق بإدارة تخصيص الكائنات ، أقترح عليك إلقاء نظرة على std::unique_ptr للملكية الصارمة/المفردة ، std::shared_ptr بالنسبة للملكية المشتركة ، أو تنفيذ مؤشر ذكي آخر ، اعتمادًا على احتياجاتك.

نصائح أخرى

تعيين مؤشرات على Null بعد حذف ما أشار إليه بالتأكيد لا يمكن أن يؤذي ، ولكن غالبًا ما يكون ذلك قليلاً من الإسعافات الأولية حول مشكلة أكثر جوهرية: لماذا تستخدم مؤشرًا في المقام الأول؟ أستطيع أن أرى سببين نموذجيين:

  • أردت ببساطة شيء مخصص على الكومة. في هذه الحالة ، كان لفه في كائن Raii أكثر أمانًا وأنظف. قم بإنهاء نطاق كائن Raii عندما لم تعد بحاجة إلى الكائن. هكذا std::vector يعمل ، وهو يحل مشكلة ترك المؤشرات بطريق الخطأ للتعامل مع الذاكرة حولها. لا توجد مؤشرات.
  • أو ربما كنت تريد بعض دلالات الملكية المشتركة المعقدة. عاد المؤشر من new قد لا يكون هو نفسه الذي delete يسمى. قد تكون الكائنات المتعددة قد استخدمت الكائن في وقت واحد في هذه الأثناء. في هذه الحالة ، كان من الأفضل مؤشر مشترك أو شيء مشابه.

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

لدي أفضل ممارسة أفضل: حيثما أمكن ، إنهاء نطاق المتغير!

{
    Foo* pFoo = new Foo;
    // use pFoo
    delete pFoo;
}

أنا دائما ضبط مؤشر ل NULL (حاليا nullptr) بعد حذف الكائن (الكائنات) التي يشير إليها.

  1. يمكن أن يساعد في التقاط العديد من الإشارات إلى الذاكرة المحررة (على افتراض عيوب منصة الخاصة بك على deref من مؤشر فارغ).

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

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

  4. في كثير من الحالات ، سيقوم برنامج التحويل البرمجي بتحسينه بعيدًا. لذا فإن الحجة القائلة بأنها غير ضرورية لا تقنعني.

  5. إذا كنت تستخدم بالفعل RAII ، فلا يوجد الكثير deleteS في الكود الخاص بك لتبدأ ، وبالتالي فإن الوسيطة القائلة بأن المهمة الإضافية التي تسبب الفوضى لا تقنعني.

  6. غالبًا ما يكون من المناسب ، عند تصحيح الأخطاء ، رؤية القيمة الفارغة بدلاً من المؤشر الذي لا معنى له.

  7. إذا كان هذا لا يزال يزعجك ، فاستخدم مؤشرًا ذكيًا أو مرجعًا بدلاً من ذلك.

لقد قمت أيضًا بتعيين أنواع أخرى من مقابض الموارد على قيمة عدم الموارد عندما يكون المورد مجانيًا (والذي عادةً ما يكون فقط في مدمرة غلاف Raii مكتوب لتغليف المورد).

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

تحديث: تعتقد Microsoft أنها ممارسة جيدة للأمن وتنصح الممارسة في سياسات SDL الخاصة بهم. يبدو أن MSVC ++ 11 stomp المؤشر المحذوف تلقائيًا (في العديد من الظروف) إذا قمت بالتجميع مع خيار /SDL.

أولاً ، هناك الكثير من الأسئلة الحالية حول هذا الموضوع والمواضيع ذات الصلة الوثيقة ، على سبيل المثال لماذا لا يضع حذف المؤشر على Null؟.

في الكود الخاص بك ، فإن المشكلة ما يحدث (استخدم P). على سبيل المثال ، إذا كان لديك رمز مثل هذا:

Foo * p2 = p;

ثم يؤدي تعيين P إلى NULL إلى تحقيق القليل جدًا ، حيث لا يزال لديك المؤشر P2 للقلق.

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

إذا كان هناك المزيد من الكود بعد delete, ، نعم. عندما يتم حذف المؤشر في مُنشئ أو في نهاية الطريقة أو الوظيفة ، لا.

الهدف من هذا المثل هو تذكير المبرمج ، أثناء وقت التشغيل ، بأن الكائن قد تم حذفه بالفعل.

تتمثل الممارسة الأفضل في استخدام المؤشرات الذكية (المشتركة أو المنتشر) التي تحذف كائناتها المستهدفة تلقائيًا.

كما قال آخرون ، delete ptr; ptr = 0; لن يتسبب في إخراج الشياطين من أنفك. ومع ذلك ، فإنه يشجع استخدام ptr كعلم من نوع ما. يصبح الرمز مليئًا بـ delete ووضع المؤشر إلى NULL. الخطوة التالية هي التناثر if (arg == NULL) return; من خلال الكود الخاص بك للحماية من الاستخدام العرضي ل NULL مؤشر. تحدث المشكلة بمجرد الشيكات ضد NULL كن الوسيلة الأساسية للتحقق من حالة كائن أو برنامج.

أنا متأكد من أن هناك رائحة رمز حول استخدام مؤشر كعلامة في مكان ما لكنني لم أجد واحدة.

سأغير سؤالك قليلاً:

هل يمكنك استخدام مؤشر غير مهيئ؟ أنت تعرف ، واحدة لم تقم بتعيينها على Null أو تخصيص الذاكرة التي تشير إليها؟

هناك سيناريوهان حيث يمكن تخطي وضع المؤشر على NULL:

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

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

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

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

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

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

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

optional<Foo*> p (new Foo);
// (use p.get(), but must test p for truth first!...)
delete p.get();
p = optional<Foo*>();

ولكن إذا كنت تريد حقًا أن يعرف الناس أن المؤشر قد "أصبح سيئًا" ، فسوف أتوافق بنسبة 100 ٪ مع أولئك الذين يقولون إن أفضل ما يجب فعله هو جعل الأمر خارج النطاق. ثم تستخدم المترجم لمنع احتمال حدوث عمليات تخزين سيئة في وقت التشغيل.

هذا هو الطفل في كل حمام C ++ ، يجب ألا يطرده. قون

في برنامج منظم بشكل جيد مع فحص الخطأ المناسب ، لا يوجد سبب ليس لتعيينه فارغ. 0 يقف بمفرده كقيمة غير صالحة معترف بها عالميا في هذا السياق. فشل بجد وفشل قريبا.

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

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

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

من هناك ، لا يوجد لديك سوى نطاق محدود للغاية وسياق قد يوجد فيه مؤشر فارغ (أو يُسمح به).

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

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

اسمحوا لي أن أوسع ما وضعته بالفعل في سؤالك.

إليك ما وضعته في سؤالك ، في شكل نقطة رصاصة:


تعيين المؤشرات على NULL بعد الحذف ليس ممارسة جيدة عالمية في C ++. هناك أوقات عندما:

  • إنه لأمر جيد القيام به
  • والأوقات التي لا معنى لها ويمكنها إخفاء الأخطاء.

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

لذلك ، إذا كنت في شك ، فقط لاغية.

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

نعم.

"الضرر" الوحيد الذي يمكن أن تفعله هو إدخال عدم الكفاءة (عملية متجر غير ضرورية) في برنامجك - ولكن هذا النفقات العامة سيكون غير مهم فيما يتعلق بتكلفة تخصيص وتخفيف كتلة الذاكرة في معظم الحالات.

إذا لم تفعل ذلك ، فأنت إرادة لديك بعض المؤشرات سيئة derefernce البق في يوم من الأيام.

أنا دائما أستخدم ماكرو للحذف:

#define SAFEDELETE(ptr) { delete(ptr); ptr = NULL; }

(وتشبه صفيف ، مجاني () ، مقابض إطلاق)

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

static void TreeItem::DeleteSubtree(TreeItem *&rootObject)
{
    if (rootObject == NULL)
        return;

    rootObject->UnlinkFromParent();

    for (int i = 0; i < numChildren)
       DeleteSubtree(rootObject->child[i]);

    delete rootObject;
    rootObject = NULL;
}

تعديل

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

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

بالطبع ، المثال أعلاه هو مجرد خطوة نحو مؤشر تلقائي. وهو ما لم أقترحه لأن البروتوكول الاختياري كان يسأل على وجه التحديد عن حالة عدم استخدام مؤشر تلقائي.

"هناك أوقات يكون فيها أمرًا جيدًا ، والأوقات التي لا معنى لها فيها ويمكنها إخفاء الأخطاء"

أستطيع أن أرى مشكلتين: هذا الرمز البسيط:

delete myObj;
myobj = 0

يصبح إلى خطوط في بيئة متعددة مؤشرات الترابط:

lock(myObjMutex); 
delete myObj;
myobj = 0
unlock(myObjMutex);

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

خطر آخر هو الاعتماد على هذه التقنية في رمز الاستثناءات:

try{  
   delete myObj; //exception in destructor
   myObj=0
}
catch
{
   //myObj=0; <- possibly resource-leak
}

if (myObj)
  // use myObj <--undefined behaviour

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

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

هناك دائما مؤشرات متدلية للقلق.

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

كما قال الكثيرون ، من الأسهل بالطبع استخدام المؤشرات الذكية.

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

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

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

shared_ptr<Foo> p(new Foo);
//No more need to call delete

يؤدي العد المرجعي وهو آمن الخيط. يمكنك العثور عليه في مساحة الاسم TR1 (STD :: TR1 ، #include <memory>) أو إذا لم يوفرها المترجم الخاص بك ، احصل عليه من Boost.

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