ויקימחשבים
Advertisement

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


Achtung

שימו לב:

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


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

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

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

/* Useless attempt for a function that swaps variables' values. */
void swap(int x, int y)
{
  int 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.

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

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

חזרה לניסיון כתיבת 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, ולכן הוא מצביע לp. אם נבקש לראות את הערך בתא ש 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 שימושית מאד. נדבר עוד עליה בהקצאת זיכרון דינאמית.



0px

כדאי לדעת:

כדי להשתמש בקבוע NULL, אפשר לרשום בראשי הקבצים המשתמשים בו:
#include <stddef.h>
.


הערך המוצבע[]

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

*<ptr>

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

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

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

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

p = &m1;

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


Thumbs up

עכשיו תורך:

נסה לחזות מה ידפיס הקוד.



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

int m0;
p = &m0;
int m1;

מצביע p לm0:

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

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

השורה:

*p = 3;

משימה למשתנה אליו מצביע p את הערך p. היות ש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(int *px, int *py)
{
  int 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, ולכן בסוף הפונקציה, הזיכרון ייראה כך:

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

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

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

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

עניינים סגנוניים[]

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

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

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); ישנם סגנונות אחרים השוללים מוסכמות אלה. אין הכרעה חד משמעית בשאלות אלה.


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