سؤال

#include <stdio.h>

int main(void)
{
   int i = 0;
   i = i++ + ++i;
   printf("%d\n", i); // 3

   i = 1;
   i = (i++);
   printf("%d\n", i); // 2 Should be 1, no ?

   volatile int u = 0;
   u = u++ + ++u;
   printf("%d\n", u); // 1

   u = 1;
   u = (u++);
   printf("%d\n", u); // 2 Should also be one, no ?

   register int v = 0;
   v = v++ + ++v;
   printf("%d\n", v); // 3 (Should be the same as u ?)

   int w = 0;
   printf("%d %d\n", ++w, w); // shouldn't this print 1 1

   int x[2] = { 5, 8 }, y = 0;
   x[y] = y ++;
   printf("%d %d\n", x[0], x[1]); // shouldn't this print 0 8? or 5 0?
}
هل كانت مفيدة؟

المحلول

C لديه مفهوم السلوك غير المحدد، أي بعض البنيات اللغوية صالحة سارية ولكن لا يمكنك التنبؤ بالسلوك عند تشغيل الرمز.

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

لذلك، مع وضع ذلك في الاعتبار، لماذا هذه "القضايا"؟ تقول اللغة بوضوح أن بعض الأشياء تؤدي إلى السلوك غير محدد. وبعد لا توجد مشكلة، لا يوجد "يجب" المشاركة. إذا تغير السلوك غير المحدد عند إعلان أحد المتغيرات المعنية volatile, هذا لا يثبت أو يغير أي شيء. أنه غير معرف; ؛ لا يمكنك السبب في السلوك.

مثالتك الأكثر إثارة للاهتمام، واحد مع

u = (u++);

هو مثال كتاب نصي لسلوك غير محدد (انظر دخول ويكيبيديا نقاط التسلسل).

نصائح أخرى

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

هذا ما أحصل عليه على جهازي، مع ما أعتقد أنه يحدث:

$ cat evil.c
void evil(){
  int i = 0;
  i+= i++ + ++i;
}
$ gcc evil.c -c -o evil.bin
$ gdb evil.bin
(gdb) disassemble evil
Dump of assembler code for function evil:
   0x00000000 <+0>:   push   %ebp
   0x00000001 <+1>:   mov    %esp,%ebp
   0x00000003 <+3>:   sub    $0x10,%esp
   0x00000006 <+6>:   movl   $0x0,-0x4(%ebp)  // i = 0   i = 0
   0x0000000d <+13>:  addl   $0x1,-0x4(%ebp)  // i++     i = 1
   0x00000011 <+17>:  mov    -0x4(%ebp),%eax  // j = i   i = 1  j = 1
   0x00000014 <+20>:  add    %eax,%eax        // j += j  i = 1  j = 2
   0x00000016 <+22>:  add    %eax,-0x4(%ebp)  // i += j  i = 3
   0x00000019 <+25>:  addl   $0x1,-0x4(%ebp)  // i++     i = 4
   0x0000001d <+29>:  leave  
   0x0000001e <+30>:  ret
End of assembler dump.

(أنا ... لنفترض أن تعليمة 0x00000014 كانت نوعا من التحسين المترجم؟)

أعتقد أن الأجزاء ذات الصلة من معيار C99 هي 6.5 تعبيرات، §2

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

و 6.5.16 مشغلي الواجب، §4:

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

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

الانتقال إلى السلوك غير محدد, ، في مشروع C99 قياسي الجزء6.5 فقرة 3 يقول (التركيز لي):

يشار إلى إنشاء مجموعة المشغلين والمعاملين من قبل Syntax.74) باستثناء ما هو محدد لاحقا (لاستدعاء الوظيفة ()، &&، mether|،؟: ومشغلي الفواصل)، ترتيب تقييم الفرع الفرعي والنظام الذي يحدث فيه الآثار الجانبية غير محددة.

لذلك عندما يكون لدينا خط مثل هذا:

i = i++ + ++i;

نحن لا نعرف ما إذا كان i++ أو ++i سيتم تقييمها أولا. هذا هو أساسا لإعطاء المحول البرمجي خيارات أفضل للتحسين.

