לדלג לתוכן

מה זה Preprocessor בשפת C - השלב שרץ לפני שהקומפיילר בכלל מתחיל

לפני שקומפיילר של C בכלל מנתח את הקוד שלכם, שלב שלם ונפרד כבר סיים לעבוד על הקובץ ולשנות אותו. השלב הזה נקרא Preprocessor, מעבד מקדים, והוא אחראי על כל השורות שמתחילות בסולמית - כמו #include ו-#define. רוב המתכנתים משתמשים בפקודות האלה כל יום בלי לעצור ולשאול מה בדיוק קורה מתחתיהן.

אז מה בדיוק עושה ה-Preprocessor

הפרעפרוססור הוא בעצם תוכנית טקסטואלית - הוא לא מבין C, הוא לא בודק טיפוסים, והוא לא יודע כלום על תחביר השפה. כל מה שהוא עושה זה לעבור על הקובץ שלכם שורה אחר שורה, לזהות פקודות שמתחילות בסולמית, ולבצע החלפת טקסט פשוטה לפי מה שכל פקודה אומרת. התוצאה היא קובץ חדש, ארוך יותר, שרק אז מועבר לקומפיילר עצמו לניתוח אמיתי.

include - הדבקה מילולית של קבצים

הפקודה #include היא כנראה הכי מוכרת מכולן. כשכותבים #include <stdio.h>, הפרעפרוססור פשוט לוקח את כל התוכן של הקובץ stdio.h, ומדביק אותו מילולית במקום שבו כתבתם את השורה הזאת. אין שום קסם - זו העתקה והדבקה טהורה, שקורית לפני שהקומפיילר בכלל מתחיל לעבוד.

זו הסיבה שקבצי header מכילים בדרך כלל רק הצהרות על פונקציות ולא את המימוש המלא שלהן - אחרת, כל קובץ שכולל אותן היה מקבל עותק שלם של הקוד, ומכפיל אותו שוב ושוב בכל קובץ שמכיל את ה-include.

define - מאקרו שמחליף טקסט לפני שמישהו קורא אותו

הפקודה #define מגדירה מאקרו - הוראת החלפה. #define MAX_SIZE 100 אומר לפרעפרוססור, בכל מקום שבו מופיע MAX_SIZE בקוד, להחליף אותו במילולית ב-100, עוד לפני שהקומפיילר בכלל רואה את הקובץ. אפשר גם להגדיר מאקרו עם פרמטרים, שמתנהג כמעט כמו פונקציה:

#define SQUARE(x) ((x) * (x))

וכאן בדיוק מסתתרת אחת המלכודות הכי מפורסמות ב-C. אם תקראו ל-SQUARE(x) עם ביטוי שיש לו תופעת לוואי, כמו SQUARE(i++), הפרעפרוססור יחליף את זה טקסטואלית ל-((i++) * (i++)) - שתי הפעלות של הביטוי במקום אחת, כי המאקרו לא "יודע" מה זה פונקציה, הוא רק מחליף טקסט. זו הסיבה שמאקרואים דורשים סוגריים בכל מקום, ולמה במקרים רבים עדיפה פונקציה רגילה על פני מאקרו, במיוחד ב-C מודרנית.

קומפילציה מותנית - קוד שנעלם לפני שהוא בכלל מתחיל

פקודות כמו #ifdef, #ifndef ו-#endif מאפשרות לכלול או להשמיט קטעי קוד שלמים, בהתאם לתנאים שמוגדרים בזמן קומפילציה - למשל, מערכת הפעלה שונה, או דגל דיבוג. הקוד שבתוך תנאי שלא מתקיים פשוט נעלם לגמרי לפני שהקומפיילר בכלל רואה אותו, כאילו מעולם לא נכתב.

השימוש הכי נפוץ בטכניקה הזו הוא header guards - מנגנון שמונע מקובץ header להיכלל פעמיים באותו קובץ מקור:

#ifndef POINT_H
#define POINT_H
// תוכן הקובץ
#endif

בפעם הראשונה ש-POINT_H לא מוגדר, הקוד נכלל, ומיד גם מוגדר הסימן POINT_H. אם אותו קובץ נכלל שוב, בטעות או דרך שרשרת של includes אחרים, התנאי כבר לא מתקיים, והקוד נדלג לגמרי. בלי המנגנון הזה, אפשר לקבל הצהרות כפולות ושגיאות קומפילציה מוזרות שקשה מאוד לאבחן.

למה כדאי להבין את זה, ולא רק להשתמש בזה

מתכנתים רבים משתמשים ב-include וב-define שנים בלי להבין שזו בכלל לא C - זו שכבה נפרדת של עיבוד טקסט שרצה קודם. ההבנה הזאת חשובה כי היא מסבירה למה שגיאות קומפילציה לפעמים מצביעות על מקום מוזר בקוד שלכם, ולמה דיבוג מאקרו דורש לפעמים להסתכל על הפלט של הפרעפרוססור עצמו ולא רק על הקוד המקורי.

זה בדיוק סוג ההבנה של "מה קורה מתחת למכסה" שאנחנו בונים בקורס ליבת המחשב, משלב הפרעפרוססור ועד קוד המכונה הסופי.

ואם מאקרו מסתורי גרם לכם לבאג שאתם לא מצליחים להבין, יש קהילה שכבר ראתה את זה.

הצטרפו לקהילה בדיסקורד

לסיכום

ה-Preprocessor הוא לא חלק מ-C עצמה, הוא שלב טקסטואלי שרץ לפניה ומכין את הקרקע - מדביק קבצים, מחליף מאקרואים, ומשמיט קוד לפי תנאים. ברגע שרואים אותו בתור מה שהוא באמת, מעבד החלפת טקסט פשוט, גם מלכודות כמו תופעות לוואי במאקרו וגם הצורך ב-header guards הופכים להיגיוניים לגמרי.