لماذا ينبغي لمبرمجي C++ تقليل استخدام "جديد"؟




memory-management heap (12)

لأن المكدس سريع و مضمون

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

تعثرت على Stack Overflow question تسرب الذاكرة مع std :: string عند استخدام std :: list <std :: string> ، وواحد من التعليقات يقول هذا:

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

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


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

خذ بعين الاعتبار مستخدم "حذراً" الذي يتذكر التفاف الكائنات في المؤشرات الذكية:

foo(shared_ptr<T1>(new T1()), shared_ptr<T2>(new T2()));

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

SolutIon: استخدم make_shared .


أرى أن هناك عددًا قليلًا من الأسباب المهمة لعمل أقل عدد ممكن من المستخدمين الجدد:

المشغل new لديه وقت تنفيذ غير محدد

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

المشغل new عبارة عن تزامن مؤشر ترابط ضمني

نعم سمعتموني ، يحتاج نظام التشغيل الخاص بك إلى التأكد من أن جداول الصفحات الخاصة بك متناسقة وأن مثل هذا الاتصال new سيؤدي إلى حصول سلسلة المحادثات على قفل mutex ضمني. إذا كنت تتصل باستمرار من العديد من سلاسل الرسائل ، فأنت تقوم في الواقع بتسلسل سلاسل الرسائل الخاصة بك (لقد فعلت ذلك مع 32 وحدة معالجة مركزية ، كل مرة تقوم بضربها للحصول على بضع مئات بايت لكل منها ، أوش! الذي كان بيتا مينا للتصحيح)

وقد ذكر الباقي من قبل الإجابات الأخرى البقية ، مثل البطيء والتجزئة والخطأ الخ.


أعتقد أن الملصق يعني أن تقول You do not have to allocate everything on the heap بدلاً من stack .

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


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

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

 {
    File foo = File("foo.dat");

    // do things

 }

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

كنت قد كتبت

 {
     File * foo = new File("foo.dat");

قد ترغب في مطابقته بشكل صريح مع

     delete foo;
  }

أو حتى أفضل ، قم بتخصيص File * الخاص بك على أنه "مؤشر ذكي". إذا لم تكن حذرا بشأن ذلك يمكن أن يؤدي إلى تسرب.

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

  class File {
    private:
      FileImpl * fd;
    public:
      File(String fn){ fd = new FileImpl(fn);}

ثم لا يزال يتم تخصيص FileImpl على المكدس.

ونعم ، من الأفضل أن تكون متأكدًا

     ~File(){ delete fd ; }

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


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

سيناريو آخر هو أن المكتبة التي نستخدمها توفر دلالات قيمة وتجعل التخصيص الديناميكي غير ضروري. Std::string هي مثال جيد.

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

تستخدم أنماط التصميم التقليدية ، ولا سيما تلك المذكورة في دفتر GoF ، الكثير new ، لأنها رمز OO نموذجي.


سببان:

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

عند استخدام جديد ، يتم تخصيص الكائنات إلى الكومة. يستخدم بشكل عام عندما تتوقع التوسع. عندما تعلن عن كائن مثل ،

Class var;

يتم وضعها على المكدس.

يجب عليك دائماً استدعاء تلف على الكائن الذي قمت بوضعه على كومة الذاكرة المؤقتة مع الجديد. هذا يفتح إمكانية تسرب الذاكرة. الكائنات التي وضعت على المكدس ليست عرضة للذاكرة تتسرب!


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

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

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

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

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


new() لا ينبغي أن يستخدم بأقل قدر ممكن. يجب استخدامه بعناية قدر الإمكان. وينبغي استخدامه في كثير من الأحيان حسب الضرورة كما تمليه البراغماتية.

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

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


newيخصص الكائنات على كومة الذاكرة المؤقتة. خلاف ذلك ، يتم تخصيص الكائنات على المكدس. ابحث عن الفرق بين الاثنين .


  • لا يستخدم C ++ أي مدير للذاكرة من تلقاء نفسه. لغات أخرى مثل C # ، Java يحتوي على مجمّع garbage لمعالجة الذاكرة
  • يمكن C ++ باستخدام إجراءات نظام التشغيل لتخصيص الذاكرة والكثير جداً / حذف جزء الذاكرة المتوفرة
  • مع أي تطبيق ، إذا تم استخدام الذاكرة بشكل متكرر ، فمن المستحسن تخصيصها مسبقًا والإفراج عنها عند عدم الحاجة إليها.
  • قد تؤدي إدارة الذاكرة غير السليمة إلى حدوث تسرب للذاكرة ومن الصعب جدًا تتبعها. لذا ، فإن استخدام كائنات مكدسة ضمن نطاق الوظيفة هو أسلوب مثبت
  • الجانب السلبي من استخدام كائنات المكدس ، فإنه ينشئ نسخ متعددة من الكائنات عند العودة ، ويمر إلى وظائف إلخ. ومع ذلك ، فإن المجمعين الأذكياء يدركون جيدًا هذه المواقف وقد تم تحسينها بشكل جيد للأداء
  • انها حقا مملة في C ++ إذا تم تخصيص الذاكرة وإطلاقها في مكانين مختلفين. تعتبر مسؤولية الإصدار دائمًا مسألة ، ونعتمد في الغالب على بعض المؤشرات التي يمكن الوصول إليها عمومًا ، والأجسام المكدسة (أقصى حد ممكن) وتقنيات مثل auto_ptr (كائنات RAII)
  • أفضل شيء هو أنك تتحكم في الذاكرة وأسوأ شيء هو أنك لن تتحكم في الذاكرة إذا استخدمنا إدارة ذاكرة غير مناسبة للتطبيق. تحطم سبب بسبب تلف الذاكرة هي الأكثر شدة وصعوبة في التتبع.




c++-faq