חלק זה הוא המשך לחלק הראשון במדריך-מאמר שלי. החלקים כמעט נפרדים לחלוטין ולקרוא את האחד אינו מחייב לקרוא את האחר.
והפעם… קיים בקוד אך לא בתוכנית? איך זה ייתכן? הרי ידוע כי הקומפיילר מוסיף קוד מאחורי הקלעים, אך האם הוא באמת מוחק קוד? מה באמת קורה שם? כל זאת בטריקים לקומפיילר – חלק ב'.

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

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

לפני שאתחיל, אציין כי אני משתמש במערכת Windows עם הקומפיילר Microsoft Visual Studio 2008 וייתכן כי אומר דברים שתקפים למערכת זאת או לקומפיילר זה בלבד. יש לקחת זאת בחשבון.
בנוסף, בעוד שהמדריך הקודם התמקד ב-C המדריך הזה יתמקד יותר ב-C++.

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

class Class
{
public:
int GetValue() { return Value; }
private:
int Value;
};

נוכל לוותר על כל הפעולות שצוינו ממקודם:

class Class
{
public:
inline int GetValue() { return Value; }
private:
int Value;
};

וכך בעצם ליצור גישה ישירה וחוקית (כמו get-accessor ב-C#) למשתנה שהוא פרטי בצורה בטוחה מכיוון ולא ניתן לשנות אותו ישירות בצורה זו. דבר זה יעיל מכיוון ואורך הקוד לא יגדל ואולי אף יקטן כיוון ונחסוך יותר פעולות ממה שנבצע בצורה כזו. בהנחה ונרצה לעשות אותו הדבר עבור פונקציה Set כאשר שם יש סינוני קלטים אנו עלולים להגדיל את אורך הקוד עבור כל שימוש ב-Set, וזאת מכיוון שיש יותר פעולות לבצע ממה שחסכנו, ולכן במקום קריאה בודדת לקטע הקוד נבצע את אותו קטע הקוד מספר פעמים, ולכן בעצם נעדיף שלא להגדיר פונקציות כאלו כ-inline.
הערה: כאשר אתם משתמשים באחד הפרמטרים, במידה והוא הועבר על ידי ערך (By Value), הקומפיילר כן ייצור העתק ולא ישתמש בפרמטר עצמו, וזאת כיוון שעתק הועבר והמתכנת מצפה כי ההעתק ישתנה ולא הפרמטר הראשי. המנעו מכך בפונקציות inline ותחסכו העתקות במידה ואין צורך.
בהקשר זה, ארצה להסביר מעט על הגדרותיו המגוונות של הקומפיילר של מייקרוסופט, לפחות אלו שחשובות לנו כרגע במאמר הזה.

הגדרות לקומפיילר:

לחצו Alt+F7 (או לחלופין, Tools -> Options) ותגיעו להגדרות הפרוייקט שלכם. אנו נתמקד בעיקר ב-General, Debugging, C++ & Linker Options.
General:
בעיקר הגדרות של שמות הקבצים שהקומפיילר ייצור. $(SolutionDir) זה המיקום המלא של התיקייה שבה נמצא ה-Solution ו-$(ConfigurationName) זה שם התוכנית. $(IntDir) זו התיקייה שבה נמצאים הקבצים, Debug או Release.
לרוב נשאיר כרגיל, אלא אם בטעות שינינו שם או פתחנו פרוייקט מהסוג הלא נכון או שנרצה להחליף ואז נשנה את Configuration Type ב-EXE/DLL/LIB/Makefile.
הגדרה חשובה אחת שהיא קריטית עבור מתכנת מתכנת מתחיל ב-Win32 APIs והיא הבעיה של ערכת התווים – אם להשתמש ב-ASCII או Unicode בשביל מחרוזות ובשביל פונקציות. הפונקציות שנגמרות ב-A בסוף משתמשות ב-ASCII בעוד שפונקציות שנגמרות ב-W משתמשות ב-Wide-Chars. (שתווים בגודל 2 בתים – Unicode)
מן הסתם אנחנו לא רוצים קוד של C++.NET – שפה שננטשה, אנחנו נבחר ב-No Common Language Runtime support.
Debugging:
אפשרויות דיבאגר. Command – פקודת פתיחה, Command Arguments – העברת פרמטרים וכה, אנגלית בסיסית.
C++:
לרוב אני לא נוגע באפשרויות במצב Debug, כעת אסביר על הגדרות ליצירת קובץ מתאים לשיתוף, וזאת על ידי ביטול כל הגדרות הדיבאגינג, שילוב הפונקציות מקבצים חיצוניים בתוך הקובץ הרצה, ניפוי אזהרות, אופטימיזציות וכה.

  • General – כללי:
    • Debug Information Format: Disabled. נבטל גם אפשרות ל-Debug Information בכלל, פשוט להמנע מאזהרות של שילוב הגדרות לא חוקי.
    • Warning Level: Level 4. רצוי להעלות את רמת האזהרות למקסימלית, לוודא כי לא ביצענו שום דבר לא חוקי או לא תקין, או אולי המרות עם איבוד מידע פוטנציאלי וכה. במידה ותקבלו אזהרות – תקנו אותם!
    • Treat Warnings As Errors: Yes. אני אוהב להכריח את עצמי לתקן את האזהרות. 🙂
  • Optimization – אופטימיזציות:
    • Optimization כרצונכם. מומלץ Full Optimization, אך לעיתים נרצה אופטימיזציות מהירות או גודל בלבד. (בחירה באחת מהאופטימיזציות עשוייה לאפשר לנו אופטימיזציות טובות יותר. לדוגמה, נסתכן בשימוש רב של פקודות מהירות או נקריב זמן ריצה עבור פקודות שלוקחות מעט בתים. לרוב, לא קריטי)
    • Inline Function Expantion כרצונכם. מומלץ Any Suitable. לעיתים נרצה שאך ורק מה שבחרנו להגדיר כ-inline יוגדר כ-inline ובמקרה כזה נבחר Only __inline. (לדוגמה, במקרה של המאמר הקודם – ב-Hooks לעיתים נרצה שהכל יעבוד כפי שתכננו, ללא אופטימיזציות מינוריות שעלולות להרוס לנו את הפעולות המצופות מהתוכנה)
    • Favor Size or Speed כרצונכם. בחרו בהתאם ל-Optimization. לדוגמה, עבור כלי ביתי הייתי רוצה שהתוצאה תהיה קטנה ככל האפשר בעוד שעבור אתגרים כגון Project Euler ירוצו מהר ככל האפשר.
  • Preprocessor, שעליו נלמד בהמשך המאמר. ניתן להגדיר הגדרות כלליות עבור הקדם-מעבד דרך הגדרות הפרוייקט. אישית, אני מעדיף להגדיר אותן ידנית בקוד, לחסוך מאנשים את הפעולה של לשנות את ההגדרות ידנית בעצמם (או לחלופין שליחה קובץ נוסף של ההגדרות) וכמו-כן שליטה יותר מלאה על הגדרות הקדם-מעבד. (הגדרה וביטול כרצוננו)
  • Code Generation – יצירת קוד:
    • Enable C++ Exceptions – לעיתים נרצה להשתמש בחריגות ב-C++. אישית, אני לא משתמש רבות בחריגות אך לעיתים כן צריך.
    • Runtime Library: Multi-threaded. אפשרות זו אמנם תגדיל במקצת את הקובץ הסופי אך במספר בתים ספורים. כל אחת מן האפשרויות האלו מגדירות את ה-Runtime Library שברצוננו להשתמש בו, ונרצה להגדיר כזה שלא ידרוש מאיתנו קבצים נוספים (DLLs) ולא קשור ל-Debugging. מידע נוסף כאן וכאן. השתמשו בהתאם, כנ"ל לגבי Debug mode. (ברירת מחדל Multi-threaded Debug DLL)
    • Struct Member Alignment בהתאם לתוכנית (ברירת מחדל: Default – בהתאם למערכת). דבר חשוב שנלמד בהמשך המאמר וראינו הצצה לגביו בחלק א'. במקרים רבים מאד, הרבה מעבר ל-Hooks, בכל תוכנית שלנו נרצה לקבוע יישור קבוע מראש עבור מבנים. אישית, כמו ב-Preprocessor, אעדיף לקבוע זאת בעזרת קוד מאותן סיבות. ראו בהמשך.
    • Enable Enhanced Instruction Set בהתאם לתוכנית. שימוש ב-SSE.
    • Floating-point Precision בהתאם. במידה ויש לנו תוכנית שאינה צריכה דיוק רב, נוכל להשתמש ב-Fast בשביל מהירות או Precise לדיוק. יכול להיות שימושי, במידה ואיננו כותבים פתרונות ל-Project Euler לדוגמה.
    • Enable Floating Point Exceptions כרצונכם. יכול להיות שימושי במקרים מסויימים. (זה למאמר אחר 😉 )
  • Precompiled Headers:
    • Create/Use Precompiled Headers כרצונכם. אישית, לא יצא לי להשתמש בזה רבות. במידה ואצטרך להגדיר משהו מראש, אגדיר אותו בקובץ הראשי או בהגדרות הקומפיילר. לעומת זאת, כן יכול להיות שימושי בפרוייקטים יותר גדולים. עבור מתחילים הייתי ממליץ להתעלם מהם, לחסוך בקבצים ולא להשתמש ב-Precompiled Headers ביצירת הפרוייקט.
  • Advanced – מתקדם:
    • Calling Convention בהתאם לתוכנית. כפי שנאמר במאמר הקודם, ברירת המחדל היא cdecl. למידע נוסף, ראה חלק א' של המאמר.
    • Compile As בהתאם לתוכנית. ניתן גם לקמפל קודי C בקומפיילר. (אפשרות זו משתנה בהתאם לשם הקובץ, אלא אם הוגדר אחרת)
    • Disable Specific Warning כרצונכם. לעיתים נרצה לבטל אזהרות כלשהן במקרים ואנו מודעים לאזהרה ואומרים לקומפיילר שזה בסדר. כרגיל, מעדיף לעשות זאת בקוד מאותן הסיבות כמו מקודם.
    • Force Includes – רשימת קבצים להכליל בפרוייקט שלנו כברירת מחדל.
    • Undefine Preprocessor Definitions כרצונכם. כרגיל, מעדיף לעשות זאת בקוד.

Linker:

  • Input – קלט:
    • בעיקר קבצים לשימוש ביצירת התוכנית. ניתן להוסיף ספריות לשימוש, רשימת התעלמות מהן וכו'. שוב, מעדיף בעזרת קוד מאותן סיבות.
  • Debugging – ניפוי שגיאות:
    • Generate Debug Info: No. כשאנחנו רוצים לכתוב תוכנית לשיתוף, אנחנו לא רוצים שיהיה מידע לגבי איך לדאבג את הקובץ. 😉 זה כמו לכתוב בדף אינטרנט את חורי האבטחה שבו. 🙂 אחרת, כן נייצר כל מה שקשור ל-Debugging, כולל הגדרות קודמות ששינינו.
  • System – מערכת:
    • SubSystem בהתאם לתוכנית. במידה וכתבנו קונסול, חלון או כל דבר אחר.
    • שאר ההגדרות שיש כאן נדבר עליהן בהמשך המאמר. בעיקר הגדרות לגבי הקובץ כמו גודל המחסנית, ערימה וגודל שמור עבור המחסנית וערימה במקרה שנגמר הזכרון. שימושי מאד, במקרים בהם נרצה להקצות מערך גדול במחסנית ולא יהיה לנו מספיק מקום. (נעשה זאת בשביל הקצאה מהירה של הזכרון ואי-הצורך לדאוג למחיקת המערך שגם תקח זמן. יעיל כאשר קוראים לפונקציה הזאת מספר פעמים רב)
  • Optimizations – אופטימיזציות:
    • References: Eliminate Unreferenced Data. ברצוננו למחוק כל מידע שלא ביצענו הפניות אליו.
    • Enable COMDAT Folding: Remove Redundant COMDATs. מחק כל מידע שלא שימש אותנו.
  • Advanced – מתקדם:
    • כאן אנו יכולים לציין את נקודת הכניסה, אם קיימת כזו (נרצה לבטל את אפשרות לנקודת הכניסה למשל בספריות שלא נרצה בהן כזו), מידע לגבי כתובת הבסיס, ספריות לייבוא ועוד אפשרויות רבות שישנו את תוכן ה-Header של הקובץ שלנו.

שימו לב שהגדרות שונות ייתנו תוצאות שונות. בעוד שחלק ייגרמו לפונקציות להראות בצורה מסויימת ואחרות ייגרמו לפונקציות להיות inline כאשר חלק יוסיפו זבל שלא בהכרח נחוץ.
כעת כשאנחנו מוכנים עם הגדרות טובות לשיתוף הפרוייקט והקומפיילר יבצע אופטימיזציות שלא יכלו לחלום עליהן לפני שנים ספורות, נדבר קצת על שינוי ההגדרות בקוד. במידה ומשהו לא מובן או לא מוכר לכם, תוכלו לחפש כאן ולמצוא מידע מפורט.
כפי שכבר אמור להיות לכם ידוע, הסימן # ב-C/C++ אומר ל-Preprocessor לבצע פעולה כלשהי, בינהן include, define, pragma ועוד רבים מאד.
נתחיל ב-pragma pack ליישור מבנים, הרחבה לאותו החלק במאמר הקודם.

קדם-מעבד – Preprocessor:

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

למידע נוסף, Preprocessor Reference.

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

#define N 50

(עבור הגדרות קדם-מעבד אין צורך בנקודה-פסיק. כמו ב-include# לדוגמה)

ובכל מקום בתוכנית נוכל להשתמש בשם, N, ובמידה ונרצה לשנות את הערך נצטרך לשנות רק את ערכו של N ולא כל הופעה שלו בתוכנית.

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

נקח לדוגמה את ההגדרה הבאה:

#define DEF    "Hello, World!\n");

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

#include <stdio.h>
#define DEF    "Hello, World!\n");

int main()
{
 printf(DEF
 getchar();
 return 0;
}

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

#include <stdio.h>

int main()
{
 printf("Hello, World!\n");
 getchar();
 return 0;
}

(שימו לב כי הנקודה-פסיק היא חלק מהתוכן של השם, והיא מתווספת גם-כן)

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

#define print(x) printf(x)

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

#define PrintError() printf("Error: %u\n", GetLastError())

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

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

#define MAX(a, b) (a > b ? a : b)

int Max = MAX(5, 8);

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

#define DOUBLE(x) x+x

במידה והיינו משתמשים בקוד בצורה כזו:

int Result = DOUBLE(5) * 2;

במקום לקבל את התוצאה המתבקשת, x כפול 2 כפול 2, כלומר 4x, נקבל 3x, וזאת מכיוון שסדר הפעולות חשבון הוא כפל לפני חיבור, והקוד הוא למעשה כזה:

int Result = x + x * 2;

ולכן יש לתחום כל קוד שכזה בסוגריים, כך:

#define DOUBLE(x) (x + x)

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

if (n > 5)
{

#ifdef _DEBUG

printf("Error at file %s at line %u\n", __FILE__, __LINE__);

#endif

exit(0);
}

בקוד הזה בעצם יצרתי Error Log שנגיש רק בגרסת Debug על ידי שימוש בשם DEBUG_ שהקומפיילר מגדיר. בנוסף, ניתן לבצע תנאים מורכבים יותר על סמך אותו העקרון:

#if defined(_DEBUG) && !defined(_UNICODE)
 #define TEXT "ASCII string for debug mode"
#elif !defined(_DEBUG) && defined(_UNICODE)
 #define TEXT L"Unicode string for release mode"
#else
 #error Must choose either ASCII-Debug or Unicode-Release!
#endif

במידה ולא נגדיר סט תווים של ASCII ב-Debug וגם לא נגדיר סט תווים Unicode ב-Release נקבל שגיאה "Must choose either ASCII-Debug or Unicode-Release!".

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

#define PRINT(x) printf("Hello, World!")x

פרמטר אחד עבורו לא נקבל שגיאה יכול להיות נקודה-פסיק, לדוגמה:

PRINT(;)

ואז למעשה ביצענו:

printf("Hello, World!");

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

#define PRINT(x) printf("%s", #x)x

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

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

#include <stdio.h>

typedef unsigned char	BYTE;
typedef unsigned short	WORD;

#pragma pack(1)
typedef union
{
	struct
	{
		BYTE Carry:							1;
		BYTE Reserved1:						1;
		BYTE Parity:						1;
		BYTE Reserved2:						1;
		BYTE Adjust:						1;
		BYTE Reserved3:						1;
		BYTE Zero:							1;
		BYTE Sign:							1;
		BYTE Trap:							1;
		BYTE Interrupt:						1;
		BYTE Direction:						1;
		BYTE Overflow:						1;
		BYTE IOPL:							2;
		BYTE Nested:						1;
		BYTE Reserved4:						1;
	} Flag;
	WORD Register;
} Flags;
#pragma pack()

__declspec(naked) Flags GetFlags()
{
	__asm
	{
		PUSHFD
		POP EAX
		RETN
	}
}

__declspec(naked) void __stdcall SetFlags(Flags f)
{
	__asm
	{
		POP EAX
		POPFD
		JMP EAX
	}
}

#define PrintFlag(x) printf("%s: %s\n", #x, (f.Flag.x ? "1 (On)" : "0 (Off)"))

int main()
{
	Flags f = GetFlags();

	printf("Flags Register: 0x%08X\n", f.Register);
	PrintFlag(Carry);
	PrintFlag(Parity);
	PrintFlag(Adjust);
	PrintFlag(Zero);
	PrintFlag(Sign);
	PrintFlag(Trap);
	PrintFlag(Direction);
	PrintFlag(Overflow);

	getchar();

	return 0;
}

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

#ifdef _UNCIODE
    #define MessageBox(hWnd, lpText, lpCaption, uType) MessageBoxW(hWnd, lpText, lpCaption, uType)
#else
<pre>    #define MessageBox(hWnd, lpText, lpCaption, uType) MessageBoxA(hWnd, lpText, lpCaption, uType)
#endif

כעת משהגדרנו פונקציות עוטפות שישתנו בהתאם להגדרות קומפילציה (ASCII או Unicode) עבור הפונקציות המקוריות, נגדיר גם פונקציית מאקרו שתגדיר טקסט כ-ASCII או Unicode בהתאם, ללא צורך בשינויים בקוד:

#ifdef _UNICODE
    #define TEXT(x) L##x
#else
    #define TEXT(x) x
#endif

האות L לפני מחרוזת אומרת לקומפיילר שזוהי מחרוזת Unicode ולא ASCII. אחרת, ברירת המחדל היא מחרוזת ASCII.
כעת נוכל לכתוב תוכניות שמתאימות גם ל-ASCII וגם ל-Unicode. למעשה, בצורה זו ניתן גם לכתוב תוכניות Cross-Platform – למספר גרסאות שונות של מערכות הפעלה, מערכות הפעלה שונות ואף למעבדים שונים במידת הצורך.

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

#include <stdio.h>

#define FOREVER(Code) \
 for (;;) \
 { \
 Code \
 }

int main()
{
 FOREVER(
 printf("Hello!\n");
 )

 getchar();

 return 0;
}

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

#include <stdio.h>

#define SUM(Array, Length, Condition, Result) \
 Result = 0; \
 for (int __i = 0; __i < Length; __i++) \
 { \
 if (Condition) \
 Result += Array[__i]; \
 }

int main()
{
 int Sum;
 int Array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
 SUM(Array, sizeof(Array) / sizeof(int), Array[__i] % 2 == 0, Sum);
 printf("Even values sum: %d\n", Sum); //2 + 4 + 6 + 8 + 10 = 30
 SUM(Array, sizeof(Array) / sizeof(int), __i % 2 == 0, Sum);
 printf("Even indecies sum: %d\n", Sum); //1 + 3 + 5 + 7 + 9 = 25

 /**********************
 Even values sum: 30
 Even indecies sum: 25
 **********************/

 getchar();

 return 0;
}

השם SUM מכיל קוד שסוכם איברים בתנאי מסויים שהועבר כפרמטר. את המשתנה i__ של הלולאה הגדרתי עם "__" בשם מכיוון וייתכן ומשתנה בשם i כבר קיים, אך סביר לניח שלא קיים משתנה בשם "i__" ולכן נהוג לכתוב שמות משתנים כך. אמנם התחביר אינו הכי נוח למתכנת אך כאשר ברור מה הקוד עושה או אם הקוד אינו שלך וקיימות הערות מובנות בקוד, לא אמורה להיות בעיה. הרי כשחושבים על זה, ללא תיעוד לא היינו מסוגלים לכתוב אפילו תוכניות Hello World בשל חוסר עקביות ולכן צריך להיות עקביים ומתואמים.
ובפעם הבאה – נלמד יותר על הגדרות לקומפיילר, פקודות קדם-מעבד, אופטימיזציות, טריקים והאקים נוספים. 😉
וכעת, מעט על מבנים.

יישור מבנים:

תחילה – מהו יישור? למה זה נחוץ?
יישור (Alignment) קובע את גודלם של מופעים שיווצרו ממבנה או מחלקה.
נקח לדוגמה את המחלקה הבאה:

#include <stdio.h>

class Pack
{
 int iPack; //sizeof(int) = 4 bytes
 char cPack; //sizeof(char) = 1 byte
};

int main()
{
 printf("sizeof(Pack): %d\n", sizeof(Pack));

 getchar();

 return 0;
}

המכילה 4 בתים ל-int iPack ובית נוסף עבור char cPack. הגודל הצפוי הוא 5 בתים, אך למעשה (כאשר Structure Alignment מוגדר ל-4) הגודל הוא 8, וזה אכן מה שיתקבל כפלט.
על מנת לבטל את היישור (שניתן לתיאור על ידי המספר הקרוב ביותר כלפי מעלה שמתחלק ב-x כאשר x הוא ה-Structure Alignment שהוגדר בהגדרות) ניתן להשתמש בהגדרות או לחלופין בעזרת קוד – pragma directive.

#include <stdio.h>

#pragma pack(1)
class Pack
{
 int iPack; //sizeof(int) = 4 bytes
 char cPack; //sizeof(char) = 1 byte
};

int main()
{
 printf("sizeof(Pack): %d\n", sizeof(Pack));

 getchar();

 return 0;
}

כעת נקבל את הפלט המתבקש, 5.
אבל מדוע מתבצע יישור שכזה? מטעמי יעילות ונוחות גישה לחברי המבנה. למעבד 32-ביט, לדוגמה, הכי קל לגשת ולהשתמש באוגרי 32-ביט או בערכי 32-ביט בכלל. לגשת לבית או מילה ייקח יותר זמן וגם יותר מקום מכיוון והפקודות 32-ביט הן בעדיפות ראשונה בעוד שפקודות למילה (16-ביט) הן בעדיפות אחרונה, ולכן יספקו להן ייצוג שונה וארוך יותר מאשר אחרות.
לעומת זאת, אפשר לנצל גדלים שונים של מבנים לטובתנו, כאלו שלא מיושרים בכפולות של 4. נניח ונרצה לכתוב פונקציה שתשנה את הערך Value במבנה הבא:

struct Structure
{
 BYTE Boolean;
 WORD Value;
};

בהנחה ונבצע יישור הקומפיילר ישנה את אורכו של Boolean ל-WORD כאשר הבית הגבוה (High-Word) בלתי נגיש ישירות, וזאת מכיוון ועל כל פעולה תבוצע פעולת AND על המשתנה להשגת ערכו של הבית הראשון.

כלומר, נניח ונרצה לשנות מבנה מסוג זה בזכרון משחק כלשהו, אנחנו בעצם נקבל ערכים לא מדוייקים מכיוון והכתובות מיושרות ואינן מייצגות את הכתובות כמו שהן באמת במשחק. כלומר, בהנחה ונציב ב-Value את הערך 0x1234 נקבל שכל המבנה בזכרון (4 בתים) יכיל 0x1234CCCC מכיוון ובוצע יישור, כאשר הבתים 0xCC מייצגים ערך שלא אותחל והבית המסומן שייך ל-Boolean. מהבית השני מתעלמים. בהנחה ונציב 0x11 ב-Boolean נקבל את הערך 0x1234CC11 וכאשר נציב את הערך 0x1111 נקבל את אותו הערך ולמעשה אפילו לא נקבל אזהרה לגבי איבוד מידע פוטנציאל, וזאת מכיוון ו-Boolean הוא למעשה WORD.

לכן, בזכרון המשחק, המבנה יכיל ב-Boolean את הערך 0xCC כרגיל אך המשחק יתעלם מן הערך 0x12 וערכו של Value יהיה 0x34CC. על מנת לתקן, נאמר לקומפיילר שלא לבצע יישור (או יותר מדוייק, יישור לבית אחד) בעזרת הוראת הקדם-מעבד, pragma pack, בצורה כזו:

#pragma pack(1)
struct Structure
{
 BYTE Boolean;
 WORD Value;
};
#pragma pack()

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

בהנחה ונציב את אותם הערכים, 0x1234 ב-Value ואת 0x1111 ב-Boolean נקבל את הערך (עבור כל המבנה) 0xCC123411 וכמו-כן נקבל אזהרה לגבי איבוד מידע פוטנציאלי מכיוון ו-Boolean הוא BYTE ואינו יכול להכיל את הערך 0x1111, אלא רק את הבית הנמוך (המסומן).

ומה לגבי הבית 0xCC שלמרות שהיישור היה 1 הוא נשאר? כפי שכבר אמרתי, יותר קל לגשת לערכי 32-ביט ולכן כל משתנה הוא למעשה ביישור של 4 בתים (32-ביט), אך כאשר רמת היישור אינה 4 יתווספו בתים ליישור בסוף המבנה ולא עבור כל משתנה שדורש זאת.

לסיכום:
השתמשו ב-inline עבור פונקציות קצרות. אל תשתמשו ב-inline עבור פונקציות ארוכות. אל תשנו את ערכי הפרמטרים בפונקציות inline.

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

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

ובפעם הבאה…

עוד לגבי הגדרות הקומפיילר – יותר בהרחבה.

פקודות קדם-מעבד נוספות ושינוי מבנה התוכנית.

שימוש בתבניות (templates) בתכנות מונחה עצמים.

הרחבה על המתרחש מאחורי הקלעים והתוצר הסופי.

ועוד…