לדלג לתוכן

שפת C/מצביעים

מתוך ויקיספר, אוסף הספרים והמדריכים החופשי

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

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


שימו לב:

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

הצורך במצביעים

[עריכה]

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

להלן ניסיון שגוי לכתוב פונקציה כזו:

/* Useless attempt for a function that swaps variables' values. */
void swap(char x, char y)
{
 char temp = x;
  x = y;
  y = temp;
}

מעט מחשבה תראה לנו שהפונקציה לא תעזור לנו במטרתנו. להלן קטע קוד שקורא לפונקציה:

char a = 'a', b = 'b';

swap(a, b);

printf("%c %c\n", a, b);

אם נריץ קוד זה, נראה פלט

a b

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

מבנה הזיכרון ומשתנים

[עריכה]

מודל פשוט לזיכרון

[עריכה]

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

נתבונן, לדוגמה, בקטע הקוד הבא:

char c = 'a';

int d;

התרשים הבא מראה מצב אפשרי של קטע מהזיכרון.

דוגמה לתו ושלם בזיכרון המחשב.
דוגמה לתו ושלם בזיכרון המחשב.

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

  • המשתנה c הוא מסוג תו, ותופס תא אחד. תא זה הוא (במקרה) תא מספר 2000. נהוג לומר שהוא בכתובת 2000. המשתנה מכיל את התו 'a'.
  • המשתנה d הוא מסוג מספר שלם, ותופס 4 תאים (בדוגמה זו; במחשב אחר, המשתנה יכל לתפוס 8 תאים, לדוגמה). הוא מופיע 4 תווים לאחר c, ולכן כתובתו 2004. המשתנה טרם אותחל, ולכן ערכו הוא מה שהזיכרון הכיל במקרה ב-4 תאיו (במקרה זה, 1334, לדוגמה).

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

מציאת כתובות משתנים

[עריכה]

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

&<var>

כאשר var הוא משתנה רגיל (למציאת כתובת מערך או מחרוזת, ראה הקשר בין מצביעים למערכים).

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

char c = 'a';

printf("%p", &c);

הקטע ידפיס את כתובת המשתנה c.

מחסנית הזיכרון

[עריכה]
האיור מציג בצורה גרפית את תאוריית המחסנית כך שכול ריבוע כחול מיצג פונקציה

הפונקציות בתוכנה מאוחסנות כמו במחסנית של רובה. כך שהכדור האחרון שניכנס יהיה הראשון לצאת או לחליפין הפונקציה האחרונה תצא ראשונה. בכל פעם שאנו קוראים לפונקציה , המחשב שומר בזכרונו את המצב האחרון בו היינו. אם, למשל, קראנו פעם אחת לפונקציה "printMe" מה-main, המחשב שומר בזכרונו את המצב האחרון בו הייתה ה-main - באיזו שורה היינו, מה מצב המשתנים המקומיים שלה, וכן הלאה. אם הפונקציה לה קראנו קראה לפונקציה נוספת - גם מצבה של זו נשמר, וכן הלאה. כל המידע הזה נשמר במבנה ה"מחסנית" (באנגלית - Stack), בו הנתון האחרון שנכנס(כפי שהוסבר בהתחלה) - הוא הנתון הראשון שיוצא. כשמסתיימת פעולתו של מערך - הוא נמחק מהמחסנית.

חזרה לניסיון כתיבת swap

[עריכה]

כעת, לאחר שראינו את מבנה הזיכרון, הבה נחזור חזרה לניסיון הכושל לכתיבת swap שראינו. נניח שהמשתנה a מכיל את התו 'a', והמשתנה b מכיל את התו 'b'. המשתנים a ו-b יושבים במקומות כלשהם בזיכרון. כאשר נקרא לפונקציה swap, ייווצרו המשתנים x ו-y במקומות אחרים לחלוטין בזיכרון, וערכי המשתנים a ו-b יועתקו אליהם בהתאמה. הזיכרון עשוי להראות כך:

מצביע למשתנה תו.
מצביע למשתנה תו.

בסוף הפונקציה, אכן מוחלפים ערכיהם של x ו-y:

מצביע למשתנה תו.
מצביע למשתנה תו.

כעת ברור מדוע אין לכך שום השפעה על ערכי a ו-b.

מהם מצביעים?

[עריכה]

משמעות מצביע

[עריכה]

מצביע הוא משתנה שערכו כתובת בזיכרון, ייתכן שבפרט כתובת זיכרון שבה יושב משתנה אחר.

התרשים הבא, לדוגמה, מראה מצב אפשרי של הזיכרון כאשר יש שלושה משתנים:

  • המשתנים c ו-d הם אלה שהוסברו מוקדם יותר.
  • המשתנה p הוא מצביע למשתנה מסוג תו. הערך היושב בו הוא 2000, שהוא בדיוק כתובתו של c. אומרים שp מצביע על c.
מצביע למשתנה תו.
מצביע למשתנה תו.

בתרשימים מסוג זה, לרוב מסמנים חץ מהמשתנה המצביע למשתנה שאליו הוא מצביע:

הגדרה בקוד

[עריכה]

מצהירים על מצביע כך:

<type> *<name>;

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


נתבונן, לדוגמה, בקוד הבא:

char c = 'a';

int d;

char *p = &c;

נשים לב לשורה השלישית. המשתנה p מוגדר כמצביע לתו. הוא מאותחל לכתובתו של c, ולכן הוא מצביע ל-c. אם נבקש לראות את הערך בתא ש-p מצביע עליו נגלה שהוא מצביע על הערך 'a'.

כדי להגדיר מספר מצביעים (מאותו סוג) באותה שורה, רושמים כך:

