כל מתכנת 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) { /* assigns 10, then tests 10 -> true */
printf("x is ten\n"); /* always prints; x is now 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); /* missing & : passes the value, not the address */
סיבה: scanf כותבת לתוך המשתנה שלכם, ולכן היא צריכה את הכתובת שלו. העברת age מוסרת את הזבל שהיה בו, ו-scanf מתייחסת לזה כמקום לכתוב אליו, וזה בדרך כלל segfault.
תיקון: & לפני המשתנה, לכל טיפוס חוץ ממערך, שהוא כבר כתובת:
שתי מלכודות סמוכות מאותה משפחה: השתמשו ב-%lf עבור double ב-scanf (%f שם פירושו float, וכתיבה של בתים בגודל float לתוך double משאירה אותו שגוי), ותמיד הגבילו %s ברוחב, %49s עבור חוצץ של 50 בתים, אחרת מילה ארוכה גולשת ממנו.
עוד יותר טוב: קראו שורה שלמה עם fgets ונתחו אותה, מה שלא יכול לגלוש ולא משאיר קלט תועה בחוצץ. עוד על שניהם ב-scanf.
השוואת מחרוזות עם ==
תסמין: שתי מחרוזות שברור שהן זהות משוות כשונות.
char a[] = "hello";
char b[] = "hello";
if (a == b) { /* comparing two addresses: false */
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, not 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;
/* no return statement */
}
סיבה: הגעה לסוף של פונקציה שאינה void בלי להחזיר ערך נותנת ערך לא מוגדר, בפועל מה שבמקרה היה ברגיסטר ההחזרה. זו התנהגות לא מוגדרת אם מי שקרא לפונקציה משתמש בו.
הצורה הערמומית יותר מחזירה ערך בחלק מהמסלולים ולא באחרים:
int classify(int n) {
if (n > 0) return 1;
if (n < 0) return -1;
/* n == 0 falls off the end */
}
תיקון: החזירו ערך בכל מסלול, וקמפלו עם -Wall: ההודעה "control reaches end of non-void function" של GCC תופסת את שתי הגרסאות.
משתנים לא מאותחלים
תסמין: פלט זבל, או תוצאות שמשתנות בין הרצות ובין רמות אופטימיזציה.
int total; /* contains whatever was on the stack */
for (int i = 1; i <= 5; i++) {
total += i; /* adding to garbage */
}
printf("%d\n", total); /* some enormous number */
סיבה: משתנים מקומיים לא מאופסים. משתנה גלובלי או static מקבל אפס אוטומטית; משתנה מקומי מתחיל עם הבתים שכבר היו בכתובת הזו במחסנית.
תיקון: אתחלו במקום ההצהרה. זה לא עולה כלום ומעלים את כל סוג הבאגים הזה:
-Wall -Wextra מזהיר על רבים מאלה ("may be used uninitialized"), ו--fsanitize=memory או valgrind תופסים את השאר. המקרה הזה מגעיל במיוחד, כי מצביע לא מאותחל מוביל ישר ל-segmentation fault.
טעות של אחד
תסמין: הפריט האחרון מפוספס, או שנוגעים באיבר אחד יותר מדי והתוכנית מתנהגת לא כשורה מאוחר יותר.
int arr[5];
for (int i = 0; i <= 5; i++) { /* touches arr[5], which does not exist */
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.