السلوك غير المعرف (Undefined Behavior) والسلوك غير المحدد (Unspecified Behavior) في لغة ++C: تحليل معمق
تُعدّ لغة البرمجة ++C من اللغات القوية والعالية الأداء التي تُستخدم على نطاق واسع في تطوير البرمجيات الحرجة والأنظمة المعقدة، بدءاً من أنظمة التشغيل وقواعد البيانات إلى التطبيقات الصناعية والعسكرية. غير أن هذه القوة تأتي مصحوبة بتحديات تقنية دقيقة، لعل من أبرزها مفهومي “السلوك غير المعرف” (Undefined Behavior) و”السلوك غير المحدد” (Unspecified Behavior)، وهما مفهومان أساسيان في معمارية اللغة والمعايير التي تُبنى عليها. تتسبب هذه السلوكيات في نتائج غير متوقعة، مما يجعل فهمها وتفاديها من أهم أولويات مطوري ++C.
أولاً: نظرة عامة على خصائص لغة ++C
تتميز ++C بعدة خصائص تجعلها مفضلة في البرمجة منخفضة المستوى، من بينها:
-
التحكم المباشر في الذاكرة.
-
إمكانية كتابة كود قريب من الآلة.
-
التوافق مع لغة C.
-
الأداء العالي دون تدخل من “آلة افتراضية” مثل ما نجده في Java أو C#.
لكن هذه الميزات تجعل اللغة أكثر عرضة للأخطاء الدقيقة التي تنشأ من تجاوز حدود التعريف السلوكي، وهو ما تُحدده معايير ++C الرسمية (ISO/IEC 14882).
ثانياً: تعريف السلوك غير المعرف (Undefined Behavior)
السلوك غير المعرف هو حالة يُترك فيها تصرف البرنامج غير محدد من قبل معيار اللغة. أي أن ما سيحدث فعليًا عند تنفيذ كود معين يُصنّف ضمن هذه الحالة ليس من مسؤولية مترجم اللغة أو النظام التنفيذي. والنتائج قد تتراوح بين التنفيذ السليم الظاهري، إلى تعطل البرنامج (crash)، أو حتى تنفيذ عمليات غير متوقعة تؤثر في الذاكرة أو النظام.
أمثلة شائعة للسلوك غير المعرف
-
تجاوز حدود المصفوفة:
cppint arr[5]; arr[10] = 42; // سلوك غير معرفالكتابة في مكان خارج حدود المصفوفة يُعد سلوكاً غير معرف لأنه يُمكن أن يؤدي إلى تلف الذاكرة أو الكتابة في موقع نظامي حساس.
-
قسمة عدد صحيح على صفر:
cppint a = 10; int b = 0; int c = a / b; // سلوك غير معرففي ++C، ليس من المسموح قسمة عدد صحيح على صفر، وتُصنّف النتيجة على أنها سلوك غير معرف، مما قد يؤدي إلى تعطل البرنامج.
-
استخدام مؤشر غير مُهيّأ:
cppint* ptr; *ptr = 100; // سلوك غير معرفاستخدام مؤشر لم يُعيَّن بعد يُمكن أن يوجّه البرنامج للوصول إلى منطقة غير مخصصة في الذاكرة.
-
التعديل والقراءة من نفس المتغير بدون ترتيب تسلسلي (Sequence Point):
cppint i = 1; i = i++ + ++i; // سلوك غير معرفالمعايير لا تضمن ترتيب تنفيذ العمليات هنا، مما يجعل النتيجة غير متوقعة.
ثالثاً: تعريف السلوك غير المحدد (Unspecified Behavior)
السلوك غير المحدد هو نوع من السلوكيات التي يترك فيها معيار ++C التنفيذ لمترجم اللغة ليختار من بين مجموعة من السلوكيات المحتملة، بشرط أن يكون أي منها صالحًا ولا يؤدي إلى كسر المعايير. بمعنى آخر، يكون السلوك قانونياً ولكنه غير قابل للتنبؤ عبر جميع البيئات.
أمثلة على السلوك غير المحدد
-
ترتيب تقييم المعاملات (Operands):
cppvoid print(int a, int b) { std::cout << a << " " << b << std::endl; } int x = 5; print(x++, ++x); // السلوك غير محددفي هذا المثال، لا يضمن معيار ++C أي ترتيب معين لتقييم
x++و++x. قد يُقيَّم أحدهما قبل الآخر حسب ما يراه المترجم مناسباً، لكن كل منهما يبقى ضمن السلوك المحدد من قبل المعيار. -
استخدام نفس المتغير في معاملات مختلفة ضمن دالة واحدة:
cppint arr[3] = {1, 2, 3}; int index = 1; int value = arr[index] + (index = 2); // السلوك غير محددالترتيب الذي تُنفذ به العمليات ليس محددًا في هذه الحالة، ما يعني أن قيمة
valueقد تختلف بين بيئات التطوير المختلفة.
الفرق الجوهري بين السلوك غير المعرف وغير المحدد
| الخاصية | Undefined Behavior (UB) | Unspecified Behavior (USB) |
|---|---|---|
| تعريف المعيار | لا يُعرف إطلاقاً | يُعرف ضمن مجموعة من السلوكيات |
| نتائج التنفيذ | قد تكون مدمّرة وغير متوقعة | تكون صحيحة ولكنها مختلفة |
| مسؤولية المترجم | غير مسؤول عن تصحيح السلوك | يختار أحد السلوكيات الممكنة |
| القابلية للتنبؤ | منخفضة جداً | متوسطة (يعتمد على المترجم) |
| إمكانية كشف الخطأ وقت الترجمة | غالباً لا يمكن | غالباً لا يتم التنبيه |
| أمثلة | قسمة على صفر، تجاوز مصفوفة | ترتيب تقييم المعاملات، استدعاء الدوال |
رابعاً: أثر السلوك غير المعرف وغير المحدد على أمان البرامج
السلوك غير المعرف يشكل خطرًا حقيقيًا على أمن النظام، حيث يُمكن أن:
-
يُستخدم في استغلالات أمان مثل تجاوز المكدس (Stack Overflow).
-
يُؤدي إلى تسرب بيانات حساسة في بعض الحالات.
-
يُنتج أخطاء يصعب تتبعها وإصلاحها في البيئات متعددة الخيوط (Multithreading).
أما السلوك غير المحدد فقد يؤدي إلى نتائج غير متوقعة تجعل من الصعب ضمان الاستقرار عبر المنصات المختلفة، مما يُعقّد من اختبار البرمجيات وصيانتها.
خامساً: كيف يتعامل المترجم مع هذه السلوكيات
معالجات لغة ++C الحديثة مثل GCC وClang وMSVC تستفيد من خاصية “السلوك غير المعرف” لتحسين الأداء. بما أن المعيار لا يُلزم المترجم بتنفيذ كود معين في حالة UB، فإن المترجم قد يقوم بحذف أقسام كاملة من الكود أو إعادة ترتيب التعليمات بما يُحقق أداء أعلى.
مثال:
cppint foo(int* p) {
if (p)
return *p;
else
return 0;
}
إذا كان المترجم قادراً على استنتاج أن p لا يمكن أن يكون غير مهيأ، فقد يقوم بحذف التحقق من if (p) تماماً.
هذا يجعل الكود أسرع، لكنه يجعل من أي سلوك غير معرف تهديدًا خطيرًا للسلامة التشغيلية.
سادساً: طرق كشف وتفادي UB و USB في البرامج
أدوات تحليل الاستاتيكي
-
Clang Static Analyzer
-
Cppcheck
-
Coverity Scan
هذه الأدوات تقوم بتحليل الكود دون تنفيذه وتحدد الأماكن التي قد يحدث فيها سلوك غير معرف أو غير محدد.
أدوات تحليل الديناميكي
-
Valgrind
-
AddressSanitizer (ASan)
-
UndefinedBehaviorSanitizer (UBSan)
تُستخدم أثناء تنفيذ البرنامج وتكشف عن حالات مثل الوصول إلى ذاكرة غير مخصصة أو تجاوز المؤشرات أو استخدام متغيرات غير مهيأة.
ممارسات برمجية سليمة
-
استخدام المؤشرات الذكية (مثل
std::unique_ptr,std::shared_ptr) بدلاً من المؤشرات العادية. -
تفعيل تحذيرات المترجم (
-Wall -Wextra). -
مراجعة الكود بشكل دوري ومكثف.
-
تجنب إعادة استخدام المتغيرات في نفس السطر عندما يكون الترتيب غير واضح.
-
اعتماد أساليب كتابة واضحة ومحددة ومثبتة.
سابعاً: دور معيار ++C في تعريف السلوكيات
معايير ++C المتتالية (C++98، C++11، C++14، C++17، C++20) تعرّف بتفصيل كبير أنواع السلوكيات وتُوضح متى تكون النتيجة معرفة أو غير معرفة. ويجدر بالمطورين مراجعة وثائق المعيار أو ملاحق مثل Annex J التي تعرض قائمة موسعة من حالات Undefined Behavior.
ثامناً: أثر السلوكيات على قابلية النقل (Portability)
البرامج التي تتضمن UB أو USB تكون عرضة للتصرفات المختلفة على المنصات المختلفة، مما يجعل من الصعب ضمان أن يعمل البرنامج بنفس الشكل على جميع الأنظمة (ويندوز، لينكس، ماك، إلخ). هذا يؤثر سلبًا على قابلية الصيانة والاختبار والنشر.
تاسعاً: السلوكيات الأخرى ذات الصلة
السلوك المعرف (Well-defined Behavior)
هو السلوك الذي يحدده معيار اللغة بدقة، ويُتوقع أن يتصرف بنفس الطريقة على جميع المترجمات والأنظمة.
السلوك المحدد بتنفيذ معين (Implementation-defined Behavior)
هو سلوك يُترك تحديده لتوثيق المترجم أو النظام، مثل حجم نوع int أو ترتيب البايتات (Endianness).
عاشراً: السلوكيات في السياقات متعددة الخيوط (Multithreading)
في بيئات تعدد الخيوط، السلوك غير المعرف يصبح أكثر خطورة، حيث أن الوصول المتزامن إلى نفس المتغير من عدة خيوط دون مزامنة مناسبة (مثل mutex) يُصنّف كسلوك غير معرف وفقًا للمعيار. وهذا يمكن أن يُؤدي إلى مشاكل متقطعة تظهر فقط في ظروف خاصة من التزامن، ما يصعّب كشفها.
الحادي عشر: الخلاصة
يُعتبر فهم السلوك غير المعرف والسلوك غير المحدد من الضروريات الملحة لكل مبرمج يتعامل مع لغة ++C، حيث أن تجاهلهما قد يُؤدي إلى كود غير موثوق أو حتى غير آمن. ويقع على عاتق المطور مسؤولية التأكد من خلو برنامجه من هذه السلوكيات قدر الإمكان، عبر أدوات الكشف، والممارسات السليمة، والاطلاع الدائم على المستجدات في معيار اللغة.
المراجع
-
ISO/IEC 14882:2020 — Programming Language C++
-
Bjarne Stroustrup, The C++ Programming Language, 4th Edition, Addison-Wesley.

