لماذا نستخدم التكرارات بدلاً من مؤشرات المصفوفة؟

StackOverflow https://stackoverflow.com/questions/131241

  •  02-07-2019
  •  | 
  •  

سؤال

خذ السطرين التاليين من التعليمات البرمجية:

for (int i = 0; i < some_vector.size(); i++)
{
    //do stuff
}

وهذا:

for (some_iterator = some_vector.begin(); some_iterator != some_vector.end();
    some_iterator++)
{
    //do stuff
}

قيل لي أن الطريقة الثانية هي المفضلة.لماذا هذا بالضبط؟

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

المحلول

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

T elem = some_vector[i];

فأنت تفترض أن الحاوية بها operator[](std::size_t) مُعرف.مرة أخرى، هذا ينطبق على المتجهات ولكن ليس على الحاويات الأخرى.

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

يمكنك تحسين التعليمات البرمجية الخاصة بك بشكل أكبر باستخدام الخوارزميات القياسية.اعتمادًا على ما تحاول تحقيقه، قد تختار استخدامه std::for_each(), std::transform() وما إلى ذلك وهلم جرا.باستخدام خوارزمية قياسية بدلاً من حلقة صريحة، فإنك تتجنب إعادة اختراع العجلة.من المرجح أن تكون التعليمات البرمجية الخاصة بك أكثر كفاءة (نظرًا لاختيار الخوارزمية الصحيحة)، وصحيحة وقابلة لإعادة الاستخدام.

نصائح أخرى

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

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


واو، لا يزال هذا يتم التصويت عليه بشكل سلبي بعد ثلاثة أسابيع.أعتقد أنه لا فائدة من أن تكون ساخرًا قليلاً.

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

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

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

ولذلك فإنه يوفر شيئين:

  • تجريد الاستخدام:أنت فقط تريد تكرار بعض العناصر، ولا تهتم بكيفية القيام بذلك
  • أداء

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

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

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

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

قد ترغب في استخدام مكرر إذا كنت ستضيف/تزيل عناصر إلى المتجه أثناء التكرار عليه.

some_iterator = some_vector.begin(); 
while (some_iterator != some_vector.end())
{
    if (/* some condition */)
    {
        some_iterator = some_vector.erase(some_iterator);
        // some_iterator now positioned at the element after the deleted element
    }
    else
    {
        if (/* some other condition */)
        {
            some_iterator = some_vector.insert(some_iterator, some_new_value);
            // some_iterator now positioned at new element
        }
        ++some_iterator;
    }
}

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

فصل المخاوف

من الجيد جدًا فصل رمز التكرار عن الاهتمام "الأساسي" للحلقة.إنه تقريبًا قرار تصميمي.

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

وأيضا في std::for_each الطريق، أنت أخبر المجموعة بما يجب فعله، بدلاً من السؤال إنه شيء يتعلق بدواخله

سيقدم معيار 0x عمليات إغلاق، مما سيجعل هذا النهج أكثر سهولة في الاستخدام - ألق نظرة على القوة التعبيرية على سبيل المثال.روبي [1..6].each { |i| print i; }...

أداء

ولكن ربما تكون المشكلة التي يتم الإشراف عليها كثيرًا هي أن استخدام for_each النهج يعطي فرصة لجعل التكرار متوازيًا - كتل خيوط إنتل يمكن توزيع كتلة التعليمات البرمجية على عدد المعالجات في النظام!

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

يجب قراءة المرجع على التكرارات هو الكتاب "المحكمة الخاصة بلبنان الموسعة".

لدى GoF فقرة صغيرة جدًا في نهاية نمط التكرار، والتي تتحدث عن هذا النوع من التكرار؛يطلق عليه "المكرر الداخلي".الق نظرة هنا, ، أيضاً.

لأنه أكثر توجهاً للكائنات.إذا كنت تتكرر مع فهرس فأنت تفترض:

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

باستخدام المُكرِّر، أنت تقول "أعطني كل شيء حتى أتمكن من العمل معه" دون معرفة ما هو التنفيذ الأساسي.(في Java، هناك مجموعات لا يمكن الوصول إليها من خلال الفهرس)

أيضًا، مع المكرر، لا داعي للقلق بشأن الخروج عن حدود المصفوفة.

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


for(std::vector<Foo>::const_iterator pos=foos.begin(); pos != foos.end(); ++pos)
{
    // Foo & foo = *pos; // this won't compile
    const Foo & foo = *pos; // this will compile
}

بصرف النظر عن جميع الإجابات الممتازة الأخرى ... int قد لا تكون كبيرة بما يكفي لناقلك.بدلاً من ذلك، إذا كنت تريد استخدام الفهرسة، فاستخدم ملف size_type للحاوية الخاصة بك:

