לדלג לתוכן

3.3 פוינטרים פתרון

📘 פתרון תרגול – פוינטרים, מחרוזות, מערכים ו־scanf

1. קלט מספר שלם מהמשתמש

#include <stdio.h>

int main() {
    int a;
    printf("הכנס מספר:\n");
    scanf("%d", &a);
    printf("המספר שהכנסת הוא: %d\n", a);
    return 0;
}

2. קלט אות מהמשתמש, הדפסת ערך ה־ASCII שלה

#include <stdio.h>

int main() {
    char c;
    printf("הכנס אות:\n");
    scanf(" %c", &c); // שים לב לרווח לפני %c כדי לדלג על תווי whitespace
    printf("ערך ה־ASCII של %c הוא: %d\n", c, c);
    return 0;
}

3. הדפסת מחרוזת וכתובת בזיכרון

#include <stdio.h>

int main() {
    char* str = "hello";
    printf("המחרוזת: %s\n", str);
    printf("הכתובת בזיכרון: %p\n", str);
    return 0;
}

4. קלט של שלושה מספרים לתוך מערך

#include <stdio.h>

int main() {
    int arr[3];

    printf("הכנס שלושה מספרים:\n");
    scanf("%d", &arr[0]);
    scanf("%d", &arr[1]);
    scanf("%d", &arr[2]);

    printf("המספרים הם: %d, %d, %d\n", arr[0], arr[1], arr[2]);
    return 0;
}

5. שימוש בפוינטר לגישה למערך

#include <stdio.h>

int main() {
    int arr[3] = {10, 20, 30};
    int* p = arr;

    printf("arr[0] = %d, *(p+0) = %d\n", arr[0], *(p + 0));
    printf("arr[1] = %d, *(p+1) = %d\n", arr[1], *(p + 1));
    printf("arr[2] = %d, *(p+2) = %d\n", arr[2], *(p + 2));

    return 0;
}

6. קלט מחרוזת מהמשתמש

#include <stdio.h>

int main() {
    char name[100];

    printf("הכנס מילה (מחרוזת ללא רווחים):\n");
    scanf("%s", name);
    printf("הכנסת את המחרוזת: %s\n", name);

    return 0;
}

7. הדפסת argc ו־argv

#include <stdio.h>

int main(int argc, char** argv) {
    printf("כמות הארגומנטים: %d\n", argc);
    printf("שם התוכנה: %s\n", argv[0]);

    if (argc > 1) {
        printf("הארגומנט הראשון: %s\n", argv[1]);
    }

    return 0;
}

8. הדפסת התו הראשון, השני והשלישי של הארגומנט הראשון

#include <stdio.h>

int main(int argc, char** argv) {
    if (argc > 1) {
        char* p = argv[1];
        printf("תו ראשון: %c\n", p[0]);
        printf("תו שני: %c\n", p[1]);
        printf("תו שלישי: %c\n", p[2]);
    } else {
        printf("לא הוזן ארגומנט ראשון\n");
    }

    return 0;
}

💡 תרגיל בונוס – שימוש ב־scanf עם כמה סוגים

#include <stdio.h>

int main() {
    char name[100];
    int age;
    int fav;

    printf("הכנס שם:\n");
    scanf("%s", name);

    printf("הכנס גיל:\n");
    scanf("%d", &age);

    printf("הכנס מספר אהוב:\n");
    scanf("%d", &fav);

    printf("שלום %s, אתה בן %d והמספר האהוב עליך הוא %d\n", name, age, fav);
    return 0;
}

ניקח את הקוד הבא:

#include <stdio.h>

int main() {
    char name[100];
    int age;
    int fav;

    printf("הכנס שם:\n");
    scanf("%s", name);

    printf("הכנס גיל:\n");
    scanf("%d", &age);

    printf("הכנס מספר אהוב:\n");
    scanf("%d", &fav);

    printf("שלום %s, אתה בן %d והמספר האהוב עליך הוא %d\n", name, age, fav);

    return 0;
}

נניח שקימפלנו עם:

gcc -m32 -g -o bonus bonus.c

שימו לב ש-g הוא דגל debug בgcc, ונותן לנו עוד מידע בקובץ האובייקט שיכול לשמש אותנו שאנחנו חוקרים את התוכנה עם objdump.

ונשתמש ב־objdump -d bonus > bonus.asm כדי לראות את קוד האסמבלי שנוצר.


חלק מהאסמבלי שיתקבל (מפושט לצורך הסבר):

push   ebp
mov    ebp, esp
sub    esp, 0x70       ; מקצה מקום ל־name, age, fav (100 בתים + 4 + 4)

lea    eax, [ebp-0x64] ; כתובת name
push   eax
push   offset .LC0     ; פורמט "%s"
call   scanf

lea    eax, [ebp-0x4]  ; כתובת age
push   eax
push   offset .LC1     ; פורמט "%d"
call   scanf

lea    eax, [ebp-0x8]  ; כתובת fav
push   eax
push   offset .LC2     ; פורמט "%d"
call   scanf

lea    eax, [ebp-0x64] ; name
mov    edx, [ebp-0x4]  ; age
mov    ecx, [ebp-0x8]  ; fav
push   ecx
push   edx
push   eax
push   offset .LC3     ; פורמט "שלום %s, אתה בן %d..."
call   printf

💡 הסבר שלב-שלב:

  • sub esp, 0x70: מקצה מקום ל־char name[100] ול־2 משתנים בגודל 4 בתים כל אחד (סה"כ 112 בתים = 0x70).

  • כל lea eax, [ebp-offset] – מחשבת את הכתובת של משתנה מקומי בזיכרון.

  • push offset .LCx – שולח לפונקציה את מחרוזת הפורמט (כמו "%s", "%d"...).

  • push eax – שולח את כתובת המשתנה לפונקציה.

  • call scanf / call printf – קריאה לפונקציית ספרייה שמוגדרת בלינקר.

  • ההדפסה הסופית עושה שימוש ב־push עבור כל משתנה לפי הסדר ש־printf דורש.


סיכום:

  • הקוד ב־C מקצה משתנים בזיכרון ה־Stack.

  • כל קריאה ל־scanf או printf מתבצעת על ידי שימוש ב־push של פרמטרים ו־call.

  • הכתובות לזיכרון מקומי מחושבות יחסית ל־ebp – כך המעבד ניגש למשתנים בפונקציה.

  • הקוד שמתקבל תואם בצורה מלאה את ההרצאות הקודמות על Stack, קריאות לפונקציה, שימוש ב־lea וכו'.