מאקרו הוא שם שה-preprocessor מחליף בטקסט כלשהו לפני שהקומפיילר רץ. זה כל המנגנון, וכל כלל בעמוד הזה נובע ממנו: למאקרו אין טיפוסים, הם לא מכבדים טווח, הם לא מחשבים ארגומנטים והם לא יודעים מה זו קריאה לפונקציה. הם מעתיקים טקסט.
בשימוש זהיר הם הכרחיים: מתגים לניידות, מאקרו של assertion שלוכדים את מספר השורה, קבועים בזמן קומפילציה שעובדים בגודלי מערכים. בשימוש רשלני הם מייצרים באגים שנראים בלתי אפשריים עד שמרחיבים את הקובץ.
מאקרו פשוטים (object-like)
הצורה הפשוטה ביותר מגדירה שם שמייצג ערך:
לפי המוסכמה, השמות האלה נכתבים ב-UPPER_SNAKE_CASE כדי שקורא יוכל לזהות במבט שסמל הוא מאקרו ולא משתנה. המוסכמה הזו חשובה כאן יותר מהרגיל, כי מאקרו מתעלמים מטווח: #define בתוך פונקציה עדיין חל על שאר הקובץ, והוא ישכתב בשמחה משתנה עם אותו שם בפונקציה אחרת.
#undef NAME מסיר הגדרה, כך שאפשר להגדיר שם מחדש בהמשך בלי אזהרה על הגדרה כפולה.
מאקרו עם פרמטרים (function-like)
שימו רשימת פרמטרים ישירות אחרי השם, בלי רווח לפני הסוגר הפותח, והמאקרו מקבל ארגומנטים:
כלל הרווח אמיתי: #define SQUARE (x) ((x)*(x)) מגדיר מאקרו פשוט בשם SQUARE שטקסט ההחלפה שלו מתחיל ב-(x). שגיאת הקומפילציה שתבוא אחר כך לא תזכיר רווחים.
כללי הסוגריים
יש שניים, ושניהם קיימים כי הגוף מודבק לתוך ביטוי שאתם לא רואים כשאתם כותבים את המאקרו.
כלל 1: שימו סוגריים סביב כל פרמטר. בלי זה, ארגומנט שהוא בעצמו ביטוי נקרע לגזרים על ידי סדר הקדימויות:
#define SQUARE_BAD(x) x * x
SQUARE_BAD(2 + 3) /* הופך ל-2 + 3 * 2 + 3 == 11, לא 25 */
כלל 2: שימו סוגריים סביב כל הגוף. בלי זה, הביטוי שמסביב קורע לגזרים את התוצאה:
#define DOUBLE_BAD(x) (x) + (x)
10 / DOUBLE_BAD(5) /* הופך ל-10 / (5) + (5) == 7, לא 1 */
הריצו את שני הכישלונות זה לצד זה עם הגרסאות המתוקנות:
שתי הגרסאות הגרועות מתקמפלות בלי אזהרה אחת ומפיקות מספרים שגויים. ההרגל שצריך לבנות הוא מכני: עטפו כל פרמטר, ואז עטפו את התוצאה. ((a) > (b) ? (a) : (b)) נראה רועש, והרעש הוא בדיוק העניין.
המלכודת של הערכה כפולה
סוגריים לא יכולים להציל אתכם מהסכנה השנייה. מאקרו מדביק את טקסט הארגומנט בכל מקום שבו הפרמטר מופיע, כך שארגומנט עם תופעת לוואי קורה יותר מפעם אחת:
המאקרו מתרחב ל-((i++) > (j) ? (i++) : (j)). ההשוואה מגדילה את i ל-6 ומשווה את 5 ל-3; הענף האמיתי מגדיל את i שוב ל-7 ומחזיר 6. כך ש-m הוא 6 ו-i הוא 7, ואף אחד מהם לא מה ש"הגדול מבין i ו-j" אמור לתת. פונקציה max(i++, j) הייתה נכונה, כי הארגומנט מחושב פעם אחת לפני הקריאה.
אותה מלכודת תופסת כל דבר עם תופעת לוואי: SQUARE(rand()) קורא ל-rand() פעמיים ומכפיל שני מספרים שונים. CHECK(read_byte()) צורך שני בתים.
שתי הגנות, לפי סדר עדיפות:
- השתמשו בפונקציה אמיתית. אם צריך אותה לכמה טיפוסים, כתבו אחת לכל טיפוס, או השתמשו בפונקציית
static inlineב-header. - אם זה חייב להיות מאקרו, תעדו בבירור שהארגומנטים מחושבים יותר מפעם אחת, והשאירו את מקומות הקריאה בלי תופעות לוואי.
מאקרו מרובי שורות ו-do-while(0)
גוף של מאקרו יכול להתפרס על כמה שורות אם כל שורה חוץ מהאחרונה מסתיימת ב-backslash. הגרסה התמימה נראית בסדר:
#define LOG_TWICE(msg) \
printf("%s\n", msg); \
printf("%s\n", msg)
ואז היא נשברת, בשקט, בדיוק במקום שבו זה חשוב:
if (verbose)
LOG_TWICE("hello");
else
printf("quiet\n");
אחרי ההצבה ה-if מחזיק רק את ה-printf הראשון, השני רץ בלי תנאי, ול-else כבר אין if תואם: שגיאת קומפילציה שההודעה שלה לא מצביעה לשום מקום מועיל. עטיפת הגוף בסוגריים מסולסלים רגילים לא עדיפה: ה-; אחרי LOG_TWICE("hello") הופך לפקודה ריקה שמסיימת את ה-if, וה-else נשבר שוב.
הביטוי שעובד הוא do { ... } while (0):
do { ... } while (0) היא פקודה אחת, היא מריצה את הגוף שלה בדיוק פעם אחת, והיא דורשת נקודה-פסיק אחריה, כך שמקום הקריאה נקרא כמו קריאה רגילה לפונקציה ומתנהג כמוה בכל הקשר של זרימת בקרה. שימו לב ל-backslashes: רווח תועה אחרי backslash מסיים את ההמשך ומפיק שגיאה מבלבלת.
מאקרו מוגדרים מראש ודפוסי assertion
ה-preprocessor מגדיר בעצמו כמה מאקרו, והם הסיבה שחלק מהדברים פשוט לא יכולים להיות פונקציות:
__FILE__ ו-__LINE__ מתרחבים במקום שבו הם כתובים, ולכן הכנסה שלהם לתוך מאקרו לוכדת את המיקום של הקוראת. פונקציה לא הייתה יכולה לעשות את זה: בתוך פונקציה הם תמיד היו מדווחים על הקובץ והשורה של פונקציית הלוג עצמה. זה בדיוק האופן שבו המאקרו הסטנדרטי assert מדווח על המיקום של הביטוי שנכשל.
שמות מוגדרים מראש שימושיים נוספים: __DATE__, __TIME__ ו-__STDC_VERSION__ (למשל 201710L עבור C17).
שני אופרטורים שכדאי להכיר
בתוך גוף של מאקרו, # הופך פרמטר למחרוזת מילולית ("stringizing") ו-## מדביק טוקנים יחד ("token pasting").
SHOW(width * height) הופך ל-printf("width * height" " = %d\n", (width * height));: הקומפיילר מחבר מחרוזות מילוליות צמודות, כך שקריאה אחת מדפיסה גם את הטקסט של הביטוי וגם את הערך שלו. זה טריק דיבוג ששווה לזכור.
## נדיר יותר ומופיע בעיקר במאקרו שמייצרים קוד: #define MAKE_VAR(n) int var_##n הופך את MAKE_VAR(3) ל-int var_3. השתמשו בו במשורה; אי אפשר לחפש לפי שם מזהים שה-preprocessor בנה, וזה מקשה על הניווט בקוד.
מאקרו מול פונקציות מול const
השתמשו בכלי החלש ביותר שעושה את העבודה:
- משתנה
const: ל-const double PI = 3.14159;יש טיפוס, הוא כפוף לטווח, מופיע ב-debugger, ו-header לא קשור לא יכול להגדיר אותו מחדש. העדיפו אותו לערכים שמשמשים בזמן ריצה. enum:enum { MAX_USERS = 100 };נותן קבוע שלם עם שם שעובד במקומות שבהם C דורשת קבוע בזמן קומפילציה, עם טיפוס וטווח. טוב לגבולות שלמים.- פונקציית
static inline: עוברת בדיקת טיפוסים, מחשבת כל ארגומנט בדיוק פעם אחת, וקומפיילרים מודרניים עושים לה inline בדיוק כמו למאקרו. זה התחליף הנכון לכמעט כל מאקרו עם פרמטרים. - מאקרו: כשצריך
__LINE__, כשהדבר חייב לעבוד לפני שיש טיפוסים (גודלי מערכים בקוד ישן), כשמחליפים קוד עם קומפילציה מותנית, או כשמייצרים קוד חוזר.
מקום אחד שבו למאקרו עדיין אין מתחרים הוא חישוב גודל של מערך, כי התוצאה חייבת להיות קבוע בזמן קומפילציה:
גם לזה יש הסתייגות ידועה משלו: הוא נכון רק עבור מערך אמיתי. העבירו מצביע, וזה מה שמערך הופך אליו כשמעבירים אותו לפונקציה, ו-sizeof ימדוד את המצביע, וייתן תשובה שגויה בביטחון מלא ובשקט.
דיבוג של מאקרו
כשמאקרו מתנהג לא כמו שצריך, אל תבהו בו. הרחיבו אותו:
gcc -E program.c | tail -30
הטקסט אחרי ההצבה אומר לכם מיד אם הבעיה היא סוגריים חסרים, הערכה כפולה או שם שמתנגש במשהו אחר. קמפלו גם עם -Wall -Wextra: GCC ו-clang מוסיפים הערות "in expansion of macro" שמחברות את שורת השגיאה המדווחת חזרה להגדרה.
הבא בתור: קובצי header, שבהם #define ו-#include משתלבים כדי לאפשר לפרויקט אחד להתפרס על הרבה קובצי מקור בלי להדביק את אותן הצהרות פעמיים.
שאלות נפוצות
מה זה מאקרו ב-C?
קטע טקסט עם שם שה-preprocessor מציב בקוד המקור לפני הקומפילציה. #define MAX 100 הופך כל MAX בהמשך ל-100; #define SQUARE(x) ((x) * (x)) מקבל ארגומנטים ומציב אותם בתבנית. למאקרו אין טיפוסים והם לא כפופים לכללי טווח: הם החלפת טקסט.
למה מאקרו ב-C צריכים כל כך הרבה סוגריים?
כי הגוף מודבק לתוך הקוד שמסביב ואז מנותח כמכלול. #define SQUARE(x) x * x הופך את SQUARE(2 + 3) ל-2 + 3 * 2 + 3, שהוא 11 ולא 25. עטיפה של כל ארגומנט ושל כל הגוף, ((x) * (x)), גורמת לטקסט המוצב להתקבץ כמו שהתכוונתם, לא משנה מה נמצא סביבו.
מה ההבדל בין מאקרו לפונקציה ב-C?
פונקציה מקומפלת פעם אחת, עוברת בדיקת טיפוסים ומחשבת כל ארגומנט בדיוק פעם אחת. מאקרו מודבק בכל שימוש, לא בודק כלום ועלול לחשב ארגומנט כמה פעמים, כך ש-MAX(i++, j) יכול להגדיל את i פעמיים. העדיפו פונקציות (ו-const/enum עבור קבועים); השתמשו במאקרו לדברים שפונקציות לא יכולות לעשות, כמו לכידת __LINE__ או יצירת קוד.
למה מאקרו מרובה שורות עטוף ב-do { ... } while (0)?
כדי שיתנהג כמו פקודה אחת. גוף { ... } חשוף נשבר כשבא אחריו נקודה-פסיק בתוך if/else (הנקודה-פסיק מסיימת את ה-if מוקדם), ורצף רגיל של פקודות נשבר ב-if בלי סוגריים מסולסלים. do { ... } while (0) היא פקודה אחת שמקבלת נקודה-פסיק בסופה ומריצה את הגוף שלה בדיוק פעם אחת.