פקודות קדם מעבד של C   C Preprocessor 

 


 

תוכן הפרק

הכללת קבצים #include (file inclusion) 1

החלפות מקרו  Macro substitution  2

החלפת מקרו פשוטה  2

החלפת מקרו עם ארגומנטים  3

ביטול הגדרה  #undef 4

האופרטור # של הקדם מעבד - פיתוח למחרוזת  4

האופרטור ## של הקדם מעבד - שירשור tokens  4

שיטת החלפת ה tokens  4

האופרטור ## של הקדם מעבד מיועד לשירשור tokens. 6

 

 

בשפת C מוגדרת גם שפה של פקודות קדם עיבוד. פקודות אלו (הפקודות מהצורה: #xxx  כגון, #include #define)  עוסקות בעריכת טכסט קובץ המקור ב C, כהכנה לשלבי ההידור הבאים. אפשר לראות את הפקודות הללו כשלב ראשון בתהליך ההידור (compilation).  למעשה, פקודות קדם העיבוד משנות את טכסט תוכנית ה C מבלי להתייחס למשמעות מבחינת שפת C, כלומר מחליפות תווים בתווים אחרים. פקודות קדם המעבד הנפוצות ביותר:

     #include  -  פקודה המשמשת להכללת           תוכן של קובץ.

     #define  -    פקודה המשמשת להחליף token ברצף שרירותי של תווים.

בנוסף ישנם גם פקודות להכללת קוד על תנאי (conditional compilation) ו macros  עם ארגומנטים.

 

הכללת קבצים #include (file inclusion)

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

     #include "filename" -  הקומפילר מחפש תחילה את הקובץ  filename במדריך(directory) של הקובץ ה C בו מבצעים את ה #include.  אם הקובץ אינו שם הקומפילר מחפש את הקובץ במקומות שהוגדרו לו מראש.

     #include <filename>  -  הקומפילר מחפש את הקובץ filename רק במקומות שהוגדרו לו מראש.

 

     קובץ שעושים לו #include  יכול להכיל בתוכו שורות #include  אחרות.  צריך להיזהר שלא ייווצרו מעגלים.

 

כאמור, משתמשים ב #include לאסוף הגדרות משותפות למספר קבצים במקום אחד. ע"י כך נמנעים מ bugים  שנובעים מהגדרות  שונות  לאותם דברים.  ברור שאם משנים משהו בקובץ הגדרות צריך לקמפל מחדש את כל הקבצים שעושים לו #include.

דוג':

#include <stdio.h>/* the compiler knows where this file is located */

#include "my_file.h" /* file is in in current directory */

#include "C:\\dir\\my_file.h" /* full path: DOS */

#include "..\\dir\\my_file.h" /* relative to current path: DOS */

#include "~/dir/my_file.h" /* full path: UNIX */

 

חזרה להתחלה

החלפות מקרו  Macro substitution

 

החלפת מקרו פשוטה 

לאחר שמופיעה שורה מהסוג:

#define identifier replacement-text

 

נתנו הוראה לבצע את הצורה הפשוטה ביותר של החלפת מקרו: כל מופע של  identifier  יוחלף     ב replacement-text  כאשר:

     identifier  -  יכול להיות כל שם חוקי של משתנה (מתחיל באות וממשיך עם אותיות ומספרים).

     replacement-text  -  טכסט שרירותי עד סוף השורה.  אפשר להמשיך הגדרות ארוכות למספר שורות ע"י הוספת \ בסוף השורה (הדבר מתפרש כאיחוד שורות).

 

     השם identifier מוכר מהשורה בו הוא מוגדר ועד סוף הקובץ.

     הגדרה יכולה להשתמש בהגדרות קודמות.

 

     החלפות מתבצעות רק עבור tokens ולא מתבצעות בתוך מחרוזות.

     לא מחליפים חלקי tokens.

     הגדרה של אותו identifier יותר מפעם אחת חוקית, רק אם ה replacement-text מכיל את אותו רצף של tokens בכל ההגדרות.

דוג,

#define YES 1

 

cout << "YES"); /* no replacment in strings        */

