C++/העמסת אופרטורים: הבדלים בין גרסאות בדף

מתוך ויקיספר, אוסף הספרים והמדריכים החופשי
< C++
תוכן שנמחק תוכן שנוסף
Ybungalobill (שיחה | תרומות)
אין תקציר עריכה
Ybungalobill (שיחה | תרומות)
אין תקציר עריכה
שורה 63: שורה 63:


תחילה יתבצע הביטוי {{קוד בשורה|<nowiki>(bar += foo)</nowiki>}}, ערך משתנה bar יהפוך ל-{{קוד בשורה|<nowiki>{6.0, -4.0}</nowiki>}}, ביטוי זה יחזיר משתנה יחוס ל-bar אשר ישמש לנו כאופרנד לפעולת ההשמה. כשתתבצע פעולת ההשמה, יועתק ערכו החדש של bar למשתנה foo.
תחילה יתבצע הביטוי {{קוד בשורה|<nowiki>(bar += foo)</nowiki>}}, ערך משתנה bar יהפוך ל-{{קוד בשורה|<nowiki>{6.0, -4.0}</nowiki>}}, ביטוי זה יחזיר משתנה יחוס ל-bar אשר ישמש לנו כאופרנד לפעולת ההשמה. כשתתבצע פעולת ההשמה, יועתק ערכו החדש של bar למשתנה foo.

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


== קריאה לפונקציית האופרטור ==
== קריאה לפונקציית האופרטור ==


כאמור פונקציית האופרטור היא פונקציה רגילה בעלת שם מיוחד אשר ניתן לקרוא לה בצורה קצרה, ע"י שימוש בסימן האופרטור. נוכל לקרוא לפונקציית האופרטור באופן גלוי, בעמצעות שמה המלא, להלן שתי שורות מהדוגמות שלמעלה בהן קוראים לפונקציות האופרטור בדרך זו:
כאמור פונקציית האופרטור היא פונקציה רגילה בעלת שם מיוחד, אך ניתן לקרוא לה בצורה קצרה, ע"י שימוש בסימן האופרטור. נוכל לקרוא לפונקציית האופרטור באופן גלוי, באמצעות שמה המלא, להלן שתי שורות מהדוגמות שלמעלה בהן קוראים לפונקציות האופרטור בדרך זו:
<div style="direction: ltr;"><source lang="cpp">
<div style="direction: ltr;"><source lang="cpp">
Vector2D klop = operator + (foo, bar);
Vector2D klop = operator + (foo, bar);
שורה 73: שורה 75:


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

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


''טיפ:'' כשתתקלו בביטוי מסובך ולא ברור נסו לדמיין או לכתוב אותו בצורה כזאת ולהבין אילו בדיוק אופרטורים נקראים לפי טיפוסי האופרנדים שלהם.
''טיפ:'' כשתתקלו בביטוי מסובך ולא ברור נסו לדמיין או לכתוב אותו בצורה כזאת ולהבין אילו בדיוק אופרטורים נקראים לפי טיפוסי האופרנדים שלהם.


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

בשפת C++ ישנם אופרטורים אוּנָרִיים (בעלי אופרנד אחד), בִינָרִיים (בעלי שני אופרנדים), טֵרְנָרִיים (בעלי שלושה אופרנדים) ואפילו אופרטור קריאה לפונקציה () שמספר האופרנדים בו משתנה. כנאמר למעלה, חלק מהאופרטורים אנו יכולים להגדיר כחברי המחלקה, חלק כפונקציות סטטיות, חלק מותר גם כך וגם כך וחלק אין באפשרותינו להעמיס.

בחלק זה נחלק את האופרטורים למספר קטגוריות כדי להקל את ההבנה. נפרט על כל קטגוריה בנפרד.

=== פעולות חשבוניות בינאריות ===

נייחס לקטגוריה זו את הפעולות המתמטיות ואת הפעולות על סיביות:
<div style="direction: ltr;"><source lang="cpp">
+ - * / %
& | ^ >> <<
</source></div>
אופרטורים אלה נוכל להגדיר גם כחברי המחלקה וגם כאופרטורים סטטיים (בתוך מרחב שם, כולל גולובלי). כיוון שאופרטורים אלה הם בינאריים הם מקבלים שני אופרנדים.

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

