שפת C/הקדם מעבד
הקדם מעבד (preprocessor בלעז) הוא יישום הפועל על קבצי התוכנית לפני פעולתו של המהדר, ויכול לשנות את הטקסט שאותו מהדרים.
מהו הקדם מעבד?
[עריכה]הקדם מעבד פועל על קבצי טקסט, ויכול לערוך את הקוד לפני שהמהדר מהדר אותו. הקדם מעבד סורק קבצי טקסט, ומחפש פקודות המתחילות בתו #. כל פעולה כזו מחליפה קטע טקסט בקטע טקסט אחר.
שימו לב: הקדם מעבד אינו "מבין" את שפת C - הוא פועל על קטעי טקסט בלבד, בלי קשר לשאלות האם הוא עורך קטעי קוד תקינים, או האם לאחר העריכה התקבל קוד תקין. מסיבה זאת, הקדם מעבד מוגבל פחות מהמהדר, והוא רב עוצמה אך מסוכן. |
בפרק זה לא נתמקד בפעולות שאפשר לבצע בעזרת הקדם מעבד (נושא לספר שלם בפני עצמו), אלא רק בפעולות שקשה (או בלתי אפשרי) לבצע בלעדיו, ואפשר (בזהירות רבה) לבצע אתו.
שלבי ההידור
[עריכה]כאשר אנו מהדרים קובץ, מתרחשים למעשה שני שלבים:
- הפקודה מפעילה ראשית את הקדם מעבד. הוא קורא את הקובץ, משנה (אולי) את התוכן שקרא, ולאחר מכן,
- המהדר מקבל את תוצאת השלב הראשון, ומהדר אותה
לכן, על אף שעד עתה דיברנו בספר על הידור קבצים, מדובר ליתר דיוק בשני שלבים, וכך נתייחס אליהם בפרק זה. עם זאת, נשים לב שאין צורך בפקודה מיוחדת כדי להפעיל את הקדם-מעבד.
הגדרת קבוע
[עריכה]אפשר להשתמש בקדם-מעבד כדי להגדיר קבועים. לדוגמה, אפשר לקבוע שהקבוע RED הוא 1 (מיד נראה כיצד). כאשר הקדם-מעבד יגיע לרצף האותיות RED, הוא יחליף זאת ב-1.
לדוגמה:
printf("%d", RED);
ידפיס 1. הקדם-מעבד יהפוך את הקטע לקטע הבא:
printf("%d", 1);
והמהדר כמובן יהפוך זאת לקוד שמדפיס 1.
הגדרת קבוע בקוד
[עריכה]מגדירים קבוע בקוד כך:
#define <const> <val>
כאשר const הוא הקבוע, ו-val הוא הערך.
לדוגמה, נוכל לכתוב תוכנית כזו, שתדפיס 1:
#include <stdio.h>
#define RED 1
int main()
{
printf("%d", RED);
return 0;
}
הנה עוד דוגמה, בה נכתוב מחדש את שלום עולם:
#include <stdio.h>
#define GREETING "Hello, world"
int main()
{
printf(GREETING);
return 0;
}
כדאי לדעת: בתחליפים לחלק מיכולות הקדם מעבד נראה תחליפים טובים יותר לדוגמאות אלו. |
אפשר אפילו להודיע שקבוע מסויים מוגדר, אפילו בלי להגדיר שהוא מוגדר למשהו מסויים (נראה את השימוש לכך בהידור מותנה). עושים זאת בצורה:
#define <const>
כאשר const הוא קבוע. לדוגמה, השורה:
#define RED
מודיעה שהקבוע RED מוגדר, אבל איננה מגדירה אותו לערך מסויים.
הגדרת קבוע בפקודת ההידור
[עריכה]לא חייבים להגדיר כל קבוע בתוך הקוד. אפשר גם להגדיר קבוע בפקודת ההידור.
#include <stdio.h>
int main()
{
printf("%d", RED);
return 0;
}
הדבר משתנה בין המערכות השונות.
gcc בלינוקס או Cygwin
[עריכה]כדי להגדיר שקבוע כלשהו מוגדר לערך כלשהו, כותבים
gcc -D<const>=<val> <source_file> -o <executable>
כאשר const הוא הקבוע, val הוא ערכו, ו(כפי שראינו בבניית והרצת שלום עולם!), source_file הוא שם קובץ הקוד, ו-executable הוא שם קובץ התוכנית המתקבלת. לדוגמה,
gcc -DRED=1 red_test.c -o red_test.out
תהדר את הקובץ red_test.c, ובכל מקום בו תזהה בו את הרצף RED, היא תחליפו ל1.
כדי להגדיר שקבוע כלשהו סתם מוגדר (לא לערך כלשהו), כותבים
gcc -D<const> <source_file> -o <executable>
כאשר const הוא הקבוע, וכל השאר כמקודם. לדוגמה,
gcc -DDEBUG red_test.c -o red_test.out
תהדר את הקובץ red_test.c, ותזהה שהקבוע DEBUG מוגדר (נראה את השימוש לכך בהידור מותנה).
סביבת פיתוח בחלונות
[עריכה]ב-Microsoft Visual Studio תוכלו להוסיף קבוע הידור בהגדרות הפרוייקט כולו או עבור כל קובץ הידור בנפרד. כדי לעשות זאת יש להכנס להגדרות הפרוייקט (Project -> Properties) או הקובץ (קליק ימני על הקובץ בחלון ה-Solution Explorer ומהתפריט לבחור Properties) ובהגדרות הקדם-מעבד (Preprocessor) להוסיף את הקבועים הרצויים בשדה Preprocessor Definitions.
פרק זה לוקה בחסר. אתם מוזמנים לתרום לוויקיספר ולהשלים אותו. ראו פירוט בדף השיחה.
כללים סגנוניים לגבי שמות קבועים
[עריכה]יש המקפידים על כך שכל קבוע הידור יורכב אך ורק מאותיות אנגליות עיליות (capital letters), קווים תחתונים, ומספרים. זאת כדי לזהות שמדובר בקבוע הידור. לפי שיטה זו, ההגדרה הבאה היא בסדר:
#define MY_COLOR 1
אך ההגדרה הבאה איננה:
#define MyColor 1
מומלץ לא להתחיל קבוע הידור בקו תחתון, ובשום פנים ואופן אין להתחיל קבוע הידור בשני קווים תחתונים. השורה הבאה, לדוגמה:
#define __MY_COLOR 3
עלולה לגרום לתוצאות לא צפויות במערכות שונות.
קבועים מקובלים
[עריכה]ישנם מספר קבועים בעלי משמעות מקובלת:
- הקבוע DEBUG מציין שמהדרים קוד בגרסה לתיקון שגיאות.
- הקבוע NDEBUG מציין שמהדרים קוד בגרסה שאיננה כוללת תיקון שגיאות.
- הקבוע __FILE__ תמיד מוחלף על ידי הקדם-מעבד לקובץ שבתוכו הוא מופיע.
- הקבוע __LINE__ תמיד מוחלף על ידי הקדם-מעבד למספר השורה שבה הוא מופיע.
יש עוד מספר קבועים מקובלים, אך אלה הם העיקריים.
עכשיו תורכם: הקוד הבא נשמר בקובץ preproc_test.c. מה תדפיס התוכנית? |
#include <stdio.h>
int main()
{
printf("Hello world from line %d of file %s", __LINE__, __FILE__);
return 0;
}
Hello world from line 6 of preproc_test.c
הכלת קבצים
[עריכה]במהלך הספר נתקלנו מספר פעמים בשורות כגון:
#include <stdio.h>
שורות אלו גורמות לקדם-מעבד להכיל קובץ אחד בתוך קובץ שני.
נניח שהתוכן של הקובץ foo.h הוא:
int foo();
void bar();
נניח גם שהתוכן של הקובץ foo.c הוא:
#include "foo.h"
int main()
{
return 0;
}
כלומר, שורה להכלת הקובץ foo.h, ולאחריה שורות תוכן אחרות. כאשר נהדר את הקובץ foo.c, ראשית יכיל הקדם-מעבד את תוכן foo.h במקום השורה הנ"ל. כלומר, המהדר למעשה יהדר את התוכן:
int foo();
void bar();
int main()
{
return 0;
}
נדון בשימוש הנפוץ של הכלת קבצים כשנגיע להכלת קבצי כותרת.
הידור מותנה
[עריכה]בעזרת הקדם-מעבד אפשר לקבוע שקטע קוד יהודר אך ורק בהתאם לשאלה האם קבוע הוגדר.
התנייה בהגדרת קבוע
[עריכה]נתבונן בקטע הקוד הבא:
#ifdef DEBUG
int is_sorted(const int *a, unsigned int length)
{
for(i = 1; i < length; ++i)
if(a[i] < a[i - 1])
return 0;
return 1;
}
#endif /* ifdef DEBUG */
זהו קטע קוד שעטוף בזוג #ifdef-#endif
. הקוד במקרה זה הוא פונקציה is_sorted
, המקבל מערך באורך נתון, ובודק האם המערך ממויין (בסדר עולה).
כאשר מגיעים לקטע קוד זה, יש שתי אפשרויות: הקבוע DEBUG מוגדר או לא. אם הקובע DEBUG מוגדר, אז הקדם מעבד יחליף את הקטע הנ"ל בקטע:
int is_sorted(const int *a, unsigned int length)
{
for(i = 1; i < length; ++i)
if(a[i] < a[i - 1])
return 0;
return 1;
}
אם הקובע DEBUG אינו מוגדר, אז הקדם מעבד יחליף את הקטע הנ"ל בקטע:
כלומר בקוד ריק.
לסיכום, חלק הקוד בין #ifdef DEBUG
לבין #endif
יהודר אך ורק אם DEBUG מוגדר בנקודה זו.
באופן כללי, מסמנים התניה בהגדרת קבוע בצורה:
#ifdef <const>
...
#endif
כאשר const הוא קבוע.
התניה באי-הגדרת קבוע
[עריכה]אפשר גם לקבוע שקטע קוד יהודר אך ורק אם קבוע אינו מוגדר. עושים זאת בצורה:
#ifndef <const>
...
#endif
כאשר const הוא קבוע.
שימושים
[עריכה]קוד לניפוי שגיאות
[עריכה]אחד השימושים הנפוצים בקדם-מעבד הוא להידור מותנה של קטעי קוד שלמים שכל מטרתם היא ניפוי שגיאות. בדרך כלל אפשר לזהות קוד זה כך:
#ifdef DEBUG
...
#endif /* ifdef DEBUG */
נשים לב לשימוש בקבוע המקובל DEBUG.
נראה דוגמה לכך בדוגמה לקוד לניפוי שגיאות.
קבצי כותרת
[עריכה]
שקלו לדלג על נושא זה שקול לחזור לכאן במהלך קריאתך את מודולים. |
שיטה מקובלת בתכנות C היא סימון קבצי כותרת בהתניה. קבצי כותרת לעתים קרובות נראים מהצורה:
#ifndef <const>
#define <const>
...
#endif
כאשר const הוא קבוע כלשהו. כלומר, כל תוכן הקובץ נמצא בין רצף #ifndef-#define
ל-#endif
.
אם נתבונן בקובץ ששמו file_1.h, לדוגמה - תוכנו עשוי מאד להראות כך:
#ifndef FILE_1_H
#define FILE_1_H
...
#endif /* ifndef FILE_1_H */
אוסף הפקודות הזה מביא לכך שתוכן הקובץ מוכלל אך ורק פעם אחת:
- בפעם הראשונה בה הקובץ נקרא, מוגדר הקבוע const (כאן, לדוגמה, FILE_1_H).
- בפעמים הבאות בהן הקובץ נקרא, הקבוע כבר מוגדר. היות שתוכן הקובץ עטוף בזוג
#ifndef-#endif
- המהדר יתעלם ממנו.
הסיבה לשימוש בסימון זה היא שפעמים רבות ייקראו קבצי כותרת פעמים רבות (לפעמים מקבצי כותרת אחרים), מה שעלול לגרום לבעיות בהידור הקובץ (אם, למשל, יוגדר struct זהה יותר מפעם אחת - התוכנית תיכשל בשלב ההידור).
עניינים סגנוניים
[עריכה]הערות לסוגר תנאי
[עריכה]יש המוסיפים הערה ל-#endif
המציינת את תוכן השורה שהתחילה את הקטע, לדוגמה כך:
#ifdef DEBUG
...
#endif /* ifdef DEBUG */
או כך:
#ifndef FILE_1_H
#define FILE_1_H
...
#endif /* ifndef FILE_1_H */
הדבר עשוי לעזור למתכנת להבין היכן החל קטע הקוד המותנה.
קבועים לקבצי כותרת
[עריכה]כאשר ממציאים קבוע לקובץ כותרת, רצוי לבחור בשיטה אחידה. שיטה אחת מקובלת היא להשתמש בשם הקובץ באותיות אנגליות גדולות, ולאחריו _H. כך, לדוגמה, אם קובץ הכותרת הוא file_1.h, אז הקבוע בשיטה זו יהיה FILE_1_H:
#ifndef FILE_1_H
#define FILE_1_H
...
#endif /* #ifndef FILE_1_H */
המאקרו assert
[עריכה]המאקרו assert הוא כלי מועיל לניפוי שגיאות.
כדאי לדעת: קטעי הקוד שבנושא זה משתמשים בספרייה הסטנדרטית. נדון בספריות באופן מעמיק יותר כאן. לעת עתה, פשוט יש לזכור לרשום בראשי הקבצים המשתמשים בקטעי הקוד שבנושא זה#include <assert.h>
|
הבעיה
[עריכה]נתבונן בקטע הקוד הבא:
void foo(int x)
{
float y = 1.0 / x;
...
}
קל לראות שתתרחש טעות אם x = 0 בזמן הקריאה, אך נניח שאנו יודעים שהפוקנציה foo לא תיקרא לעולם כאשר x = 0, כי חלקים אחרים בקוד דואגים לכך. מה יקרה אם יש שגיאה בחלקי הקוד הדואגים לכך? ברוב המערכות, התוכנית תיעצר, ותודפס הערה קצרה למדי. קשה יהיה להבין אפילו איזו שורה גרמה לטעות.
כדי להתמודד עם זאת, היינו יכולים לכתוב את הפונקציה כך (הנח ששם הקובץ הוא test.c, והפונקציה כתובה באיזור שורה 80):
void foo(int x)
{
if(!(x != 0))
{
printf("Assertion failed: 'x != 0' in line 80 test.c");
exit(-1);
}
float y = 1.0 / x;
...
}
אך יש מספר בעיות עם פתרון זה:
- הקוד הופך להיות פחות יעיל, מפני שאם אכן x לעולם אינו 0 - ביצענו בדיקה מיותרת.
- הקוד מסורבל יותר; קשה יותר להבין מה הפונקציה עושה.
- אם נעביר את הקוד לקובץ אחר או לשורה אחרת - הודעת השגיאה המודפסת סתם תבלבל.
כדי לפתור זאת, נוכל להשתמש בהתניה באי-הגדרת קבוע ובקבועים מקובלים, כך:
void foo(int x)
{
#ifndef NDEBUG
if(!(x != 0))
{
printf("Assertion failed: 'x != 0' in line %d %s", __LINE__, __FILE__);
exit(-1);
}
#endif /* #ifndef NDEBUG */
float y = 1.0 / x;
...
}
אך הקוד הופך להיות אפילו יותר מסורבל.
כעת נראה איך להשתמש במאקרו assert להשגת אותה התוצאה, אך בפחות סרבול.
השימוש במאקרו
[עריכה]משתמשים במאקרו assert בצורה הבאה:
assert(<cond>);
כאשר cond הוא ערך בוליאני. לדוגמה, אפשר לכתוב כך:
assert(x != 0);
אם התנאי לא יתקיים - התוכנית תדפיס הודעה כזו:
Assertion failed: 'x != 0' in line 80 test.c
ותעצר.
אם יש שורה שלעולם אין להגיע אליה, אפשר לכתוב:
assert(0);
אם בכל אופן התוכנית תגיע לשורה אי פעם (כלומר - יש טעות בקוד), התוכנית תדפיס הודעה כזו:
Assertion failed: '0' in line 100 foo.c
ותעצר.
דוגמאות
[עריכה]נוכל להשתמש במאקרו כדי לפתור את הבעיה הקודמת שראינו:
void foo(int x)
{
assert(x != 0);
float y = 1.0 / x;
...
}
לפעמים גם מוסיפים את המאקרו ב-switch-case שלא אמור לקבל ערך ברירת-מחדל לעולם. לדוגמה, נתבונן בקטע הבא:
switch(b)
{
case 1:
...
case 3:
...
case 15:
...
};
...
קטע הקוד עשוי להתאים למצב בו b אמור לקבל בדיוק אחד משלושת הערכים 1, 3, ו15. במקרה זה, נוכל להוסיף את החלק הבא:
switch(b)
{
case 1:
...
case 3:
...
case 15:
...
default:
assert(0);
};
...
כך, אם אירעה שגיאה בקוד, נקבל הודעת שגיאה מתאימה.
דוגמה לקוד לניפוי שגיאות
[עריכה]פרק זה לוקה בחסר. אתם מוזמנים לתרום לוויקיספר ולהשלים אותו. ראו פירוט בדף השיחה.
#include <assert.h>
#ifdef _DEBUG
int is_sorted(const int *a, unsigned int length)
{
for(i = 1; i < length; ++i)
if(a[i] >= a[i - 1])
return 0;
return 1;
}
#endif /* ifdef DEBUG */
int main()
{
int a[] = {2, 3, 5};
assert( is_sorted(a, 3) );
print( binary_search(a, 3, 10) );
return 0;
}
תחליפים לחלק מיכולות הקדם מעבד
[עריכה]לקדם מעבד עוד יכולות מספר. נעבור על חלקן בקצרה, ונציג תחליפים להן.
הקדם מעבד יכול להחליף מופע טקסט בטקסט אחר. לדוגמה, הפקודה:
#define RED 2
#define BLUE 3
תגרום לכך שבכל מקום בהמשך הקובץ בו מופיע הרצף RED, הוא יוחלף ב-2, ובכל מקום בהמשך הקובץ בו יופיע הרצף BLUE, הוא יוחלף ב-3.
במקום זאת אפשר להשתמש במשתנים קבועים:
const int red = 2;
const int blue = 3;
הקדם מעבד גם מאפשר להגדיר פקודות מאקרו, שהן כמעין פונקציות פשוטות. לדוגמה, הפקודה:
#define MIN(a, b) a < b? a: b
תגרום לכך שבכל מקום בהמשך הקובץ בו מופיע הרצף MIN(2, 3)
, הוא יוחלף ב2 < 3? 2: 3
.
במקום זאת אפשר פשוט להשתמש בפונקציה רגילה:
int min(int a, int b)
{
return a < b? a: b;
}
סכנות
[עריכה]שימוש ביכולות הקדם-מעבד מספק כמה יכולות רבות עוצמה, אך מצד שני - טומן בחובו לא מעט סכנות. ראו לדוגמה את המאקרו התמים הבא:
#define MY_FOO(x) x * x
כעת חשבו על קטע הקוד הבא, אשר עושה שימוש במאקרו:
int foo(int x, int y) {
return MY_FOO(x + y);
}
אחרי מעבר הקדם מעבד, תהייה התוצאה:
int foo(int x, int y) {
return x + y * x + y;
}
תוצאה בלתי מתוכננת!
על בעייה זו אפשר להתגבר בעזרת שימוש בסוגריים, כלומר:
#define MY_FOO(x) ((x) * (x))
האמנם הצלחנו לכתוב קוד בטוח? מה יקרה במקרה הבא?
int x = 3;
MY_FOO(++x)
האופרטור ++ יתבצע פעמיים (במקום, כפי שרצינו, פעם אחת), וגם כאן יביא לתוצאה שאינה מתוכננת (ולפעמים קשה מאוד למציאה!).
המסקנה משתי דוגמאות אלו היא שחובה לנהוג זהירות רבה לפני שימוש במאקרו. בפרט, הקפדה על שימוש בסוגריים והמנעות ממאקרו שכולל בתוכו את אותו המשתנה יותר מפעם אחת יפחיתו את מספר הטעויות האפשריות.
הפרק הקודם: פלט וקלט קבצים |
הקדם מעבד תרגילים |
הפרק הבא: מודולים |