C Language Essentials & Preprocessing

מבחנים

120 שאלות מעקב קוד מעשיות ביסודות שפת C ופרה-מעבד – קראו קוד וחזו את הפלט. השאלות מכסות מלכודות #define, enum, struct, union, switch fall-through, אריתמטיקה על char/ASCII ופעולות bitwise. מבוסס על סגנון מיון סייבר כהכנה ליחידה 8200 ולתוכנית גאמא סייבר. רמות קושי: קלה, בינונית, קשה.

  • enum
  • struct
  • union
  • switch
  • #define
  • #ifdef / #undef
  • # (stringify)
  • ## (token paste)
  • char / ASCII
  • bitwise ops
  • endianness
  • atoi
טיפ: קראו את המדריך המלא למטה לפני שמתחילים – שימו לב במיוחד למלכודות סוגריים ב-#define ולכללי fall-through ב-switch.

בהצלחה!

enum – ערכי ברירת מחדל, פערים וחשבון

ב-enum ברירת המחדל מתחילה מ-0 ועולה ב-1. אם מציבים ערך מפורש, הערכים הבאים ממשיכים ממנו. במבחנים, שאלות enum דורשות לעקוב אחרי הערך המספרי של כל איבר.

דוגמה 1: ערכי ברירת מחדל

#include <stdio.h>

enum Color { RED, GREEN, BLUE };

int main() {
    printf("%d %d %d\n", RED, GREEN, BLUE);
    return 0;
}
// פלט: 0 1 2

דוגמה 2: ערכים מפורשים ופערים

#include <stdio.h>

enum Rank { A = 5, B, C = 20, D, E };

int main() {
    printf("A=%d B=%d C=%d D=%d E=%d\n", A, B, C, D, E);
    return 0;
}
// פלט: A=5 B=6 C=20 D=21 E=22
// B ממשיך מ-A (5+1=6), D ממשיך מ-C (20+1=21)

דוגמה 3: חישוב אריתמטי עם enum

#include <stdio.h>

enum Vals { X = 3, Y = 7, Z };

int main() {
    int result = X + Y + Z;
    printf("%d\n", result);
    return 0;
}
// Z = 7+1 = 8
// result = 3 + 7 + 8 = 18
// פלט: 18

דוגמה 4: enum עם ערך 0 מפורש

#include <stdio.h>

enum Level { LOW = 0, MED = 0, HIGH };

int main() {
    printf("%d %d %d\n", LOW, MED, HIGH);
    printf("%d\n", LOW == MED);
    return 0;
}
// פלט:
// 0 0 1
// 1
// שני איברים יכולים לחלוק את אותו ערך!
// HIGH = MED+1 = 0+1 = 1

טבלה: כללי ערכים ב-enum

הגדרה ערכים הסבר
{ A, B, C } 0, 1, 2 ברירת מחדל – מתחיל מ-0
{ A=5, B, C } 5, 6, 7 B ו-C ממשיכים מ-5
{ A, B=10, C } 0, 10, 11 A=0 (ברירת מחדל), C ממשיך מ-10
{ A=3, B=3, C } 3, 3, 4 ערכים כפולים מותרים
{ A=1, B=5, C=2, D } 1, 5, 2, 3 D ממשיך מ-C (2+1=3) – לא חייב סדר עולה
{ A=-2, B, C } -2, -1, 0 ערכים שליליים מותרים
כלל מפתח: כל איבר שלא קיבל ערך מפורש = ערך האיבר הקודם + 1. תמיד ספרו מהאיבר המפורש האחרון.

struct – אתחול, גישה, sizeof ו-packed

דוגמה 1: אתחול וגישה

#include <stdio.h>

struct Point {
    int x;
    int y;
};

int main() {
    struct Point p = {10, 20};
    p.x += 5;
    printf("%d %d\n", p.x, p.y);
    return 0;
}
// פלט: 15 20

דוגמה 2: sizeof עם padding

#include <stdio.h>

struct A {
    char  c;   // 1 byte + 3 bytes padding
    int   i;   // 4 bytes
    char  d;   // 1 byte + 3 bytes padding
};