نحن ايضا لدينا السلوك غير محدد هنا أيضا لأن البرنامج هو تعديل المتغيرات (i, u, ، إلخ.) أكثر من مرة نقاط التسلسل. وبعد من مشروع القسم القياسي 6.5 فقرة 2(التركيز لي):

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

يستشهد بأمثلة التعليمات البرمجية التالية باعتبارها غير محددة:

i = ++i + 1;
a[i++] = i; 

في كل هذه الأمثلة، يحاول الرمز تعديل كائن أكثر من مرة في نفس نقطة التسلسل، والتي ستنتهي مع ; في كل واحدة من هذه الحالات:

i = i++ + ++i;
^   ^       ^

i = (i++);
^    ^

u = u++ + ++u;
^   ^       ^

u = (u++);
^    ^

v = v++ + ++v;
^   ^       ^

السلوك غير محدد يتم تعريف في مشروع C99 قياسي في قسم 3.4.4 كما:

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

و السلوك غير محدد يتم تعريفه في القسم 3.4.3 كما:

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

ويلاحظ أن:

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

معظم الإجابات هنا نقلت عن C قياسي تؤكد أن سلوك هذه البنيات غير محددة. لفهم لماذا سلوك هذه البنيات غير محددة, ، دعونا نفهم هذه الشروط أولا في ضوء قياسي C11:

تسلسل (5.1.2.3)

إعطاء أي تقييمين A و B, ، إذا A هو تسلسل قبل B, ، ثم تنفيذ A يجب أن تسبق تنفيذ B.

غير مدروس:

إذا A لا تتسلسل قبل أو بعد B, ، ومن بعد A و B لم يتم العثور على.

يمكن أن تكون التقييمات واحدة من شيئين:

  • حسابات القيمة, التي تعمل على نتيجة تعبير؛ و
  • آثار جانبية, ، والتي هي تعديلات للكائنات.

نقطة التسلسل:

وجود نقطة تسلسل بين تقييم التعبيرات A و B يعني أن كل شيء حساب القيمة و اعراض جانبية مرتبط ب A هو تسلسل قبل كل حساب القيمة و اعراض جانبية مرتبط ب B.

الآن القادمة إلى السؤال، بالنسبة للتعبيرات مثل

int i = 1;
i = i++;

يقول قياسي:

6.5 التعبيرات:

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

لذلك، يستدعي التعبير أعلاه UB لأن اثنين من الآثار الجانبية على نفس الكائن i غير مدرج بالنسبة لبعضها البعض. هذا يعني أنه لا يتسلسل ما إذا كان التأثير الجانبي عن طريق التعيين i سيتم القيام به قبل أو بعد التأثير الجانبي من قبل ++.
اعتمادا على ما إذا كانت المهمة تحدث قبل أو بعد الزيادة، سيتم إنتاج نتائج مختلفة وهذا هو واحد من حالة السلوك غير محدد.

يتيح إعادة تسمية i في يسار المهمة يكون il وعلى يمين المهمة (في التعبير i++) يكون ir, ، ثم يكون التعبير مثل

il = ir++     // Note that suffix l and r are used for the sake of clarity.
              // Both il and ir represents the same object.  

ونقطة مهمة فيما يتعلق بوسفيكس ++ المشغل هو:

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

وهذا يعني التعبير il = ir++ يمكن تقييمها إما كما

temp = ir;      // i = 1
ir = ir + 1;    // i = 2   side effect by ++ before assignment
il = temp;      // i = 1   result is 1  

أو

temp = ir;      // i = 1
il = temp;      // i = 1   side effect by assignment before ++
ir = ir + 1;    // i = 2   result is 2  

مما أدى إلى نتائج مختلفة 1 و 2 الذي يعتمد على تسلسل الآثار الجانبية حسب الواجب و ++ وبالتالي يستدعي UB.

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

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

الجزء الثاني، i = i++, ، هو أسهل قليلا لفهم. يحاول شخص ما بوضوح زيادة I، وتعيين النتيجة مرة أخرى إلى الأول. ولكن هناك عدة طرق للقيام بذلك في C. الطريقة الأساسية لإضافة 1 إلى i، وتعيين النتيجة مرة أخرى، هي نفسها في أي لغة برمجة تقريبا:

i = i + 1

ج، بالطبع، لديه اختصار مفيد:

i++

هذا يعني "إضافة 1 إلى الأول، وتعيين النتيجة مرة أخرى إلى الأول". لذلك إذا قمنا ببناء HodgePodge من الاثنين، عن طريق الكتابة

i = i++

ما نقوله حقا هو "إضافة 1 إلى الأول، وتعيين النتيجة مرة أخرى، وتعيين النتيجة مرة أخرى إلى الأول". نحن في حيرة من أمرنا، لذلك لا يزعجني الكثير مما إذا كان المترجم يحصل على حيرة من أمره أيضا.

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

اعتدنا أن تنفق ساعات لا حصر لها على comp.lang.c مناقشة تعبيرات مثل هذه و لماذا انهم غير محدد. اثنان من إجاباتي الأقدسة، التي تحاول أن تشرح حقا السبب، يتم أرشفة على الويب:

أنظر أيضا السؤال 3.8. وبقية الأسئلة في القسم 3 التابع ج قائمة التعليمات.

غالبا ما يرتبط هذا السؤال باعتباره نسخة مكررة من الأسئلة المتعلقة بموجب التعليمات البرمجية

printf("%d %d\n", i, i++);

أو

printf("%d %d\n", ++i, i++);

أو المتغيرات المماثلة.

في حين أن هذا هو أيضا السلوك غير محدد كما هو مذكور بالفعل، هناك اختلافات خفية عندما printf() يشارك عند مقارنة ببيان مثل:

x = i++ + i++;

في البيان التالي:

printf("%d %d\n", ++i, i++);

ال ترتيب التقييم من الحجج في printf() يكون غير محدد. وبعد هذا يعني التعبيرات i++ و ++i يمكن تقييمها بأي ترتيب. C11 قياسي لديه بعض الأوصاف ذات الصلة على هذا:

الملحق J، السلوكيات غير المحددة

يتم تقييم الترتيب الذي يتم فيه تقييم وظيفة الوظائف والجدات والتفضيلية داخل الحجج في مكالمة دالة (6.5.2.2).

3.4.4، السلوك غير محدد

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

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

ال السلوك غير محدد نفسها ليست مشكلة. النظر في هذا المثال:

printf("%d %d\n", ++x, y++);

هذا أيضا لديه السلوك غير محدد لأن ترتيب تقييم ++x و y++ غير محدد. لكنه بيان قانوني وصحيح تماما. هناك رقم سلوك غير محدد في هذا البيان. لأن التعديلات (++x و y++) القيام به ل خامد أشياء.

ما الذي يجعل البيان التالي

printf("%d %d\n", ++i, i++);

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


تفاصيل أخرى هي أن فاصلة المشاركة في دعوة printf () هي فاصل, ، وليس مشغل فاصلة.

هذا هو تمييز مهم لأن مشغل فاصلة هل تقدم أ نقطة التسلسل بين تقييم المعاملات الخاصة بهم، مما يجعل القانونية التالية:

int i = 5;
int j;

j = (++i, i++);  // No undefined behaviour here because the comma operator 
                 // introduces a sequence point between '++i' and 'i++'

printf("i=%d j=%d\n",i, j); // prints: i=7 j=6

يقوم مشغل الفاصلة بتقييم معاملاتها من اليسار إلى اليمين وتصلح فقط قيمة المعامل الأخيرة. لذلك في j = (++i, i++);, ++i الزيادات i ل 6 و i++ غلة قيمة قديمة i (6) الذي تم تعيينه ل j. وبعد ثم i يصبح 7 بسبب ما بعد الزيادة.

لذلك إذا كان فاصلة في دعوة الوظيفة كانت تكون مشغل فاصلة ثم

printf("%d %d\n", ++i, i++);

لن تكون مشكلة. لكنه يستدعي السلوك غير محدد بسبب ال فاصلة هنا هو فاصل.


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