for (std::vector<Foo>::size_type i = 0; i < myvector.size(); ++i)
{
    Foo& this_foo = myvector[i];
    // Do stuff with this_foo
}

ربما يجب أن أشير إلى أنه يمكنك الاتصال أيضًا

std::for_each(some_vector.begin(), some_vector.end(), &do_stuff);

توجد مكررات STL في الغالب بحيث يمكن أن تكون خوارزميات STL مثل الفرز مستقلة عن الحاوية.

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

إنها أقل كتابة وأسهل في التحليل بالنسبة لمعظم البشر.سيكون من الرائع أن تحتوي لغة C++ على حلقة foreach بسيطة دون المبالغة في استخدام سحر القالب.

for( size_t i = 0; i < some_vector.size(); ++i )
{
   T& rT = some_vector[i];
   // now do something with rT
}
'

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

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

for(size_t i = 0; i < myvector.size(); i++)
{
    MyClass &item = myvector[i];

    // Do stuff to "item".
}

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

يمثل النموذج الثاني ما تفعله بشكل أكثر دقة.في المثال الخاص بك، أنت لا تهتم بقيمة i، حقًا - كل ما تريده هو العنصر التالي في المكرر.

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

for (some_iterator = some_vector.begin(); some_iterator != some_vector.end();
    some_iterator++)
{
    //do stuff
}

وهذه الحلقة:

for (int i = 0; i < some_vector.size(); i++)
{
    //do stuff
}

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

while (it != end){
    //do stuff
    ++it;
}

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

الفهرسة تتطلب اضافية mul عملية.على سبيل المثال، ل vector<int> v, ، يقوم المترجم بالتحويل v[i] داخل &v + sizeof(int) * i.

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

لم يذكر أحد حتى الآن أن إحدى ميزات المؤشرات هي أنها لا تصبح غير صالحة عند إلحاقك بحاوية متجاورة مثل std::vector, ، حتى تتمكن من إضافة عناصر إلى الحاوية أثناء التكرار.

هذا ممكن أيضًا مع التكرارات، ولكن يجب عليك الاتصال reserve(), ، وبالتالي تحتاج إلى معرفة عدد العناصر التي ستقوم بإلحاقها.

عدة نقاط جيدة بالفعل.لدي بعض التعليقات الإضافية:

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

  2. للتخلص من أي تبعيات على تحسين المترجم، يمكنك نقل some_vector.size() خارج الحلقة في التعليمات البرمجية المفهرسة، كما يلي:

    const size_t numElems = some_vector.size();
    for (size_t i = 0; i 
  3. قم دائمًا بتكرارات الزيادة المسبقة ومعاملة الزيادات اللاحقة كحالات استثنائية.

for (some_iterator = some_vector.begin();some_iterator != some_vector.end();++some_iterator) {// افعل الأشياء }

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

بشكل عام، يُفضل استخدام المُكرِّرات لأن الخوارزميات تستفيد منها ويمكن التحكم في السلوك (وتوثيقه ضمنيًا) عن طريق تغيير نوع المُكرِّر.يمكن استخدام مواقع المصفوفات بدلاً من التكرارات، لكن الاختلاف النحوي سيظل واضحًا.

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

for(int i=0;i<anims.size();i++)
  for(int j=0;j<bones.size();j++)
  {
     int animIndex = i;
     int boneIndex = j;


     // in relatively short code I use indices i and j
     ... animation_matrices[i][j] ...

     // in long and complicated code I use indices animIndex and boneIndex
     ... animation_matrices[animIndex][boneIndex] ...


  }

لا أريد حتى اختصار أشياء مثل "animation_matrices[i]" إلى بعض المكررات العشوائية المسماة "anim_matrix" على سبيل المثال، لأنه بعد ذلك لا يمكنك أن ترى بوضوح من أي مصفوفة نشأت هذه القيمة.

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

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

أفضل من "إخبار وحدة المعالجة المركزية بما يجب القيام به" (ضروري) هو "إخبار المكتبات بما تريد" (وظيفيًا).

لذلك بدلاً من استخدام الحلقات، يجب أن تتعلم الخوارزميات الموجودة في stl.

من أجل استقلال الحاوية

أستخدم دائمًا فهرس المصفوفة لأن العديد من تطبيقاتي تتطلب شيئًا مثل "عرض الصورة المصغرة".لذلك كتبت شيئا مثل هذا:

some_vector[0].left=0;
some_vector[0].top =0;<br>

for (int i = 1; i < some_vector.size(); i++)
{

    some_vector[i].left = some_vector[i-1].width +  some_vector[i-1].left;
    if(i % 6 ==0)
    {
        some_vector[i].top = some_vector[i].top.height + some_vector[i].top;
        some_vector[i].left = 0;
    }

}

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

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