Keyboard shortcuts

Press ← or → to navigate between chapters

Press ? to show this help

Press Esc to hide this help

Studies

מצביעים

הגדרת הבעיה

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

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

למזלנו, כפי שראינו כשדנו בנושא הזכרון בתחילת הקורס, לשאלה ״איפה נמצא המשתנה x בזכרון״ יש תשובה פשוטה - הכתובת שלו בזכרון! וכפי שראינו, האופרטור & נותן את אותה הכתובת. מעתה, נתחיל להשתמש בכתובות אלו ע״י משתנים מיוחדים הנקראים מצביעים.

מצביעים

מצביע (pointer) הוא משתנה שערכו הוא כתובת של משתנה אחר בזכרון.

נגדיר שני אופרטורים חדשים שישמשו אותנו עם מצביעים:

  1. אופרטור ה-address-of (לקיחת הכתובת): &. עבור משתנה כלשהו x, הביטוי &x הוא הכתובת של המשתנה x בזכרון.

  2. אופרטור ה-dereference (גישה לכתובת): *. עבור מצביע כלשהו ptr, הביטוי *ptr הוא הערך של המשתנה אליו ptr מצביע.

Note

השתמשנו רבות כבר באופרטור לקיחת הכתובת כאשר העברנו משתנים לפונקציה scanf, למשל: scanf("%d", &n) עבור n משתנה int כלשהו.

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

#include <stdio.h>
void swap(int *a_ptr, int *b_ptr) {
    int tmp = *a_ptr;
    *a_ptr = *b_ptr;
    *b_ptr = tmp;
}
int main() {
    int a = 1;
    int b = 5;
    printf("before call to swap: a = %d, b = %d", a, b);
    swap(&a, &b);
    printf("after call to swap: a = %d, b = %d", a, b);
}

נפרק:

  1. הפונקציה swap כעת מקבלת שני משתנים מטיפוס * int במקום מטיפוס int. זהו הסינטקס למשתנה שהוא מצביע למשתנה מטיפוס int.

  2. בקריאה לפונקציה אנחנו מעבירים את &a, &b במקום את a, b. אופרטור לקיחת הכתובת לוקח את a, b ממשתנים מטיפוס int ומחזיר את הכתובת שלהם - מצביעים אליהם מטיפוס * int כפי שהפונקציה דורשת.

  3. הפונקציה משתמשת באופרטור ה-dereference כמה פעמים - למשל, בשורה הראשונה היא כותבת למשתנה tmp את הערך אליו a_ptr מצביעה ע״י האופרטור a_ptr*.

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

אתחול ושימוש של מצביע למשתנה מסוג type נראה כך:

type var = some_value;
type *ptr = &var;
type another_var = *ptr;

נפרק את השורות שכתבנו:

  1. בשורה הראשונה אנחנו מגדירים משתנה כללי מטיפוס type, שיכול להיות כל טיפוס - int, char וכו’.

  2. בשורה השנייה אנחנו מצהירים על משתנה מטיפוס * type. משתנה מהטיפוס * type הוא מצביע למשתנה אחר מטיפוס type. ערכו של המצביע הוא הכתובת של המשתנה var, הנלקח ע״י אופרטור ה-&.

  3. לבסוף אנחנו מגדירים משתנה נוסף מטיפוס type ומגדירים שערכו הוא ptr* - כלומר, ערכו של המשתנה אליו ptr מצביע, שהוא var. אופרטור ה-dereference למעשה מורה למעבד לגשת אל הכתובת בזכרון שכתובה במצביע ולקחת ממנה את הערך.

בקצרה, כאשר קיים משתנה בשם x ומשתנה אחר המצביע אליו בשם xptr, הביטוי xptr* והביטוי x שקולים לחלוטין.

לסיכום: העברה של משתנה לפונקציה ע״י הכתובת שלו עם מצביע אליו נקראת pass by reference.

סגנונות בכתיבת מצביעים

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

int x = 42;
int *xptr = &x;
int* xptr2 = &x;
int * xptr3 = &x;

אנחנו נדבוק בסגנון int *xptr = &x מכמה סיבות:

  1. הוא פופולרי מאוד.
  2. כאשר אנחנו כותבים int *xptr = &x, יש הגיון מסוים בטיפוסים - המשתנה *xptr הוא משתנה מסוג int, בדיוק כמו המשתנה x.
  3. וגם כי…

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

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

כאשר מצהירים על מספר משתנים באותה שורה:

int *ptr1, ptr2, ptr3;

מתברר כי המשתנים ptr2 ו-ptr3 כלל אינם מצביעים, אלא מטיפוס int. לכן יש להקפיד על הכוכבית במקום הנכון:

int *ptr1, *ptr2, *ptr3;

או, הרבה יותר טוב, פשוט להצהיר על כל אחד בשורה משלו:

int *ptr1;
int *ptr2;
int *ptr3;

מצביע * void ו-casting של מצביעים

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

int x = 42;
void *ptr = &x;

אי אפשר לעשות dereference למצביע מסוג * void:

#include <stdio.h>
int main() {
    int x = 42;
    void *p = &x;
    printf("%d\n", *p);
}
void_deref.c:5:20: error: argument type 'void' is incomplete

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

#include <stdio.h>

void print_ptrs(char *char_ptr, int *int_ptr) {
    printf("Accessing through char pointer: %c\n", *char_ptr);
    printf("Accesing through int pointer: %d\n", *int_ptr);
    printf("\n");
}

int main() {
    int i = 123456;
    char *char_ptr = (char *)&i;
    int *int_ptr = &i;
    print_ptrs(char_ptr, int_ptr);

    char c = 65;
    char_ptr = &c;
    int_ptr = (int *)&c;
    print_ptrs(char_ptr, int_ptr);
}

Caution

להוסיף פה תרשים של הבייטים

דוגמא

כתבו פונקציה שמקבלת מצביעים ל-3 משתני char בשם a, b, c, שמובטח שמכילים אותיות קטנות באנגלית. הפונקציה תסדר אותם כך ש-a יכיל את האות הנמוכה ביותר, b את הבאה אחריה ו-c את הבאה אחריה.

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

  1. a <= b
  2. a <= c
  3. b <= c

אם מתקיים למשל a >= b, נוכל להחליף בין a ל-b ואז מובטח שהיחס ההפוך מתקיים - לכן נכתוב פונקציית עזר שתחליף בין שני char. נקבל את הקוד הבא:

#include <stdio.h>
void swap(char *x, char *y) {
    char tmp = *x;
    *x = *y;
    *y = tmp;
}
void sort(char *a, char *b, char *c) {
    if (a <= b) {
        swap(a, b);
    }
    if (a <= c) {
        swap(a, c);
    }
    if (b <= c) {
        swap(b, c);
    }
}

int main() {
    char x = 5, y = 4, z = 7;
    sort(&x, &y, &z);
    printf("%c %c %c\n", x, y, z);
}

נזכיר כי למרות שמדובר על משתני char אין שום מניעה להשוות ביניהם עם < או > - כמו שראינו בעבר, משתני char הם מספרים שאנחנו מפרשים כתווים עם מיפוי ASCII.