هذا المشنور: غير محدد، سلوك غير محدد وغير محدد والتنفيذ هو أيضا ذات الصلة.

على الرغم من أنه من غير المحتمل أن يفعل أي مترجمات ومعالجات فعلا ذلك، فسيكون ذلك قانونيا، ضمن المعيار C، للمترجم لتنفيذ "I ++" مع التسلسل:

In a single operation, read `i` and lock it to prevent access until further notice
Compute (1+read_value)
In a single operation, unlock `i` and store the computed value

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

إذا كان المترجم هو الكتابة i++ كما هو مبين أعلاه (قانونية بموجب المعيار) وكان من المفترض أن تقاطع التعليمات المذكورة أعلاه طوال تقييم التعبير الشامل (قانوني أيضا)، وإذا لم يحدث ذلك لاحظ أن أحد التعليمات الأخرى التي حدثت للوصول إليها i, ، سيكون من الممكن (وقانونيا) للمترجم لتوليد سلسلة من التعليمات التي من شأنها أن الجمود. للتأكد، من المؤكد أن مترجم سيكتشف من المؤكد أن المشكلة في القضية التي يكون فيها نفس المتغير i يستخدم في كلا المكانين، ولكن إذا قبل روتين المراجع إلى مؤشرين p و q, واستخدامات (*p) و (*q) في التعبير أعلاه (بدلا من استخدام i مرتين) لن يتطلب التحويل البرمجي التعرف أو تجنب الجمود الذي سيحدث إذا تم تمرير عنوان الكائن نفسه لكليهما p و q.

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

i = i++;
i = i++ + ++i;

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

ومع ذلك، يمكن زيادة تفوق اثنين من المتغيرات المختلفة بين نقطتين التسلسلين.

while(*src++ = *dst++);

ما سبق هو ممارسة ترميز مشتركة أثناء نسخ / تحليل السلاسل.

بينما ال بناء الجملة تعبيرات مثل a = a++ أو a++ + a++ هو قانونية، سلوك من هذه البنيات هي غير معرف لأن أ سوف في C القياسية لا تطيع. C99 6.5P2.:

  1. بين نقطة التسلسل السابقة والمتسلسلة التالية يجب أن يكون لدى كائن قيمة مخزنة في معظمها مرة واحدة من خلال تقييم التعبير. [72] علاوة على ذلك، يجب قراءتها القيمة السابقة فقط لتحديد القيمة المراد تخزينها [73

مع الحاشية 73. مزيد من توضيح ذلك

  1. تمنح هذه الفقرة تعبيرات بيان غير محددة مثل

    i = ++i + 1;
    a[i++] = i;
    

    بينما السماح

    i = i + 1;
    a[i] = i;
    

يتم سرد نقاط التسلسل المختلفة في المرفق ج C11.C99.):

  1. فيما يلي نقاط التسلسل الموضحة في 5.1.2.3:

    • بين تقييمات مصممة الوظيفة والحجج الفعلية في مكالمة دالة والمكالمة الفعلية. (6.5.2.2).
    • بين تقييمات المعاملات الأولى والثانية للمشغلين التاليين: منطقي و && (6.5.13)؛ منطقية أو ||. (6.5.14)؛ فاصلة، (6.5.17).
    • بين تقييمات المعامل الأول من الشرطية؟ : يتم تقييم المشغل وأيهما من المعاملات الثانية والثالثة (6.5.15).
    • نهاية المعلن الكامل: المعلنات (6.7.6)؛
    • بين تقييم تعبير كامل والتعبير الكامل المقبل الذي سيتم تقييمه. فيما يلي تعبيرات كاملة: المهجع الذي ليس جزءا من مركب حرفي (6.7.9)؛ التعبير في بيان التعبير (6.8.3)؛ التعبير السيطرة على بيان الاختيار (إذا كان أو تبديل) (6.8.4)؛ التعبير السيطرة على بعض الوقت أو التصوير (6.8.5)؛ كل من التعبيرات (الاختيارية) للبيان (6.8.5.3)؛ التعبير (الاختياري) في بيان الإرجاع (6.8.6.4).
    • مباشرة قبل عودة وظيفة المكتبات (7.1.4).
    • بعد الإجراءات المرتبطة بكل مواصفات تحويل وظيفة الإدخال / الإخراج المنسق (7.21.6، 7.29.2).
    • مباشرة قبل وبعد كل مكالمة مباشرة إلى وظيفة المقارنة، وكذلك بين أي مكالمة إلى وظيفة المقارنة وأي حركة للأشياء التي تم تمريرها كحجج إلى تلك الدعوة (7.22.5).

صياغة نفسه الفقرة في C11. يكون:

  1. إذا لم يتم العثور على تأثير جانبي على كائن Scalar نسبي إما تأثير جانبي مختلف على كائن Scalar نفسه أو حساب القيمة باستخدام قيمة الكائن المتماثل نفسه، فإن السلوك غير محدد. إذا كان هناك العديد من طلبات المسموح بها للتأكد من تعبير التعبير، فإن السلوك غير محدد إذا حدث مثل هذا التأثير الجانبي غير المرجعي في أي من الطلبات .84)

يمكنك اكتشاف هذه الأخطاء في برنامج من خلاله استخدام إصدار حديث من دول مجلس التعاون الخليجي مع -Wall و -Werror, ، ثم سوف يرفض دول مجلس التعاون الخليجي تجميع البرنامج الخاص بك. فيما يلي إخراج دول مجلس التعاون الخليجي (Ubuntu 6.2.0-5ubuntu12) 6.2.0 20161005:

% gcc plusplus.c -Wall -Werror -pedantic
plusplus.c: In function ‘main’:
plusplus.c:6:6: error: operation on ‘i’ may be undefined [-Werror=sequence-point]
    i = i++ + ++i;
    ~~^~~~~~~~~~~
plusplus.c:6:6: error: operation on ‘i’ may be undefined [-Werror=sequence-point]
plusplus.c:10:6: error: operation on ‘i’ may be undefined [-Werror=sequence-point]
    i = (i++);
    ~~^~~~~~~
plusplus.c:14:6: error: operation on ‘u’ may be undefined [-Werror=sequence-point]
    u = u++ + ++u;
    ~~^~~~~~~~~~~
plusplus.c:14:6: error: operation on ‘u’ may be undefined [-Werror=sequence-point]
plusplus.c:18:6: error: operation on ‘u’ may be undefined [-Werror=sequence-point]
    u = (u++);
    ~~^~~~~~~
plusplus.c:22:6: error: operation on ‘v’ may be undefined [-Werror=sequence-point]
    v = v++ + ++v;
    ~~^~~~~~~~~~~
plusplus.c:22:6: error: operation on ‘v’ may be undefined [-Werror=sequence-point]
cc1: all warnings being treated as errors

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

j = (i ++, ++ i);

محددة جيدا، وسوف زيادة i من قبل واحد، مما يؤدي إلى القيمة القديمة، تجاهل هذه القيمة؛ ثم في شركة فاصلة، تسوية الآثار الجانبية؛ ثم الزيادة i من قبل واحد، وتصبح القيمة الناتجة قيمة التعبير - أي أن هذا مجرد وسيلة مفتعلة للكتابة j = (i += 2) وهو مرة أخرى بطريقة "ذكية" للكتابة

i += 2;
j = i;

ومع ذلك، و , في قوائم وسيطة الوظيفة هي ليس مشغل فاصلة، وليس هناك نقطة تسلسل بين تقييمات الحجج المميزة؛ بدلا من ذلك، يتم إرجاع تقييماتهم فيما يتعلق ببعضهم البعض؛ لذلك دعوة وظيفة

int i = 0;
printf("%d %d\n", i++, ++i, i);

لديها السلوك غير محدد لأن لا توجد نقطة تسلسل بين تقييمات i++ و ++i في وسيطات الوظيفة, ، وقيمة i وبالتالي يتم تعديلها مرتين، حسب كليهما i++ و ++i, ، بين نقطة التسلسل السابقة والآخر.

