تصحيح الأخطاء
من الشائع جدًا أثناء حل المسائل البرمجية أن لا يعمل البرنامج كما هو متوقع تمامًا. تُعرف عملية اكتشاف سبب الخطأ وإصلاحه باسم تصحيح الأخطاء (debugging). لا توجد طريقة واحدة مثالية لتصحيح الأخطاء، ولكن هناك تقنيات تساعد في اكتشاف الخطأ بسرعة أكبر.
أسلوب كتابة برنامج
كلما كانت البرنامج أنظف وأكثر تنظيمًا، قلَّ احتمال وقوع الأخطاء. تأكد من تسمية المتغيرات بشكل واضح حتى لا تخلط بينها. كما يُستحسن استخدام المسافات والهوامش (indentation) المناسبة لتحسين قابلية القراءة، خاصة أثناء تصحيح الأخطاء لاحقًا.
برنامج سيئ:
#include<iostream>
#include<cmath>
#include<cassert>
#include<iomanip>
#include<algorithm>
using namespace std;
int main () {
int a,b;
cin>>a>>b;
int n[a];
for(int m=a-1;m>-1;m-=1){
cin>>n[m];
}
cout << n[b];}برنامج جيد:
#include<iostream>
using namespace std;
int main () {
int n, m;
cin >> n >> m;
int a[n];
for (int i = n - 1 ; i >= 0 ; i--) { // إدخال المصفوفة من اليمين إلى اليسار
cin >> a[i];
}
cout << a[m];
} - أضف المكتبات اللازمة فقط، وتجنب استخدام القوالب الضخمة.
- استخدم
nوmللأحجام أو العدّادات — فهذا هو العُرف الشائع في المسائل البرمجية مما يجعل البرنامج أكثر وضوحًا. - استخدم
iوjوkكمتغيرات تكرار — وهي الأسماء القياسية في الحلقات التكرارية. - استخدم هوامش موحّدة: كل مستوى من التداخل يُزاد بمقدار تبويب واحد (أو 4 مسافات).
- أزل الهوامش عند إغلاق الكتل البرمجية — فهذا يساعدك بصريًا على معرفة مكان انتهاء الحلقات أو الشروط.
- أضف التعليقات عند الحاجة لتوضيح الهدف من البرنامج ومقارنتها بما فعليًا يحدث أثناء التنفيذ.
الطباعة
الفكرة هي استخدام cout لتتبّع قيم المتغيرات. فإذا لاحظنا أن متغيرًا يحمل قيمة غير متوقعة، فقد يعطينا ذلك موقعًا على سبب المشكلة.
افترض أننا نحاول طباعة مصفوفة بالعكس:
int a[5] = {1, 2, 3, 4, 5};
for (int i = 4 ; i >= 0 ; i++) {
cout << a[i] << ' ';
}بعد تشغيل البرنامج نحصل على شيء مثل:
Output:
5 0 -1150222592 1577706123 1165962832 32767 126...
وهذا لا يبدو صحيحًا…
لنطبع قيمة i قبل كل عملية طباعة لمعرفة أي موقع يتم طباعته فعليًا.
int a[5] = {1, 2, 3, 4, 5};
for (int i = 4 ; i >= 0 ; i++) {
cout << "index 'i' is: " << i << '\n';
cout << a[i] << '\n'; // غيّرنا إلى \n لتصبح المخرجات أكثر وضوحًا
}Output:
index 'i' is: 4
5
index 'i' is: 5
0
index 'i' is: 6
1781710592
index 'i' is: 7
1056066187...
كنا نتوقع أن تكون قيم i هي 4, 3, 2, 1, 0، لكنها بدلاً من ذلك تزداد. السبب أن تعبير التحديث في حلقة for يحتوي على زيادة (i++) بدلًا من نقصان (i--)، وهو الخطأ البرمجي.
int a[5] = {1, 2, 3, 4, 5};
for (int i = 4 ; i >= 0 ; i--) {
cout << a[i] << ' ';
}Output:
5 4 3 2 1
الدالة assert
تستقبل الدالة assert تعبيرًا منطقيًا (boolean expression) كوسيط، فإذا كانت نتيجته false ينتهي البرنامج مباشرة. وفي معظم المنصات البرمجية يظهر حكم Runtime Error عند هذا النوع من الإنهاء. تُستخدم هذه الدالة عادةً عند وجود عدة فروع في البرنامج ويصدر البرنامج حكمًا آخر غير متوقع.
لاستخدام هذه الدالة يجب تضمين مكتبة cassert.
على سبيل المثال، لدينا مسألة تنص على: أعط عددًا صحيحًا \(N\)، وقيّم مجموع \(1 + 2 + ... + N\).
#include<iostream>
using namespace std;
int fast (int n) {
return n * (n + 1) / 2;
}
int main () {
int n;
cin >> n;
cout << fast(n);
}عند إرسال هذا البرنامج نحصل على Wrong Answer. قد يكون لدينا حل آخر نثق بصحته لكنه أبطأ، مثل الحل الذي يجمع القيم باستخدام حلقة.
يمكننا عندها استخدام assert للتحقق مما إذا كان الناتج من slow وfast متطابقًا.
#include<iostream>
#include<cassert>
using namespace std;
int slow (int n) {
int ans = 0;
for (int i = 1 ; i <= n ; i++) ans += i;
return ans;
}
int fast (int n) {
return n * (n + 1) / 2;
}
int main () {
int n;
cin >> n;
assert(fast(n) == slow(n));
cout << fast(n);
}لذلك هناك احتمالان فقط: إما نحصل على Time Limit Exceeded مما يعني أننا نجحنا في الاختبارات الصغيرة، أو نحصل على Runtime Error مما يعني أن النتيجتين مختلفتان.
إذا حصلنا على Runtime Error يمكننا مراجعة الصيغة والتأكد من صحتها، أما إذا حصلنا على Time Limit Exceeded فيمكننا استنتاج أن الصيغة تفشل عند الأعداد الكبيرة — وهو ما قد يدل على مشكلة تجاوز (overflow).
نصائح أخرى
الإخراج والتنسيق
- تأكد أن تنسيق المخرجات يطابق تمامًا نص المسألة (بدون مسافات أو أسطر إضافية).
- احذف جميع طباعات التصحيح قبل تسليم الحل.
الإدخال والحالات الحدّية
تعامل مع جميع الحالات الحدّية مثل
N = 0,N = 1, الإدخال الفارغ، وأكبر القيم الممكنة.في المسائل ذات عدة حالات اختبار، أعد تهيئة جميع المتغيرات والهياكل بين كل حالة وأخرى.
- الأخطاء غالبًا تظهر عندما تتبع حالة صغيرة حالة اختبار كبيرة.
مراجعة المسألة والبرنامج
- أعد قراءة نص المسألة بعناية.
- أعد قراءة شيفرتك — من الأخطاء الشائعة الخلط بين
NوMأو بينiوj. - راقب وجود متغيرات مظللة (shadowed) أو غير مستخدمة أو غير مهيّأة.
السلوك غير المعرّف (Undefined Behavior)
- المتغيرات غير المهيّأة.
- الوصول إلى مصفوفة أو متّجه خارج النطاق.
- غياب جملة
returnفي دوال غيرvoid. - تجاوز السعة للأعداد الموقعة (استخدم
long long). - إزاحة البتات (bit-shifting) بمقدار ≥ 32 على أعداد 32-بت.
الأعداد العشرية (Floating Point)
- تحقق من وجود
NaN(مثل ناتجsqrtلعدد سالب). - استخدم
long doubleعند الحاجة إلى دقة أعلى. - طابق عدد الخانات المطلوبة في المخرجات باستخدام
setprecision().
الاختبار (Testing)
- نفّذ اختبارات يدوية على أمثلة بسيطة.
- اختبر التحمل (stress test) بمقارنة ناتج الحل السريع مع حل بطيء يعتمد على القوة الغاشمة (brute-force).
خطأ وقت التشغيل (Runtime Error)
- هل هناك سلوك غير معرّف؟
- هل هناك استدعاء لـ
assertقد يفشل؟ - هل هناك قسمة على الصفر (أو باقي قسمة على صفر)؟
- هل توجد استدعاءات دالّية لا نهائية (recursion)؟
- هل هناك موقعات أو مكرّرات (iterators) غير صالحة؟
- هل البرنامج يستهلك ذاكرة زائدة عن الحد؟
تجاوز المهلة (Time Limit Exceeded)
- هل توجد حلقات لا نهائية؟
- ما تعقيد خوارزميتك؟
- هل أزلت جميع طباعات التصحيح قبل التسليم (مثل الطباعة الزائدة إلى المخرجات)؟
- هل هناك نسخ غير ضروري للبيانات؟ فكّر في تمرير المتغيرات بالإشارة (by reference).
- جرّب استبدال المتّجهات بالمصفوفات إن أمكن.
الحل الأخير
- أعد كتابة الحل من البداية.
- احفظ نسخة من الحل الأصلي، فقد تُدخل أخطاء جديدة أثناء إعادة الكتابة.