struct B {
    int   i;   // 4 bytes
    char  c;   // 1 byte
    char  d;   // 1 byte + 2 bytes padding
};

int main() {
    printf("sizeof(A) = %lu\n", sizeof(struct A));
    printf("sizeof(B) = %lu\n", sizeof(struct B));
    return 0;
}
// פלט:
// sizeof(A) = 12    (1+3+4+1+3)
// sizeof(B) = 8     (4+1+1+2)
// סדר השדות משפיע על הגודל!

דוגמה 3: __attribute__((packed))

#include <stdio.h>

struct __attribute__((__packed__)) Packed {
    char  c;   // 1 byte
    int   i;   // 4 bytes
    char  d;   // 1 byte
};

int main() {
    printf("sizeof(Packed) = %lu\n", sizeof(struct Packed));
    return 0;
}
// פלט: sizeof(Packed) = 6    (1+4+1, ללא padding)

דוגמה 4: struct מקונן

#include <stdio.h>

struct Inner {
    int a;
    int b;
};

struct Outer {
    struct Inner in;
    int c;
};

int main() {
    struct Outer o = { {1, 2}, 3 };
    printf("%d %d %d\n", o.in.a, o.in.b, o.c);
    return 0;
}
// פלט: 1 2 3

דוגמה 5: memcpy בין structs

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

struct Data {
    int x;
    int y;
};

int main() {
    struct Data a = {10, 20};
    struct Data b;
    memcpy(&b, &a, sizeof(struct Data));
    b.x = 99;
    printf("a: %d %d\n", a.x, a.y);
    printf("b: %d %d\n", b.x, b.y);
    return 0;
}
// פלט:
// a: 10 20   (a לא השתנה – memcpy יוצר העתק)
// b: 99 20

union – שיתוף זיכרון ובדיקת בתים

דוגמה 1: קריאה מאותו שדה (מוגדר)

#include <stdio.h>

union Val {
    int   i;
    float f;
    char  c;
};

int main() {
    union Val v;
    v.i = 65;
    printf("i=%d c=%c\n", v.i, v.c);
    return 0;
}
// פלט: i=65 c=A
// כל השדות חולקים את אותו זיכרון.
// v.c קורא את הבית הראשון של v.i, שהוא 65 = 'A'

דוגמה 2: קריאה משדה שונה (UB)

#include <stdio.h>

union Mix {
    float f;
    int   i;
};

int main() {
    union Mix m;
    m.f = 3.14f;
    printf("i=%d\n", m.i);
    return 0;
}
// פלט: i=1078523331  (ייצוג IEEE 754 של 3.14 כ-int)
// קריאת שדה שונה מהשדה שנכתב אליו – UB (התנהגות לא מוגדרת)
// אבל ברוב המימושים: מקבלים את הביטים כפי שהם

דוגמה 3: בדיקת בתים ברמה נמוכה

#include <stdio.h>

union ByteCheck {
    int            num;
    unsigned char  bytes[4];
};

int main() {
    union ByteCheck bc;
    bc.num = 0x41424344;
    printf("bytes: %c %c %c %c\n",
           bc.bytes[0], bc.bytes[1],
           bc.bytes[2], bc.bytes[3]);
    return 0;
}
// ב-Little-Endian (רוב המחשבים):
// bytes[0]=0x44='D', bytes[1]=0x43='C',
// bytes[2]=0x42='B', bytes[3]=0x41='A'
// פלט: bytes: D C B A
שאלת מבחן קלאסית: אם num = 0x41424344, מה bytes[0] ב-Little-Endian? תשובה: 0x44 = 'D'. הבית הנמוך (LSB) נשמר בכתובת הנמוכה ביותר.

Endianness – סדר בתים ב-Little-Endian

ב-Little-Endian (x86, כמעט כל מחשב PC), הבית הנמוך ביותר (LSB) נשמר בכתובת הנמוכה ביותר. זה הפוך מהסדר שאנחנו כותבים בקוד.