YESMAN = 1;    /* no replacement of partial tokens */

YESMAN = YES;  /* replaced to YESMAN  = 1;        */

 

#define FOREVER for(;;) /* infinite loop */

 

 

 

#define ONE TWO TWO

#define TWO FOUR FOUR

#define FOUR + EIGHT + EIGHT

 

void main()

{

    int EIGHT = 8;

    if (ONE == 64)

        putchar('g');

}

 

 

void main()

{

    int EIGHT = 8;

    if (+ EIGHT + EIGHT + EIGHT + EIGHT + EIGHT

        + EIGHT + EIGHT + EIGHT == 64)

        putchar('g');

}

 

OUTPUT:  G

 

בחלק העליון של הדוגמא רואים שישנן מספר הגדרות המתבססות אחת על השניה. בחלק התחתון של הדוגמא רואים את הקובץ לאחר ההחלפות.

חזרה להתחלה

החלפת מקרו עם ארגומנטים

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

 

#define identifier(identifier-list) replacement-text using identifiers as parameters

     identifier - שם המקרו בדומה להחלפת מקרו פשוטה.

     חשוב להקפיד שה (  יהיה צמוד ל identifier כלומר:   identifier(    ולא    identifier (,   אז יודעים שמדובר בהחלפת מקרו עם פרמטרים ולא החלפת מקרו פשוטה.

     identifier-list - רשימה של מזהים מופרדים בפסיקים. 

     ה replacement-text הוא שרירותי. כל token בתוכו שהוא מזהה מתוך ה identifier-list יוחלף ע"י הארגומנט המתאים לו בזמן ה"קריאה" למקרו.

 דוג':

#define max(A, B) ((A) > (B) ? (A) : (B))

בדוגמא הזו כל מופע של max יוחלף בביטוי שערכו הוא המקסימום בין שני הארגומנטים.

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

x = max(p+q, r+s);

==> x = ((p+q) > (r+s) ? (p+q) : (r+s));

היתרונות של max כזה:

     הוא שהוא עובד עבור כל שילוב של ארגומנטים אריתמטיים.

     הקוד מתפתח inline ז.א. אין קריאה לפונקציה - יעילות בביצוע.

החסרונות של max כזה:

     כל ארגומנט מחושב כמספר המופעים שלו ב replacement-text בניגוד לפעם אחת כפי שקורה בקריאה לפונקציה.

     מהחיסרון הקודם נובעות בעיות של side effects:

max(i++, j++)

==> ((i++) > (j++) ? (i++) : (j++)) /* in function: only one increment */

 

 

 

#define ABSDIFF(A, B) ((A)>(B) ? (A)-(B) : (B)-(A))

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

 

     זהירות עם הסוגריים:

#define square(x) x * x /* not good */

 

square(q+1)

==> q + 1 * q + 1 /* error */

 

#define square(x) ((X)*(x)) /* correct */

 

 

#define swap(t, a, b) {t tmp; tmp = a; a = b; b = tmp; }

swap(int, x, y)

==> {int tmp; tmp = a; a = b; b = tmp; }

 

     בקובץ stdio.h מגדירים את getchar, putchar  כמקרו  בכדי למנוע תקורה של קריאה לפונקציה עבור פעולה עם תו בודד.

 

ביטול הגדרה  #undef

     אפשר לבטל שמות שהוגדרו קודם ע"י שורה:  #undef name    .    לרוב, משתמשים  כאשר רוצים להבטיח שבאמת קוראים לפונקציה ולא למקרו.

דוג'

#include <stdio.h>                   #include<stdio.h>

                                     #undef getchar

c = getchar(); /* macro call */      c = getchar(); /* function call */

 

בכדי להגדיר פונקציה שכבר מוגדרת כמקרו חייבים #undef:

#include<stdio.h>             

#undef getchar      

int getchar(void) { ... } /* error if we don't use #undef! */

חזרה להתחלה

האופרטור # של הקדם מעבד - פיתוח למחרוזת

אמנם לא מבצעים החלפות בתוך מחרוזות אבל יש אפשרות לפתח פרמטר של מקרו להיות בתוך מחרוזת.

דוג' -  הדפסת debug לביטוים מטיפוס double:

#define dprint(expr) printf(#expr " = %g\n", expr)

#define sprint(s) printf("expression is: %s\n", #s)

 

dprint(x/y);

==> printf("x/y" " = %g\n", x/y); /* remember string concatination */

     בכדי שהארגומנט יתפתח למחרוזת בצורה תקינה כל " מוחלף ב \"   וכל  \  מוחלף ב \\.

sprint("abcd" \n)

==>printf("expression is: %s\n", "\"abcd\"\\n")

 

חזרה להתחלה

האופרטור ## של הקדם מעבד - שירשור tokens

שיטת החלפת ה tokens

     בכל פעם שנתקלים בשורה מהסוג #define name....  מכניסים את name לטבלא וזוכרים שאם נתקל ב token כזה נצטרך להחליפו בטכסט מסוים (עם או בלי ארגומנטים).

     את הטכסט סורקים מהתחלה ואם בזמן הסריקה מגלים token שמוגדרת לו החלפה מחליפים את ה token בטכסט המתאים לו וממשיכים בסריקת הטכסט החדש (ממקום ההחלפה). כך ממשיכים עד שלא נותר מה להחליף. אפשר לראות את שיטת ההחלפה כהחלפה לעומק - נחליף  באותו מקום עד שלא יישאר מה להחליף ורק אז נתקדם להחליף את ה tokens הבאים.

     כאשר מפתחים מקרו עם פרמטרים (שאינו כולל ##):

     מפרידים את הפרמטרים (מופרדים ע"י פסיקים שאינם במחרוזת או סוגריים)

     לאחר שבודדו הפרמטרים מציבים במקום כל פרמטר את סדרת ה tokens המתפתחת מהפרמטר המתאים לו.

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

     אם לאחר ההחלפה קיבלנו שורה המתחילה עם # לא מפרשים אותה כשורת פקודה חדשה ל preprocessor.

 


האופרטור ## של הקדם מעבד מיועד לשירשור tokens. 

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

     מייד לאחר שמפרידים את הפרמטרים, מכניסים אותם למקומם (ללא פתוח לעומק), מצמידים כל שני tokens אשר ביניהם מופיע ## להיות לtoken אחד (מוחקים את האופרטור ## עם כל ה white-space שמסביבו). רק אחרי שלב זה (שלב יצירת tokens חדשים) סורקים את מה שמתקבל ומחליפים לעומק, כפי שתיארנו.

     שורת מקרו המכילה ## מוחלפת לרוחב  -   קודם מציבים פרמטרים לאחר מכן משרשרים tokens ורק אז מפתחים לעומק.

     ה tokens המתקבלים לאחר שלב ההצמדה חייבים להיות חוקיים (לפי חוקי ה tokens של C) אחרת התנהגות ה preprocessor אינה מוגדרת.

     אם תוצאת ההצמדה תלויה בסדר ההפעלה של האופרטורים ## -  התנהגות ה preprocessor אינה מוגדרת.

     אסור שהאופרטור ## יופיע בהתחלה או בסוף ה replacment-text.

דוג'

#define cat(x, y) x ## y

 

cat(var, 123)

==> var123

 

cat(cat(1, 2), 3)

==> cat ( 1 , 2 )3 /* error */

 

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

#define xcat(x, y) cat(x, y)

xcat(xcat(1,2),3)

     xcat(1,2) ==> cat(1,2) ==> 1 ## 2 ==> 12

==> cat(12 ,3)

==> 12 ## 3

==> 123

 

 הסיבה לכך שהבעיה נפתרה  היא שהמקרו xcat לא מכיל ##. לכן הארגומנט הראשון שלו פותח לעומק בנגוד ל cat.

 

#define Q(x, y, z) x ## y ## z

 

void main()

{

    int Q(Avi, Yosi, Moshe) = 5;

    int Q(Yosi, Avi, Moshe) = 9;

    int Q(Avi, Moshe, Yosi) = 19;

 

    cout << Q(Avi, Yosi, Moshe);

    cout << Q(Yosi, Avi, Moshe);

    cout << Q(Avi, Moshe, Yosi);

}

 

void main()

{

    int AviYosiMoshe = 5;

    int YosiAviMoshe = 9;

    int AviMosheYosi = 19;

 

    cout << AviYosiMoshe;

    cout << YosiAviMoshe;

    cout << AviMosheYosi;

}

OUTPUT: 5 9 19

 

 


שירשור של ארגומנטים.    ארגומנט יכול להופיע מספר פעמים.    אסור שתהיה חשיבות לסדר הצמדת ה tokens. 

הכללת קוד על תנאי  Conditional Inclusion

אפשר לשלוט בפעולת ה preprocessor  ע"י conditional statements  - תנאים המתפתחים ומחושבים בזמן קומפילציה.  בשיטה זו ניתן להכליל או להשמיט קוד במידה ותנאי מתקיים בזמן קומפילציה.

פקודת #if  מחשבת ערך של  קבוע integer. אם הערך שונה מ 0 כל הקוד מהשורה הבאה ועד : #elif או#else  או  #endifיכללו בקובץ.

 

     בביטוי מותר לכלול כל אופרטור של C הפועל על מספרים שלמים

 

     בביטוי ה integer אסור לכלול: sizeof casts  או

enum-constants.

 

#if integer expr0

   ....

#elif integer expr1

   ....

#elif integer expr2

   ....

#else integer expr3

   ....

#endif

 

אפשר לשלב את כל האפשרויות של if, else, else-if . כל תנאי מסתיים תמיד ה #endif.

#if iexpr   #if iexpr    #if iexpr

   ....       ...         ...

#endif      #else        #elif

              ...         ...

            #endif       #endif

 

     הביטוי defined(name)  יהיה 1 אם name הוגדר קודם לכו ע"י #define ו 0 אחרת.

דוג' שיטה להתגונן מפני הכללה של קובץ יותר מפעם אחת:

#if !defined(HDR)

#define HDR

 

   /* content of hdr.h go here */

 

#endif

כאשר עושים #include לקובץ hdr.h בפעם הראשונה מגדירים את ה token HDR.  אם נבצע #include נוסף נקבל שהתנאי של ה #if לא מתקיים ולא כוללים את תוכן הקובץ hdr.h. 

שיטה זו שימושית במיוחד אם בתוך כל  קובץ H מבצעים #include  לכל קבצי ה H שהקובץ זקוק להם. במקרה כזה סביר שיכלל אותו קובץ מספר פעמים. אם נשתמש במנגנון שתואר בצורה עקבית (בכל קבצי ה H ) לא נכלול אותו קובץ יותר מפעם  אחת.

 

בדוגמא הבאה בוחרים header-file בהתאם לסוג מערכת ההפעלהשאנו משתמשים בה:

#if SYSTEM == MSDOS

    #define HDR "msdos.h"

#elif SYSTEM == UNIX

    #define HDR "unix.h"

#elif SYSTEM == SYSV

    #define HDR "sysv.h"

#else

    #define HDR "default.h"

#endif

#include HDR

בגלל השימוש הרב שיש ב if defined   יצרו שני קיצורים:  #ifdef  #ifndef  לכן ההגנה בפני הכללה מרובה יכולה להכתב כך:

#ifndef HDR

#define HDR

 

   /* content of hdr.h go here */

#endif

אופציות נוספות:

     #line    - מגדיר לקומפילר מספר שורה למטרות טיפול בשגיאות.

     #error tokens -   השורה תגרום ל preprocessor להוציא הודעת שגיאה: tokens.

     #pragma tokens - משמש למתן הוראות לקומפילר.

     שורה מהצורה # - מתעלמים ממנה.

     שמות מוגדרים מראש:  __LINE__  (מספר שורה) ,  __FILE__  (שם הקובץ) ,   __DATE__  (תאריך),  __TIME__  (השעה בזמן בצוע הקומפילציה).

 

חזרה להתחלה