int *p0, *p1;

יעד ההצבעה

[עריכה]

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

לדוגמה:

int m0, m1;
int *p;

p = &m0;
p = &m1;

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

לאחר השורה

int *p;

מכיל p ערך שרירותי כלשהו; מכנים זאת לעתים "ערך זבל", שיכול להשתנות מריצה לריצה. הוא מצביע למקום שרירותי בזיכרון.

לאחר השורה:

p = &m0;

מצביע p לm0:

מצביע למשתנה שלם ראשון.
מצביע למשתנה שלם ראשון.

m0 וm1 לא אותחלו, ולכן הם מכילים ערכי זבל. בפרט, בדוגמה זו, m0 מכיל את הערך 99322, וm1 מכיל את הערך 1334.

לאחר השורה:

p = &m1;

מצביע p לm1:

מצביע למשתנה שלם שני.
מצביע למשתנה שלם שני.

כתובת האפס NULL

[עריכה]

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

int *p = NULL;

הכתובת NULL שימושית מאד. נדבר עוד עליה בהקצאת זיכרון דינאמית.


כדאי לדעת:

כדי להשתמש בקבוע NULL, אפשר לרשום בראשי הקבצים המשתמשים בו:

#include <stddef.h> .

הערך המוצבע

[עריכה]

כאשר מצביע מצביע לכתובת חוקית, אפשר לגשת לערך באותה כתובת. עושים כך בצורה הבאה:

*<ptr>

כאשר ptr הוא שם המצביע.

נתבונן לדוגמה בקטע הקוד הבא:

int m0;
int *p;
p = &m0;
int m1;

*p = 3;
m1 = m0 + *p;

p = &m1;

printf("%d %d %d", m0, *p, m1);

עכשיו תורכם:

נסה לחזות מה ידפיס הקוד.
הפתרון
3 6 6


כמו שראינו מקודם, לאחר השורות:

int m0;
int *p = &m0;
int m1;

מצביע p לm0:

מצביע למשתנה שלם ראשון.
מצביע למשתנה שלם ראשון.

m0 וm1 לא אותחלו, ולכן הם מכילים ערכי זבל. בפרט, שוב בדוגמה זו, m0 מכיל את הערך 99322, וm1 מכיל את הערך 1334.

השורה:

*p = 3;

משימה למשתנה אליו מצביע p את הערך 3. היות שp מצביע לm0, תשים השורה את הערך 3 לm0:

.
.


נתבונן בשורה:

m1 = m0 + *p;

ערכו של m0 הוא 3; היות שp מצביע לm0, אז *p, כלומר הערך במשתנה אליו מצביע p, גם כן 3. סכומם הוא 6 כמובן, וזה הערך שיושם בm1:

.
.

השורה:

p = &m1;

משימה לp את כתובתו של m1, ולכן יצביע p לm1:

.
.

שימוש במצביעים להעברת משתנים לפונקציה

[עריכה]

בשפת C אפשר להעביר משתנים לפונקציות בשתי צורות. העברה by value מעתיקה ערך משתנים, והעברה by reference מעתיקה את כתובת המשתנים (על ידי מצביעים).

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

void swap(char* px, char* py)
{
  char temp = *px;
  *px = *py;
  *py = temp;
}

הפונקציה מקבלת כעת שני מצביעים תו במקום תווים.

נקרא כך לפונקציה:

char a = 'a', b = 'b';

swap(&a,&b);

printf("%c %c\n", a, b);

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

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

swap(&a,&b);

שורה זו קוראת לפונקציה swap עם כתובותיהם של a וb. שוב מועתקים ערכים בקריאה לפונקציה, אך הפעם יש הבדל מהותי. היות שכתובות המשתנים הועתקו, אז px וpy אכן מצביעים למקום של המשתנים המקוריים. בתחילת הפונקציה swap, לכן, הזיכרון נראה כך:

מצביע למשתנה תו.
מצביע למשתנה תו.

במהלך הפונקציה מעתיקים בין ערכי המשתנים המוצבעים על ידי px ו-py, ולכן בסוף הפונקציה, הזיכרון ייראה כך:

מצביע למשתנה תו.
מצביע למשתנה תו.

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

זהירות בשימוש במצביעים

[עריכה]

פרק זה לוקה בחסר. אתם מוזמנים לתרום לוויקיספר ולהשלים אותו. ראו פירוט בדף השיחה.



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

עניינים סגנוניים

[עריכה]

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

[עריכה]

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

int *a_pointer;
int* another_pointer;
int     *     yet_another_pointer;

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

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

int* p0, p1, p2;

ונשים לב שרק p0 הוא מצביע (p1 וp2 הם משתנים רגילים מסוג int). המסקנה לדעתם היא שהסגנון בשורה השניה מבלבל.

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

int a;

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

בחירת שמות למצביעים

[עריכה]

ישנם סגנונות תכנות שלפיהם כל שם מצביע אמור להתחיל בp, או בp_; ישנם סגנונות (אחרים) לפיהם שם מצביע יכלול את הרצף ptr (קיצור של המילה pointer); ישנם סגנונות אחרים השוללים מוסכמות אלה. אין הכרעה חד משמעית בשאלות אלה.

גודל הטיפוס מצביע

[עריכה]

גודלו של המשתנה מסוג מצביע תמיד זהה לסוג מערכת ההפעלה. לדוגמא:
במערכת הפעלה 64bit גודל המצביע תמיד יהיה 64bit.
במערכת הפעלה 32bit גודל המצביע תמיד יהיה 32bit.
במערכת הפעלה 16bit גודל המצביע תמיד יהיה 16bit.


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