في https://stackoverflow.com/questions/29505280/arcrenting-array-index-in-c.c. سأل شخص ما عن بيان مثل:

int k[] = {0,1,2,3,4,5,6,7,8,9,10};
int i = 0;
int num;
num = k[++i+k[++i]] + k[++i];
printf("%d", num);

الذي يطبع 7 ... يتوقع المرجع أن يطبع 6.

ال ++i الزيادات غير مضمونة لجميع كاملة قبل بقية الحسابات. في الواقع، سوف تحصل محمولات مختلفة على نتائج مختلفة هنا. في المثال الذي قدمته، أول 2 ++i نفذت، ثم قيم k[] تم قراءتها، ثم الأخير ++i ومن بعد k[].

num = k[i+1]+k[i+2] + k[i+3];
i += 3

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

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

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

أفترض أنك سمعت التعريف الأساسي ل C ++ و -- المشغلون الآن، وكيف نموذج البادئة ++x يختلف عن شكل postfix x++. وبعد لكن من الصعب التفكير في هؤلاء المشغلين، لذا تأكد من أنك مفهوم، ربما كتبت برنامجا لاختبار صغير ينطوي على شيء مثل

int x = 5;
printf("%d %d %d\n", x, ++x, x++);

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

أو ربما كنت تبحث في التعبير الصعب فهم مثل

int x = 5;
x = x++ + ++x;
printf("%d\n", x);

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

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

ما الذي يجعل التعبير غير محدد؟ هي التعبيرات التي تنطوي على ++ و -- غير محدد دائما؟ بالطبع لا: هذه مشغلون مفيدون، وإذا كنت تستخدمها بشكل صحيح، فهي محددة جيدا.

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

دعنا نعود إلى الأمثلة التي استخدمتها في هذه الإجابة. عندما كتبت

printf("%d %d %d\n", x, ++x, x++);

السؤال هو، قبل الاتصال printf, ، هل التحويل البرمجي يحسب قيمة x أولا، أو x++, ، أو ربما ++xب لكنه اتضح نحن لا نعرف. وبعد لا توجد قاعدة في C التي تقول إن الحجج الموجودة في الوظيفة يتم تقييمها من اليسار إلى اليمين، أو اليمين إلى اليسار، أو في ترتيب آخر. لذلك لا يمكننا القول ما إذا كان المحول البرمجي سيفعل x أولا ثم ++x, ، ومن بعد x++, ، أو x++ ومن بعد ++x ومن بعد x, أو بعض الطلب الآخر. ولكن الأمر يهم بوضوح، لأنه اعتمادا على النظام الذي يستخدمه مترجم، سنحصل بوضوح على نتائج مختلفة مطبوعة printf.

ماذا عن هذا التعبير المجنون؟

x = x++ + ++x;

المشكلة في هذا التعبير هي أنه يحتوي على ثلاثة محاولات مختلفة لتعديل قيمة X: (1) x++ يحاول الجزء إضافة 1 إلى x، تخزين القيمة الجديدة في x, ، وإعادة القيمة القديمة لل x; ؛ (2) ++x يحاول الجزء إضافة 1 إلى x، تخزين القيمة الجديدة في x, ، وإرجاع القيمة الجديدة لل x; ؛ و (3) x = الجزء يحاول تعيين مجموع اثنين آخرين الظهر إلى x. أي من تلك المحاولة الثلاث سوف "الفوز"؟ أي من القيم الثلاث ستتم تعيينها بالفعل xب مرة أخرى، وربما من المستغرب، لا توجد حكم في ج لتخبرنا.

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


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

هذه التعبيرات هي كلها بخير:

y = x++;
z = x++ + y++;
x = x + 1;
x = a[i++];
x = a[i++] + b[j++];
x[i++] = a[j++] + b[k++];
x = *p++;
x = *p++ + *q++;

هذه التعبيرات هي كلها غير محددة:

x = x++;
x = x++ + ++x;
y = x + x++;
a[i] = i++;
a[i++] = i;
printf("%d %d %d\n", x, ++x, x++);

والسؤال الأخير هو، كيف يمكنك معرفة التعبيرات المعرفة جيدا، وما هي التعبيرات غير المحددة؟

