Menu

קומפילציה מותנית ב-C: #ifdef, #ifndef, #if והדגל -D

קמפלו קוד שונה לבניות שונות עם #ifdef, #ifndef, #if, #elif ו-#else: מתגי דיבאג, ענפים לפי פלטפורמה, הגדרת מאקרו משורת הפקודה עם -D, ושימוש ב-#if 0 כדי לנטרל קוד.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

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

זה ההבדל מ-if רגיל. שניהם בוחרים בין מסלולים; רק אחד מהם בוחר לפני שהתוכנית קיימת.

#ifdef ו-#ifndef

השאלה הפשוטה ביותר היא אם מאקרו מוגדר בכלל:

מחקו את השורה #define DEBUG וכל הדפסות הדיבאג נעלמות מהבנייה: לא מנוטרלות, נעלמות. הריצו פעם אחת כמו שזה כתוב, ואז הסירו את השורה והריצו שוב.

#ifndef הוא השלילה: שמרו את הבלוק רק אם המאקרו לא מוגדר. השימוש הנפוץ ביותר שלו הוא שומר ההכללה שמוסבר בקובצי כותרת, וזו גם הדרך לספק ברירת מחדל שמי שמשתמש בקוד יכול לדרוס:

כל #ifdef ו-#ifndef חייבים להיסגר ב-#endif. הוספת הערה לסוגר בקבצים ארוכים, #endif /* DEBUG */, חוסכת זמן אמיתי בהמשך.

#if, #elif, #else

#if מקבל ביטוי שלם קבוע ושומר את הבלוק כשהוא שונה מאפס. זה מאפשר השוואות של גרסאות ורמות:

שנו את LOG_LEVEL ל-0 או ל-3 והריצו שוב כדי לראות את התוכנית עצמה משנה צורה.

את הביטוי מחשב קדם-המעבד, כלומר הוא יכול להשתמש רק בקבועים שלמים, באופרטורים של חשבון והשוואה, ובמאקרו שמתרחבים לאלה. הוא לא רואה את sizeof, ערכי enum, משתנים או כל דבר שצריך את הקומפיילר. מאקרו לא מוגדר בתוך #if מחושב כ-0 במקום להיות שגיאה, וזה נוח ולפעמים מפתיע:

#if FEATURE_X        /* FEATURE_X is never defined anywhere -> 0 -> block dropped */

defined(NAME) הופך את השאלה של #ifdef למשהו שאפשר להשתמש בו בתוך #if, כך שאפשר לשלב בדיקות:

#if defined(DEBUG) && !defined(NDEBUG)
    /* debug build, and assertions are on */
#endif

#if defined(LINUX) || defined(BSD)
    /* either unix-like system */
#endif

#if defined(X) ו-#ifdef X אומרים אותו דבר; פנו לראשון כשצריך &&, || או !.

הגדרת מאקרו משורת הפקודה

הכוח האמיתי של המתגים האלה הוא שקוד המקור לא צריך להשתנות בכלל. gcc -D מגדיר מאקרו לכל הקומפילציה:

gcc -DDEBUG program.c -o program        # DEBUG defined as 1
gcc -DLOG_LEVEL=3 program.c -o program  # specific value
gcc -DDEBUG -DBUFFER_SIZE=512 a.c b.c -o app

-DNAME לבדו שקול ל-#define NAME 1. -U NAME מבטל הגדרה, וזה חשוב כשקובץ כותרת מגדיר משהו שאתם רוצים כבוי.

אז הסידור המקובל הוא: קוד המקור מכיל ברירות מחדל שמוגנות ב-#ifndef, ופקודת הבנייה בוחרת תצורה.

/* config.h */
#ifndef LOG_LEVEL
#define LOG_LEVEL 1        /* quiet by default */
#endif