// המספר 0x41424344 בזיכרון (Little-Endian):
//
// כתובת:  [0]   [1]   [2]   [3]
// ערך:    0x44  0x43  0x42  0x41
// ASCII:   'D'   'C'   'B'   'A'
//
// שימו לב: 44 (הבית הנמוך) נמצא בכתובת [0]

דוגמה: זיהוי endianness עם union

#include <stdio.h>

union Endian {
    int            val;
    unsigned char  byte;
};

int main() {
    union Endian e;
    e.val = 1;  // 0x00000001
    if (e.byte == 1)
        printf("Little-Endian\n");
    else
        printf("Big-Endian\n");
    return 0;
}
// פלט (במחשב רגיל): Little-Endian
// כי הבית הראשון (byte) מקבל את ה-LSB שהוא 0x01

דוגמה: ערך hex עם bytes

#include <stdio.h>

union HexView {
    unsigned int   val;
    unsigned char  b[4];
};

int main() {
    union HexView h;
    h.val = 0xAABBCCDD;
    printf("%02X %02X %02X %02X\n",
           h.b[0], h.b[1], h.b[2], h.b[3]);
    return 0;
}
// פלט (Little-Endian): DD CC BB AA

switch – fall-through, קיבוץ cases ו-default

דוגמה 1: fall-through בלי break

#include <stdio.h>

int main() {
    int x = 2;
    switch (x) {
        case 1: printf("one ");
        case 2: printf("two ");
        case 3: printf("three ");
        default: printf("end");
    }
    printf("\n");
    return 0;
}
// פלט: two three end
// נכנס ב-case 2, ואז נופל דרך case 3 ו-default (אין break!)

דוגמה 2: קיבוץ cases

#include <stdio.h>

int main() {
    char grade = 'B';
    switch (grade) {
        case 'A':
        case 'B':
            printf("Excellent\n");
            break;
        case 'C':
            printf("Good\n");
            break;
        default:
            printf("Other\n");
    }
    return 0;
}
// פלט: Excellent
// 'B' נופל ל-case 'A' ושם רואים שאין break, אז ממשיך ל-case 'B'
// שניהם חולקים את אותו הקוד

דוגמה 3: fall-through חלקי

#include <stdio.h>

int main() {
    int n = 1;
    switch (n) {
        case 1: printf("A");
        case 2: printf("B"); break;
        case 3: printf("C");
        default: printf("D");
    }
    printf("\n");
    return 0;
}
// פלט: AB
// case 1 → הדפסת A, אין break → נופל ל-case 2 → הדפסת B → break!
// case 3 ו-default לא מגיעים

דוגמה 4: default באמצע

#include <stdio.h>

int main() {
    int x = 5;
    switch (x) {
        case 1: printf("1 "); break;
        default: printf("def ");
        case 2: printf("2 "); break;
        case 3: printf("3 ");
    }
    printf("\n");
    return 0;
}
// פלט: def 2 
// x=5 לא תואם אף case → נכנס ל-default → הדפסת "def "
// אין break ב-default → נופל ל-case 2 → הדפסת "2 " → break

טבלה: התנהגות fall-through ב-switch

מצב קוד מה מודפס (x=2)
עם break בכל case case 1: ..break; case 2: ..break; case 3: ..break; רק הפלט של case 2
בלי break בשום case case 1: .. case 2: .. case 3: .. default: .. פלט case 2 + case 3 + default
break רק ב-case 3 case 1: .. case 2: .. case 3: ..break; default: .. פלט case 2 + case 3 (ועצירה)
cases מקובצים (ללא קוד) case 1: case 2: case 3: ..break; הפלט המשותף (case 1-3 חולקים קוד)
x לא תואם + default בלי break default: .. case 5: ..break; default + פלט case 5

char/ASCII – אריתמטיקה על תווים

דוגמה 1: ערך מספרי של תו

#include <stdio.h>

int main() {
    char c = 'A';
    printf("%c %d\n", c, c);
    printf("%c %d\n", c + 1, c + 1);
    printf("%c %d\n", c + 25, c + 25);
    return 0;
}
// פלט:
// A 65
// B 66
// Z 90

