Menu

העמסת אופרטורים ב-C++: אופרטורים +, == ו-<< משלכם

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

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

להרגיש כמו טיפוס מובנה

אתם כבר יודעים ש-std::string מאפשר לכתוב a + b כדי לשרשר ו-cout << s כדי להדפיס. אלה לא טריקים מיוחדים של הקומפיילר: אלה פונקציות רגילות עם שמות משונים. העמסת אופרטורים היא התכונה שמאפשרת למחלקות שלכם להתחבר לאותו תחביר, כך שטיפוס Vector2 או Money יכול להתחבר, להשתוות ולהודפס בדיוק כמו int.

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

שימו לב שהפונקציה מקבלת את שני האופרנדים כ-const&: חשבון לא אמור לשנות את הקלטים שלו, והפניות חוסכות העתקה. היא מחזירה Vector2 חדש לפי ערך: p + q חייב להפיק תוצאה חדשה בלי לגעת ב-p או ב-q, בדיוק כמו ש-2 + 3 לא משנה את 2.

פונקציית איבר מול פונקציה חיצונית

יש שני מקומות להגדיר אופרטור: כאיבר של המחלקה או כפונקציה חופשית (חיצונית). כאיבר, האופרנד השמאלי הוא ה-this המרומז, ולכן אופרטור בינארי מקבל רק פרמטר מפורש אחד:

ה-const שאחרי רשימת הפרמטרים חשוב: a + b לא אמור לשנות את a, ולכן האיבר מסומן const. השתמשו בצורת האיבר לאופרטורים שקשורים מטבעם לאופרנד השמאלי ולא צריכים המרות עליו: +=, [], (), ->, ואופרטורים אונריים כמו -x או ++x.

הבעיה עם איברים: אי אפשר להמיר את האופרנד השמאלי. עם ה-operator+ שהוא איבר למעלה, a + 50 עובד (50 מומר ל-Money בצד הימני), אבל 50 + a לא מתקמפל: האופרנד השמאלי 50 הוא int, ואי אפשר להוסיף פונקציית איבר ל-int. אופרטור חיצוני פותר את זה, כי שני האופרנדים הם פרמטרים מפורשים ושניהם יכולים לעבור המרה:

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

העמסת אופרטור ה-stream

האופרטור שמעמיסים הכי הרבה הוא << להדפסה. אי אפשר להפוך אותו לאיבר של המחלקה שלכם, כי האופרנד השמאלי הוא std::ostream (כמו cout) ולא הטיפוס שלכם, ו-ostream לא שייך לכם. לכן הוא תמיד פונקציה חיצונית שמקבלת את ה-stream בהפניה שאינה const ומחזירה אותו:

שני פרטים גורמים לזה לעבוד. ה-stream מועבר ומוחזר בהפניה (ostream&): אי אפשר להעתיק streams, והחזרת אותו stream היא מה שמאפשר לשרשר cout << "p = " << p << "\n". כל << מחזיר את ה-stream כדי של-<< הבא יהיה למה להיקשר. שכחו את return os; והשרשור נשבר.

אופרטורי השוואה

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

כתיבה ידנית של כל שש ההשוואות (==, !=, <, <=, >, >=) מייגעת ומועדת לטעויות. C++20 הוסיפה את אופרטור ההשוואה התלת-כיוונית <=> (ה"חללית"). הגדרתו כ-default יחד עם == מייצרת בשבילכם את כל ההשוואות:

= default אומר לקומפיילר להשוות איברים לפי סדר ההצהרה, וזה בדיוק הסדר הלקסיקוגרפי שהייתם כותבים ידנית. העדיפו את זה בקומפיילרים מודרניים.

אופרטור ההשמה והמלכודות שלו

operator= (השמת העתקה) הוא מיוחד: הקומפיילר מייצר אחד בשבילכם, ולמחלקות פשוטות ברירת המחדל הזו נכונה. צריך לכתוב אחד משלכם רק כשהמחלקה מנהלת משאב, כמו זיכרון גולמי או ידית קובץ, שבו העתקה איבר אחר איבר תהיה שגויה. החתימה הקנונית מחזירה *this בהפניה כדי שהשמות יוכלו להשתרשר (a = b = c):

שתי מלכודות חיות בפונקציה הקצרה הזו. הראשונה היא בדיקת ההשמה העצמית if (this == &other): בלעדיה, a = a היה מבצע delete[] data ואז קורא מה-other.data ששוחרר זה עתה: התנהגות לא מוגדרת. השנייה היא שהסדר חשוב: בגרסה שכותבים ידנית אסור למחוק את החוצץ הישן לפני שהעתקתם בבטחה את החדש (מימוש אמיתי לרוב מקצה קודם, או משתמש ב-idiom של copy-and-swap, כך שהקצאה שנכשלה משאירה את האובייקט שלם).

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

הבא בתור: מצייני גישה

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

שאלות נפוצות

מהי העמסת אופרטורים ב-C++?

העמסת אופרטורים מאפשרת להגדיר מה המשמעות של אופרטורים מובנים כמו +, == או << עבור הטיפוסים שלכם. כותבים פונקציה עם שם מיוחד, operator+, operator== וכן הלאה, והקומפיילר קורא לה בכל פעם שהאופרטור מופיע עם אופרנדים מהמחלקה שלכם. כך string + string משרשר ו-cout << obj מדפיס אובייקט מותאם.

האם אופרטורים צריכים להיות פונקציות איבר או פונקציות חיצוניות (friend) ב-C++?

השתמשו בפונקציית איבר כשהאופרנד השמאלי הוא המחלקה שלכם ולא יזדקק להמרות (למשל +=, [], ()). השתמשו בפונקציה חיצונית (לעיתים קרובות friend) כשהאופרנד השמאלי עשוי להיות טיפוס מובנה או כשרוצים המרות סימטריות בשני הצדדים. זה הכרחי עבור operator<<, כי האופרנד השמאלי הוא std::ostream ולא המחלקה שלכם.

אילו אופרטורים ב-C++ אי אפשר להעמיס?

אי אפשר להעמיס את :: (פענוח טווח), . (גישה לאיבר), .* (גישה דרך מצביע לאיבר), ?: (טרנרי) ו-sizeof. אי אפשר גם להמציא אופרטורים חדשים לגמרי או לשנות את מספר האופרנדים או את הקדימות של אופרטור: + תמיד בינארי ובאותה קדימות, בין אם הוא מחבר int ובין אם את ה-Vector2 שלכם.

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

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

להתחיל