פרמטרים מול ארגומנטים
הפרמטרים של פונקציה הם המשתנים בעלי השם בהגדרה שלה; הארגומנטים הם הערכים בפועל שמעבירים לה כשקוראים לה. העמוד הקודם הראה איך מגדירים פונקציות וקוראים להן; העמוד הזה עוסק באופן שבו הערכים האלה בעצם נכנסים, כי C++ נותנת כמה דרכים, והבחירה משפיעה גם על נכונות וגם על מהירות.
ברירת המחדל ב-C++ היא העברה לפי ערך (pass by value): הפונקציה מקבלת עותק.
בתוך addTen, n הוא משתנה נפרד שמאותחל מ-score. השמה חדשה ל-n נוגעת רק בעותק הזה, ולכן score נשאר ללא שינוי ב-main. זה בטוח וצפוי: הפונקציה לא יכולה לדרוס בטעות את הנתונים שלכם, וזו בדיוק הסיבה שזו ברירת המחדל.
העברה לפי הפניה: לתת לפונקציה לשנות את הקורא
לפעמים רוצים שהפונקציה תשנה את המשתנה של הקורא. הוסיפו & לטיפוס הפרמטר והוא הופך להפניה (reference): שם נוסף למקור, לא עותק:
ההבדל היחיד מהדוגמה הראשונה הוא ה-&, אבל עכשיו n ו-score הם אותו אובייקט. זו הדרך המקובלת "להחזיר" יותר מערך אחד או לעדכן משהו במקום. שימוש קלאסי הוא החלפה בין שני משתנים:
בלי ה-&, swapValues הייתה מערבבת שני עותקים ו-main לא הייתה רואה שום שינוי: באג נפוץ מאוד אצל מתחילים.
הפניות const: גישה זולה לקריאה בלבד
העברה לפי ערך מעתיקה את הארגומנט. עבור int זה לא עולה כלום, אבל העתקה של string או vector גדולים בכל קריאה היא עבודה אמיתית ומבוזבזת. הפתרון הוא הפניית const (const T&): מקבלים את המהירות של הפניה (בלי העתקה) וגם הבטחה שהמהדר אוכף, שהארגומנט לא ישתנה.
כלל אצבע שימושי: העבירו טיפוסים מובנים קטנים (int, double, char, bool, מצביעים) לפי ערך, והעבירו אובייקטים גדולים שצריך רק לקרוא לפי הפניית const. שמרו T& רגיל שאינו const למקרים שבהם אתם באמת מתכוונים לשנות את האובייקט של הקורא.
מלכודת עדינה: int& n רגיל לא יכול להיקשר לערך זמני או לליטרל. addTen(5) מהדוגמה הראשונה לא היה מתהדר אם הפרמטר היה int&, כי 5 אינו משתנה שאפשר לתת לו שם נוסף. const int& כן יכול להיקשר ל-5, וזו עוד סיבה לכך שהפניות const נפוצות כל כך.
ארגומנטים ברירת מחדל
אפשר לתת לפרמטר ערך חלופי כך שהקוראים יוכלו להשמיט אותו. אם הארגומנט חסר, משתמשים בברירת המחדל:
שני כללים מכשילים אנשים. ראשית, ברירות המחדל חייבות להיות בסוף: ברגע שלפרמטר יש ברירת מחדל, גם לכל הפרמטרים שאחריו חייבת להיות. אי אפשר לכתוב void f(int a = 1, int b), כי לא הייתה דרך לספק את b תוך דילוג על a. שנית, כשפונקציה מוצהרת ב-header ומוגדרת במקום אחר, שימו את ברירת המחדל רק בהצהרה, ולעולם אל תחזרו עליה בהגדרה: חזרה עליה היא שגיאת הידור.
העברת מערכים ו-vectors
מערך גולמי עובר decay למצביע כשמעבירים אותו, ולכן הפונקציה מאבדת את הגודל שלו. כמעט תמיד מעבירים את האורך יחד איתו:
מכיוון שהמערך הפך למצביע, sizeof(arr) בתוך sum היה נותן את הגודל של מצביע ולא של המערך: באג ידוע לשמצה. ב-C++ מודרנית העדיפו std::vector (או std::span ב-C++20) שמועבר לפי הפניית const, ונושא את הגודל שלו בעצמו:
שימו לב ל-const&: השמיטו אותו וכל קריאה תעתיק את כל ה-vector. עבור vector של ארבעה איברים זה לא מזיק, אבל עבור מיליון איברים זה חור ביצועים שקט.
פרמטרים מסוג מצביע
אפשר גם להעביר מצביע (T*). כמו הפניה, זה מאפשר לפונקציה להגיע לנתונים של הקורא, אבל אפשר לכוון מצביע מחדש והוא יכול להיות null, ולכן זה הכלי הנכון כש"אין ערך" היא אפשרות לגיטימית:
הקורא מעביר &value כדי לשתף את הכתובת שלו, והפונקציה כותבת דרך *out. ההבדל המרכזי מהפניות: מצביע יכול להיות nullptr, ולכן פונקציה שמקבלת מצביע צריכה לבדוק לפני dereference. דילוג על הבדיקה הזו ו-dereference של מצביע null הם undefined behavior, בדרך כלל קריסה. אם "אין ערך" אף פעם לא הגיוני, הפניה נקייה יותר, כי היא בכלל לא יכולה להיות null.
הבא בתור: הפניות
פרמטרים הם המקום שבו הפניות מוכיחות את עצמן, אבל הפניות הן תכונה בפני עצמה: שמות נוספים שאפשר ליצור לכל משתנה, לא רק בתוך חתימה של פונקציה. העמוד הבא צולל לאופן שבו הפניות עובדות בפני עצמן: איך מצהירים עליהן, למה חייבים לאתחל אותן מיד, מה ההבדל בין הפניית lvalue להפניית const, והדרכים העדינות שבהן הפניה יכולה להפוך לתלויה.
שאלות נפוצות
מה ההבדל בין העברה לפי ערך להעברה לפי הפניה ב-C++?
העברה לפי ערך מעתיקה את הארגומנט לפרמטר, כך ששינויים בתוך הפונקציה לא משפיעים על הקורא. העברה לפי הפניה (int&) הופכת את הפרמטר לשם נוסף למשתנה של הקורא, כך ששינויים נראים גם מבחוץ. השתמשו ב-void f(int x) כדי להעתיק וב-void f(int& x) כדי לשנות את המקור.
מתי כדאי להשתמש בפרמטר מסוג הפניית const ב-C++?
השתמשו ב-const T& כשרוצים לקרוא אובייקט גדול בלי להעתיק אותו ובלי לאפשר לפונקציה לשנות אותו, למשל void print(const string& s). זה נותן את המהירות של העברה לפי הפניה עם הבטיחות של העברה לפי ערך. לטיפוסים קטנים כמו int או char, העברה רגילה לפי ערך מהירה בדיוק באותה מידה.
מה הם ארגומנטים ברירת מחדל ב-C++?
ארגומנטים ברירת מחדל מאפשרים לפרמטר לקבל ערך חלופי כשהקורא משמיט אותו, למשל void greet(string name = "there"). ברירות המחדל חייבות להיות בפרמטרים האחרונים ברשימה, ומציינים אותן רק בהצהרה, לא בהגדרה, אם השתיים נפרדות.