Menu
flag Ar iconالعربيةdown icon

أخطاء C الشائعة: أخطاء المبتدئين وكيفية إصلاحها

الأخطاء التي يرتكبها كل مبرمج C مرة واحدة على الأقل - فاصلة منقوطة ناقصة، = بدل ==، التصاريح الضمنية، scanf بلا &، مقارنة السلاسل النصية بـ ==، القسمة الصحيحة، المتغيّرات غير المهيّأة، والانزياح بمقدار واحد - كلٌّ منها كعَرَض وسبب وعلاج.

تحتوي هذه الصفحة على محررات قابلة للتشغيل - حرّر، شغّل، وشاهد النتيجة فوراً.

يصادف كل مبرمج C القائمة القصيرة نفسها من الأخطاء، في الأسبوع الأول عادةً وبعد عقد أحيانًا. وما يجعلها تستحقّ الفهرسة هو أن معظمها يُنتج رسائل خطأ لا تصفها - أو، وهو أسوأ، لا يُنتج رسالة إطلاقًا.

كل مدخل أدناه هو عَرَض، والسبب وراءه، والعلاج.

الفاصلة المنقوطة الناقصة وشلّال الأخطاء

العَرَض: جدار من الأخطاء، كلها مُبلَّغ عنها في أسطر تبدو صحيحة.

program.c:6:5: error: expected ';' before 'printf'
program.c:7:5: error: expected declaration specifiers before 'return'

السبب: تنهي C جملها بـ ;. احذف واحدة فيلصق المترجم السطر التالي بالحالي، ثم يبلّغ عن الارتباك عند النقطة التي يتوقّف فيها النصّ المدموج عن كونه ذا معنى - في السطر التالي عادةً.

int main(void) {
    int x = 5      /* missing semicolon */
    printf("%d\n", x);
    return 0;
}

العلاج: حين تظهر دفعة من الأخطاء، أصلِح الأول فقط وأعد الترجمة. فكل ما بعده قد يكون تداعيات. وافحص دائمًا السطر الذي قبل الذي يسمّيه المترجم.

والشلّال نفسه يأتي من قوس غير مغلق أو تعليق /* غير مُنهى، حيث يمكن أن تهبط الأخطاء على بعد عشرات الأسطر.

ملاحظة إضافية: لا فاصلة منقوطة بعد ترويسة if أو for أو while، ولا بعد القوس الخاتم لتعريف دالة. فـ if (x > 0); تُترجَم بنجاح ولا تفعل شيئًا - إذ إن ; هي الجسم كله.

الإسناد بدل المقارنة

العَرَض: شرط محقّق دائمًا، أو متغيّر يتغيّر بشكل غامض.

int x = 5;
if (x = 10) {              /* يُسنِد 10، ثم يفحص 10 -> صحيح */
    printf("x is ten\n");  /* يطبع دائمًا؛ وx صارت الآن 10 */
}

السبب: = تُسنِد، و== تقارن. وقيمة الإسناد هي القيمة المُسنَدة، فـ if (x = 10) تفحص 10، وهي غير صفرية، أي صحيحة. ويقبلها المترجم لأنها أحيانًا ما يقصده الناس.

العلاج: استخدم == في كل شرط، وفعّل -Wall ليحذّرك المترجم:

warning: suggest parentheses around assignment used as truth value

وإن كنت تقصد الإسناد داخل شرط فعلًا - وهو شائع مع while ((c = getchar()) != EOF) - فإن الأقواس الإضافية تقول ذلك وتُسكت التحذير.

التصريح الضمني عن دالة

العَرَض:

warning: implicit declaration of function 'printf'
warning: implicit declaration of function 'malloc'

...تتبعها أحيانًا نتائج غريبة أثناء التشغيل أو خطأ ربط.

السبب: صادف المترجم استدعاءً لدالة لا تصريح لديه عنها. وفي اللهجات السابقة لـ C99 كان يفترض أن الدالة تُعيد int ويمضي؛ وعلى الأنظمة ذات 64 بتًا يقتطع ذلك الافتراض مؤشّرًا مُعادًا إلى 32 بتًا، وهكذا يحوّل غياب <stdlib.h> دالة malloc إلى انهيار.

العلاج: ضمّن الترويسة الصحيحة.

الدالةالترويسة
printf, scanf, fopen<stdio.h>
malloc, free, atoi, rand, exit<stdlib.h>
strlen, strcpy, strcmp<string.h>
sqrt, pow, fabs<math.h>
isdigit, toupper<ctype.h>
time<time.h>

أما لدوالك أنت، فالتحذير نفسه يعني أنك استدعيت واحدة قبل تعريفها. ضع نموذجًا فوق main:

scanf بلا العامل &

العَرَض: ينهار البرنامج عند الإدخال، أو لا يقرأ شيئًا ويترك المتغيّر دون تغيير.

