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

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


מבחינת המהדר אופרטור הוא פונקציה שכדי לקרוא לה נשתמש בתחביר שונה מזה של קריאה לפונקציה רגילה. להלן חלק מהגדרת מבנה המייצג ווקטור מתמי דו מימדי ופונקציית האופרטור המחבר שני ווקטורים:
מבחינת המהדר אופרטור הוא פונקציה שכדי לקרוא לה נשתמש בתחביר שונה מזה של קריאה לפונקציה רגילה. להלן חלק מהגדרת מבנה המייצג ווקטור מתמטי דו מימדי ופונקציית האופרטור שמחבר שני ווקטורים נתונים:
<div style="direction: ltr;"><source lang="cpp">
<div style="direction: ltr;"><source lang="cpp">
struct Vector2D
struct Vector2D
שורה 82: שורה 82:
== פרטי האופרטורים הניתנים להעמסה ==
== פרטי האופרטורים הניתנים להעמסה ==


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


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


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


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


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

אם אופרטורים אלה לא יהיו חברי מחלקה, אזי שני הפרמטרים שלהם ייצגו את האופרנדים. למשל:
<div style="direction: ltr;"><source lang="cpp">
<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>
</source></div>


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


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


=== פעולות קלט ופלט ===
=== פעולות קלט ופלט ===
שורה 131: שורה 127:


וכתוצאה יודפס: {{קוד בשורה|<nowiki>pos = (12.0, 32.0)</nowiki>}}.
וכתוצאה יודפס: {{קוד בשורה|<nowiki>pos = (12.0, 32.0)</nowiki>}}.

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

נייחס לקטגוריה זו את הפעולות:
<div style="direction: ltr;"><source lang="cpp">
> >= < <= == !=
&& ||
</source></div>

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

אופרטורים אלה ניתן להגדיר גם כחברי מחלקה וגם כפונקציות סטטיות. גם להם שני אופרנדים. סדר הקיבוץ גם כאן הוא משמאל לימין: {{קוד בשורה|(a && b && c)}} זהה ל-{{קוד בשורה|((a && b) && c)}}.

לדוגמה:
<div style="direction: ltr;"><source lang="cpp">
bool operator == (const Vector2D &a, const Vector2D &b)
{
return a.x == b.x && a.y == b.y;
}
</source></div>

''הערה:'' בעבודה עם נקודה צפה יש לעשות השוואה עם epsilon עקב אי-דיוק ועיגול התוצאות.

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

נייחס לקטגוריה זו את הפעולות:
<div style="direction: ltr;"><source lang="cpp">
= *= /= %= += -= <<= >>= &= |= ^=
</source></div>

כיוון שהמשמעות המקורית של אופרטורים אלה היא לבצע שינוי באופרנד השמאלי, אופרטורים אלה צריכים להיות מוגדרים בתוך המחלקה. דבר זה מבטיח שהאופרנד השמאלי ימצא בזיכרון (יהיה lvalue), ומסיבה זו לפונקציות האופרטורים האלה יהיה תמיד רק פרמטר אחד (האופרנד הימני). סדר קיבוץ פעולות אלה הוא מימין לשמאל: {{קוד בשורה|<nowiki>(a = b = c)</nowiki>}} זהה ל-{{קוד בשורה|<nowiki>(a = (b = c))</nowiki>}}.

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


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

גרסה מ־10:27, 3 ביולי 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++ ישנם אופרטורים אוּנָרִיים (בעלי אופרנד אחד), בִינָרִיים (בעלי שני אופרנדים), טֵרְנָרִיים (בעלי שלושה אופרנדים) ואפילו אופרטור קריאה לפונקציה () שמספר האופרנדים בו משתנה. כנאמר למעלה, חלק מהאופרטורים אנו יכולים להגדיר כחברי המחלקה, חלק כפונקציות סטטיות, וחלק אין באפשרותינו להעמיס.

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

+   -   *   /   %   &   |   ^   ,
<<  >>  <=  >=  ==  &&  ||  !=

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

=   +=  -=  *=  /=  %=  &=  |=  ^=  <<= >>= []  ->*

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

~   !   -   +   *

אופרטורים אונריים הניתנים להעמסה רק בתוך המחלקה הם, לפונקציות אופרטורים אלה אף פעם לא יהיו פרמטרים:

->  ++  --  &

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

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

פעולות קלט ופלט

הספרייה התקנית של C++ (שמה STL) מגדירה את המחלקות istream ו-ostream שמהוות את מחלקות האב (למד הורשה בהמשך) למחלקות רבות אחרות שמאפשרות קלט ופלט. מחלקות לעבודה עם קבצים, למשל, נגזרות ממחלקות אלה. גם טיפוסי האובייקטים cout ו-cin, איתם כבר עבדנו, הם ostream ו-istream בהתאמה. כדי לקלוט ולפלוט משתנים מטיפוסים מובנים אנו משתמשים באופרטורים << ו->> שהם אופרטורים בינאריים המוגדרים בספריית STL. באופן דומה נוכל להעמיס אופרטורים אלה כדי לקלוט מ-istream ולפלוט ל-ostream את הטיפוס שאנו הגדרנו. אופרטור כזה יקבל כאופרנד ראשון את ה-stream ממנו אנו קולטים או אליו אנו פולטים, וכאופרנד שני את האובייקט עצמו. לדוגמה:

ostream& operator << (ostream& s, const Vector2D &v)
{
    s << "(" << v.x << ", " << v.y << ")";
    return s;
}

אנו החזרנו את s כערך כדי שנוכל לשרשר את הפלט בעתיד. עכשיו נוכל להשתמש באופרטור זה:

Vector2D pos = {12, 32};
cout << "pos = " << pos << endl;

וכתוצאה יודפס: pos = (12.0, 32.0).

הגדלה והפחתה

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

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

העמסת new ו-delete