דוגמה 2: הפרש בין תווים

#include <stdio.h>

int main() {
    char a = 'd';
    char b = 'a';
    printf("%d\n", a - b);

    char upper = a - 32;  // 'a'-'A' = 97-65 = 32
    printf("%c\n", upper);
    return 0;
}
// פלט:
// 3        ('d'=100, 'a'=97, 100-97=3)
// D        (100-32=68='D')

דוגמה 3: חיבור שני char

#include <stdio.h>

int main() {
    char x = '0';  // '0' = 48 (לא 0!)
    char y = 5;
    printf("%c\n", x + y);
    printf("%d\n", x + y);
    return 0;
}
// פלט:
// 5        (48+5=53='5')
// 53
ערכי ASCII חשובים למבחן: 'A'=65, 'Z'=90, 'a'=97, 'z'=122, '0'=48, '9'=57. ההפרש בין אות גדולה לקטנה הוא תמיד 32.

long – פורמט ואריתמטיקה

#include <stdio.h>

int main() {
    long a = 100000L;
    long b = 200000L;
    long c = a * b;
    printf("%ld\n", c);
    printf("sizeof(long) = %lu\n", sizeof(long));
    return 0;
}
// פלט (64-bit Linux):
// 20000000000
// sizeof(long) = 8
//
// פלט (32-bit / Windows):
// overflow (long הוא 4 bytes)
// sizeof(long) = 4
שימו לב: %ld להדפסת long. שימוש ב-%d במקום %ld עלול לגרום לפלט שגוי.

atoi – המרה חלקית ומלאה

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

int main() {
    printf("%d\n", atoi("42"));
    printf("%d\n", atoi("123abc"));
    printf("%d\n", atoi("abc"));
    printf("%d\n", atoi("-99"));
    printf("%d\n", atoi("  55"));
    printf("%d\n", atoi("0x1A"));
    return 0;
}
// פלט:
// 42       (המרה מלאה)
// 123      (עוצר ב-'a' – תו לא מספרי)
// 0        (אין ספרות – מחזיר 0)
// -99      (מזהה סימן מינוס)
// 55       (מדלג על רווחים בהתחלה)
// 0        (עוצר ב-'x' – לא מזהה hex)

פעולות Bitwise – דגלים, הזזה ופורמט hex

דוגמה 1: הזזה ו-OR

#include <stdio.h>

int main() {
    unsigned int a = 0x0F;  // 00001111
    unsigned int b = a << 4; // 11110000 = 0xF0
    unsigned int c = a | b;  // 11111111 = 0xFF
    printf("a=0x%X b=0x%X c=0x%X\n", a, b, c);
    return 0;
}
// פלט: a=0xF b=0xF0 c=0xFF

דוגמה 2: AND למיסוך

#include <stdio.h>

int main() {
    unsigned int val = 0xABCD;
    unsigned int low_byte = val & 0xFF;   // 0xCD
    unsigned int high_byte = (val >> 8) & 0xFF; // 0xAB
    printf("low=0x%X high=0x%X\n", low_byte, high_byte);
    return 0;
}
// פלט: low=0xCD high=0xAB

דוגמה 3: NOT (השלמה)

#include <stdio.h>

int main() {
    unsigned char x = 0x0F;  // 00001111
    unsigned char y = ~x;    // 11110000 = 0xF0
    printf("x=0x%02X y=0x%02X\n", x, y);
    return 0;
}
// פלט: x=0x0F y=0xF0

דוגמה 4: הדלקה וכיבוי ביטים

#include <stdio.h>

int main() {
    unsigned int flags = 0;

    // הדלקת ביט 2 (ספירה מ-0)
    flags |= (1 << 2);     // flags = 0x4 = 00000100
    printf("0x%X\n", flags);

    // הדלקת ביט 0
    flags |= (1 << 0);     // flags = 0x5 = 00000101
    printf("0x%X\n", flags);

    // כיבוי ביט 2
    flags &= ~(1 << 2);    // flags = 0x1 = 00000001
    printf("0x%X\n", flags);

    return 0;
}
// פלט:
// 0x4
// 0x5
// 0x1