كما قلت في وقت سابق، فإن التعبيرات غير المحددة هي تلك التي يحدث فيها كثيرا في وقت واحد، حيث لا يمكنك التأكد من حدوث الأشياء التي تحدث في الأمر، وحيث يهم النظام:

  1. إذا كان هناك متغير واحد يتم تعديله (المعين) في أماكن مختلفة أو أكثر، كيف تعرف أي تعديل يحدث أولا؟
  2. إذا كان هناك متغير يتم تعديله في مكان واحد، فإن وجود قيمته المستخدمة في مكان آخر، كيف تعرف ما إذا كان يستخدم القيمة القديمة أو القيمة الجديدة؟

كمثال على رقم 1، في التعبير

x = x++ + ++x;

هناك ثلاث محاولات لتعديل `x.

كمثال على رقم 2، في التعبير

y = x + x++;

كلانا يستخدمون قيمة x, ، وتعديلها.

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

تفسير جيد حول ما يحدث في هذا النوع من الحساب يتم توفيره في المستند N1188. من موقع ISO W14.

أشرح الأفكار.

القاعدة الرئيسية من ISO 9899 القياسية التي تنطبق في هذه الحالة هي 6.5P2.

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

نقاط التسلسل في تعبير مثل i=i++ من قبل i= و بعد i++.

في الورق الذي نقلته أعلاه، أوضح أنه يمكنك معرفة البرنامج بأنه يتكون من صناديق صغيرة، كل صندوق يحتوي على التعليمات بين 2 نقطة تسلسل متتالية. يتم تعريف نقاط التسلسل في المرفق C من المعيار، في حالة i=i++ هناك 2 نقاط تسلسل تحدد التعبير الكامل. مثل هذا التعبير هو مكافئ سنوي مع دخول expression-statement في شكل Backus-Naur من قواعد القواعد (يتم توفير قواعد اللغة في الملحق أ من المعيار).

لذلك ترتيب التعليمات داخل مربع ليس له طلب واضح.

i=i++

يمكن تفسيرها كما

tmp = i
i=i+1
i = tmp

أو وكذلك

tmp = i
i = tmp
i=i+1

لأن كل هذه النماذج لتفسير الرمز i=i++ صالحة ولأن كلاهما ينشئ إجابات مختلفة، فإن السلوك غير محدد.

لذلك يمكن رؤية نقطة التسلسل من خلال بداية بداية كل مربع وتحويل البرنامج [الصناديق هي وحدات ذرية في C] وداخل مربع لا يتم تعريف ترتيب التعليمات في جميع الحالات. تغيير هذا الطلب يمكن للمرء تغيير النتيجة في بعض الأحيان.

تعديل:

مصدر جيد آخر لشرح مثل هذا الغموض هم المدخلات من C-FAQ. الموقع (نشر أيضا ككتاب) ، يسمى هنا و هنا و هنا .

السبب هو أن البرنامج يقوم بتشغيل سلوك غير محدد. تكمن المشكلة في ترتيب التقييم، لأنه لا توجد نقاط تسلسل مطلوبة وفقا لمعيار C ++ 98 (لا توجد عمليات تسلسل قبل أو بعد آخر وفقا لمصطلحات C ++).

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

  • أولا أولا إلى دول مجلس التعاون الخليجي: استخدام نوين مينغو 15 دول مجلس التعاون الخليجي 7.1 سوف تحصل على:

    #include<stdio.h>
    int main(int argc, char ** argv)
    {
    int i = 0;
    i = i++ + ++i;
    printf("%d\n", i); // 2
    
    i = 1;
    i = (i++);
    printf("%d\n", i); //1
    
    volatile int u = 0;
    u = u++ + ++u;
    printf("%d\n", u); // 2
    
    u = 1;
    u = (u++);
    printf("%d\n", u); //1
    
    register int v = 0;
    v = v++ + ++v;
    printf("%d\n", v); //2
    

    }

كيف يعمل دول مجلس التعاون الخليجي؟ يقوم بتقييم التعبيرات الفرعية عند اليسار إلى الترتيب الصحيح للجانب الأيمن (RHS)، ثم تعيين القيمة إلى الجانب الأيسر (LHS). هذا هو بالضبط كيف يتصرف Java و C # معاييرهم وتحديد معاييرهم. (نعم، حدد البرمجيات المكافئة في Java و C # السلوكيات). تقوم بتقييم كل تعبير فرعي واحدا تلو الآخر في بيان RHS في اليسار إلى الترتيب الصحيح؛ لكل تعبير فرعي: يتم تقييم ++ ج (ما قبل الزيادة) أولا ثم يتم استخدام القيمة C للتشغيل، ثم الزيادة وظيفة C ++).

وفقا ل GCC C ++: المشغلون

في دول مجلس التعاون الخليجي C ++، يتحكم الأسبقية من المشغلين في الترتيب الذي يتم فيه تقييم المشغلين الفرديين

الكود المكافئ في السلوك المحدد C ++ كما يفهم دول مجلس التعاون الخليجي:

#include<stdio.h>
int main(int argc, char ** argv)
{
    int i = 0;
    //i = i++ + ++i;
    int r;
    r=i;
    i++;
    ++i;
    r+=i;
    i=r;
    printf("%d\n", i); // 2

    i = 1;
    //i = (i++);
    r=i;
    i++;
    i=r;
    printf("%d\n", i); // 1

    volatile int u = 0;
    //u = u++ + ++u;
    r=u;
    u++;
    ++u;
    r+=u;
    u=r;
    printf("%d\n", u); // 2

    u = 1;
    //u = (u++);
    r=u;
    u++;
    u=r;
    printf("%d\n", u); // 1

    register int v = 0;
    //v = v++ + ++v;
    r=v;
    v++;
    ++v;
    r+=v;
    v=r;
    printf("%d\n", v); //2
}

ثم نذهب إلى استوديو مرئي. وبعد Visual Studio 2015، تحصل على:

#include<stdio.h>
int main(int argc, char ** argv)
{
    int i = 0;
    i = i++ + ++i;
    printf("%d\n", i); // 3

    i = 1;
    i = (i++);
    printf("%d\n", i); // 2 

    volatile int u = 0;
    u = u++ + ++u;
    printf("%d\n", u); // 3

    u = 1;
    u = (u++);
    printf("%d\n", u); // 2 

    register int v = 0;
    v = v++ + ++v;
    printf("%d\n", v); // 3 
}

كيف يعمل Visual Studio، يستغرق نهج آخر، أنه يقوم بتقييم جميع التعبيرات المسبقة بزيادات في المرة الأولى، ثم يستخدم قيم المتغيرات في العمليات في المرة الثانية، وتعيينها من RHS إلى LHS في تمريرة ثالثة، ثم في آخر تمرير يقوم بتقييم كل شيء التعبيرات بعد الزيادة في تمريرة واحدة.

لذلك يعادل ما يعادل السلوك المحدد C ++ كما يفهم Visual C ++:

#include<stdio.h>
int main(int argc, char ** argv)
{
    int r;
    int i = 0;
    //i = i++ + ++i;
    ++i;
    r = i + i;
    i = r;
    i++;
    printf("%d\n", i); // 3

    i = 1;
    //i = (i++);
    r = i;
    i = r;
    i++;
    printf("%d\n", i); // 2 

    volatile int u = 0;
    //u = u++ + ++u;
    ++u;
    r = u + u;
    u = r;
    u++;
    printf("%d\n", u); // 3

    u = 1;
    //u = (u++);
    r = u;
    u = r;
    u++;
    printf("%d\n", u); // 2 

    register int v = 0;
    //v = v++ + ++v;
    ++v;
    r = v + v;
    v = r;
    v++;
    printf("%d\n", v); // 3 
}

كما وثائق Visual Studio تنص على الأسبقية وترتيب التقييم:

حيث تظهر العديد من المشغلين معا، لديهم أسبقية متساوية ويتم تقييمها وفقا للشرقية. يتم وصف المشغلين في الجدول في الأقسام التي تبدأ بمشغلي postfix.

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