ما هو السلوك الشائع غير المحدد/غير المحدد لـ C الذي تواجهه؟[مغلق]

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

  •  01-07-2019
  •  | 
  •  

سؤال

مثال على السلوك غير المحدد في لغة C هو ترتيب تقييم الوسائط لوظيفة ما.قد يكون من اليسار إلى اليمين أو من اليمين إلى اليسار، أنت لا تعرف.وهذا من شأنه أن يؤثر على كيفية foo(c++, c) أو foo(++c, c) يتم تقييمه.

ما هو السلوك الآخر غير المحدد الذي يمكن أن يفاجئ المبرمج غير الواعي؟

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

المحلول

سؤال لمحامي اللغة.همكاي.

أعلى 3 الخاص بي:

  1. انتهاك قاعدة التعرج الصارمة
  2. انتهاك قاعدة التعرج الصارمة
  3. انتهاك قاعدة التعرج الصارمة

    :-)

يحرر فيما يلي مثال صغير يخطئ مرتين:

(افترض أن عدد صحيح 32 بت وendian صغير)

float funky_float_abs (float a)
{
  unsigned int temp = *(unsigned int *)&a;
  temp &= 0x7fffffff;
  return *(float *)&temp;
}

يحاول هذا الرمز الحصول على القيمة المطلقة للتعويم عن طريق التلاعب بالبت مع بت الإشارة مباشرة في تمثيل التعويم.

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

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

هناك ثلاث طرق صالحة لفعل الشيء نفسه.

استخدم مؤشر char أو void أثناء عملية الإرسال.هذه الأسماء المستعارة دائمًا لأي شيء، لذا فهي آمنة.

float funky_float_abs (float a)
{
  float temp_float = a;
  // valid, because it's a char pointer. These are special.
  unsigned char * temp = (unsigned char *)&temp_float;
  temp[3] &= 0x7f;
  return temp_float;
}

استخدم ميمكوبي.يأخذ Memcpy مؤشرات باطلة، لذلك سيفرض الاسم المستعار أيضًا.

float funky_float_abs (float a)
{
  int i;
  float result;
  memcpy (&i, &a, sizeof (int));
  i &= 0x7fffffff;
  memcpy (&result, &i, sizeof (int));
  return result;
}

الطريقة الثالثة الصحيحة:استخدام النقابات.هذا صراحة غير محدد منذ C99:

float funky_float_abs (float a)
{
  union 
  {
     unsigned int i;
     float f;
  } cast_helper;

  cast_helper.f = a;
  cast_helper.i &= 0x7fffffff;
  return cast_helper.f;
}

نصائح أخرى

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

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

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

  • char ليس بالضرورة (غير) موقع.
  • يمكن أن يكون int بأي حجم بدءًا من 16 بت.
  • العوامات ليست بالضرورة منسقة أو متوافقة مع IEEE.
  • الأنواع الصحيحة ليست بالضرورة مكملة لاثنين، ويؤدي تجاوز السعة الحسابية للأعداد الصحيحة إلى سلوك غير محدد (لن تتعطل الأجهزة الحديثة، ولكن بعض تحسينات المترجم ستؤدي إلى سلوك مختلف عن السلوك الملتف على الرغم من أن هذا هو ما تفعله الأجهزة.على سبيل المثال if (x+1 < x) قد يكون الأمثل كما هو الحال دائما عندما x تم التوقيع على النوع:يرى -fstrict-overflow الخيار في دول مجلس التعاون الخليجي).
  • "/" ، "." و ".." في #include ليس لها أي معنى محدد ويمكن معاملتها بشكل مختلف من قبل مجمعين مختلفين (هذا يختلف فعليًا ، وإذا حدث خطأ فسيدمر يومك).

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

  • خيوط POSIX ونموذج الذاكرة ANSI.الوصول المتزامن إلى الذاكرة ليس محددًا جيدًا كما يعتقد المبتدئون.volatile لا يفعل ما يعتقده المبتدئون.لم يتم تحديد ترتيب الوصول إلى الذاكرة بشكل جيد كما يعتقد المبتدئون.الوصول يستطيع يتم نقلها عبر حواجز الذاكرة في اتجاهات معينة.تماسك ذاكرة التخزين المؤقت غير مطلوب.

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

