המצגת נטענת. אנא המתן

המצגת נטענת. אנא המתן

העמסת אופרטורים בנאים הורסים והשמות המרות friend

מצגות קשורות


מצגת בנושא: "העמסת אופרטורים בנאים הורסים והשמות המרות friend"— תמליל מצגת:

1 העמסת אופרטורים בנאים הורסים והשמות המרות friend
תרגול מס' 9 העמסת אופרטורים בנאים הורסים והשמות המרות friend

2 העמסת אופרטורים חדשים הגבלות דוגמה - Complex
מבוא לתכנות מערכות

3 ניתן להגדיר את האופרטור כמתודה במקרה זה הארגומנט הראשון הוא ה-this
העמסת אופרטורים class Complex { double re, im; public: Complex(double r, double i); Complex operator+(const Complex& c) const; Complex operator*(const Complex& c) const; }; int main() { Complex a = Complex(1, 3.1); Complex b = Complex(1.2, 2); Complex c = b; a = b + c; // b.operator+(c) b = b + c * a; // b + (c * a) c = a * b + Complex(1, 2); return 0; } מבחינת C++ אופרטורים כמו + או * הם פונקציות רגילות ניתן לקרוא לכל אופרטור בשתי דרכים בעזרת התחביר המיוחד השמור לו, למשל עבור אופרטור+ בעזרת a+b בעזרת תחביר של פונקציה רגילה ששמה operator<op>, למשל operator+(a,b) ניתן להעמיס על אופרטורים בדומה לפונקציות רגילות עבור מחלקות ניתן להגדיר את האופרטור כמתודה במקרה זה הארגומנט הראשון הוא ה-this מבוא לתכנות מערכות

4 העמסת אופרטורים ניתן להעמיס על האופרטורים הבאים:
+ - * / % ^ & | ~ ! = < > += -= *= /= %= ^= &= |= << >> >>= <<= == != <= >= && || >* , -> [] () new new[] delete delete[] לא ניתן להעמיס על האופרטורים הבאים: :: . .* ? : sizeof typeid לא ניתן להגדיר אופרטורים חדשים (למשל **) מספר הפרמטרים, קדימותם והאסוציאטיביות שלהם נשמרים אחד הפרמטרים לאופרטור חייב להיות מחלקה או enum לאופרטור - יש שתי גרסאות, אונארית ובינארית. לאופרטור * יש שתי גרסאות, אונארית ובינארית. (הגרסה האונארית משמשת עבור קריאת מצביעים למשל) אופרטור ^ הוא bitwise xor לאופרטור & יש שתי גרסאות, בגרסה הבינארית הוא bitwise and ובגרסה האונארית זהו האופרטור & המחזיר כתובת של משתנה אופרטור | הוא bitwise or אופרטור ~ הוא bitwise not =^, =&, =| הם גרסות ההשמה של האופרטורים הקודמים >>, << הם אופרטורים עבור bit shift =>>, =<< כנ"ל עם השמה אמנם ניתן להעמיס על && ו-|| אך לא ניתן לשמור על ההערכה העצלה שלהם (העובדה שהפרמטר השני לא מוערך אם ערך הביטוי כבר נקבע) ולכן לא נהוג להעמיס עליהם לאופרטורים -- ו-++ יש שתי גרסאות, prefix ו-postfix. אם הם מוגדרים עם פרמטר יחיד אז הם prefix, כדי להגדיר postfix מוסיפים פרמטר דמה. (מופיע בהמשך) *<- הוא אופרטור המשמה לקריאת מצביע למחלקה והרצת מצביע למתושה של המחלקה (מחוץ לחומר) אופרטור ה-, משמש לביצוע מספר פעולות בבת אחת. למשל בהכזרת מספר משתנים או ביצוע מספר פעולות בלולאת for. לא נהוג להשתמש בו בדרך כלל. אופרטור () הוא אופרטור ההפעלה. (קיים עבור מצביע לפונקציה) ניתן להעמיס אותו עם כל מספר של פרמטרים. העמסת האופרטורים עבור new ו-delete לא תלמד בתרגולים האופרטור :: הוא עבור שרשור שמות namespace לשמות בקוד. *. משמש לקריאת מצביע למתודה של מחלקה (מחוץ לחומר הקורס) typeid משמש לקבלת טיפוס של עצם בזמן ריצה (לא נלמד בתרגולים, לפעמים נלמד בהרצאה) תחביר העמסת האופרטורים משמש גם להעמסת אופרטורי המרה ששמם כשם הטיפוס אליו ממירים והתחביר שלהם קצת שונה (מופיע בהמשך) לא ניתן להגדיר אופרטורים חדשים בכלל החשש לדו משמעות: למשל אם נגדיר ** מה פירוש הביטוי a**b הערה למתקדמים: עבור הגדרה מחדש של new או delete אין את ההגבלה הדורשת שאחד המשתנים יהיה מטיפוס מחלקה (מחוץ לחומר) מבוא לתכנות מערכות

5 העמסת אופרטורים ניתן להגדיר את האופרטורים כמתודות או כפונקציות חיצוניות אם אופרטור המקבל 𝑛 פרמטרים מוגדר כמתודה הפרמטר הראשון הוא ה-this ויש להכריז על 𝒏−𝟏 פרמטרים נוספים אם האופרטור מוגדר כפונקציה חיצונית יש להכריז על כל 𝒏 הפרמטרים לחלק מהאופרטורים קיימות גרסה אונארית וגם גרסה בינארית, למשל - או * class Complex { double re, im; public: Complex(double r, double i); Complex operator+(const Complex& c) const; Complex operator-() const; // c1 = -c2; }; Complex operator-(const Complex& c1, const Complex& c2); מבוא לתכנות מערכות

6 העמסת אופרטורים בגלל סיבות היסטוריות (תאימות ל-C) לכל מחלקה מוגדרים האופרטורים הבאים על ידי הקומפיילר: = (השמה), & (כתובת של) ו-, (סדרתיות) ניתן להעמיס את האופרטורים האלה מחדש או למנוע מהמשתמש במחלקה גישה מהם על ידי הכרזתם כפרטיים. class X { private: void operator=(const X&); void operator&(); void operator,(const X&); //... }; לא נהוג להשתמש ב-, מאחר ועיקר השימוש שלו זה להכניס מספר פעולות בשורה. פעולה זו גורמת לקוד להיות פחות קריא בדרך כלל. לא נהוג להעמיס על & מבוא לתכנות מערכות

7 אופרטור - אונארי יוצר מספר מרוכב חדש, לכן התוצאה מוחזרת כעותק חדש
דוגמה - מחלקת Complex class Complex { double re, im; public: Complex(double r, double i); Complex& operator+=(const Complex& c); Complex& operator-=(const Complex& c); Complex operator-() const; bool operator==(const Complex& c) const; }; Complex operator+(const Complex& a, const Complex& b); Complex operator-(const Complex& a, const Complex& b); אופרטורים הכוללים השמה כמו += מאפשרים שרשור ולכן נשמור על ההתנהגות הזו אצלנו אופרטור - אונארי יוצר מספר מרוכב חדש, לכן התוצאה מוחזרת כעותק חדש אופרטורים סימטריים נהוג להכריז מחוץ למחלקה בגלל המרות (דוגמה בהמשך התרגול) מבוא לתכנות מערכות

8 דוגמה - מחלקת Complex Complex& Complex::operator+=(const Complex& c) { re += c.re; im += c.im; return *this; } Complex& Complex::operator-=(const Complex& c) { return this->operator+=(-c); // or *this += -c Complex Complex::operator-() const { return Complex(-re, -im); Complex operator+(const Complex& a, const Complex& b) { Complex c = a; return c += b; מבוא לתכנות מערכות

9 דוגמה - מחלקת Complex bool Complex::operator==(const Complex& c) const { return c.re == re && c.im == im; } int main() { Complex a = Complex(1, 3.5); Complex b = Complex(1.5, 2); Complex c = a+b; // c= i c -= a; cout << (c == b) << endl; return 0; הערה לגבי operator==: שימוש ב-== על ערכים מטיפוס double לא מדויק והאופרטור מובא כאן רק כחלק מהדוגמא. בפועל הוא לא אמין. מבוא לתכנות מערכות

10 העמסת אופרטורים אופרטור [] (subscripting) יכול לשמש להגדרת אוספים
class Array { int* array; int size; public: //... int& operator[](int index); }; int& Array::operator[](int index) { assert(index >= 0 && index < size); return array[index]; } אופרטור [] (subscripting) יכול לשמש להגדרת אוספים חייב להיות מוגדר בתוך המחלקה מקבל פרמטר נוסף (לא בהכרח int) אופרטור () (function call) יכול להיות מוגדר לכל מספר של פרמטרים גם חייב להיות מוגדר בתוך המחלקה דוגמאות לשימוש ב-(): החזרת איבר ממטריצה החזרת תת-טווח של אוסף עצמים המתנהגים כפונקציות (דוגמאות לכך בתרגול 12) האופרטורים האלו חייבים להיות מוגדרים בתוך המחלקה כדי שהאיבר הראשון שלהם יהיה lvalue. עוד על כך בהמשך אחרי שנראה המרות. האופרטור -> חייב גם הוא להיות מתודה של המחלקה. בנוסף ערך ההחזרה שלו חייב להיות מטיפוס עליו גם ניתן להפעיל את אופרטור <-. זו נשמעת כמו התנהגות מגבילה אך בפועל היא מאפשרת לנו לעשות את השימוש הרצוי לנו באופרטור הזה. דוגמה לאופרטור <- מופיעה בתרגול 12 במימוש מצביעים חכמים. class Random { int seed; public: Random(int seed); int operator()(int max); }; //... Random r(5); int random = r(100); מבוא לתכנות מערכות

11 העמסת אופרטורים לאופרטורים ++ ו--- יש שתי גרסאות: pre ו-post
כדי להגדיר ++i מגדירים את האופרטור כרגיל כדי להגדיר את i++ מוספים פרמטר דמה שימוש בהעמסת אופרטורים כדי "להמציא שפה" פוגע בקריאות הקוד list.add(item) יותר ברור מ- list += item שימוש באופרטורים מתאים רק עבור מקרים בהם קיימת שפה משותפת ידועה (למשל אופרטור * עבור כפל מטריצות) class X { //... public: X& operator++(); // ++x X operator++(int); // x++ }; עבור ++x ניתן לעדכן את ערכו של x ולהחזיר את האיבר עצמו כמו שמתקבל עבור טיפוסים פרימיטיביים. אך עבור x++ יש להחזיר את ערכו הקודם של x לפני שעודכן. לכן בדרך כלל שומרים עותק של x עם הערך הישן ומחזירים אותו. איבר זמני חייבים להחזיר by value כי הוא משתחרר בסוף הפונקציה. לעובדה הזו קיימת השפעה עבור שימוש באופרטור ++ ולכן עבור טיפוסים שאינם פרימטיביים ייתכן ש-++x מהיר יותר מ-x++ מסיבה זו נהוג לקרוא ל-++x בקוד המשתמש בטיפוסים מורכבים עם אופרטור זה. מדוע ערך ההחזרה שונה? מבוא לתכנות מערכות

12 העמסת אופרטורים - סיכום
ניתן להתייחס לאופרטורים כפונקציות רגילות ב-C++ ניתן להעמיס על אופרטורים קיימים כך שישמשו עבור מחלקות את האופרטורים ניתן להגדיר כמתודות (אשר הפרמטר הראשון שלהן הוא this) וכפונקציות מחוץ למחלקות לחלק מהאופרטורים יש הגבלות מיוחדות מומלץ להימנע מהעמסת אופטורים כל עוד היא לא אינטואיטיבית מבוא לתכנות מערכות

13 בנאים בנאי העתקה הורסים אופרטור ההשמה
בנאים, הורסים והשמות בנאים בנאי העתקה הורסים אופרטור ההשמה מבוא לתכנות מערכות

14 בנאים והורסים - C’tors & D’tors
המשתמש יכול לשכוח לאתחל/לשחרר את העצם המשתמש יכול לאתחל/לשחרר את העצם פעמיים לשם כך נוספה ב-C++ האפשרות להגדיר פונקציות ייעודיות לאתחול ושחרור עצמים - בנאים (Constuctors) והורסים (Destructors) כל עצם שנוצר ב-C++ מאותחל על ידי בנאי וכל עצם שמשוחרר מטופל על ידי הורס מבוא לתכנות מערכות

15 איזה בנאי נקרא בכל אחת מהשורות?
בנאים - Constructors class X { public: X(); X(int n); }; X global; int main() { X x1; X x2(5); X* ptrx1 = new X; X* ptrx2 = new X(); X* arrayx = new X[10]; array[0] = X(); array[1] = X(5); // ... return 0; } בנאי מוגדר על ידי מתודה ששמה כשם המחלקה לבנאי אין ערך החזרה ניתן להגדיר מספר בנאים בהתאם לחוקי העמסת פונקציות לבנאי שאינו מקבל פרמטרים קוראים גם default c’tor אם לא מוגדר בנאי בצורה מפורשת למחלקה הקומפיילר ייצור בנאי חסר פרמטרים בעצמו הבנאי הנוצר על ידי הקומפיילר קורא לבנאי חסר הפרמטרים של כל אחד מהשדות שלו אם מוגדר בנאי כלשהו הקומפיילר לא ייצור בנאי באתחול של מערך נקרא בנאי חסר פרמטרים לכל אחד מהאיברים לא ניתן ליצור מערך של עצמים שאין להם בנאי מתאים X global; // Before entering main, X::X() called for global int main() { X x1; // X::X() called X x2(5); // X::X(int) called X* ptrx1 = new X; // X::X() X* ptrx2 = new X(); //X::X() X* arrayx = new X[10]; // 10*X::X() array[0] = X(); // temp X array[1] = X(5); // also temp // ... return 0; } איזה בנאי נקרא בכל אחת מהשורות? מבוא לתכנות מערכות

16 רשימת אתחול חשוב להבדיל ב-C++ בין אתחול להשמה: מה הבעיה בקוד הזה?
int n(5); int n = 5; n = 5; חשוב להבדיל ב-C++ בין אתחול להשמה: אתחול מתבצע על ידי בנאי השמה מתבצעת על ידי אופרטור= מה הבעיה בקוד הזה? הבנאי אחראי לאתחול השדות של המחלקה, אתחול השדות מתבצע ברשימת האתחול לפני הכניסה לגוף הפונקציה על ידי קריאה לבנאים אם עבור שדה מסוים לא מצוין כיצד יש לאתחל אותו הוא יאותחל על ידי בנאי חסר פרמטרים אם אין בנאי מתאים לשדה תתקבל שגיאת קומפילציה אתחול השמה Complex::Complex(double r, double i) : re(r), im(i) { } Complex::Complex(double r, double i) { re = r; im = i; } התחביר עבור רשימת אתחול הוא: התו : מציין שמופיעה רשימת אתחול מפורשת. לכל שדה אותו מאתחלים רושמים את שמו ובסוגריים את הערכים לבנאי שלו. אם קיימים מספר שדות מפרידים אותם בפסיק. אם שדות מוגדרים כ-const אז ניתן לקבוע את ערכם רק ברשימת האתחול של הבנאי. למשל בקוד הבא יש שגיאת קומפילציה: class X { const int n; public: X(int m) { n = m; // error, n is a read-only member } }; אתחול נכון ייראה כך: X(int m) : n(m) {} re() ו-im() נקראים כאן מבוא לתכנות מערכות

17 בנאי העתקה - Copy C’tor לבנאי המקבל פרמטר מטיפוס העצם אותו הוא מאתחל קוראים בנאי העתקה (Copy C’tor) ויש לו מעמד מיוחד חתימת בנאי ההעתקה היא: X::X(const X& x) למה הפרמטר מועבר by reference? בנאי ההעתקה משמש להעברת פרמטרים והחזרת ערכים by value:  אם לא מוגדר בנאי העתקה הקומפיילר ייצור אחד בעצמו בנאי ההעתקה שנוצר על ידי הקומפיילר מעתיק את איברי המחלקה אחד אחד בעזרת בנאי ההעתקה המתאים של כל שדה שימו לב: בנאי העתקה נוצר על ידי הקומפיילר גם אם הוגדרו בנאים אחרים הפרמטר לבנאי ההעתקה חייב להיות מעובר by reference, אם היה מועבר by value היה צריך להעתיק אותו, דבר המתבצע בעזרת בנאי ההעתקה שאותו אנו מנסים להריץ. כלומר הייתה לולאה אינסופית. הפרמטר לבנאי ההעתקה לא חייב להיות const ואכן במקרים נדירים ייתכנו בנאי העתקה שמשנים את את העצם המועתק. עדיף להימנע מהתנהגות זו כמובן. Y f(X x, Y& y) { // X::X(const X&) // ... return y; // Y::Y(const Y&) } מבוא לתכנות מערכות

18 בנאי העתקה - Copy C’tor בנאי ההעתקה הנוצר אוטומטית עבור Stack ייראה כך: מדוע הוא לא מתאים לנו? בנאי העתקה מתאים ייראה כך: Stack::Stack(const Stack& s) : data(s.data), size(s.size), nextIndex(s.nextIndex) { } בנאי ההעתקה הסטנדרטי אינו מתאים לנו מכיוון ש-data מצביע למערך. העתקתו ישירות תגרום לכך ששתי המחסניות ישתמשו באותו מערך ולכן הפעלת פעולות על אחת מהן תגרום לבאגים בשניה. כדי ליצור התנהגות נכונה עבור העתקה עלינו להעתיק את כל המערך, כמו שנעשה בבנאי ההעתקה השני. Stack::Stack(const Stack& s) : data(new int[s.size]), size(s.size), nextIndex(s.nextIndex) { for(int i = 0; i<nextIndex; i++) { data[i] = s.data[i]; } מבוא לתכנות מערכות

19 מתי נקראים הורסים בקוד הזה?
הורסים - Destructors X global; int main() { { X x1; } X x2; X* ptrx1 = new X; X* ptrx2 = new X; delete ptrx1; X* arrayx = new X[10]; array[0] = X(); delete[] arrayx; return 0; ההורס של מחלקה X מוגדר כמתודה ששמה ~X הורס אינו מקבל פרמטרים ואינו מחזיר כלום ניתן להגדיר הורס יחיד כאשר עצם משוחרר נקרא ההורס שלו ההורס נקרא אוטומטית - לא כותבים קריאה מפורשת להורס אם לא מוגדר הורס הקומפיילר יגדיר אחד בעצמו הורס זה אינו מבצע קוד לאחר סיום ההורס של עצם כלשהו ייקראו ההורסים של כל השדות שלו הורס אינו יכול לקבל פרמטרים מאחר והוא נקרא בצורה לא מפורשת לא צריך לקרוא להורס, הוא נקרא אוטומטית. הקריאה להורס מתבצעת בזמן שחרור העצם, הזמן הזה מוגדר לפי סוג המשתנה. הזכרנו קיימים 4 סוגים עיקריים של משתנים: מקומיים, גלובליים, סטטיים של פונקציה ודינאמיים. עבור משתנה לוקלי ההורס ייקרא בסוף הבלוק עבור משתנה גלובלי ההורס ייקרא לאחר היציאה מ-main עבור משתנה סטטי של פונקציה ההורס ייקרא לאחר היציאה מ-main עבור משתנה דינאמי ההורס ייקרא על ידי delete לאזור הזיכרון בו מאוחסנן העצם (עצמים) קריאה ל-delete, מלבד שחרור הזיכרון קוראת גן להורסים המתאימיים. X global; int main() { { X x1; } // end of block ~X() called X x2; X* ptrx1 = new X; X* ptrx2 = new X; delete ptrx1; // ~X() for *ptrx1 X* arrayx = new X[10]; array[0] = X(); // ~X() for temp // object called delete[] arrayx; // 10 ~X() called return 0; } // ~X() for x2 called // ~X() for global called // ~X() for *ptrx2 not called at all מתי נקראים הורסים בקוד הזה? מבוא לתכנות מערכות

20 זמני קריאה class X { int n; public: X(int n) : n(n) { cout << "X::X():" << n << endl; } ~X() { cout << "X::~X():" << n << endl; } }; class Y { X x1, x2; Y() : x1(1), x2(2) { cout << "Y::Y()" << endl; } ~Y() { cout << "Y::~Y()" << endl; } }; int main() { Y y; return 0; } רשימת האתחול של בנאי מבוצעת לפני הכניסה לקוד הבנאי עצמו (בתוך ה-{ }) סדר הקריאה לאתחול השדות הוא לפי סדר הגדרתם במחלקה לאחר ביצוע הקוד שבתוך { } בהורס ייקראו הורסים לכל שדותיו של העצם הנהרס (בסדר ההפוך מבאתחול העצם) אם רשימת האתחול נרשמת לא בסדר הנכון הקומפיילר יוציא אזהרה (הסכנה כאן היא הסתמכות באתחול שדה אחד על הערך של שדה שכבר אותחל ובמקרה כזה אוסר לטעות בסדר) כלל האצבע הוא - השחרור מתבצע בסדר הפוך מהאתחול. כך אין סכנה לקריאת זבל עבור משתנים שתלויים במשנתים שאותחלו לפניהם. הפלט מהקוד הוא: X::X():1 X::X():2 Y::Y() Y::~Y() X::~X():2 X::~X():1 מה יודפס? מבוא לתכנות מערכות

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

22 מחיקת מידע ישן, בניגוד לבנאי העתקה צריך לנקות את המידע הקודם
אופרטור השמה אופרטור השמה למחסנית Stack& Stack::operator=(const Stack& s) { if (this == &s) { return *this; } delete[] data; data = new int[s.size]; size = s.size; nextIndex = s.nextIndex; for(int i=0 ;i < nextIndex; i++) { data[i] = s.data[i]; בדיקת השמה עצמית מדוע בדיקה זו הכרחית? מחיקת מידע ישן, בניגוד לבנאי העתקה צריך לנקות את המידע הקודם ביצוע הקצאות חדשות החזרת *this עבור שרשור החזרת *this עבור שרשור מימוש זה לא מתאים עבור מקרה של חריגות (שיילמדו בהמשך), נראה מימוש טוב יותר לאופרטור= בעתיד מבוא לתכנות מערכות

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

24 Big three בין בנאי ההעתקה, ההורס ואופרטור ההשמה קיים קשר: אם צריך אחד מהם, צריך את שלושתם איך יודעים מתי צריך לממש את הפונקציות האלו? אם מעורבים מצביעים במחלקה בדרך כלל יש צורך בפונקציות האלו אם יש שימוש ב-new ו-delete אם מתבצעות הקצאות של משאבים אחרים (למשל קבצים) מבוא לתכנות מערכות

25 בנאים, הורסים והשמות - סיכום
בנאים משמשים לאתחול עצמים לפני הכניסה לקוד הבנאי נקראים הבנאים של כל השדות של העצם אחרי ביצוע הקוד בהורס נקראים ההורסים של כל שדות העצם בנאי ההעתקה משמש להעברת והחזרת ערכים by value ואתחול העתקים אופרטור ההשמה (=) משמש להעתקת ערכים בין שני עצמים קיימים בכתיבת אופרטור השמה חשוב לשים לב לשחרור המידע הקיים והשמות עצמיות הקומפיילר יכתוב בעצמו בנאי חסר פרמטרים, בנאי העתקה, אופרטור השמה והורס. אין צורך לכתוב את הפונקציות האלו אם המימוש של הקומייפלר מתאים אם צריך בנאי העתקה, אופרטור השמה או הורס לא טריוויאלים אז צריך את שלושתם מבוא לתכנות מערכות

26 המרות על ידי בנאי העמסת אופרטור ההמרה בנאים מפורשים
המרות אוטומטיות המרות על ידי בנאי העמסת אופרטור ההמרה בנאים מפורשים מבוא לתכנות מערכות

27 המרות לפעמים נרצה מספר פעולות בין טיפוסים שונים
למשל, פעולות חיבור מספר מרוכב עם ממשי Complex operator+(const Complex&, const Complex&); Complex operator+(const double&, const Complex&); Complex operator+(const Complex&, const double&); לא נרצה לממש מספר פונקציות למטרה זו ניתן להגדיר המרות בין טיפוסים. כך למשל הקומפיילר יוכל להמיר double ל-Complex ונצטרך רק פונקציה אחת עבור החיבור קיימות שתי דרכים להגדיר המרות ב-C++: על ידי בנאים על ידיד העמסת אופרטור המרה מבוא לתכנות מערכות

28 המרה על ידי בנאי class Complex { double re, im; public: Complex(double re) : re(re), im(0.0) {} //... }; Complex operator+(const Complex&, const Complex&); int main() { Complex c = 3.0; Complex c2(1.0,1.0); c = 4.0; c = c ; return 0; } אם למחלקה יש בנאי המקבל פרמטר יחיד אז בנאי זה ישמש את הקומפיילר לביצוע המרות אוטומטיות המרות המוגדרות על ידי המשתמש ישמשו את הקומפיילר לבחירה בין פונקציות מועמסות המרות אלו מקבלות עדיפות נמוכה יותר מהמרות מובנות בנאי עם ערכי ברירת מחדל היכול לקבל ארגומנט יחיד גם יתאים להמרות, למשל: Complex(double re = 0.0,double im = 0.0) : re(re), im(im) {} דרך נוחה יותר ליצור את בנאי ההמרה היא על ידי נתינת ערכי ברירת מחדל לבנאי הרגיל: Complex(double re=0.0, double im=0.0) : re(re), im(im) {} נתינת ערך ברירת מחדל גם לחלק הממשי מאפשרת לנו לקבל גם בנאי טבעי למקרה של 0 פרמטרים מבוא לתכנות מערכות

29 העמסת אופרטור המרה class Rational { int num, denom; public: Rational(int num, int denom); // ... operator double() const; }; Rational::operator double() const { return double(num)/denom; } int main() { Rational r(1,2); double d = 0.3; cout << (d+r) << endl; return 0; השימוש בבנאי עבור המרה אינו מאפשר לנו להמיר עצם ממחלקה לערך בסיסי או להמיר עצם ממחלקה חדשה למחלקה ישנה ניתן להעמיס על אופרטור המרה - שם האופרטור כשם הטיפוס אליו נרצה להמיר את האופרטור יש להגדיר כמתודה האופרטור מוגדר ללא ערך חזרה, טיפוס ערך החזרה נקבע לפי שם האופרטור מבוא לתכנות מערכות

30 המרות אוטומטיות מומלץ להימנע מהגדרת המרות אוטומטיות ולהעדיף הגדרת פונקציות מפורשות לביצוע המרות המרות אוטומטיות עלולות לגרום לדו-משמעות המרות אוטומטיות עלולות לגרום לקוד מוזר להתקמפל class Rational { //... Rational(int num, int denom=1); operator double() const; }; Rational operator+(const Rational& a, const Rational& b); void f(const Rational& r, int n) { r+n; // error, ambiguous } השימוש בפונקציה מפורשת כגון toDouble תמנכ את החסרונות של השימוש בהמרות אוטומטיות. אם בהמשך רואים שמספר הקריאות לפונקציה גדול ומסרבל את הקוד ניתן להחליף להמרה אוטומטית. הביטוי r+n יוצר דו-משמעות מאחר ויש לקומפיילר שתי דרכים לפתור את ההעמסה המדורגות בצורה זהה: המרת ה-int ל-Rational ושימוש בחיבור של מספרים רציונליים המרת ה-Rational ל-double ושימוש בפונקצית החיבור המובנית של int ו-double הביטוי array = 50 מוזר ולא ברור מה מטרתו. ברוב המקרים מדובר בטעות, למשל משתמש שהתכוון ל-array[0] = 50 או משהו בסגנון. class Array { //... public: Array(int size); }; void f(Array& array) { array = 50; // Compiles } מבוא לתכנות מערכות

31 המרות אוטומטיות ניתן להוסיף את המילה השמורה explicit בתחילת בנאי כדי לציין שבנאי זה לא ישמש להמרה לא מפורשת מומלץ להכריז על בנאים המקבלים ארגומנט יחיד כ-explicit במיוחד אם הארגומנט הוא מטיפוס בסיסי (למשל int) הקומפיילר לא יבצע שתי המרות אוטומטיות על אותו ארגומנט, במקרים בהם זה דרוש יש להמיר בצורה מפורשת למשל Rational לא יומר ל-double שיומר ל-Complex class Array { //... explicit Array(int size); }; void f(Array& array) { array = 50; // error } בנאי המוגדר כ-explicit אינו יכול להשתמש בתחביר ה-= לאתחול. כלומר אם אכן נגדיר את Array כך שיהיה לו רק בנאי מפורש, השורה הבאה לא תתקמפל יותר: Array array = 50; // Will not compile if Array::Array(int size) is explicit כמובן, שנתין להמשיך לקמפל עם תחביר האתחול הרגיל של C++: Array array(50); // O.K. void f(const Rational& r, const Complex& c) { Complex c3 = c+r; // error Complex c2 = c+double(r); // O.K. double -> Complex } מבוא לתכנות מערכות

32 המרות אוטומטיות מתודות ניתן להפעיל על עצם מהטיפוס המתאים בלבד
הקומפיילר לא ימיר משתנה כדי להפעיל עליו מתודה class Complex { double re, im; public: // ... Complex(double re); double abs() const; }; void f(double& d) { d.abs(); // error Complex(d).abs(); // o.k. } מבוא לתכנות מערכות

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

34 פונקציות חברות מחלקות חברות העמסת אופרטורי קלט ופלט
חברים - friends פונקציות חברות מחלקות חברות העמסת אופרטורי קלט ופלט מבוא לתכנות מערכות

35 friend - פונקציות חברות
class A { int n; public: A(int n) { this->n = n; } friend int getN(const A& a); }; int getN(const A& a) { return a.n; } int main() { A a(5); cout << getN(a) << endl; return 0; לעתים נצטרך לאפשר לפונקציה חיצונית לגשת לשדות פרטיים כדי לאפשר לפונקציה לגשת לחלקים פרטיים של מחלקה מסוימת נכריז עליה בתוך המחלקה אחרי המילה השמורה friend הקוד שמופיע בגוף הפונקציה שהוכרזה כחברה רשאי לגשת לחלקים פרטיים של המחלקה בה הוכרז ה-friend ניתן לממש את הפונקציה יחד עם ההכרזה או בחוץ כרגיל משמעות ההרשאה היא חד-כיוונית והיא תקפה לזמן הקומפילציה. הקומפיילר ירשה לקוד שרשום ב-scope של הפונקציה בחברה לגשת לחלקים הפרטיים של המחלקה (פונקציות, שדות וטיפוסים) לא חשוב איפה מכריזים על ה-friend ניתן להכריז ולממש בבת אחת את הפונקציה, במקרה כזה הפונקציה היא עדיין פונקציה חיצונית של המחלקה - כלומר ללא this ושמה המלא אינו כולל את שם המחלקה כ-scope ניתן כמובן להכריז על מתודה של מחלקה אחרת בעזרת שמה המלא: friend void OtherClass::method(); מבוא לתכנות מערכות

36 friend - מחלקות חברות ניתן להכריז על מחלקה שלמה כ-friend
class A { int n; public: A(int n) { this->n = n; } friend class B; }; class B { void printA(const A& a) { cout << a.n << endl; } int main() { A a(5); B b; b.printA(a); return 0; ניתן להכריז על מחלקה שלמה כ-friend קוד אשר נכתב ב-scope של המחלקה B שהוכרזה כחברה של A רשאי לגשת לחלקים פרטיים של A לא ניתן לבצע את ההיפך - לגשת מ-A לחלקים פרטיים של B מומלץ להימנע משימוש ב-friend - בדרך כלל זהו תסמין של תכן רע של המערכת ועודף תלויות בין החלקים השונים בקוד עם זאת קיימים מקרים בהם השימוש ב-friend חשוב דוגמאות לשימוש ב-friend: העמסת אופרטורים שדורשים התנהגות סימטרית כגון + (תרגול 10) העמשת אופרטורי ההדפסה והקלט: >> ו-<< (תרגול 10) יצירת פונקציה חיצונית למחלקה אשר היא חלק מהמנשק הפומבי של המחלקה (אין דוגמה טובה לכך בקורס) חלוקת מחלקה לשתי מחלקות אשר עובדות ביחד, אם זהו אכן התכן הנכון לבעיה נעדיף לתת להן גישת friend מאשר להוסיף למנשק הפומבי של המחלקות פונקציות ש"אסור" לקרוא להן מבחוץ. בכל מקרה - לפני שאתם מגדירים friend - חפשו תחילה פתרון המשתמש בתכן אחר לבעיה מבוא לתכנות מערכות

37 friends והעמסת אופרטורים
void f(const double& d, const Complex& c) { cout << (c == d) << (d == c) << endl; } מה הבעיה בפונקציה f כאשר אופרטור == ממומש כמתודה? כדי לאפשר התנהגות סימטרית של האופרטור עלינו להגדיר אותו כפונקציה חיצונית כדי לאפשר גישה לשדות עבור הפונקציה החדשה נצטרך להכריז עליה כ-friend class Complex { //... bool operator==(const Complex&) const; }; bool Complex::operator==(const Complex& c) const { return c.re == re && c.im == im; } למי שכבר שכח, הקומפיילר לא יבצע המרה אוטומטית עבור משתנה שישמש כ-this. לכן אופרטור כמו == לא יכול להיות מתודה. במקרה שלנו, הביטוי (c==d) מתקמפל (כי d מומר ל-Complex) ואילו (d==c) לא מתקמפל כי הקומפיילר לא ימיר את d כדי להריץ מתודה על העצם המומר. אופרטורים פופולריים נוספים שנרצה שיתנהגו בצורה סימטרית הם: !=, <, >, <=, >=, +, -, /, * לא צריך לשים friend אוטומטית. אנחנו רוצים לממש את האופרטור כפונקציה חיצונית, ניתן לעשות זאת גם ללא friend. את ה-friend נוסיף רק אם עלינו לחלקים בעצם שאינם זמינים מבחוץ. דוגמה טובה למקרה בו ניתן לממש אופרטור מבחוץ ללא friend היא אופרטור +, בד"כ ניתן לממש אותו פשוט על ידי שימוש ב-+=: X operator+(const X& a, const X& b) { return X(a)+=b; } class Complex { //... friend bool operator==(const Complex&, const Complex&); }; bool operator==(const Complex& a, const Complex& b) { return a.re == b.re && a.im == b.im; } מבוא לתכנות מערכות

38 העמסת אופרטור הפלט אופרטור הפלט >> מועמס כדי לאפשר הדפסה נוחה של עצמים לא נוכל להגדיר את אופרטור >> כמתודה של מחלקה שכתבנו, מדוע? נגדיר את אופרטור >> כפונקציה חיצונית: במקרים רבים נצטרך גישה לשדות פרטיים - במקרים אלו נכריז על האופרטור כ-friend את אופרטור הקלט מעמיסים בצורה דומה עבור המחלקה istream ostream& operator<<(ostream& os, const Complex& c) { const char* sign = c.im < 0 ? "" : "+"; return os << c.re << sign << c.im << "i"; } לא ניתן להגדיר את >> כאופרטור במחלקה שלנו X מאחר ובמקרה זה העצם הראשון הנשלח לאופרטור יהיה מטיפוס X והפרמטר השני יהיה ostream. כתוצאה מכך יתקבל תחביר לא נכון לשימוש עבור הדפסה. העצם אותו מדפיסים הוא בדרך const כי אין צורך לשנות עצם כדי להדפיס אותו. ostream אינו יכול להיות const, הדפסה ל-stream משנה אותו. עבור אופרטור הקלט גם העצם אליו קוראים אינו יכול להיות const class Complex { //... friend ostream& operator<<(ostream& os, const Complex& c); }; מבוא לתכנות מערכות

39 מחלקת Complex המעודכנת
class Complex { double re, im; public: Complex(double re = 0.0, double im = 0.0); Complex& operator+=(const Complex& c); Complex& operator-=(const Complex& c); Complex operator-() const; friend bool operator==(const Complex& a, const Complex& b); friend ostream& operator<<(ostream& os, const Complex& c); friend istream& operator>>(istream& is, Complex& c); }; Complex operator+(const Complex& a, const Complex& b); Complex operator-(const Complex& a, const Complex& b); מאפשר המרות ותאחול מערכים איפה בנאי ההעתקה, ההורס ואופרטור ההשמה? למה? הכרזה כ-friend משמשת גם כהכרזה עבור אורפטורים אלו אין צורך ב-friend מבוא לתכנות מערכות

40 מחלקת Complex המעודכנת
Complex::Complex(double re, double im) : re(re), im(im) {} Complex& Complex::operator+=(const Complex& c) { re += c.re; im += c.im; return *this; } Complex& Complex::operator-=(const Complex& c) { return *this += (-c); Complex Complex::operator-() const { return Complex(-re, -im); ערכי ברירת מחדל צריכים להופיע רק בהכרזה האתחול re(re) נראה מוזר אבל הוא תקין. המשתנה המקומי re מסתיר את השדה re בפונקציה, אך למרות זאת רשימת אתחול יכולה להתייחס רק לשדות של העצם ולכן האתחות מתבצע לשדה re על ידי המשתנה המקומי re. מבוא לתכנות מערכות

41 מחלקת Complex המעודכנת
Complex operator+(const Complex& a, const Complex& b) { return Complex(a) += b; } Complex operator-(const Complex& a, const Complex& b) { return Complex(a) -= b; ostream& operator<<(ostream& os, const Complex& c) { const char* sign = c.im < 0 ? "" : "+"; return os << c.re << sign << c.im << "i"; istream& operator>>(istream& is, Complex& c) { return is >> c.re >> c.im; מחזירים את os כדי לאפשר שרשור למה אין const? מבוא לתכנות מערכות

42 מחלקת Complex המעודכנת
bool operator==(const Complex& a, const Complex& b) { return a.re == b.re && a.im == b.im; } int main() { Complex a = 1.0; Complex b; cin >> b; cout << (-a+b) << endl; return 0; תזכורת, אופרטור == עבור מספרי נקודה צפה כמו double אינו מדויק וכאי להימנע משימוש בו. מבוא לתכנות מערכות

43 friend - סיכום ניתן להכריז על פונקציה שאינה שייכת למחלקה כחברה שלה ולאפשר לה גישה לשדות פרטיים ניתן להכריז על מחלקה כחברה של מחלקה אחרת כך שכל המתודות שלה ייחשבו כחברות משתמשים ב-friend עבור העמסת אופרטורים שצריכים להיות מוכרזים כפונקציות חיצוניות (רק אם אכן יש צורך בכך) מבוא לתכנות מערכות

44 דוגמה - String מבוא לתכנות מערכות

45 דוגמה - המחלקה String השימוש במחלקות, בנאים, הורסים והעמסת אופרטורים מאפשר לנו להגדיר מחלקה עבור String כך שייחסכו מאיתנו החסרונות של השימוש ב-char* בזכות בנאים והורסים לא נצטרך לנהל את הזיכרון ידנית בזכות העמסת אופרטורים נוכל לשמור על כל הנוחות של char*, למשל גישה כמערך בזכות העמסת אופרטורים נוכל לאפשר פעולות בסיסיות בצורה נוחה - למשל שרשור מבוא לתכנות מערכות

46 String class String { int length; char* data; static char* allocate_and_copy(const char* data, int size); void verify_index(int index) const; public: String(const char* str = ""); // String s1; or String s1 = "aa"; String(const String& str); // String s2(s1); ~String(); int size() const; String& operator=(const String&); // s1 = s2; מבוא לתכנות מערכות

47 String String& operator+=(const String& str); // s1 += s2; const char& operator[](int index) const; // c = s1[5] char& operator[](int index); // s1[5] = 'a' friend ostream& operator<<(ostream&,const String&); // cout << s1; friend bool operator==(const String&, const String&); // s1==s2 friend bool operator<(const String&, const String&); // s1<s2 }; bool operator!=(const String& str1, const String& str2); bool operator<=(const String& str1, const String& str2); bool operator>(const String& str1, const String& str2); bool operator>=(const String& str1, const String& str2); String operator+(const String& str1, const String& str2); מבוא לתכנות מערכות

48 מימוש String בהמשך נחליף את error בשימוש בחריגות
void error(const char* str) { cerr << "Error: " << str << endl; exit(0); } char* String::allocate_and_copy(const char* str, int size) { return strcpy(new char[size+1], str); בהמשך נחליף את error בשימוש בחריגות פונקצית עזר סטטית, אין לנו כאן צורך ב-this מבוא לתכנות מערכות

49 מימוש String String::String(const char* str) : length(strlen(str)), data(allocate_and_copy(str, length)) { } String::String(const String& str) : length(str.size()), data(allocate_and_copy(str.data, length)) { String::~String() { delete[] data; int String::size() const { return length; מבוא לתכנות מערכות

50 מימוש String String& String::operator=(const String& str) { if (this == &str) { return *this; } delete[] data; data = allocate_and_copy(str.data, str.size()); length = str.length; } String& String::operator+=(const String& str) { char* new_data = allocate_and_copy(data, str.size() + size()); strcat(new_data, str.data); length += str.length; data = new_data; מבוא לתכנות מערכות

51 מימוש String void String::verify_index(int index) const { if (index >= size() || index < 0) { error("Bad index"); } return; const char& String::operator[](int index) const { verify_index(index); return data[index]; char& String::operator[](int index) { פונקציות המחזירות & צריכות בדרך כלל שתי גרסאות - עבור עצמים רגילים ועבור קבועים מבוא לתכנות מערכות

52 מימוש String bool operator==(const String& str1, const String& str2) { return strcmp(str1.data, str2.data) == 0; } ostream& operator<<(ostream& os, const String& str) { return os << str.data; bool operator<(const String& str1, const String& str2) { return strcmp(str1.data, str2.data) < 0; מבוא לתכנות מערכות

53 מימוש String bool operator!=(const String& str1, const String& str2) { return !(str1 == str2); } bool operator<=(const String& str1, const String& str2) { return !(str2 < str1); bool operator>(const String& str1, const String& str2) { return str2 < str1; bool operator>=(const String& str1, const String& str2) { return str2 <= str1; String operator+(const String& str1, const String& str2) { return String(str1) += str2; מבוא לתכנות מערכות

54 דוגמה - קוד המשתמש ב-String
int main(int argc, char **argv) { String s = "So long"; String s2 = "and thanks for all the fish."; String sum = s + " " + s2; sum[sum.size() - 1] = '!'; cout << sum << endl; return 0; } כל האפשרויות הקיימות עבור char* נתמכות במחלקה החדשה רק כעת אין צורך בניהול זיכרון מפורש מבוא לתכנות מערכות

55 std::string קובץ ה-header string מהספריה הסטנדרטית של C++ מכיל מימוש של המחלקה std::string אשר תומכת בכל הפעולות שמימשנו כאן ועוד הקפידו להשתמש ב-std::string ולא ב-char* עבור מחרוזות ב-++C מבוא לתכנות מערכות


הורד את "ppt "העמסת אופרטורים בנאים הורסים והשמות המרות friend

מצגות קשורות


מודעות Google