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

السلوك غير المعرف (Undefined Behavior)

السلوك غير المعرف معرف في معيار C11 مادة ٣.٤.٣‏pl:

هو سلوك، نتج عن إستخدم تركيبة برمجية غير portable أو خاطئة أو بيانات خاطئة، لم يحدد له هذا المعيار الدولي أية شروط.

في الهامش توجد هذه الملحوظة:

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

مثال من مواضع السلوك غير المعرف سلوك البرنامج عند حدوث طفح لعدد صحيح (Integer Overflow).

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

/* تتبع مؤشر انقضت فترة حياته */
// C11 §6.2.4p2    
char *x = malloc(4); 
free(x); 
printf("%p\n", (void *)x); //سلوك غير معرف 

/* طرء عدة تغييرات على كائن دون فواصل (Sequence Points) ‏ */
// C11 §6.5p2
int x = 42; 
x = x++; //سلوك غير معرف 
 
/* تأشير المؤشر إلى الخانة التالية لنهاية مصفوفة */
// C11 §6.5.6p8
int a[3];
*p = a + 4; //سلوك غير معرف 
 
/* محاولة تغيير ثابت نصّي */
// C11 §6.4.5p7
char *x = "foo";
x[0] = 'b';  //سلوك غير معرف 
 

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

علاوة على ذلك، فالسلوك غير المعرف يجعل البرنامج بأكمله غير معرف وليس فقط النقطة التي بها الكود المسبب. تأمل هذا المثال من "Defining the undefinedness of C":

#include <stdio.h>

int main(void) {
int r = 0, d = 0;
for (int i = 0; i < 5; i++) {
printf("%d\n", i);
r += 5 / d; // divides by zero
}
return r;
}

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

السلوك غير المحدد (Unspecified Behavior)

السلوك غير المحدد معرف في معيار C11 مادة ٤.٤.٣‏:

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

ويوجد مثال:

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

يذكر معيار السي ٥٠ طريقة لحدوث السلوك غير المحدد. هنا بعض الأمثلة:

/* نتوج نفس المصفوفة عن ثابتين نصيين */
// C11 §6.4.5p7
char *a = "foo";
char *b = "foo";
printf("%d\n", a == b); //سلوك غير محدد 
    
/* ترتيب تنفيذ التعبيرات الفرعية 
 * غير المفصولة من خلال 
 * Sequence Points */
// C11 §6.5
f() && g(); // يتم الإستدعاء من اليسار إلى اليمين لأن &&فاصل 
x = f() + g(); // غير محدد أيهم يتم تنفيذه قبل الأخر

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

سلوك معرف من قبل البيئة (Implementation-defined Behavior)

السلوك المعرف من قبل البيئة معرف في C11 مادة ١.٤.٣‏:

هو سلوك غير محدد توثق فيه البيئة على أي أساس تم الإختيار

مصحوبة بهذا المثال:

مثال من أمثلة السلوك المعرف من قبل البيئة هو مد أكبر بت (bit) عند إجراء عملية shift لليمين على عدد صحيح ذو إشارة.

يذكر معيار السي ما لا يقل عن ١٠٠ طريقة مختلفة لحدوث السلوك المحدد من قبل البيئة. هنا بعض الأمثلة:

/* أي تعريف غير معياري للدالة الرئيسية
 * أي عدا
 *     int main(void)
 *     int main(int argc, char *argv[])
 * و ما يماثلهم  
 */
// C11 §5.1.2.2.1p1
void main(void) { // ربما يتم السماح بها
                  // لو وثقت البيئة ذلك 
    return 0;
} 
    
/* عدد البت في البايت الواحد */
// C11 §3.6 and §5.2.4.2.1p1
printf("%d\n", CHAR_BIT); // سلوك محدد من قبل البيئة
                          // لا يجوز أن يقل عن ٨

/* القيم العددية للحروف 
 * وقت تشغيل البرنامج 
 */
// C11 §5.2.1p1
printf("%d\n", 'a'); // سلوك محدد من قبل البيئة 
    
/* ناتج تحويل المؤشر لنوع عدد صحيح والعكس */
// C11 §6.3.2.3p5 and §6.3.2.3p6
char y, *x = &y;
int i = (int)x; // سلوك محدد من قبل البيئة. وغير معرف 
                // إذا تعدى الناتج السعة التخزينية للنوع 

الخلاصة

عند البرمجة بالسي، من المهم جدا أن يكون هناك على الأقل معرفة سطحية بنوعية الأشياء التي تولد سلوكا غير معرف أو غير محدد أو معرف من قبل البيئة. يوفر معيار السي قائمة مفيدة في نهاية Annex J:

  • J.1 ‏ Unspecified behavior
  • J.2 ‏ Undefined behavior
  • J.3 ‏ Implementation-defined behavior

روابط ذات صلة

مراجع

  1. Ellison, C., & Rosu, G. (2012). Defining the undefinedness of C (pdf).

تعليق المترجم

  • يعرف معيار C11 البيئة (Implementation) كالآتي

    particular set of software, running in a particular translation environment under particular control options, that performs translation of programs for, and supports execution of functions in, a particular execution environment

    أي أنها تشمل برنامج الترجمة (Compiler) وبيئة التشغيل (libc). تم إستخدم لفظ بيئة لتعذر إيجاد لفظ مناسب ل Implementation دون لبس مع Application.
  • Portable تعني أن البرنامج خال من الأشياء غير المعرفة تماما من قبل السي. الكلمة العربية "متنقل" لم أرتئيها مناسبة.
  • يمكن شراء المعيار C11 من منظمة ISO أو مطالعة نسخة بدون غلاف الأيزو من هنا .