Menu

הפניות מול מצביעים ב-C++: מתי להשתמש בכל אחד

השוואה מעשית בין הפניות למצביעים ב-C++: מה משותף להם, במה הם שונים (הפניה מחדש, null, אריתמטיקה), וכלל ברור לבחירה ביניהם בקוד יומיומי.

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

שתי דרכים להתייחס למשהו

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

הגרסה הקצרה: הפניה היא כינוי. ברגע ש-int& r = x; רץ, r הוא x: אותו אובייקט, שם אחר. מצביע הוא אובייקט נפרד שבמקרה מחזיק את הכתובת של אובייקט אחר. ההבדל היחיד הזה מניע את כל השאר.

הפניה היא כינוי

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

שימו לב שאין * לגישה ואין & כדי "לקחת את הכתובת" בנקודת השימוש: קוראים וכותבים את alias בדיוק כמו int רגיל. ה-& ב-int& alias הוא חלק מהטיפוס, לא אופרטור הכתובת.

במה הם שונים

ההתנהגויות שבהמשך הן כל הסיבה ששני הכלים קיימים. זו הטבלה ששווה לשנן.

//                      reference            pointer
// must be initialized? yes                  no (but should be)
// can be null?         no                   yes (nullptr)
// can be reseated?     no                   yes
// pointer arithmetic?  no                   yes
// syntax to use it     just the name        *p  or  p->member
// taking address       &ref == &original    &p is the pointer's own address

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

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

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

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

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

אי אפשר לקרוא לגרסת ההפניה (addTax(cart)) עם "כלום", ולכן בתוך הפונקציה לעולם לא בודקים null: מובטח שהאובייקט שם. גרסת המצביע (applyDiscount(&cart)) מודיעה בנקודת הקריאה, דרך ה-&, שהארגומנט עשוי להשתנות, ומאפשרת למי שקורא להעביר nullptr כדי לומר "לא רלוונטי". בחרו את זו שההבטחה שלה מתאימה לפונקציה שלכם.

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

כלל אצבע פשוט

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

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

אם אף אחד מאלה לא חל, הפניה היא הבחירה הנקייה והבטוחה יותר, כי הקומפיילר אוכף בשבילכם "תמיד תקין, לעולם לא מופנה מחדש".

טעויות נפוצות שכדאי להימנע מהן

  • לצפות ש-ref = other יפנה מחדש. במקום זאת זה משים ערך לתוך האובייקט שההפניה מתייחסת אליו. הפניות קשורות לכל חייהן: אם צריך הפניה מחדש, השתמשו במצביע.
  • החזרת הפניה (או מצביע) למשתנה מקומי. int& f() { int x = 5; return x; } מחזירה הפניה תלויה: x מת כש-f חוזרת, ושימוש בתוצאה הוא התנהגות לא מוגדרת. אותה מלכודת פוגעת גם במצביעים (return &x;).
  • זיוף "הפניית null". כתיבת int& r = *p; כאשר p הוא nullptr היא התנהגות לא מוגדרת ברגע הגישה, לא הפניה "ריקה" בטוחה. בטאו אופציונליות עם מצביע או עם std::optional.
  • שימוש במצביע מתוך הרגל. אם הארגומנט תמיד קיים ולעולם לא תפנו אותו מחדש, הפניה מסלקת סוג שלם של בדיקות null וקריסות. אל תשלמו על יכולות שאתם לא משתמשים בהן.

הבא בתור: זיכרון דינמי

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

שאלות נפוצות

מה ההבדל בין הפניה למצביע ב-C++?

הפניה היא כינוי לאובייקט קיים: חייבים לאתחל אותה, היא לעולם לא יכולה להיות null, ואי אפשר לגרום לה אחר כך להתייחס לאובייקט אחר. מצביע הוא משתנה נפרד שמחזיק כתובת: הוא יכול להיות null, אפשר להפנות אותו למקום אחר, והוא תומך באריתמטיקת מצביעים. עם הפניות משתמשים בתחביר &, ועם מצביעים ב-*/->.

מתי להשתמש במצביע במקום בהפניה ב-C++?

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

האם הפניה יכולה להיות null ב-C++?

לא. הפניה תקינה תמיד מתייחסת לאובייקט אמיתי, ולכן לעולם לא בודקים אותה מול null. אם יוצרים הפניה מגישה דרך מצביע null (int& r = *p; כאשר p הוא null), מקבלים התנהגות לא מוגדרת, לא הפניית null. כשצריך לבטא "אולי כלום", השתמשו במצביע או ב-std::optional.

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

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

להתחיל