מצביעים
הגדרת הבעיה
ננסה לחשוב על מימוש אחר לפונקציית ה-swap שלנו על מנת שהיא תוכל לערוך את המשתנים בפונקציה הקוראת.
לפי עקרון הבידוד שבהגדרנו בפרק הפונקציות, קיימת בעיה עקרונית לעשות זאת - פונקציות יכולות לגשת לארגומנטים והמשתנים הלוקאליים שלהן, ואנחנו רוצים לתת להן גישה למידע שנמצא בזכרון ב-stack frame אחר מהם. למעשה, אם נזקק את הבעיה לבסיסה, אנחנו נרצה דרך להגיד לפונקציה swap ״איפה״ בזכרון המשתנים שאנו רוצים להחליף נמצאים.
למזלנו, כפי שראינו כשדנו בנושא הזכרון בתחילת הקורס, לשאלה ״איפה נמצא המשתנה x בזכרון״ יש תשובה פשוטה - הכתובת שלו בזכרון! וכפי שראינו, האופרטור & נותן את אותה הכתובת. מעתה, נתחיל להשתמש בכתובות אלו ע״י משתנים מיוחדים הנקראים מצביעים.
מצביעים
מצביע (pointer) הוא משתנה שערכו הוא כתובת של משתנה אחר בזכרון.
נגדיר שני אופרטורים חדשים שישמשו אותנו עם מצביעים:
-
אופרטור ה-address-of (לקיחת הכתובת):
&. עבור משתנה כלשהוx, הביטוי&xהוא הכתובת של המשתנהxבזכרון. -
אופרטור ה-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);
}
נפרק:
-
הפונקציה
swapכעת מקבלת שני משתנים מטיפוס* intבמקום מטיפוסint. זהו הסינטקס למשתנה שהוא מצביע למשתנה מטיפוסint. -
בקריאה לפונקציה אנחנו מעבירים את
&a, &bבמקום אתa, b. אופרטור לקיחת הכתובת לוקח אתa, bממשתנים מטיפוסintומחזיר את הכתובת שלהם - מצביעים אליהם מטיפוס* intכפי שהפונקציה דורשת. -
הפונקציה משתמשת באופרטור ה-dereference כמה פעמים - למשל, בשורה הראשונה היא כותבת למשתנה
tmpאת הערך אליוa_ptrמצביעה ע״י האופרטורa_ptr*.
הסינטקס עבור מצביעים יכול להיות מאוד מבלבל - זו בעיה מוכרת בקרב מתכנתי C מתחילים, ולכן נקדיש זמן לפרק אותו בצורה מסודרת ונלמד עם הרבה דוגמאות. נמשיך בהגדרה פורמאלית של הסינטקס.
אתחול ושימוש של מצביע למשתנה מסוג type נראה כך:
type var = some_value;
type *ptr = &var;
type another_var = *ptr;
נפרק את השורות שכתבנו:
-
בשורה הראשונה אנחנו מגדירים משתנה כללי מטיפוס
type, שיכול להיות כל טיפוס -int,charוכו’. -
בשורה השנייה אנחנו מצהירים על משתנה מטיפוס
* type. משתנה מהטיפוס* typeהוא מצביע למשתנה אחר מטיפוסtype. ערכו של המצביע הוא הכתובת של המשתנהvar, הנלקח ע״י אופרטור ה-&. -
לבסוף אנחנו מגדירים משתנה נוסף מטיפוס
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 מכמה סיבות:
- הוא פופולרי מאוד.
- כאשר אנחנו כותבים
int *xptr = &x, יש הגיון מסוים בטיפוסים - המשתנה*xptrהוא משתנה מסוגint, בדיוק כמו המשתנהx. - וגם כי…
הצהרה על מספר מצביעים בשורה אחת
באופן כללי, כפי שדיברנו בעבר, רצוי להמנע מהצהרה של כמה משתנים באותה שורה. במצביעים ספציפית קיים באג קשה מאוד לפענוח שקל ליפול אליו כאשר עושים זאת.
כאשר מצהירים על מספר משתנים באותה שורה:
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 את הבאה אחריה.
בשביל לפתור את הבעיה נבחן מה השלבים שאנחנו צריכים לבצע. צריכים להתקיים בין האותיות היחסים הבאים:
a <= ba <= cb <= 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.