אם אופרטורים אלה לא יהיו חברי מחלקה, אזי שני הפרמטרים שלהם ייצגו את האופרנדים. למשל:
<div style="direction: ltr;"><source lang="cpp">
struct A {
// ...
A operator * (const A& x) const; // בסדר גמור
A operator + (const A& x, const A& y) const; // שגיאה, יותר מדי פרמטרים - צריך 1 או 0
};
A operator % (const A& x); // שגיאה, חסרים פרמטרים - צריך 2
A operator / (const A& x, int y); // עובד
void operator - (A& x, const A& y); // מותר להחזיר כל ערך, אבל זה יבלבל
</source></div>

זיכרו שהעמסת אופרטורים, מטרתה להקל את הבנת הקוד, לכן לא כדאי להגדיר אופרטורים שלא יתאימו למשמעותם האינטואיטיבית. באופרטור האחרון נצטרך להתשמש כך: {{קוד בשורה|a - b;}} כאשר a, ו-b הם משתנים מטיפוס A. במקרה זה האופרטור לא מחזיר ערך אלה משנה את האופרנד הראשון שלו (a), מה שלא משתלב עם משמעות הסימן '-' אליה אנו רגילים.

המהדר לא יחליף את סדר האופרנדים מעצמו בשום מקרה, לכן, בהנתן ההגדרות שלמעלה, לא נוכל לכתוב {{קוד בשורה|100/a}}, אולם מותר לכתוב {{קוד בשורה|a/100}}.

=== פעולות לוגיות ופעולות השוואה ===

=== פעולות השמה ===

=== פעולות אונריות ===

=== הגדלה והפחתה ===

=== פעולות למצביעים ומערכים ===

=== אופרטור קריאה לפונקציה ===

=== העמסת new ו-delete ===

גרסה מ־12:05, 13 ביוני 2007


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


כל ביטוי מורכב מפעולות ופרמטרים איתם מתבצעות הפעולות, פעולות אלו נקראות אופרטורים ואילו הפרמטרים איתם עובדים האופרטורים נקראים אופרנדים. בC++ ישנם אופרטורים רבים שחלקם מקבלים כפרמטרים טיפוסים ומרחבי שמות (כמו :: ו-sizeof), פעולתם מתבצעת רק בעת הידור התוכנית, ואילו האחרים מקבלים אובייקטים, אופרטורים אלה עובדים עם האוביקטיים בזמן ריצה. כאשר אנו מגדירים מחלקות אנו מוסיפים טיפוסים חדשים לשפה, אבל עד כה ביצענו איתם פעולות רק ע"י קריאות לפונקציות המחלקה או פונקציות רגילות. כדי להגדיר טיפוס המשתלב בשפה בדומה לטיפוסים המובנים נגדיר עבורו אופרטורים. דבר זה נקרא העמסת אופרטורים כיוון שאנו מעמיסים אופרטורים נוספים לאופרטורים הקיימים. המטרה העיקרית בהעמסת אופרטורים היא נוחות וקריאות הקוד, הרי הקוד a = b + c קריא יותר למתכנת מאשר a.set(add(b, c)). שימו לב: בשני הביטויים האלה אנו לא יודעים מהי באמת התוצאה כי לא ידועים לנו לא הטיפוסים של a, b, c ולא המשמעות של =, +, add, set עבור טיפוסים אלה.

הגדרת פונקציית האופרטור

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

struct Vector2D
{
    double x, y;
    // ...
};

inline Vector2D operator + (const Vector2D &a, const Vector2D &b)
{
    Vector2D res;
    res.x = a.x + b.x;
    res.y = a.y + b.y;
    return res;
}

נסביר את הכתוב:

  • הגדרנו טיפוס זה כמבנה ולא כמחלקה כיוון שאין לנו צורך להסתיר את משתניו, הרי לא קיימים ערכים בלתי חוקיים עבור רכיבי הווקטור ודווקא נרצה לתת אפשרות לגשת למשתנים אלו: cout << vec.x << ',' << vec.y;.
  • שם פונקציית האופרטור מורכב מהמילה operator השמורה וסימן האופרטור עצמו, במקרה זה +.
  • פונקציית האופרטור מקבלת מספר פרמטרים לפי מספר האופרנדים, במקרה זה שני פרמטרים. אלה משתני יחוס קבועים, הם יתיחסו אל האופרנדים של האופרטור במקום הקריאה. הגדרנו את הפרמטרים כמשתני יחוס כי גודל האובייקט Vector2D גדול יחסית, אולם ניתן גם לתת לפרמטרים את טיפוס את אופרנד עצמו, לא בהכרח משתנה מיוחס.
  • פונקציית האופרטור מחזירה אובייקט Vector2D שבו נמצאת התוצאה של חיבור שני הווקטורים. לא נוכל להחזיר משתנה יחוס כיוון שאנו מחזירים אובייקט לוקלי.
  • פונקציית האופרטור מוגדרת כ-inline כי היא קצרה ומחזירה אובייקט גדול יחסית. נגדיר את רוב האופרטורים הפשוטים כ-inline.

עכשיו נוכל להשתמש באופרטור שהגדרנו עבור טיפוס הווקטור, כאילו ש-Vector2D הוא טיפוס מובנה:

Vector2D foo = {10, 20};
Vector2D bar = {5, 0};
Vector2D klop = foo + bar;

לאחר ביצוע קטע זה במשתנה klop ימצא הערך {15.0, 20.0}. שימו לב שאנחנו לא חייבים להגדיר את האופרטור = ואת הבנאי המעתיק כי משמעותם כברירת מחדל מתאימה לטיפוס זה.

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

struct Vector2D
{
    // ...

    Vector2D& operator += (const Vector2D &b)
    {
        x += b.x;
        y += b.y;
        return *this;
    }
};

כאשר נקראת פונקציית אופרטור זו, קיים עבורה מצביע this, כלומר היא נקראת עבור מופע מסויים של מחלקה. מסיבה זו לפונקצייה זו יש רק פרמטר אחד, על אף שאופרטור עצמו הוא בינארי (בעל שני אופרנדים).

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

Vector2D foo = {1, 1};
Vector2D bar = {5, -5};
foo = (bar += foo);

תחילה יתבצע הביטוי (bar += foo), ערך משתנה bar יהפוך ל-{6.0, -4.0}, ביטוי זה יחזיר משתנה יחוס ל-bar אשר ישמש לנו כאופרנד לפעולת ההשמה. כשתתבצע פעולת ההשמה, יועתק ערכו החדש של bar למשתנה foo.

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

קריאה לפונקציית האופרטור

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

Vector2D klop = operator + (foo, bar);
foo.operator = (bar.operator += (foo));

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

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

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

פרטי האופרטורים הניתנים להעמסה

בשפת C++ ישנם אופרטורים אוּנָרִיים (בעלי אופרנד אחד), בִינָרִיים (בעלי שני אופרנדים), טֵרְנָרִיים (בעלי שלושה אופרנדים) ואפילו אופרטור קריאה לפונקציה () שמספר האופרנדים בו משתנה. כנאמר למעלה, חלק מהאופרטורים אנו יכולים להגדיר כחברי המחלקה, חלק כפונקציות סטטיות, חלק מותר גם כך וגם כך וחלק אין באפשרותינו להעמיס.

בחלק זה נחלק את האופרטורים למספר קטגוריות כדי להקל את ההבנה. נפרט על כל קטגוריה בנפרד.

פעולות חשבוניות בינאריות

נייחס לקטגוריה זו את הפעולות המתמטיות ואת הפעולות על סיביות:

+   -   *   /   %
&   |   ^   >>  <<

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

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

אם אופרטורים אלה לא יהיו חברי מחלקה, אזי שני הפרמטרים שלהם ייצגו את האופרנדים. למשל:

struct A {
    // ...
    A operator * (const A& x) const; // בסדר גמור
    A operator + (const A& x, const A& y) const; // שגיאה, יותר מדי פרמטרים - צריך 1 או 0
};
A operator % (const A& x); // שגיאה, חסרים פרמטרים - צריך 2
A operator / (const A& x, int y); // עובד
void operator - (A& x, const A& y); // מותר להחזיר כל ערך, אבל זה יבלבל

זיכרו שהעמסת אופרטורים, מטרתה להקל את הבנת הקוד, לכן לא כדאי להגדיר אופרטורים שלא יתאימו למשמעותם האינטואיטיבית. באופרטור האחרון נצטרך להתשמש כך: a - b; כאשר a, ו-b הם משתנים מטיפוס A. במקרה זה האופרטור לא מחזיר ערך אלה משנה את האופרנד הראשון שלו (a), מה שלא משתלב עם משמעות הסימן '-' אליה אנו רגילים.

המהדר לא יחליף את סדר האופרנדים מעצמו בשום מקרה, לכן, בהנתן ההגדרות שלמעלה, לא נוכל לכתוב 100/a, אולם מותר לכתוב a/100.

פעולות לוגיות ופעולות השוואה

פעולות השמה

פעולות אונריות

הגדלה והפחתה

פעולות למצביעים ומערכים

אופרטור קריאה לפונקציה

העמסת new ו-delete