מלכודת הסוגריים ב-#define – הנושא הנבחן ביותר!

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

דוגמה 1: MUL ללא סוגריים

#include <stdio.h>

#define MUL(a, b) a * b

int main() {
    int r = MUL(2+3, 2);
    printf("%d\n", r);
    return 0;
}
// הרחבה: r = 2+3 * 2
// סדר פעולות: כפל לפני חיבור → 2 + 6 = 8
// פלט: 8    (ולא 10!)

דוגמה 2: MUL עם סוגריים

#include <stdio.h>

#define MUL(a, b) ((a) * (b))

int main() {
    int r = MUL(2+3, 2);
    printf("%d\n", r);
    return 0;
}
// הרחבה: r = ((2+3) * (2))
// חישוב: (5) * (2) = 10
// פלט: 10   (נכון!)

דוגמה 3: SQUARE ללא סוגריים

#include <stdio.h>

#define SQR(x) x * x

int main() {
    printf("%d\n", SQR(3+1));
    printf("%d\n", 10 / SQR(2));
    return 0;
}
// שורה 1: SQR(3+1) → 3+1 * 3+1 = 3+3+1 = 7  (לא 16!)
// שורה 2: 10/SQR(2) → 10/2*2 = 5*2 = 10      (לא 2!)
// פלט:
// 7
// 10

דוגמה 4: SQUARE עם סוגריים מלאים

#include <stdio.h>

#define SQR(x) ((x) * (x))

int main() {
    printf("%d\n", SQR(3+1));
    printf("%d\n", 10 / SQR(2));
    return 0;
}
// שורה 1: SQR(3+1) → ((3+1) * (3+1)) = 4*4 = 16 ✓
// שורה 2: 10/SQR(2) → 10/((2)*(2)) = 10/4 = 2 ✓
// פלט:
// 16
// 2

דוגמה 5: ADD ללא סוגריים חיצוניים

#include <stdio.h>

#define ADD(a, b) (a) + (b)

int main() {
    int r = 2 * ADD(3, 4);
    printf("%d\n", r);
    return 0;
}
// הרחבה: r = 2 * (3) + (4)
// סדר פעולות: 2*3 + 4 = 6 + 4 = 10
// פלט: 10   (ולא 14!)
// עם #define ADD(a,b) ((a)+(b)) → 2 * ((3)+(4)) = 2*7 = 14

טבלה: מלכודות מאקרו – השוואה

מאקרו קריאה הרחבה תוצאה נכון?
#define MUL(a,b) a*b MUL(2+3, 2) 2+3*2 8 שגוי (צפוי 10)
#define MUL(a,b) ((a)*(b)) MUL(2+3, 2) ((2+3)*(2)) 10 נכון
#define SQR(x) x*x SQR(3+1) 3+1*3+1 7 שגוי (צפוי 16)
#define SQR(x) ((x)*(x)) SQR(3+1) ((3+1)*(3+1)) 16 נכון
#define SQR(x) x*x 10/SQR(2) 10/2*2 10 שגוי (צפוי 2)
#define SQR(x) ((x)*(x)) 10/SQR(2) 10/((2)*(2)) 2 נכון
#define ADD(a,b) (a)+(b) 2*ADD(3,4) 2*(3)+(4) 10 שגוי (צפוי 14)
#define ADD(a,b) ((a)+(b)) 2*ADD(3,4) 2*((3)+(4)) 14 נכון
#define HALF(x) x/2 HALF(4+2) 4+2/2 5 שגוי (צפוי 3)
#define HALF(x) ((x)/2) HALF(4+2) ((4+2)/2) 3 נכון
כלל ברזל: תמיד עטפו כל פרמטר בסוגריים וגם את כל ההגדרה בסוגריים חיצוניים: #define F(x) ((x)*2). בשאלות מבחן, הרחיבו את המאקרו ידנית לפני שמחשבים.

תופעות לוואי – SQR(++a) ו-UB

דוגמה 1: הערכה כפולה