وكما أعتقد أن نيلز ذكر بشكل عابر:

  • انتهاك قاعدة التعرج الصارمة.

تقسيم شيء بالمؤشر إلى شيء ما.لن يتم تجميعها لسبب ما ...:-)

result = x/*y;

المفضل لدي هو هذا:

// what does this do?
x = x++;

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

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

يتم زيادة x بمقدار 1

لذا يجب أن نرى x == 2 بعد ذلك.لكن هذا ليس صحيحًا في الواقع، ستجد بعض المترجمين الذين لديهم x == 1 بعد ذلك، أو ربما حتى x == 3.سيتعين عليك إلقاء نظرة فاحصة على التجميع الذي تم إنشاؤه لمعرفة سبب ذلك، ولكن الاختلافات ترجع إلى المشكلة الأساسية.في الأساس، أعتقد أن السبب في ذلك هو أنه يُسمح للمترجم بتقييم بياني التخصيص بأي ترتيب يريده، لذلك يمكنه القيام بالأمر x++ الأول، أو x = أولاً.

مشكلة أخرى واجهتها (تم تحديدها ولكنها بالتأكيد غير متوقعة).

شار شرير.

  • موقعة أو غير موقعة اعتمادًا على ما يشعر به المترجم
  • لا تفويض كما 8 بت

لا يمكنني حساب عدد المرات التي قمت فيها بتصحيح محددات تنسيق printf لتتناسب مع وسيطاتها. أي عدم تطابق هو سلوك غير محدد.

  • لا، لا يجب عليك اجتياز int (أو long) ل %x - ان unsigned int مطلوب
  • لا، لا يجب عليك اجتياز unsigned int ل %d - ان int مطلوب
  • لا، لا يجب عليك اجتياز أ size_t ل %u أو %d - يستخدم %zu
  • لا، لا يجب عليك طباعة المؤشر باستخدامه %d أو %x - يستخدم %p ويلقي إلى أ void *

ليس من الضروري أن يخبرك المترجم أنك تستدعي دالة بعدد خاطئ من المعلمات/أنواع معلمات خاطئة إذا لم يكن النموذج الأولي للوظيفة متاحًا.

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

هذا:

"x"

عبارة عن سلسلة حرفية (وهي من النوع char[2] ويتحلل إلى char* في معظم السياقات).

هذا:

'x'

هو ثابت حرف عادي (وهو، لأسباب تاريخية، من النوع int).

هذا:

'xy'

هو أيضًا ثابت ذو طابع قانوني تمامًا، لكن قيمته (التي لا تزال من النوع int) تم تحديد التنفيذ.إنها ميزة لغوية عديمة الفائدة تقريبًا تؤدي في الغالب إلى التسبب في الارتباك.

قام مطورو clang بنشر بعض أمثلة عظيمة منذ فترة، في منشور يجب على كل مبرمج C قراءته.بعض الأشياء المثيرة للاهتمام التي لم يتم ذكرها من قبل:

  • تجاوز عدد صحيح موقّع - لا، ليس من المقبول التفاف متغير موقّع بعد الحد الأقصى.
  • إلغاء الإشارة إلى مؤشر NULL - نعم، هذا غير محدد، وقد يتم تجاهله، راجع الجزء 2 من الرابط.

لقد اكتشف EE هنا للتو أن a >>-2 محفوف بعض الشيء.

أومأت برأسي وأخبرتهم أن هذا ليس طبيعياً.

تأكد دائمًا من تهيئة المتغيرات الخاصة بك قبل استخدامها!عندما بدأت للتو مع لغة C، سبب لي ذلك عددًا من الصداع.

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

ما ورد أعلاه هو للمعيار C.في C++ اختفت هذه المشاكل إلى حد كبير.أصبحت الدالة max الآن دالة مقولبة.

نسيت أن أضيف static float foo(); في ملف الرأس، فقط للحصول على استثناءات الفاصلة العائمة التي يتم طرحها عندما تُرجع 0.0f؛

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