הקומפיילר קורא מלמעלה למטה
קומפיילר C מעבד קובץ מקור במעבר אחד, מהשורה הראשונה ועד האחרונה. כשהוא מגיע לקריאה לפונקציה, הוא צריך כבר לדעת שלושה דברים: מה הפונקציה מחזירה, כמה ארגומנטים היא מקבלת ומה הטיפוסים שלהם. בלי זה הוא לא יכול לייצר קוד נכון או לבדוק את הקריאה.
אז זו בעיה:
#include <stdio.h>
int main(void) {
printf("%d\n", add(2, 3)); /* את add עוד לא ראו */
return 0;
}
int add(int a, int b) {
return a + b;
}
הפתרון הוא אב טיפוס (prototype): החתימה של הפונקציה, כתובה לפני הקריאה, עם נקודה-פסיק במקום שבו היה הגוף.
עכשיו main יכולה לבוא ראשונה, וכך מאורגנים רוב קובצי ה-C: אבות הטיפוס בראש אומרים מה הקובץ מציע, main נקראת כמו ראשי פרקים, והפרטים באים אחריה.
הצהרה מול הגדרה
שתי מילים שקל לטשטש ביניהן וכדאי להפריד:
- הצהרה (declaration) אומרת שפונקציה קיימת ונותנת את הטיפוס שלה. היא מסתיימת בנקודה-פסיק ואין לה גוף. אפשר לכתוב אותה כמה פעמים שרוצים.
- הגדרה (definition) נותנת את הגוף. היא חייבת להופיע בדיוק פעם אחת בכל התוכנית: שתי הגדרות של אותה פונקציה הן שגיאת linker ("multiple definition of").
int add(int a, int b); /* הצהרה (אב טיפוס) */
int add(int a, int b) { return a + b; } /* הגדרה, שהיא גם הצהרה */
שמות הפרמטרים אופציונליים באב טיפוס; לקומפיילר חשובים רק הטיפוסים:
int add(int, int); /* חוקי, ושקול */
int add(int a, int b); /* עדיף: השמות מתעדים את הסדר */
השתמשו בשמות. void drawRect(int, int, int, int); לא אומר לקורא כלום, ואילו void drawRect(int x, int y, int width, int height); אומר לו הכול.
מה אב הטיפוס נותן לכם
לא רק את היכולת לשנות את סדר ההגדרות: הוא מה שגורם לקומפיילר לבדוק את הקריאות שלכם.
כשאב הטיפוס בטווח, ה-3 וה-2 מומרים ל-double לפני הקריאה. בלעדיו, הם היו נדחפים כ-int ו-scale הייתה קוראת אותם כ-double: זבל, ובלי שום הודעה בדיאלקטים ישנים של C.
אב הטיפוס גם תופס מספר ארגומנטים שגוי וטיפוסים לא תואמים בזמן קומפילציה:
scale(3.0); /* שגיאה: מעט מדי ארגומנטים */
scale(3.0, "two"); /* שגיאה: מעבירים char * במקום שבו מצופה double */
אלה השגיאות שאתם רוצים: רועשות, בזמן קומפילציה, ומצביעות על השורה.
שגיאות implicit declaration
קראו לפונקציה שהקומפיילר מעולם לא שמע עליה ותקבלו את אחת ההודעות הנפוצות ביותר ב-C:
warning: implicit declaration of function 'add' [-Wimplicit-function-declaration]
ב-C89 הקומפיילר היה מנחש: מניח שהפונקציה מחזירה int ומקבל את כל הארגומנטים שהעברתם. הניחוש הזה שגוי לעתים קרובות, וכשהפונקציה האמיתית מחזירה double או מצביע, התוצאה היא שטויות. C99 הוציאה את ההצהרות המובלעות מהשפה, כך שזו שגיאה ב-C99 ואילך: GCC ו-clang מודרניים דוחים את זה כברירת מחדל בגרסאות האחרונות.
שתי סיבות, שני תיקונים:
פונקציה שלכם שעוד לא הוצהרה. הוסיפו את אב הטיפוס מעל הקריאה, או הזיזו את ההגדרה למעלה.
פונקציית ספרייה ששכחתם את ה-header שלה. אבות הטיפוס של הספרייה נמצאים ב-header שלה, אז צריך את ה-#include:
printf, scanf, fopen -> #include <stdio.h>
malloc, free, exit, atoi -> #include <stdlib.h>
strlen, strcpy, strcmp -> #include <string.h>
sqrt, pow, sin, fabs -> #include <math.h>
isdigit, toupper -> #include <ctype.h>
bool, true, false -> #include <stdbool.h>
הודעה קרובה, conflicting types for 'add', אומרת שאב הטיפוס וההגדרה לא מסכימים: טיפוס של פרמטר שונה, או טיפוס ההחזרה. תקנו את מה שלא נכון; הם חייבים להתאים בדיוק.
רקורסיה הדדית דורשת אב טיפוס
לפעמים שינוי סדר לא פותר את הבעיה: שתי פונקציות שקוראות זו לזו לא יכולות שתיהן לבוא ראשונות.
אב טיפוס אחד שובר את המעגל. זה אותו מנגנון שפונקציה רקורסיבית רגילה נשענת עליו: השם של הפונקציה עצמה נמצא בטווח בתוך הגוף שלה, כך שרקורסיה ישירה לא צריכה אב טיפוס, אבל רקורסיה הדדית כן.
אבות טיפוס בקובצי header
ברגע שתוכנית מתפרסת על כמה קובצי .c, אבות הטיפוס עוברים לheader, כך שכל קובץ רואה את אותן הצהרות ממקום אחד.
mathutils.h, הממשק:
#ifndef MATHUTILS_H
#define MATHUTILS_H
int add(int a, int b);
int multiply(int a, int b);
double average(const int values[], int count);
#endif
mathutils.c, המימוש:
#include "mathutils.h"
int add(int a, int b) {
return a + b;
}
int multiply(int a, int b) {
return a * b;
}
double average(const int values[], int count) {
if (count == 0) return 0.0;
int total = 0;
for (int i = 0; i < count; i++) total += values[i];
return (double) total / count;
}
main.c, קובץ שמשתמש בו:
#include <stdio.h>
#include "mathutils.h"
int main(void) {
printf("%d\n", add(2, 3));
return 0;
}
ואז:
gcc main.c mathutils.c -o program
שני הרגלים ששווה לאמץ כאן. העטיפה #ifndef / #define / #endif היא include guard: היא מונעת מההצהרות לעבור עיבוד פעמיים אם ה-header נכלל בשני מסלולים. ו-mathutils.c כולל את ה-header של עצמו, מה שנראה מיותר אבל לא, כי כך הקומפיילר בודק כל הגדרה מול ההצהרה שקבצים אחרים ישתמשו בה. אם הן אי פעם יתרחקו זו מזו, תגלו את זה מיד ולא בזמן הקישור. העמוד על קובצי header מרחיב על השאר.
סוגריים משולשים (<stdio.h>) מחפשים בנתיבי ה-include של המערכת; מירכאות ("mathutils.h") מחפשות קודם בתיקייה של הפרויקט שלכם. השתמשו במירכאות עבור ה-headers שלכם.
f(void) מול f()
זה באמת מפתיע, וזו הסיבה שכל דוגמה בתיעוד הזה כותבת int main(void).
void ping(void); /* לא מקבלת ארגומנטים: קריאות עם ארגומנטים נדחות */
void pong(); /* לא אומרת כלום על הפרמטרים */
void ping(void); הוא אב טיפוס: הוא מצהיר שהפונקציה לא מקבלת פרמטרים, ולכן ping(1, 2, 3) היא שגיאת קומפילציה.
void pong(); היא הצהרה בסגנון ישן שעברה בירושה מ-C שלפני התקן. היא מצהירה על טיפוס ההחזרה ולא אומרת שום דבר על הפרמטרים, ולכן הקומפיילר לא יכול לבדוק קריאות: pong(1, 2, 3) מתקמפל בשקט ועושה משהו לא מוגדר.
int main(void) { /* נכון: main לא מקבלת ארגומנטים */
int main() { /* חוקי, אבל בדיקת הארגומנטים כבויה */
תמיד כתבו (void) עבור פונקציה בלי פרמטרים. C23 משנה את () כך שתהיה זהה ל-(void), מה שסוף סוף מסיר את המלכודת, אבל הרבה קוד והרבה קומפיילרים עוד לא שם, ו-(void) נכון בכל תקן של C.
אותה הבחנה חלה על ההגדרה. void ping(void) { } היא הגדרה בסגנון אב טיפוס; void ping() { } לא, והיא לא מפעילה בדיקה של קריאות שמופיעות לפניה.
טעויות נפוצות
- נקודה-פסיק על ההגדרה.
int add(int a, int b); { return a + b; }מצהיר עלaddואז משאיר בלוק תועה. הודעת השגיאה מבלבלת; הסיבה היא תו אחד. - נקודה-פסיק חסרה באב הטיפוס. הקומפיילר ממשיך לקרוא לתוך מה שבא אחריו ומדווח על משהו מביך כמה שורות למטה.
- אב טיפוס והגדרה שלא מסכימים.
conflicting types for .... הפכו אותם לזהים, או עדיף, כללו את ה-header בקובץ המימוש כדי שהבדיקה תהיה אוטומטית. - הצהרה על פונקציה בתוך פונקציה אחרת. חוקי (
int add(int, int);בתוךmain), אבל אז ההצהרה תקפה רק בתוך אותה פונקציה. שימו אבות טיפוס בטווח הקובץ. - הגדרת פונקציה ב-header. כללו אותו משני קובצי
.cוה-linker ידווח על הגדרה כפולה. קובצי header מחזיקים הצהרות; הגדרות הולכות לקובץ.c. - להסתמך על
()לבדיקה. הוא לא בודק. כתבו(void).
שאלות נפוצות
מה זה אב טיפוס של פונקציה ב-C?
הצהרה על החתימה של פונקציה, כלומר טיפוס ההחזרה, השם וטיפוסי הפרמטרים, שמסתיימת בנקודה-פסיק במקום בגוף: int add(int a, int b);. היא אומרת לקומפיילר איך קוראים לפונקציה כדי שאפשר יהיה לבדוק את הקריאות, בלי לומר מה היא עושה.
מה ההבדל בין הצהרה להגדרה ב-C?
הצהרה מציגה את השם ואת הטיפוס (int add(int, int);) ויכולה להופיע הרבה פעמים. הגדרה מספקת את הגוף (int add(int a, int b) { return a + b; }) וחייבת להופיע בדיוק פעם אחת בכל התוכנית. כל הגדרה היא גם הצהרה.
איך מתקנים את "implicit declaration of function" ב-C?
הוסיפו אב טיפוס לפני הקריאה. עבור פונקציות שלכם, שימו returnType name(paramTypes); בראש הקובץ או ב-header; עבור פונקציות ספרייה, כללו את ה-header הנכון: #include <stdio.h> עבור printf, <stdlib.h> עבור malloc, <string.h> עבור strlen, <math.h> עבור sqrt.
מה ההבדל בין f(void) ל-f() ב-C?
void f(void); מצהיר על פונקציה שמקבלת אפס ארגומנטים, והקומפיילר דוחה כל קריאה שמעבירה ארגומנטים. void f(); היא הצהרה בסגנון ישן שלא אומרת כלום על הפרמטרים, ולכן בדיקת הארגומנטים כבויה. תמיד כתבו (void); ב-C23 שתי הצורות סוף סוף אומרות אותו דבר, אבל קוד וקומפיילרים ישנים עדיין מבדילים ביניהן.