#ifndef MAX_CONNECTIONS
#define MAX_CONNECTIONS 64
#endif
# development build
gcc -DDEBUG -DLOG_LEVEL=3 -Wall -Wextra -g src/*.c -o app-dev

# release build
gcc -DNDEBUG -O2 src/*.c -o app

NDEBUG הוא סטנדרטי: הגדרתו מנטרלת כל assert() בתוכנית, כי <assert.h> עצמו כתוב עם קומפילציה מותנית. זו התבנית בזעיר אנפין: קובץ כותרת שמתקמפל לדברים שונים לפי מה שהבנייה הגדירה.

מתגי פלטפורמה

קומפיילרים מגדירים מראש מאקרו שמזהים את מערכת היעד, כך שקובץ מקור אחד יכול לקרוא ל-API הנכון בכל אחת:

#if defined(_WIN32)
    #include <windows.h>
    #define CLEAR_SCREEN "cls"
#elif defined(__APPLE__)
    #include <unistd.h>
    #define CLEAR_SCREEN "clear"
#elif defined(__linux__)
    #include <unistd.h>
    #define CLEAR_SCREEN "clear"
#else
    #error "Unsupported platform"
#endif

הנפוצים: _WIN32 (מוגדר גם ב-Windows של 32 ביט וגם של 64 ביט), _WIN64, __linux__, __APPLE__, __unix__, __ANDROID__. לזהות הקומפיילר יש קבוצה משלה, __GNUC__, __clang__, _MSC_VER, וגם לארכיטקטורה: __x86_64__, __aarch64__.

גרסה שאפשר להריץ שמדווחת על מה היא נבנתה:

כדאי להכיר את #error בפני עצמו: הוא עוצר את הקומפילציה עם ההודעה שלכם. סיום שרשרת פלטפורמות ב-#else / #error "Unsupported platform" הופך בנייה שגויה ושקטה לכישלון ברור בראש יומן הבנייה.

כדי לראות את כל המאקרו שהקומפיילר שלכם מגדיר מראש:

gcc -dM -E - < /dev/null

הפלט הזה הוא התשובה המוסמכת לשאלה "מה אפשר לבדוק על המכונה הזו".

#if 0 לנטרול קוד

הפיכת בלוק להערה עם /* ... */ נכשלת ברגע שהבלוק מכיל הערה משלו, כי הערות C לא ניתנות לקינון: ה-*/ הראשון בפנים מסיים את ההערה החיצונית וכל מה שאחריו הופך לקוד תועה. ל-#if 0 אין בעיה כזו:

#if 0
    /* This whole region is removed, comments and all. */
    legacy_init();
    int n = old_calculation(42);  /* even this comment is fine */
    report(n);
#endif

החליפו ל-#if 1 כדי להחזיר את הקוד. עורכים ממשיכים להדגיש אותו כ-C, ואפשר לקנן #if 0 בתוך #if אחר.

זה כלי דיבאג, לא מערכת אחסון. קוד שיושב ב-#if 0 אף פעם לא מקומפל, ולכן הוא נרקב בשקט: הוא כבר לא ייבנה כשמישהו יהפוך את המתג. השתמשו בו בזמן שאתם מצמצמים בעיה, ואז מחקו את הבלוק ותנו לניהול הגרסאות לזכור אותו.

איפה קומפילציה מותנית משתבשת

שלושה אופני כישלון אחראים לרוב הכאב.

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

שזירת #ifdef בזרימת הבקרה הופכת מהר לבלתי קריאה. קשה לחשוב על דבר כזה:

if (ready) {
#ifdef FAST_PATH
    fast_send(buf);
} else {
#endif
    slow_send(buf);
}

סוגריים מסולסלים שנפתחים בענף אחד ונסגרים באחר הם חוקיים ונוראיים. העדיפו תנאים שעוטפים פונקציות שלמות, ובחרו ביניהן:

#ifdef FAST_PATH
static void send_data(const char *buf) { fast_send(buf); }
#else
static void send_data(const char *buf) { slow_send(buf); }
#endif

if בזמן ריצה הוא לרוב עדיף. אם שני הענפים היו מתקמפלים בכל מקום, if (debug_enabled) רגיל שומר ששני המסלולים ייבדקו מבחינת טיפוסים, יהיו ניתנים לבדיקה ויהיה אפשר להחליף ביניהם בלי בנייה מחדש. שמרו את קדם-המעבד למה שרק הוא יכול לעשות: קוד שבאמת לא יכול להתקמפל בפלטפורמה האחרת.

לתבנית שמשלבת את כל זה, קובץ כותרת של ברירות מחדל, -D לכל בנייה, וענפי פלטפורמה עטופים בשומרי הכללה, ראו קובצי כותרת; ול-#define עצמו, מאקרו.

שאלות נפוצות

מה זו קומפילציה מותנית ב-C?

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

מה ההבדל בין #ifdef ל-#if?

#ifdef NAME שואל רק אם המאקרו קיים, בלי קשר לערך שלו. #if expression מחשב ביטוי שלם קבוע, כך שהוא יכול להשוות ערכים: #if VERSION >= 3. השתמשו ב-#ifdef לדגלים שפשוט קיימים או לא, וב-#if כשהערך משנה. #if defined(NAME) משלב את שניהם ואפשר לחבר אותו עם && ו-||.

איך מגדירים מאקרו משורת הפקודה עם gcc?

השתמשו ב--D: gcc -DDEBUG program.c מגדיר את DEBUG כאילו הקובץ התחיל ב-#define DEBUG 1, ו--DMAX=50 נותן לו ערך מסוים. כך מערכות בנייה מפעילות תכונות בלי לערוך את קוד המקור, ו--U NAME מבטל הגדרה.

למה להשתמש ב-#if 0 במקום להפוך קוד להערה?

כי הערות /* ... */ לא ניתנות לקינון: ה-*/ הראשון בתוך האזור מסיים את ההערה מוקדם והשאר הופך לקוד שבור. #if 0 ... #endif מסיר כל כמות של קוד שמכיל כל מספר של הערות, שומר על הדגשת התחביר, ואפשר להחזיר אותו בקלות ל-#if 1.

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל