שם נוסף לאותו דבר
בעמוד הפרמטרים של פונקציות, כל ארגומנט הועתק לתוך הפונקציה. ההעתקה הזו היא הסיבה שפונקציה לא יכולה לשנות את המשתנה של מי שקרא לה: היא רואה רק את העותק שלה. הפניה שוברת את החומה הזו. היא כינוי: שם שני שקשור למשתנה קיים וחולק איתו בדיוק את אותו זיכרון.
יוצרים הפניה עם & בהצהרה. ברגע שהיא נקשרה, אי אפשר להבחין בינה לבין המקורי:
שני כללים הופכים הפניות לבטוחות וצפויות: הפניה חייבת להיות מאותחלת ברגע שמצהירים עליה (int& r; היא שגיאת קימפול), ולעולם אי אפשר להפנות אותה מחדש למשהו אחר אחר כך. השמה להפניה תמיד כותבת למה שהיא נקשרה אליו במקור.
העברה לפי הפניה: לתת לפונקציה להגיע בחזרה
התמורה האמיתית היא בפונקציות. שימו & על פרמטר והפונקציה מקבלת כינוי לארגומנט של הקורא במקום עותק. עכשיו שינויים בתוך הפונקציה נראים גם מחוץ לה:
הסירו את ה-& ו-addBonus תגדיל עותק זמני, ו-total יישאר 100. התו היחיד הזה הוא כל ההבדל. זו הדרך הקנונית לכתוב פונקציה שמחזירה יותר מתוצאה אחת או עורכת את הקלט שלה במקום. הדוגמה הקלאסית היא החלפה בין שני משתנים:
בלי הפניות, swapValues הייתה מחליפה רק עותקים מקומיים, ו-x/y היו נשארים 1 2. (בספרייה הסטנדרטית כבר יש std::swap, אבל כתיבה עצמית שלה מראה בדיוק מה פרמטר הפניה נותן לכם.)
הפניות const: קוראים מהר ומבטיחים לא לגעת
העברה לפי הפניה גם חוסכת העתקה, ובאובייקט גדול ההעתקה הזו יכולה להיות יקרה. אבל פרמטר T& רגיל מאותת "ייתכן שאשנה את זה", וזה מטעה כשרוצים רק לקרוא. הפתרון הוא const T&: מקבלים את המהירות של הפניה בלי העתקה וגם הבטחה שהקומפיילר אוכף שהפונקציה לא תשנה את הארגומנט.
הפניה שאינה const יכולה להיקשר רק למשתנה שאפשר לשנות, אבל הפניית const יכולה להיקשר גם לליטרלים ולאובייקטים זמניים, ולכן greet("literal works too") מתקמפל. כלל אצבע מעשי לבחירת טיפוס פרמטר:
void f(int x) // טיפוס זול, קריאה בלבד -> פשוט מעתיקים
void f(const string& s) // טיפוס כבד, קריאה בלבד -> הפניית const
void f(string& s) // מתכוונים לשנות את האובייקט של הקורא
השתמשו כברירת מחדל ב-const T& לכל טיפוס מחלקה שרק קוראים (string, vector, ה-structs שלכם), ושמרו הפניה שאינה const למקרים שבהם באמת מתכוונים לכתוב בחזרה.
החזרת הפניה
פונקציה יכולה גם להחזיר הפניה, וכך למסור לקורא כינוי למשהו שכבר קיים. זה נפוץ בקוד שדומה למכולות: זה מה שמאפשר ל-v[i] = 5 לעבוד, וזה מה שהעמסת אופרטורים עושה מאחורי הקלעים:
מכיוון ש-at מחזירה int&, ביטוי הקריאה at(data, 1) הוא בעצמו lvalue שאפשר להשים אליו. החזירו int רגיל במקום, ו-at(data, 1) = 42 לא יתקמפל: הייתם משימים לעותק זמני.
המלכודת הגדולה: הפניות תלויות
הפניה לא מחזיקה בשום דבר: היא רק מצביעה על זיכרון שחי במקום אחר. אם הזיכרון הזה מת בזמן שההפניה עדיין בשימוש, יש לכם הפניה תלויה, וקריאה דרכה היא התנהגות לא מוגדרת: היא עלולה להדפיס זבל, לקרוס, או להיראות כאילו היא עובדת עד שהיא הורסת לכם את היום בייצור. הטעות הקלאסית היא החזרת הפניה למשתנה מקומי:
int& broken() {
int local = 42;
return local; // באג: local מושמד כש-broken() חוזרת
} // ההפניה שהוחזרה תלויה
int main() {
int& r = broken();
cout << r << "\n"; // התנהגות לא מוגדרת: קורא זיכרון מת
}
המשתנה המקומי local נעלם ברגע ש-broken חוזרת, ולכן ההפניה מצביעה על שטח מחסנית שכבר נלקח בחזרה. החזירו הפניה רק למשהו שחי יותר זמן מהקריאה: פרמטר שהועבר לפי הפניה, משתנה איבר או משתנה static. אם הערך מחושב בתוך הפונקציה, החזירו לפי ערך במקום זאת ותנו לקומפיילר לבטל את ההעתקה באופטימיזציה. אותה מלכודת פוגעת בלולאות מבוססות טווח ובכל הפניה שנקשרה לאובייקט זמני: לעולם אל תחזיקו הפניה אחרי שהדבר שהיא נותנת לו שם סיים את חייו.
הבא בתור: העמסת פונקציות
הפניות נותנות לכם כפתור שני בכל פרמטר, עותק מול כינוי, ניתן לשינוי מול const, והכפתור הזה משפיע ישירות על הנושא הבא. בהמשך, העמסת פונקציות מאפשרת להגדיר כמה פונקציות עם אותו שם אבל רשימות פרמטרים שונות, והקומפיילר בוחר את הנכונה לפי התאמה של טיפוסי הארגומנטים, כולל האם הם מועברים לפי ערך, לפי הפניה או לפי הפניית const.
שאלות נפוצות
מהי הפניה ב-C++?
הפניה היא כינוי למשתנה קיים: שם נוסף לאותו זיכרון. יוצרים אחת עם & בהצהרה: int& r = x;. מאותו רגע r ו-x ניתנים להחלפה: שינוי של אחד משנה את השני. חייבים לאתחל הפניות בזמן ההצהרה, ולעולם אי אפשר להפנות אותן מחדש למשתנה אחר.
מה ההבדל בין העברה לפי ערך להעברה לפי הפניה ב-C++?
העברה לפי ערך (void f(int x)) מעתיקה את הארגומנט, כך שהפונקציה עובדת על עותק משלה והמשתנה של הקורא לא נפגע. העברה לפי הפניה (void f(int& x)) נותנת לפונקציה גישה ישירה למשתנה של הקורא, כך ששינויים נראים אחרי הקריאה, ולא נוצר עותק, וזה חשוב לאובייקטים גדולים.
מתי להשתמש בפרמטרים מסוג הפניה ל-const ב-C++?
השתמשו ב-const T& כשפונקציה צריכה רק לקרוא פרמטר אבל יקר להעתיק את הטיפוס (string, vector, structs גדולים). מקבלים את המהירות של הפניה בלי העתקה, ועוד הבטחה של הקומפיילר שהפונקציה לא תשנה את הערך של הקורא. לטיפוסים זולים כמו int או double, העברה רגילה לפי ערך פשוטה יותר ומהירה באותה מידה.