#include <stdio.h>

#define SQR(x) ((x) * (x))

int main() {
    int a = 3;
    int r = SQR(a++);
    printf("r=%d a=%d\n", r, a);
    return 0;
}
// הרחבה: r = ((a++) * (a++))
// a++ מבוצע פעמיים! → Undefined Behavior
// תוצאה אפשרית: r=9 a=5 או r=12 a=5 (תלוי מהדר)

דוגמה 2: ++a במאקרו לעומת פונקציה

#include <stdio.h>

#define DOUBLE_M(x) ((x) + (x))

static inline int double_f(int x) { return x + x; }

int main() {
    int a = 5;
    int r1 = DOUBLE_M(++a);  // ((++a) + (++a)) = UB!
    printf("macro: r=%d a=%d\n", r1, a);

    int b = 5;
    int r2 = double_f(++b);  // b הופך ל-6, ואז 6+6=12
    printf("func:  r=%d b=%d\n", r2, b);
    return 0;
}
// פלט מאקרו: לא ניתן לחזות (UB)
// פלט פונקציה: r=12 b=6  (מוגדר וצפוי)
הבדל קריטי: פונקציה מעריכה כל פרמטר פעם אחת לפני הקריאה. מאקרו מחליף טקסט, ולכן הפרמטר יכול להתבצע מספר פעמים.

תבניות מאקרו: SWAP, MIN/MAX, SET_FLAG

SWAP – החלפת ערכים

#include <stdio.h>

#define SWAP(a, b) do { \
    int temp = (a); \
    (a) = (b); \
    (b) = temp; \
} while(0)

int main() {
    int x = 5, y = 10;
    SWAP(x, y);
    printf("x=%d y=%d\n", x, y);
    return 0;
}
// פלט: x=10 y=5

MIN / MAX

#include <stdio.h>

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

int main() {
    printf("%d\n", MIN(3, 7));
    printf("%d\n", MAX(3, 7));
    printf("%d\n", MIN(2+1, 4));
    return 0;
}
// פלט:
// 3
// 7
// 3    (MIN((2+1),(4)) = MIN(3,4) = 3)

SET_FLAG / CLEAR_FLAG / CHECK_FLAG

#include <stdio.h>

#define SET_FLAG(val, bit)   ((val) |= (1 << (bit)))
#define CLEAR_FLAG(val, bit) ((val) &= ~(1 << (bit)))
#define CHECK_FLAG(val, bit) ((val) & (1 << (bit)))

int main() {
    unsigned int f = 0;
    SET_FLAG(f, 0);   // f = 0x1  = 00000001
    SET_FLAG(f, 3);   // f = 0x9  = 00001001
    printf("0x%X\n", f);

    CLEAR_FLAG(f, 0); // f = 0x8  = 00001000
    printf("0x%X\n", f);

    printf("%d\n", CHECK_FLAG(f, 3) ? 1 : 0);
    printf("%d\n", CHECK_FLAG(f, 0) ? 1 : 0);
    return 0;
}
// פלט:
// 0x9
// 0x8
// 1     (ביט 3 דלוק)
// 0     (ביט 0 כבוי)

do-while(0) – מאקרו בטוח

למה do { ... } while(0)? כדי שהמאקרו יתנהג כהוראה בודדת ויהיה בטוח בתוך if ללא סוגריים.

// ❌ בלי do-while(0):
#define LOG_BAD(msg) printf("LOG: "); printf(msg);

if (error)
    LOG_BAD("fail\n");  // רק printf הראשון בתוך ה-if!
                       // printf השני ירוץ תמיד!

// ✓ עם do-while(0):
#define LOG_GOOD(msg) do { printf("LOG: "); printf(msg); } while(0)

if (error)
    LOG_GOOD("fail\n"); // שתי ההדפסות בתוך ה-if ✓

אופרטור # – Stringify

אופרטור # בתוך מאקרו ממיר פרמטר למחרוזת. מחרוזות צמודות ב-C מתמזגות אוטומטית.

דוגמה 1: PRINT_VAR

#include <stdio.h>

