Menu

אריתמטיקה של מצביעים ב-C: חיבור, חיסור ומעבר על זיכרון

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

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

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

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

הוספת 1 מזיזה באובייקט אחד, לא בבית אחד

קחו מצביע, הוסיפו 1, והדפיסו את שתי הכתובות:

ה-int * קופץ 4 בתים, ה-double * קופץ 8, ה-char * קופץ 1: כל אחד נוחת בדיוק במקום שבו האובייקט הבא מהטיפוס שלו היה מתחיל. הכלל פשוט:

ptr + n  ==  (address in ptr) + n * sizeof(*ptr)

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

מעבר על מערך עם מצביע

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

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

 scores[0] scores[1] scores[2] scores[3] scores[4]
+---------+---------+---------+---------+---------+
|   88    |   92    |   75    |   60    |   100   |
+---------+---------+---------+---------+---------+
 ^         ^                                       ^
 p         p+1                                     scores+5
 (start)   (4 bytes later)                         (one past the end)

הקשר בין שתי הצורות מדויק ושווה לזכור: scores[i] מוגדר כ-*(scores + i). עוד על זה בעמוד על מצביעים ומערכים.

הגדלה, הקטנה והמלכודת של *p++

++ ו--- עובדים על מצביעים עם אותה הכפלה.

הביטוי שמבלבל את כולם הוא *p++. ++ בצורת postfix נקשר חזק יותר מה-*, ולכן המשמעות היא "קח את p הנוכחי, קדם את p, ואז בצע dereference לערך הישן":

שלושה ביטויים, שלוש משמעויות שונות:

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

*p++ הוא C אידיומטי, כך כותבים העתקות של מחרוזות ו-buffers, אבל כתבו את הסוגריים כשאתם מתכוונים לכל דבר אחר.

חיסור של שני מצביעים

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

טיפוס התוצאה הוא ptrdiff_t, טיפוס שלם עם סימן מ-<stddef.h>, שמדפיסים עם %td. חיסור מצביעים לתוך שני מערכים שונים מפיק תוצאה לא מוגדרת: התקן מגדיר את הפעולה רק בתוך אובייקט אחד.

אפשר גם לחסר מספר שלם ממצביע (p - 3 חוזר שלושה איברים אחורה), אבל אי אפשר לחבר שני מצביעים. "הסכום של שתי כתובות" לא מייצג שום דבר.

השוואת מצביעים

אופרטורי יחס עובדים על מצביעים לתוך אותו מערך, והם אומרים בדיוק את מה שפריסת הזיכרון מרמזת: p < q הוא אמת כש-p מצביע על איבר מוקדם יותר.

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

הכלל של מקום אחד אחרי הסוף

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

int arr[4];

 arr[0]   arr[1]   arr[2]   arr[3]   (no element)
+------+ +------+ +------+ +------+  +- - - -+
|      | |      | |      | |      |  |       |
+------+ +------+ +------+ +------+  +- - - -+
 ^                                    ^
 arr                                  arr + 4
 legal to use                         legal to FORM and COMPARE
                                      never legal to dereference

הכלל הזה הוא מה שהופך את צורת הלולאה הסטנדרטית לתקינה:

for (int *p = arr; p != arr + n; p++) {
    /* ... */
}

באיטרציה האחרונה p הופך ל-arr + n, ההשוואה נכשלת, והלולאה יוצאת, בלי לקרוא אף פעם את המקום הזה.

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

int *bad1 = arr + n + 1;   // לא מוגדר: רחוק מדי אחרי הסוף
int *bad2 = arr - 1;       // לא מוגדר: לפני ההתחלה

השני חשוב בפועל: לולאה אחורה שכתובה כ-for (int *p = arr + n - 1; p >= arr; p--) מחשבת את arr - 1 בהקטנה האחרונה שלה. זה עובד בכל קומפיילר נפוץ ועדיין לא מוגדר רשמית. הלולאה האחורה הנקייה נמנעת מזה:

מצביעי void והאריתמטיקה שאי אפשר לעשות

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

void *v = buffer;
// v + 1;              // לא C תקני: אין גודל להכפיל בו
char *b = v;
b + 1;                 // בסדר: בית אחד קדימה

GCC ו-Clang מקבלים אריתמטיקה על void * כהרחבה שמתייחסת אליו כמו אל char *, כך שקוד שנשען על זה מתקמפל עד היום שבו בונים אותו במקום מחמיר יותר. קמפלו עם -std=c17 -pedantic אם אתם רוצים שיגידו לכם.

מה זה נותן לכם

אריתמטיקה של מצביעים היא לא טריק אופטימיזציה שפונים אליו מדי פעם: זה המנגנון שמתחת לגישה לפי אינדקס במערכים, לטיפול במחרוזות ולכל מעבר על buffer בספרייה הסטנדרטית. strlen הוא מצביע שמתקדם עד ה-'\0' המסיים ומחוסר מההתחלה. memcpy הם שני מצביעים שצועדים יחד.

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

שאלות נפוצות

מה עושה ptr + 1 ב-C?

הוא מזיז את המצביע קדימה באובייקט אחד, לא בבית אחד. עבור int * במחשב טיפוסי זה 4 בתים; עבור double * זה 8. הקומפיילר מכפיל ב-sizeof(*ptr) בשבילכם, ולכן הטיפוס של המצביע חשוב.

אפשר לחסר שני מצביעים ב-C?

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

האם מותר להצביע מקום אחד אחרי הסוף של מערך?

כן: יצירת מצביע לאיבר אחד אחרי האחרון מותרת במפורש, כדי שלולאות כמו for (int *p = arr; p != arr + n; p++) יעבדו. מותר לחשב את המצביע הזה ולהשוות אליו, אבל dereference שלו הוא התנהגות לא מוגדרת.

למה *p++ לא זהה ל-(*p)++?

*p++ מגדיל את המצביע ומבצע dereference לערך הישן: הוא קורא את האיבר הנוכחי ואז מתקדם. (*p)++ משאיר את המצביע במקומו ומגדיל את הערך שהוא מצביע עליו. ++ בצורת postfix נקשר חזק יותר מ-*, ולכן הסוגריים הם מה שמשנה את המשמעות.

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

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

להתחיל