int age;
scanf("%d", age);     /* & ناقصة: يمرّر القيمة، لا العنوان */

السبب: تكتب scanf داخل متغيّرك، فهي تحتاج عنوان المتغيّر. وتمرير age يسلّمها أي قيمة مهملة كانت فيه فتعاملها scanf كموضع للكتابة، وهو ما يعني خطأ تجزئة عادةً.

العلاج: ضع & قبل المتغيّر - لكل نوع عدا المصفوفة، فهي عنوان أصلًا:

وثمّة فخّان مجاوران من العائلة نفسها: استخدم %lf لـ double في scanf (فـ %f هناك تعني float، وكتابة بايتات بحجم float داخل double تتركه خاطئًا)، وحُدّ دائمًا %s بعرض - %49s لمخزن من 50 بايت - وإلّا تجاوزته كلمة طويلة.

والأفضل من ذلك: اقرأ سطرًا كاملًا بـ fgets وحلّله، فذلك لا يمكن أن يفيض ولا يترك إدخالًا شاردًا في المخزن. المزيد عن الاثنين في scanf.

مقارنة السلاسل النصية بـ ==

العَرَض: سلسلتان متطابقتان بوضوح تُقارَنان كمختلفتين.

char a[] = "hello";
char b[] = "hello";

if (a == b) {              /* مقارنة عنوانين: خطأ */
    printf("same\n");
}

السبب: سلسلة C النصية ليست قيمة، بل مؤشّر إلى المحرف الأول. و== تقارن المؤشّرين. ومصفوفتان تحملان نصًا متطابقًا تعيشان في عنوانين مختلفين، فالفحص خاطئ. (وممّا يزيد الأمر حيرة أن مقارنة قيمتين حرفيتين متطابقتين تعطي صحيحًا أحيانًا، لأن المترجم قد يخزّن نسخة واحدة - وهو ما يجعل العلّة متقطّعة.)

العلاج: strcmp، وتذكّر أنها تُعيد 0 عند التساوي:

والقراءة المقلوبة توقع الناس أيضًا: فـ if (strcmp(a, b)) محقّقة حين تختلف السلسلتان، لأن نتيجة غير صفرية تعني "غير متساويتين". اكتب == 0 صراحةً دائمًا.

القسمة الصحيحة

العَرَض: متوسّط يساوي 0، أو نسبة مئوية تساوي 0 أو 100 دائمًا، أو نسبة فقدت كسرها.

int correct = 7, total = 10;
double score = correct / total;      /* 0.0، لا 0.7 */

السبب: كلا المعاملين من نوع int، فتُجري C قسمة صحيحة وتقتطع قبل إسناد النتيجة إلى double. فـ 7 / 10 تساوي 0؛ وتحويل 0 إلى double يعطي 0.0.

العلاج: اجعل أحد المعاملين من نوع فاصلة عائمة قبل القسمة:

تحويل معامل واحد يرفّع الآخر تلقائيًا. وتحويل النتيجة متأخّر جدًا - فالاقتطاع وقع سلفًا. انظر تحويل الأنواع.

والفخّ نفسه يختبئ في تعابير مثل (a + b) / 2 لنقطة المنتصف، و1 / 2 * x التي تساوي 0 دائمًا مهما كانت x.

return ناقصة أو خاطئة

العَرَض: تُعيد دالة عددًا خاطئًا يبدو معقولًا، ويختلف بين تشغيل وآخر أو بين بناء وآخر.

int add(int a, int b) {
    int sum = a + b;
    /* لا جملة return */
}

السبب: بلوغ نهاية دالة ليست void دون إعادة قيمة يعطي قيمة غير محدّدة - عمليًا أيًّا كان ما صادف وجوده في سجلّ الإرجاع. وهو سلوك غير معرَّف إن استخدمها المستدعي.

والصيغة الأمكر تعيد قيمة في بعض المسارات دون غيرها:

int classify(int n) {
    if (n > 0) return 1;
    if (n < 0) return -1;
    /* n == 0 تسقط من النهاية */
}

العلاج: أعِد قيمة في كل مسار، وترجم بـ -Wall - فرسالة GCC‏ "control reaches end of non-void function" تلتقط النسختين.

المتغيّرات غير المهيّأة

العَرَض: مخرجات مهملة، أو نتائج تتغيّر بين تشغيل وآخر وبين مستويات تحسين مختلفة.

int total;                       /* يحوي ما كان على المكدّس */
for (int i = 1; i <= 5; i++) {
    total += i;                  /* إضافة إلى قيمة مهملة */
}
printf("%d\n", total);           /* عدد ضخم ما */

السبب: المتغيّرات المحلية لا تُصفَّر. فالمتغيّر العام أو static يُضبَط على الصفر تلقائيًا؛ أما المحلي فيبدأ بأي بايتات كانت أصلًا في عنوان المكدّس ذاك.

