Menu

מצביעים ב-C: מדריך מלא עם דוגמאות

מצביע הוא משתנה ששומר כתובת זיכרון. העמוד בונה את הרעיון מהיסוד: האופרטורים & ו-*, הצהרה על מצביעים ו-dereference שלהם, למה טיפוסי מצביעים חשובים, והפונקציה swap() שמראה למה מצביעים קיימים בכלל.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

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

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

זיכרון, כתובות ולמה לכל דבר יש כתובת

כשכותבים int score = 42;, הקומפיילר שומר כמה בתים בזיכרון ונותן להם שם שאפשר להשתמש בו. לבתים האלה יש גם כתובת מספרית. חשבו על הזיכרון כעל רחוב ארוך מאוד של בתים ממוספרים: score היא המשפחה שגרה בבית מספר 0x7ffd4c2a. השם הוא בשבילכם, המספר בשביל המכונה.

האופרטור & מבקש את המספר הזה:

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

זה שהכתובות לא צפויות הוא לא בעיה, כי כמעט אף פעם לא אכפת לכם מה היא כתובת. אכפת לכם שיש לכם אותה.

הצהרה על מצביע

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

int    *p;      // p יכול להחזיק כתובת של int
double *q;      // q יכול להחזיק כתובת של double
char   *name;   // name יכול להחזיק כתובת של char

קראו את int *p; כ"*p הוא int": dereference של p נותן לכם int. הקריאה הזו שורדת כל הצהרה מסובכת ש-C תזרוק עליכם בהמשך.

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

int* a, b;   // a הוא מצביע ל-int; b הוא int רגיל, לא מצביע
int *a, *b;  // שניהם מצביעים: ה-* נקשר לשם, לא לטיפוס

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

& ו-*: שני האופרטורים

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

  • &x: אופרטור הכתובת (address-of). מפיק את הכתובת שבה x נמצא.
  • *p: dereference. הולך לכתובת שב-p ונותן לכם את האובייקט שנמצא שם.

שתי השורות האחרונות הן כל העניין של מצביעים. שום דבר לא הושם ל-age ישירות, ובכל זאת age השתנה, כי *p = 31 פירושו "שמור 31 בכתובת ש-p מחזיק", והכתובת הזו היא הכתובת של age.

כך זה נראה בזיכרון:

      p                        age
 +------------+           +----------+
 | 0x7ffd1c40 | --------> |    31    |
 +------------+           +----------+
 at 0x7ffd1c38            at 0x7ffd1c40

 p    -> the address 0x7ffd1c40
 *p   -> the int stored there, 31
 &age -> 0x7ffd1c40, the same address p holds
 &p   -> 0x7ffd1c38, where p itself lives

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

מכיוון ש-& ו-* מבטלים זה את זה, *&age הוא פשוט age, ו-&*p הוא פשוט p.

למה טיפוסי מצביעים חשובים

מצביע שומר כתובת, וכתובת היא רק מספר, אז למה הקומפיילר מתעקש לדעת אם הוא מצביע ל-int או ל-double? שתי סיבות:

  1. כמה בתים לקרוא. dereference של int * קורא 4 בתים (בדרך כלל); dereference של double * קורא 8. בלי הטיפוס, ל-*p לא הייתה משמעות.
  2. איך לפרש את הבתים האלה. אותה תבנית ביטים היא מספר אחד כ-int ומספר אחר לגמרי כ-float.

לעומת זאת, הגודל של המצביע עצמו לא תלוי במה שהוא מצביע עליו: כתובת היא כתובת:

במחשב של 64 ביט כל שלושת המצביעים הם 8 בתים, בעוד שהדברים שהם מצביעים עליהם הם 4, 8 ו-1. הטיפוס שמצביעים עליו הוא מה שנותן משמעות ל-*p ולאריתמטיקה של מצביעים.

השמה בין טיפוסי מצביעים שונים היא שגיאה שהקומפיילר יתלונן עליה, ובצדק: pi = &d; היה מכין אתכם לקרוא 4 בתים של double ולקרוא לתוצאה מספר שלם.

