انتقل إلى المحتوى
Topos Uranos

ملخص

البرنامج الذي يعطي نتيجة خاطئة لا يُصحَّح بتغيير الأسطر عشوائيًا، بل بالتحقيق. يقدّم هذا الدرس تصحيح الأخطاء بوصفه منهجًا تجريبيًا: إعادة إظهار العطل، وصوغ فرضية عن سببه، وتصميم تجربة تؤكّدها أو تستبعدها، وتضييق موضع الخطأ؛ والتنصيف يجده في عدد من الخطوات ينمو كما ينمو لوغاريتم حجم البرنامج. ويعرض الأداة (الترجمة مع -g ومن دون تحسين، وإعداد Visual Studio Code في الأنظمة الثلاثة، وجلسة حقيقية لـ GDB تبيّن ما يفعله المحرر في الخلفية)، ونقاط التوقف البسيطة والمشروطة، والخطوات، ومكدس الاستدعاءات، وفحص الحالة، من خلال مثال مركزي: القيمة الوسيطة التي تخطئ فقط حين يكون عدد البيانات زوجيًا. ويختم بالاستراتيجيات التي تفوق الأداة قيمة (اختزال الحالة، والبحث عن آخر نقطة صحيحة، والتحقق من اللامتغيرات بـ assert، وقراءة تقرير الكاشف) وبثلاث حالات: خطأ الفرق بواحد، واستدعاء ذاتي لا ينتهي، ومرجع مُبطَل.

أهداف التعلم

  1. إعداد الترجمة مع معلومات تصحيح الأخطاء (-g، من دون تحسين) وتشغيل مصحّح الأخطاء من Visual Studio Code على النظام الخاص.
  2. استعمال نقاط التوقف البسيطة والمشروطة، والتنفيذ خطوة بخطوة (الدخول، والتخطي، والخروج)، وفحص المتغيرات والعبارات ومكدس الاستدعاءات.
  3. تطبيق استراتيجية منهجية (إعادة الإظهار، والاختزال، والتنصيف، وصوغ الفرضيات واختبارها) لتحديد موضع خطأ معيّن، مع توثيق كل فرضية.
  4. الجمع بين مصحّح الأخطاء والكواشف والاختبارات لتحديد موضع خطأ في الذاكرة انطلاقًا من تقريره.

تصحيح الأخطاء تحقيق

الخطأ والعطل

العطل هو ما يُلاحَظ: نتيجة خاطئة، أو برنامج لا ينتهي، أو تقرير من الكاشف. والخطأ (أو العيب) هو السبب: سطر أو شرط أو قرار تصميمي لا يطابق المواصفات. العطل يُرى؛ أما الخطأ فلا بدّ من العثور عليه، وقد يكون بعيدًا عنه. ويلزم من ذلك أن التصحيح حيث يُرى العطل، كأن نطرح 2 من النتيجة لأنها «تزيد دائمًا 2»، يصحّح العَرَض ويترك السبب، الذي سيعود مع بيانات أخرى.

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

  1. الملاحظة وإعادة الإظهار. وصف العطل بدقة (مع أي مدخلات، وما الذي كان متوقعًا، وما الذي حصل) والتوصل إلى أن يقع كلما أردنا. والخطأ الذي لا يُعاد إظهاره لا يمكن التحقيق فيه، ولا يمكن التأكد من أنه صُحِّح.
  2. صوغ فرضية. ادعاء عن السبب يمكن أن يتبيّن أنه كاذب: «الترتيب لا يرتّب»، «دليل المعطى المركزي الثاني يزيد واحدًا».
  3. تصميم تجربة. اختيار نقطة من البرنامج والتنبؤ بالحالة التي ينبغي أن تُرى هناك إن كانت الفرضية صادقة وبالحالة التي تُرى إن كانت كاذبة.
  4. الاستنتاج والتضييق. استبعاد الفرضية أو تأكيدها؛ وفي الحالتين يصغر الجزء المشتبه فيه. ويتكرر ذلك حتى يصير الجزء سطرًا واحدًا.

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

مثالالقيمة الوسيطة مع عدد زوجي من البيانات

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

برنامج بلغة C++
#include <algorithm>#include <cstddef>#include <print>#include <vector>​// القيمة الوسيطة لقائمة من البيانات.// الشرط المسبق: values غير فارغ.// الشرط اللاحق: تعيد المعطى المركزي للقائمة المرتبة، أو متوسط المعطيين المركزيين.double median(std::vector<double> values){    std::ranges::sort(values);    const std::size_t n{values.size()};    const std::size_t middle{n / 2};    if (n % 2 == 1) {        return values[middle];    }    return (values[middle] + values[middle + 1]) / 2.0;}​int main(){    const std::vector<double> odd{7.0, 1.0, 5.0, 3.0, 9.0};    const std::vector<double> even{7.0, 1.0, 5.0, 3.0};    std::println("القيمة الوسيطة لـ {}: {}", odd, median(odd));    std::println("القيمة الوسيطة لـ {}: {}", even, median(even));}
المخرجات
القيمة الوسيطة لـ [7, 1, 5, 3, 9]: 5القيمة الوسيطة لـ [7, 1, 5, 3]: 6

الملاحظة دقيقة: مع {7,1,5,3}\{7, 1, 5, 3\}، وبعد ترتيبها {1,3,5,7}\{1, 3, 5, 7\}، كان المتوقع (3+5)/2=4(3 + 5)/2 = 4 وحصلنا على 6؛ ومع خمسة معطيات تكون النتيجة صحيحة. وتَرِد ثلاث فرضيات على الأقل: (H1) الترتيب لا يرتّب؛ (H2) دليلا المعطيين المركزيين خاطئان؛ (H3) الجمع أو القسمة مكتوبان خطأً. وكل واحدة تتنبأ بشيء مختلف في السطر 17، قبل تنفيذه: إن صدقت H1، فإن values غير مرتب؛ وإن صدقت H2، فإن values[middle] وvalues[middle + 1] ليسا 3 و5؛ وإن صدقت H3، فالمعطيان 3 و5 ومع ذلك يعطي الحساب 6. ونجاح الحالة الفردية يجعل H1 مستبعدة الاحتمال، لكن «مستبعدة الاحتمال» ليست «مُستبعَدة»؛ والتجربة، بمصحّح الأخطاء، هي إيقاف البرنامج في السطر 17 والنظر.

التنصيف

حين لا تكون هناك فرضية واضحة، يبقى منهج لا يخيب: التنصيف. يُفترض أن حالة البرنامج صحيحة في البداية وخاطئة في النهاية، وأنه يمكن في كل نقطة وسيطة الحكم بصحتها (بمقارنتها بقيمة محسوبة يدويًا، أو بالتحقق من لامتغير). وتُفحص الحالة في منتصف الجزء المشتبه فيه: إن كانت صحيحة فالخطأ في النصف الثاني؛ وإلا ففي النصف الأول. إنه البحث الثنائي من درس البحث والترتيب، مطبَّقًا على أسطر برنامج (أو على جولات حلقة، أو على إصدارات ملف).

يتتبّع الشكل تنصيف ستة عشر سطرًا والخطأ في السطر 11؛ وكل علامة تجربة على الحالة بعد ذلك السطر: صحيحة (✓) أو خاطئة (✗).

البرهان

  1. في البداية، الأسطر الستة عشر كلها مشتبه فيها: نعلم أن الحالة صحيحة قبل السطر 1 وخاطئة بعد السطر 16.

  2. بعد السطر 8 تكون الحالة صحيحة: تُبرَّأ الأسطر من 1 إلى 8.

  3. بعد السطر 12 تكون الحالة خاطئة بالفعل: الخطأ بين السطر 9 والسطر 12.

  4. بعد السطر 10، صحيحة: يبقى السطران 11 و12.

  5. بعد السطر 11، خاطئة: آخر نقطة صحيحة وأول نقطة خاطئة متجاورتان، والخطأ في السطر 11.

العدّ هو عدّ البحث الثنائي: كل تجربة تقسم الجزء إلى نصفين، فتكفي kk تجربة لـ 2k2^k سطرًا. ولبرنامج من ألف سطر تكفي عشر تجارب، لأن 210=10242^{10} = 1024. ومن هنا ميزة المنهج: كلفته تنمو كما ينمو لوغاريتم الحجم، ولا يتطلب فهم البرنامج كله، بل معرفة الحكم، في نقطة ما، بأن الحالة هي الحالة الواجبة. غير أن هذا الحكم هو الجزء الصعب: فالمواصفات واللامتغيرات هي المعيار الذي يُحكم به على كل حالة وسيطة.