Menu

מצביעים ומערכים ב-C: דעיכה, arr[i] = *(arr+i) והמלכודת של sizeof

ב-C שם של מערך הופך למצביע לאיבר הראשון שלו כמעט בכל מקום שבו משתמשים בו. העמוד מסביר את הדעיכה הזו, למה arr[i] הוא ממש *(arr+i), למה הגודל של מערך חייב לעבור בנפרד לפונקציות, ובמה מצביע למערך שונה ממערך של מצביעים.

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

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

העמוד הזה מותח את הקו בדיוק.

דעיכת מערך: הכלל האחד שמאחורי הכול

כמעט בכל ביטוי, שם של מערך מומר אוטומטית למצביע לאיבר הראשון שלו. לזה קוראים דעיכה (decay). arr הופך ל-&arr[0], מטיפוס int *.

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

יש בדיוק שלושה מקומות שבהם דעיכה לא קורית: כאופרנד של sizeof, כאופרנד של &, וכשמאתחלים מערך תווים ממחרוזת מילולית. בכל מקום אחר, הניחו שיש דעיכה.

arr[i] הוא ממש *(arr + i)

תקן C מגדיר את אופרטור האינדקס במונחים של אריתמטיקה של מצביעים: a[b] פירושו *(a + b). גישה לפי אינדקס היא לא תכונה נפרדת: היא קיצור.

נובעות מזה שתי השלכות. ראשית, אפשר לגשת לפי אינדקס גם למצביע: p[2] הוא *(p + 2), ולכן פונקציות שמקבלות מצביע עדיין יכולות להשתמש בתחביר המוכר arr[i]. שנית, 2[arr] מתקמפל: *(2 + arr) היא אותה כתובת כמו *(arr + 2). אף אחד לא כותב את זה מחוץ לחידון, אבל זה סוגר את השאלה אם גישה לפי אינדקס היא "באמת" אריתמטיקה של מצביעים.

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

בגלל הדעיכה, פונקציה אף פעם לא מקבלת מערך. היא מקבלת מצביע.

שלושה דברים ששווה להוציא מהדוגמה הזו.

הגודל ב-int arr[100] הוא תיעוד, ותו לא. הקומפיילר כותב מחדש את הפרמטר ל-int *arr ואף פעם לא בודק שהעברתם 100 של משהו.

האורך חייב לעבור בנפרד. לפונקציה יש כתובת ואין לה מושג עד כמה רחוק הנתונים משתרעים. זו הסיבה שכמעט כל פונקציית C שמקבלת מערך מקבלת גם מספר: memcpy(dst, src, n), fread(ptr, size, count, f), qsort(base, nmemb, size, cmp). מחרוזות הן החריג רק כי הן נושאות סמן סוף משלהן, ה-'\0'.

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

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

המלכודת של sizeof

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

ב-main, sizeof data הוא 40 בתים והאורך מחושב ל-10. בתוך inspect, sizeof arr הוא הגודל של מצביע, 8 במחשב של 64 ביט, וה"אורך" יוצא 2. הקוד נראה זהה והוא שגוי בשקט.

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

& של מערך הוא לא אותו טיפוס

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

אותה כתובת התחלה, צעד שונה. ל-arr יש טיפוס int * והוא צועד באיבר אחד; ל-&arr יש טיפוס int (*)[5] והוא צועד במערך כולו. זה המנגנון שגורם להעברה של שורה ממערך דו-ממדי לעבוד, והוא מוסבר בעמוד על מערכים רב-ממדיים.

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

שתי הצהרות שנראות דומות ואומרות דברים הפוכים:

int *p[5];     // מערך של 5 מצביעים ל-int
int (*q)[5];   // מצביע למערך של 5 int

ל-[] יש קדימות גבוהה יותר מזו של *, ולכן int *p[5] מנותח כ"p הוא מערך, של מצביעים". הסוגריים ב-int (*q)[5] מכריחים את * להיקשר קודם: "q הוא מצביע, למערך".

int *p[5]                     int (*q)[5]

 p[0] -> [ int ]               q -> [ int | int | int | int | int ]
 p[1] -> [ int ]                     one contiguous block of 5
 p[2] -> [ int ]
 p[3] -> [ int ]
 p[4] -> [ int ]
 five separate addresses

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

שימו לב ל-(*q)[0]: מבצעים dereference למצביע כדי לקבל את המערך, ואז ניגשים אליו לפי אינדקס. הסוגריים שוב הכרחיים, מאותה סיבה של קדימות.

איפה מערכים ומצביעים באמת שונים

זכרו את הטבלה הקצרה הזו והבלבול ייעלם:

מערך int arr[10]מצביע int *p
מה זה10 int פרוסים בזיכרוןמשתנה אחד שמחזיק כתובת
sizeof40: כל הבלוק8: רק המצביע
אפשר להשים לולא: arr = x; היא שגיאהכן: p = x; בסדר
& נותןint (*)[10]int **
מאיפה בא הזיכרוןמוצהר יחד עם המערךמכל מקום שאליו מצביעים

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

שאלות נפוצות

האם מערכים ומצביעים הם אותו דבר ב-C?

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

למה sizeof(arr) נותן תשובה שגויה בתוך פונקציה?

כי הפרמטר הוא לא מערך. void f(int arr[]) נכתב מחדש בשקט ל-void f(int *arr), כך ש-sizeof(arr) מודד מצביע (8 בתים ברוב המחשבים), לא את המערך המקורי. העבירו את האורך כפרמטר נפרד.

מה ההבדל בין int *p[5] ל-int (*p)[5]?

int *p[5] הוא מערך של 5 מצביעים ל-int. int (*p)[5] הוא מצביע יחיד למערך של 5 int. הסוגריים קושרים את ה-* לשם קודם; בלעדיהם [] מנצח כי הקדימות שלו גבוהה יותר.

האם arr[i] באמת זהה ל-*(arr + i)?

כן: התקן מגדיר את a[b] כ-*(a + b). זו גם הסיבה שהביטוי המוזר i[arr] מתקמפל ועובד: חיבור הוא חילופי, כך ש-*(i + arr) הוא אותו איבר. לעולם אל תכתבו את זה בקוד אמיתי, אבל זה מוכיח את הכלל.

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

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

להתחיל