המוטיבציה הקלאסית: swap()

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

הפונקציה מדווחת שהיא החליפה ביניהם, ו-main מדווחת ששום דבר לא קרה. broken_swap קיבלה עותקים של x ו-y. היא החליפה את העותקים שלה בצורה מושלמת, ואז העותקים האלה חדלו להתקיים כשהפונקציה חזרה.

תנו לה את הכתובות במקום:

עכשיו a ו-b עדיין עותקים, עותקים של שתי כתובות, אבל עותק של כתובת מצביע בדיוק על אותו משתנה שהמקור הצביע עליו. *a הוא x, מכל מקום שממנו קוראים לפונקציה.

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

int n;
scanf("%d", &n);   // scanf חייב להיות מסוגל לכתוב לתוך n, ולכן הוא צריך את הכתובת של n

החזרת יותר מערך אחד

פונקציה ב-C מחזירה ערך אחד. מצביעים הם הדרך לעקוף את זה: מעבירים כתובות עבור התוצאות הנוספות.

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

מצביעים למצביעים

מכיוון שמצביע הוא משתנה, אפשר לקחת את הכתובת שלו, והטיפוס של הכתובת הזו הוא "מצביע למצביע ל-int":

  pp              p               value
+------+       +------+        +------+
| &p   | ----> | &val | -----> |   8  |
+------+       +------+        +------+

תפגשו את ** באמת כשפונקציה צריכה לשנות מצביע שהקוראת מחזיקה, למשל פונקציית הקצאה שמציבה במצביע של הקוראת זיכרון חדש, וב-char *argv[] של ארגומנטים משורת הפקודה.

ארבע טעויות ששווה להכיר לפני שעושים אותן

שימוש במצביע שאף פעם לא הופנה לשום מקום. מצביע לא מאותחל מחזיק את הבתים שכבר היו שם. dereference שלו קורא או כותב לכתובת אקראית.

int *p;      // p מחזיק זבל
*p = 10;     // התנהגות לא מוגדרת: כנראה קריסה

אתחלו כל מצביע, עם כתובת אמיתית או עם NULL. ראו את העמוד על מצביעי null למשמעת סביב זה.

בלבול בין *p ל-p. p = 5; מציב במצביע את הכתובת 5 (שטות); *p = 5; שומר 5 במקום שאליו הוא מצביע. הקומפיילר יזהיר על הראשון, ושווה לקרוא אזהרות.

לשכוח את ה-& ב-scanf. scanf("%d", n) מעביר את הערך של n ככתובת. זה מתקמפל עם אזהרה ואז כותב לכל זיכרון שהמספר הזה מציין.

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

int *bad(void) {
    int local = 42;
    return &local;   // הקוראת מקבלת מצביע תלוי
}

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

לאן ממשיכים

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

שאלות נפוצות

מה זה מצביע ב-C?

מצביע הוא משתנה שהערך שלו הוא כתובת זיכרון: המיקום של אובייקט אחר בזיכרון. int *p = &age; גורם ל-p להחזיק את הכתובת של age, ואז *p קורא את המשתנה age או כותב אליו דרך הכתובת הזו.

מה ההבדל בין * ל-& ב-C?

&x הוא אופרטור הכתובת: הוא מפיק את הכתובת שבה x נמצא. *p הוא אופרטור ה-dereference: הוא הולך לכתובת ששמורה ב-p ונותן לכם את האובייקט שנמצא שם. הם מבטלים זה את זה, כך ש-*&x הוא פשוט x.

למה ה-* מופיע פעמיים: בהצהרה וגם בשימוש במצביע?

אלה שני שימושים שונים באותו סימן. ב-int *p; ה-* הוא חלק מהטיפוס, ואומר "p הוא מצביע ל-int". ב-*p = 5; ה-* הוא אופרטור ה-dereference, ואומר "שמור 5 בכתובת ש-p מחזיק". קריאת ההצהרה כ"*p הוא int" עושה סדר בשניהם.

למה צריך מצביעים ב-C?

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

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל