לדלג לתוכן

פוינטרים, מערכים, מחרוזות ו-scanf

1. מה זה Pointer (מצביע)?

הPointer (בעברית: מצביע) הוא משתנה ששומר כתובת בזיכרון - במקום לשמור ערך כמו int x = 5, המצביע שומר איפה בזיכרון יושב הערך.

נכריז על מצביע כך:

int* ptr;

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

איך שומרים כתובת של משתנה?

אפשר לקחת כתובת של משתנה קיים עם הסימן & (and):

int x = 5;
int* ptr = &x;

פעולת ה& נקראת "הreference" פעולת הreference יודעת להגיד מה הכתובת של משתנה מסוים שהגדרנו בעבר.

איך ניגשים לערך שעליו מצביע המצביע?

משתמשים בכוכבית כדי לגשת לתוכן שאליו מצביע המצביע:

int y = *ptr; // y will equal 5

פעולת ה* נקראת "הdereference" פעולת הdereference שקולה ל [] באסמבלי ויודעת להגיד מה הערך שיש מאחורי פוינטר מסוים על פי הגודל שלו.

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

איור המראה משתנה x שמכיל את הערך 10 בכתובת מסוימת בזיכרון, מצביע p ששומר את הכתובת הזו, והערך שמתקבל כשעושים dereference למצביע

דוגמה מלאה:

#include <stdio.h>

int main() {
    int x = 10;
    int a = &x;
    int* p = &x;

    printf("x = %d\n", x);     // prints the value of x
    printf("a = %d\n", a);     // prints the value of x
    printf("p = %p\n", p);     // prints the address of x
    printf("*p = %d\n", *p);   // prints the value pointed to by p
    return 0;
}

2. מה זה מערך (Array)?

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

מגדירים מערך כך:

int arr[5]; // array of 5 whole numbers

אפשר לגשת לכל איבר בעזרת אינדקס:

arr[0] = 1;
arr[1] = 2;

חשוב לדעת:

כאשר אנחנו מגדירים מערך - שם המערך הוא מצביע לאיבר הראשון בו!

int* ptr = arr; // ptr == &arr[0]

כלומר - מערכים ופוינטרים מאוד קשורים. למעשה:

arr[i] == *(arr + i)

דוגמה:

int arr[3] = {1, 2, 3};
printf("%d\n", *(arr + 1)); // prints 2

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

3. מחרוזות (Strings)

ב-C אין טיפוס string. מחרוזת היא מערך של char שמסתיים ב-NULL TERMINATOR ('\0').

כך מגדירים מחרוזת:

char* str = "hello";

הקומפיילר שומר את המחרוזת ב-data segment ושם מצביע לתחילת המחרוזת במשתנה str.

למעשה:

char str[] = {'h', 'e', 'l', 'l', 'o', '\0'};

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

הדפסה:

printf("%s\n", str);

strlen - אורך מחרוזת:

נוכל להשתמש בפונקציה בשם "strlen"ש מוגדרת בlibc כדי לקבל את האורך של מחרוזת, שימו לב לעשות include לstring.h- קובץ שמגדיר את הפונקציה בlibc.

#include <string.h>
int len = strlen(str);

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

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

4. קריאת קלט מהמשתמש - scanf

בניגוד ל-printf שמדפיס, scanf קורא קלט מהמשתמש.

תחביר:

scanf("%d", &x);

שימו לב! חובה לשים & לפני המשתנה - כי scanf משנה אותו בזיכרון, ולכן צריך את הכתובת של המשתנה

דוגמאות:

int a;
scanf("%d", &a);  // reads a whole number

char c;
scanf(" %c", &c); // reads one character (note the space before %c)

char str[100];
scanf("%s", str); // reads a word (no spaces)

חשוב:

  • scanf("%s", str) לא צריך & כי str כבר מצביע.

  • scanf עוצר בקריאת מחרוזת כשיש רווח, טאב או אנטר.

5. דוגמה משולבת - מצביעים, מערכים ו-scanf

#include <stdio.h>

int main() {
    int arr[3];
    int* p = arr;

    printf("Enter 3 numbers:\n");

    scanf("%d", &arr[0]);
    scanf("%d", &arr[1]);
    scanf("%d", &arr[2]);

    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;
}

מה זה ** בפוינטרים? - ולמה argv הוא char**

ראינו כבר שפוינטר (*) הוא משתנה ששומר כתובת של ערך כלשהו.

אז מה זה **?

char** argv;

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

במקרה של argv, הוא מייצג מערך של מחרוזות - כל מחרוזת היא מצביע (char*), והמכלול כולו הוא מצביע למערך של מצביעים → ולכן char**.

דוגמה להבנה:

אם הרצתם תוכנה כך:

./program hello world

אז בתוך main:

int main(int argc, char** argv)
  • argc יהיה 3 (כי יש 3 מילים: ./program, hello, world)

  • argv[0] = "./program"

  • argv[1] = "hello"

  • argv[2] = "world"

כל אחד מ-argv[i] הוא בעצם char* - מצביע למחרוזת.

דוגמה:

#include <stdio.h>

int main(int argc, char** argv) {
    printf("Number of arguments: %d\n", argc);
    printf("Program name: %s\n", argv[0]);
    printf("First argument (if present): %s\n", argv[1]);
    return 0;
}

אם תפעילו את הקוד עם:

./a.out amit

תקבלו:

Number of arguments: 2
Program name: ./a.out
First argument (if present): amit
  • * = מצביע לערך
  • ** = מצביע למצביע
  • argv = מצביע למערך של מצביעים למחרוזות
  • ככה תוכנית C יכולה לקרוא את הארגומנטים שהועברו אליה בשורת הפקודה

בהמשך, נשתמש בזה כדי לכתוב תוכנות שמגיבות למה שהמשתמש כותב ב-Terminal.