سؤال

التكرار المحتمل:
سلوك عامل التشغيل قبل وبعد الزيادة في C وC++ وJava وC#

هنا حالة اختبار:


void foo(int i, int j)
{
   printf("%d %d", i, j);
}
...
test = 0;
foo(test++, test);

أتوقع الحصول على إخراج "0 1" ، لكنني أحصل على "0 0" ما الذي يعطي ؟؟

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

المحلول

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

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

بشكل عام، يعد إحداث آثار جانبية في الوسائط ممارسة برمجية سيئة.

بدلاً من foo(test++, test); يجب ان تكتب foo(test, test+1);اختبار++;

سيكون معادلاً لغويًا لما تحاول تحقيقه.

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

نصائح أخرى

هذا ليس مجرد غير محدد السلوك، هو في الواقع سلوك غير محدد .

نعم، ترتيب تقييم الوسيطة هو غير محدد, ، و لكنها غير معرف لقراءة وتعديل متغير واحد دون نقطة تسلسل متداخلة ما لم تكن القراءة فقط لغرض حساب القيمة الجديدة.لا توجد نقطة تسلسل بين تقييمات وسيطات الوظيفة، لذلك f(test,test++) يكون سلوك غير محدد: test تتم قراءتها لوسيطة واحدة وتعديلها للأخرى.إذا قمت بنقل التعديل إلى دالة، فلا بأس:

int preincrement(int* p)
{
    return ++(*p);
}

int test;
printf("%d %d\n",preincrement(&test),test);

وذلك بسبب وجود نقطة تسلسل عند الدخول والخروج preincrement, لذلك يجب تقييم المكالمة إما قبل القراءة البسيطة أو بعدها.الآن الأمر عادل غير محدد.

لاحظ أيضًا أن الفاصلة المشغل أو العامل يوفر نقطة تسلسل، لذلك

int dummy;
dummy=test++,test;

على ما يرام --- الزيادة تحدث قبل القراءة، لذلك dummy تم تعيينه على القيمة الجديدة.

كل ما قلته في الأصل خطأ!النقطة الزمنية التي يتم فيها حساب التأثير الجانبي يكون غير محدد.سوف يقوم Visual C++ بإجراء الزيادة بعد استدعاء foo() إذا كان test متغيرًا محليًا، ولكن إذا تم الإعلان عن test على أنه ثابت أو عام، فسيتم زيادته قبل استدعاء foo() وينتج نتائج مختلفة، على الرغم من أن القيمة النهائية لـ الاختبار سيكون صحيحا

يجب أن تتم الزيادة في عبارة منفصلة بعد استدعاء foo().حتى لو تم تحديد السلوك في معيار C/C++ فسيكون الأمر مربكًا.قد تعتقد أن مترجمي C++ سيشيرون إلى هذا كخطأ محتمل.

هنا هو وصف جيد لنقاط التسلسل والسلوك غير المحدد.

<----بداية الخطأ الخطأ---->

يتم تنفيذ الجزء "++" من "test++" بعد استدعاء foo.لذلك قمت بتمرير (0,0) إلى foo، وليس (1,0)

فيما يلي إخراج المجمّع من Visual Studio 2002:

mov ecx, DWORD PTR _i$[ebp]
push    ecx
mov edx, DWORD PTR tv66[ebp]
push    edx
call    _foo
add esp, 8
mov eax, DWORD PTR _i$[ebp]
add eax, 1
mov DWORD PTR _i$[ebp], eax

تتم الزيادة بعد استدعاء foo().على الرغم من أن هذا السلوك مقصود، إلا أنه بالتأكيد مربك للقارئ العادي وربما ينبغي تجنبه.يجب أن تتم الزيادة بالفعل في عبارة منفصلة بعد استدعاء foo()

<----نهاية الخطأ الخطأ ---->

إنه "سلوك غير محدد"، ولكن من الناحية العملية، مع الطريقة التي يتم بها تحديد مكدس استدعاءات C، فإنه يضمن دائمًا أنك ستراها على أنها 0، 0 وليس 1، 0 أبدًا.

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

خذ توقيع printf:

int printf(const char *format, ...);

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

مع العلم أنه في لغة C (وC++) تتم معالجة المعلمات من اليسار إلى اليمين، يمكننا تحديد أبسط طريقة لتحليل وتفسير استدعاء دالة.انتقل إلى نهاية قائمة المعلمات، وابدأ في الدفع وتقييم أي عبارات معقدة أثناء تقدمك.

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

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

من الآمن كتابة foo(test, test + 1) ثم إجراء ++test في السطر التالي.على أية حال، يجب على المترجم تحسينه إن أمكن.

قد لا يقوم المترجم بتقييم الوسائط بالترتيب الذي تتوقعه.

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

(تعديل المتغيرات بين نقاط التسلسل يسمح للمترجم بشكل أساسي بالقيام بأي شيء يريده.)

حسنًا، الآن بعد أن تم تحرير OP من أجل الاتساق، فهو غير متزامن مع الإجابات.الإجابة الأساسية حول ترتيب التقييم صحيحة.لكن القيم المحتملة المحددة تختلف بالنسبة لـ foo(++test, test);قضية.

++test سوف ستتم زيادتها قبل تمريرها، لذا ستكون الوسيطة الأولى دائمًا 1.ستكون الوسيطة الثانية 0 أو 1 حسب ترتيب التقييم.

وفقًا لمعيار C، يعد وجود أكثر من مرجع لمتغير في نقطة تسلسل واحدة سلوكًا غير محدد (هنا يمكنك التفكير في ذلك على أنه بيان، أو معلمات لوظيفة) حيث يشتمل أحد هذه المراجع على تعديل قبل / بعد.لذا:foo(f++,f) <--غير محدد فيما يتعلق بموعد زيادة f.وبالمثل (أرى هذا طوال الوقت في رمز المستخدم):*p = p++ + p;

عادةً لن يغير المترجم سلوكه لهذا النوع من الأشياء (باستثناء المراجعات الرئيسية).

تجنب ذلك عن طريق تشغيل التحذيرات والانتباه إليها.

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

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

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

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