#define STR(s) #s
#define PRINT_VAR(x) printf(#x " = %d\n", x)

int main() {
    printf("%s\n", STR(hello));
    printf("%s\n", STR(3+4));

    int count = 42;
    PRINT_VAR(count);

    int total = 100;
    PRINT_VAR(total);
    return 0;
}
// פלט:
// hello          (STR(hello) → "hello")
// 3+4            (STR(3+4)  → "3+4" – לא מחשב!)
// count = 42     (PRINT_VAR(count) → printf("count" " = %d\n", count))
// total = 100

דוגמה 2: stringify עם ביטוי

#include <stdio.h>

#define SHOW(expr) printf(#expr " = %d\n", expr)

int main() {
    int a = 3, b = 4;
    SHOW(a + b);
    SHOW(a * b);
    return 0;
}
// הרחבה:
// printf("a + b" " = %d\n", a + b); → "a + b = 7"
// printf("a * b" " = %d\n", a * b); → "a * b = 12"
// פלט:
// a + b = 7
// a * b = 12

אופרטור ## – Token Pasting

אופרטור ## מצמיד שני טוקנים למזהה חדש בזמן קומפילציה.

דוגמה 1: יצירת שמות משתנים

#include <stdio.h>

#define CONCAT(a, b) a##b

int main() {
    int CONCAT(my, Var) = 42;  // → int myVar = 42;
    printf("%d\n", myVar);
    return 0;
}
// פלט: 42

דוגמה 2: שילוב ## עם #

#include <stdio.h>

#define MAKE_FUNC(name) \
    void func_##name() { printf("called: %s\n", #name); }

MAKE_FUNC(init)   // → void func_init()  { printf("called: %s\n", "init"); }
MAKE_FUNC(close)  // → void func_close() { printf("called: %s\n", "close"); }

int main() {
    func_init();
    func_close();
    return 0;
}
// פלט:
// called: init
// called: close

דוגמה 3: VAR_n pattern

#include <stdio.h>

#define VAR(n) var_##n

int main() {
    int VAR(1) = 10;   // → int var_1 = 10;
    int VAR(2) = 20;   // → int var_2 = 20;
    printf("%d %d\n", var_1, var_2);
    return 0;
}
// פלט: 10 20

#ifdef / #ifndef / #if – קומפילציה מותנית

דוגמה 1: #ifdef ו-#ifndef

#include <stdio.h>

#define DEBUG

int main() {
#ifdef DEBUG
    printf("debug on\n");
#endif

#ifndef RELEASE
    printf("not release\n");
#endif

#ifdef VERBOSE
    printf("verbose\n");
#endif

    return 0;
}
// פלט:
// debug on       (DEBUG מוגדר)
// not release    (RELEASE לא מוגדר)
// (verbose לא מודפס – VERBOSE לא מוגדר)

דוגמה 2: #if 0 לביטול קוד

#include <stdio.h>

int main() {
    printf("A\n");

#if 0
    printf("B\n");  // קוד "מבוטל" – לא ייכלל בקומפילציה
    printf("C\n");
#endif

    printf("D\n");
    return 0;
}
// פלט:
// A
// D
// (B ו-C לא מודפסים – נמצאים בתוך #if 0)

דוגמה 3: #if עם ערך מספרי

#include <stdio.h>

#define SIZE 3

int main() {
#if SIZE == 1
    printf("small\n");
#elif SIZE == 3
    printf("medium\n");
#else
    printf("large\n");
#endif
    return 0;
}
// פלט: medium
// SIZE==3 תואם את ה-#elif השני

דוגמה 4: #if DEBUG עם ערך

#include <stdio.h>

#define DEBUG 2

int main() {
#if DEBUG >= 2
    printf("detailed debug\n");
#elif DEBUG >= 1
    printf("basic debug\n");
#else
    printf("no debug\n");
#endif
    return 0;
}
// פלט: detailed debug
// DEBUG=2 → 2 >= 2 → true → הענף הראשון

#undef – ביטול והגדרה מחדש

דוגמה 1: undef ו-redefine

#include <stdio.h>

