c++ - الإعلان عن المتغيرات داخل الحلقات أو الممارسات الجيدة أو الممارسات السيئة؟




loops variable-declaration (3)

السؤال الأول: هل يعلن عن متغير داخل الحلقة ممارسة جيدة أم ممارسة سيئة؟

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

مثال:

for(int counter = 0; counter <= 10; counter++)
{
   string someString = "testing";

   cout << someString;
}

السؤال الثاني: هل يدرك معظم المترجمين أن المتغير قد تم الإعلان عنه بالفعل وتجاوز هذا الجزء فقط ، أم أنه في الواقع يقوم بإنشاء مكان له في الذاكرة في كل مرة؟


بالنسبة لـ C ++ ، يعتمد الأمر على ما تقوم به. حسنا ، إنه رمز غبي ولكن تخيل

class myTimeEatingClass
{
 public:
 //constructor
      myTimeEatingClass()
      {
          sleep(2000);
          ms_usedTime+=2;
      }
      ~myTimeEatingClass()
      {
          sleep(3000);
          ms_usedTime+=3;
      }
      const unsigned int getTime() const
      {
          return  ms_usedTime;
      }
      static unsigned int ms_usedTime;
};
myTimeEatingClass::ms_CreationTime=0; 
myFunc()
{
    for (int counter = 0; counter <= 10; counter++) {

        myTimeEatingClass timeEater();
        //do something
    }
    cout << "Creating class took "<< timeEater.getTime() <<"seconds at all<<endl;

}
myOtherFunc()
{
    myTimeEatingClass timeEater();
    for (int counter = 0; counter <= 10; counter++) {
        //do something
    }
    cout << "Creating class took "<< timeEater.getTime() <<"seconds at all<<endl;

}

سوف تنتظر 55 ثانية حتى تحصل على إخراج myFunc. فقط لأن كل حلقة المدمرة و destructor معا بحاجة إلى 5 ثوان لإنهاء.

ستحتاج إلى 5 ثوانٍ حتى تحصل على إخراج myOtherFunc.

بالطبع ، هذا مثال مجنون.

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


بشكل عام ، من الممارسات الجيدة جدًا الإبقاء عليها قريبة جدًا.

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

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

لنفترض أنك تريد تجنب هذه الإبداعات / المخصصات المتكررة ، ستكتبها على النحو التالي:

for (int counter = 0; counter <= 10; counter++) {
   // compiler can pull this out
   const char testing[] = "testing";
   cout << testing;
}

أو يمكنك سحب الثابت:

const std::string testing = "testing";
for (int counter = 0; counter <= 10; counter++) {
   cout << testing;
}

هل يدرك معظم المترجمين أن المتغير قد تم الإعلان عنه بالفعل وتجاوز هذا الجزء فقط ، أم أنه في الواقع يقوم بإنشاء مكان له في الذاكرة في كل مرة؟

يمكنه إعادة استخدام المساحة التي يستهلكها المتغير ، ويمكنه سحب الثوابت من الحلقة. في حالة صفيف char const (أعلاه) - يمكن سحب هذا الصفيف. ومع ذلك ، يجب تنفيذ المنشئ و destructor في كل التكرار في حالة كائن (مثل std::string ). في حالة std::string ، تتضمن هذه "المسافة" مؤشرًا يحتوي على التخصيص الديناميكي الذي يمثل الأحرف. إذا هذا:

for (int counter = 0; counter <= 10; counter++) {
   string testing = "testing";
   cout << testing;
}

يتطلب نسخًا احتياطيًا في كل حالة ، وتخصيصًا ديناميكيًا ومجانيًا إذا كان المتغير يجلس فوق عتبة عدد أحرف SSO (ويتم تطبيق SSO بواسطة مكتبة std).

فعل هذا:

string testing;
for (int counter = 0; counter <= 10; counter++) {
   testing = "testing";
   cout << testing;
}

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

فماذا تفعل مع كل هذه الخيارات (وأكثر)؟ اجعلها قريبة جدًا كإعداد افتراضي - حتى تفهم التكاليف بشكل جيد وتعرف متى يجب أن تنحرف.


هذا هو ممارسة ممتازة .

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

من هنا:

  • إذا كان اسم المتغير "جنياً" قليلاً (مثل "i") ، فلا يوجد مخاطرة -Wshadow مع متغير آخر بنفس الاسم في مكان ما لاحقاً في الكود (يمكن أيضًا تخفيفه باستخدام تعليمات التحذير -Wshadow على دول مجلس التعاون الخليجي) )

  • يعرف المحول البرمجي أن نطاق المتغير محدود داخل الحلقة ، وبالتالي سيصدر رسالة خطأ صحيحة إذا كان المتغير عن طريق الخطأ يسمى في مكان آخر

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

باختصار ، أنت على حق في القيام بذلك.

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

{
    int i, retainValue;
    for (i=0; i<N; i++)
    {
       int tmpValue;
       /* tmpValue is uninitialized */
       /* retainValue still has its previous value from previous loop */

       /* Do some stuff here */
    }
    /* Here, retainValue is still valid; tmpValue no longer */
}

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

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

هذا صحيح حتى خارج حلقة if(){...} . عادة ، بدلا من:

    int result;
    (...)
    result = f1();
    if (result) then { (...) }
    (...)
    result = f2();
    if (result) then { (...) }

من الأسهل أن تكتب:

    (...)
    {
        int const result = f1();
        if (result) then { (...) }
    }
    (...)
    {
        int const result = f2();
        if (result) then { (...) }
    }

قد يبدو الفرق طفيفًا ، خاصة على مثال صغير. ولكن على أساس رمز أكبر ، سيساعد: الآن لا يوجد خطر لنقل بعض قيمة result من كتلة f1() إلى f2() . تقتصر كل result بشكل صارم على نطاقها الخاص ، مما يجعل دورها أكثر دقة. من وجهة نظر المراجعين ، يكون الأمر أجمل بكثير ، حيث أن لديه متغيرات أقل في المدى البعيد للقلق والتتبع.

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

معلومات تكميلية

توفر أداة مفتوحة المصدر CppCheck (أداة تحليل ثابتة CppCheck C / C ++) بعض التلميحات الممتازة فيما يتعلق بالنطاق الأمثل للمتغيرات.

رداً على التعليق على التخصيص: القاعدة المذكورة أعلاه صحيحة في C ، ولكن قد لا تكون لبعض فئات C ++.

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

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





variable-declaration