العلاج: هيّئ عند نقطة التعريف. فذلك لا يكلّف شيئًا ويزيل هذا الصنف من العلل بأكمله:

ينبّه -Wall -Wextra إلى كثير من هذه ("may be used uninitialized")، ويلتقط -fsanitize=memory أو valgrind ما تبقّى. وهذا الخطأ خبيث بوجه خاص لأن المؤشّر غير المهيّأ يقود مباشرةً إلى خطأ تجزئة.

الانزياح بمقدار واحد

العَرَض: يُفوَّت العنصر الأخير، أو يُلمَس عنصر زائد فيسيء البرنامج التصرّف لاحقًا.

int arr[5];
for (int i = 0; i <= 5; i++) {   /* يلمس arr[5]، وهي غير موجودة */
    arr[i] = i;
}

السبب: المصفوفة ذات n عنصرًا فهارسها من 0 إلى n - 1. و<= تُنفّذ مرورًا إضافيًا.

العلاج: نمط i < n، واحسب n من المصفوفة بدل كتابة العدد مرتين:

ونسخة السلاسل النصية - نسيان مكان لـ '\0' - هي الخطأ نفسه بقبّعة مختلفة، وchar word[5] مع "hello" بداخلها تجاوز للمخزن.

خطآن أصغر يستحقّان المعرفة

فاصلة منقوطة بعد ترويسة حلقة. for (int i = 0; i < 10; i++); متبوعة بكتلة بين أقواس تُنفّذ الحلقة عشر مرات دون فعل شيء، ثم تُنفّذ الكتلة مرة واحدة. وتُترجَم بلا أي شكوى.

sizeof على مؤشّر. داخل الدالة يكون وسيط المصفوفة مؤشّرًا، فتكون sizeof(arr) حجم المؤشّر (8 بايت)، لا حجم المصفوفة. مرّر الطول كوسيط منفصل:

العادة التي تمنع معظم هذا

ترجم مع تفعيل التحذيرات، منذ أول برنامج:

gcc -Wall -Wextra -g program.c -o program

يلتقط -Wall -Wextra الإسنادَ داخل الشرط، وreturn الناقصة، والقراءة غير المهيّأة، والمتغيّر غير المستخدم، ومحدّد printf الذي لا يطابق وسيطه. وإضافة -fsanitize=address,undefined أثناء التطوير تلتقط تقريبًا كل ما تبقّى لحظة حدوثه.

عامِل كل تحذير كخطأ لم تصطدم به بعد. فبرنامج C الذي يُترجَم بلا تحذيرات ليس مضمون الصحّة - لكن تقريبًا كل برنامج C ينهار كان يحذّر من شيء أولًا.

الأسئلة الشائعة

ماذا تعني 'implicit declaration of function' في لغة C؟

لقد صادف المترجم استدعاءً لدالة لم يرَ تصريحًا عنها قطّ. وفي الغالب الأعمّ تكون قد نسيت #include - فـ printf تحتاج <stdio.h>، وmalloc تحتاج <stdlib.h>، وstrlen تحتاج <string.h>. وقد تعني أيضًا أنك استدعيت دالتك قبل تعريفها، وهو ما يعالجه نموذج فوق main.

لماذا يبلّغ برنامج C عن خطأ في سطر يبدو سليمًا؟

عادةً لأن الخطأ الحقيقي في السطر السابق. فالفاصلة المنقوطة الناقصة أو القوس غير المغلق أو التعليق غير المُنهى يجعل المترجم يقرأ سطريك كسطر واحد، فيبلّغ عن الارتباك حيث يصبح النصّ غير قابل للتحليل أخيرًا. افحص دائمًا السطر الذي فوق المُبلَّغ عنه أولًا.

لماذا لا يمكنني مقارنة السلاسل النصية بـ == في لغة C؟

لأن سلسلة C النصية هي char * - مؤشّر - فتقارن == عنوانين، لا المحارف التي يشيران إليها. وسلسلتان متطابقتان مخزّنتان في مكانين مختلفين تعطيان false. استخدم strcmp(a, b) == 0 من <string.h>، وهي تُعيد 0 عند تطابق المحتوى.

لماذا يعطي 5 / 2 القيمة 2 في لغة C؟

لأن كلا المعاملين عددان صحيحان، فتُجري C قسمة صحيحة وتُسقط الكسر. اجعل أحد الطرفين قيمة فاصلة عائمة للحصول على 2.5: 5.0 / 2، أو (double)a / b عند العمل مع متغيّرات. وتحويل النتيجة متأخّر جدًا - فـ (double)(5 / 2) تساوي 2.0.

Coddy programming languages illustration

تعلّم البرمجة مع Coddy

ابدأ الآن