#define VAL 10

int main() {
    printf("%d\n", VAL);

#undef VAL
#define VAL 20

    printf("%d\n", VAL);

#undef VAL
#define VAL 30

    printf("%d\n", VAL);
    return 0;
}
// פלט:
// 10
// 20
// 30

דוגמה 2: undef עם ifdef

#include <stdio.h>

#define FEATURE_X

int main() {
#ifdef FEATURE_X
    printf("X on\n");
#endif

#undef FEATURE_X

#ifdef FEATURE_X
    printf("X still on\n");
#else
    printf("X off\n");
#endif

    return 0;
}
// פלט:
// X on     (FEATURE_X מוגדר בשלב הזה)
// X off    (אחרי #undef, FEATURE_X כבר לא מוגדר)

inline – השוואה למאקרו

פונקציית static inline מייצרת קוד יעיל כמו מאקרו, אבל עם בדיקת טיפוסים מלאה ובלי בעיות הערכה כפולה.

#include <stdio.h>

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

static inline int max_f(int a, int b) {
    return a > b ? a : b;
}

int main() {
    int x = 3, y = 7;

    // שניהם מחזירים 7
    printf("%d\n", MAX_M(x, y));
    printf("%d\n", max_f(x, y));

    // אבל עם ++:
    int a = 3, b = 7;
    printf("%d\n", max_f(a++, b++)); // בטוח: a=4,b=8, מחזיר 7
    // MAX_M(a++, b++) → UB! (b++ מוערך פעמיים)

    return 0;
}
// פלט:
// 7
// 7
// 7

argc/argv – פרסור שורת פקודה עם atoi

דוגמה 1: הדפסת ארגומנטים

#include <stdio.h>

int main(int argc, char *argv[]) {
    printf("argc = %d\n", argc);
    for (int i = 0; i < argc; i++)
        printf("argv[%d] = %s\n", i, argv[i]);
    return 0;
}
// הרצה: ./prog hello 42
// פלט:
// argc = 3
// argv[0] = ./prog
// argv[1] = hello
// argv[2] = 42

דוגמה 2: המרת ארגומנט למספר

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

int main(int argc, char *argv[]) {
    if (argc > 1) {
        int n = atoi(argv[1]);
        printf("n = %d\n", n);
        printf("n*2 = %d\n", n * 2);
    } else {
        printf("No argument\n");
    }
    return 0;
}
// הרצה: ./prog 15
// פלט:
// n = 15
// n*2 = 30
//
// הרצה: ./prog
// פלט: No argument
שימו לב: argv[0] הוא תמיד שם התוכנית. argc כולל את שם התוכנית בספירה. לכן argc == 1 אומר שאין ארגומנטים נוספים.

סיכום

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

  • enum – ספרו ערכים מהאיבר המפורש האחרון + 1. זכרו שברירת מחדל מתחילה מ-0.
  • struct – sizeof כולל padding. סדר שדות משפיע. packed מבטל padding.
  • union – כל השדות חולקים זיכרון. sizeof = גודל השדה הגדול ביותר. שימושי לבדיקת endianness.
  • switch – ללא break, ה-case "נופל" לבא. default יכול להיות בכל מיקום.
  • #define – מלכודות סוגריים – הנושא הנבחן ביותר! הרחיבו ידנית ושימו לב לסדר פעולות.
  • תופעות לוואי++/-- במאקרו = UB. השתמשו ב-inline במקום.
  • # ו-## – stringify ו-token paste. זכרו: #x הופך ל-"x", a##b הופך ל-ab.
  • char/ASCII'A'=65, 'a'=97, '0'=48. אריתמטיקה על תווים עובדת כי char הוא מספר.
  • bitwise| להדלקה, & ~ לכיבוי, << להזזה. פורמט hex: %X.
  • endianness – ב-Little-Endian הבית הנמוך בכתובת הנמוכה: 0x41424344[44,43,42,41].
  • atoi – עוצר בתו ראשון שאינו ספרה. atoi("123abc") = 123.

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

תודה! בזכותכם נוכל להשתפר