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

Slides:



Advertisements
מצגות קשורות
ילדי גן "יסמין" וגן פרפר נחמד" נוטעים יחד עם חיילי סמ"ג חוף
Advertisements

כרטיסי מידע להכרת הציפורים שבקרבת משכנות האדם
الاشكال الهندسية צורות הנדסיות
עמל שפרעם-מודל להתמודדות עם תלמידים תת משיגים
وظائف الفجوة الخلوية وظائف الفجوة المحافظة علي استمرارية ضغط الامتلاء Turger pressure للخلية وهو هام جدا للتركيب الدعامي وللتحكم في حركة الماء.
מגמת ניהול עסקי "קציר".
מיפוי הנשים הפלסטיניות הכותבות אחרי 1948
תמליל מצגת:

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

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

ניתן להגדיר את האופרטור כמתודה במקרה זה הארגומנט הראשון הוא ה-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 מבוא לתכנות מערכות - 234122

העמסת אופרטורים ניתן להעמיס על האופרטורים הבאים: + - * / % ^ & | ~ ! = < > += -= *= /= %= ^= &= |= << >> >>= <<= == != <= >= && || ++ -- ->* , -> [] () 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 אין את ההגבלה הדורשת שאחד המשתנים יהיה מטיפוס מחלקה (מחוץ לחומר) מבוא לתכנות מערכות - 234122

העמסת אופרטורים ניתן להגדיר את האופרטורים כמתודות או כפונקציות חיצוניות אם אופרטור המקבל 𝑛 פרמטרים מוגדר כמתודה הפרמטר הראשון הוא ה-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); מבוא לתכנות מערכות - 234122

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

אופרטור - אונארי יוצר מספר מרוכב חדש, לכן התוצאה מוחזרת כעותק חדש דוגמה - מחלקת 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); אופרטורים הכוללים השמה כמו += מאפשרים שרשור ולכן נשמור על ההתנהגות הזו אצלנו אופרטור - אונארי יוצר מספר מרוכב חדש, לכן התוצאה מוחזרת כעותק חדש אופרטורים סימטריים נהוג להכריז מחוץ למחלקה בגלל המרות (דוגמה בהמשך התרגול) מבוא לתכנות מערכות - 234122

דוגמה - מחלקת 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; מבוא לתכנות מערכות - 234122

דוגמה - מחלקת 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=2.2+5.1i c -= a; cout << (c == b) << endl; return 0; הערה לגבי operator==: שימוש ב-== על ערכים מטיפוס double לא מדויק והאופרטור מובא כאן רק כחלק מהדוגמא. בפועל הוא לא אמין. מבוא לתכנות מערכות - 234122

העמסת אופרטורים אופרטור [] (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); מבוא לתכנות מערכות - 234122

העמסת אופרטורים לאופרטורים ++ ו--- יש שתי גרסאות: 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 בקוד המשתמש בטיפוסים מורכבים עם אופרטור זה. מדוע ערך ההחזרה שונה? מבוא לתכנות מערכות - 234122

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

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

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

איזה בנאי נקרא בכל אחת מהשורות? בנאים - 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; } איזה בנאי נקרא בכל אחת מהשורות? מבוא לתכנות מערכות - 234122

רשימת אתחול חשוב להבדיל ב-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() נקראים כאן מבוא לתכנות מערכות - 234122

בנאי העתקה - 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&) } מבוא לתכנות מערכות - 234122

בנאי העתקה - 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]; } מבוא לתכנות מערכות - 234122

מתי נקראים הורסים בקוד הזה? הורסים - 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 מתי נקראים הורסים בקוד הזה? מבוא לתכנות מערכות - 234122

זמני קריאה 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 מה יודפס? מבוא לתכנות מערכות - 234122

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

מחיקת מידע ישן, בניגוד לבנאי העתקה צריך לנקות את המידע הקודם אופרטור השמה אופרטור השמה למחסנית 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 עבור שרשור מימוש זה לא מתאים עבור מקרה של חריגות (שיילמדו בהמשך), נראה מימוש טוב יותר לאופרטור= בעתיד מבוא לתכנות מערכות - 234122

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

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

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

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

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

המרה על ידי בנאי 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 = c2 + 2.0; 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 פרמטרים מבוא לתכנות מערכות - 234122

העמסת אופרטור המרה 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; השימוש בבנאי עבור המרה אינו מאפשר לנו להמיר עצם ממחלקה לערך בסיסי או להמיר עצם ממחלקה חדשה למחלקה ישנה ניתן להעמיס על אופרטור המרה - שם האופרטור כשם הטיפוס אליו נרצה להמיר את האופרטור יש להגדיר כמתודה האופרטור מוגדר ללא ערך חזרה, טיפוס ערך החזרה נקבע לפי שם האופרטור מבוא לתכנות מערכות - 234122

המרות אוטומטיות מומלץ להימנע מהגדרת המרות אוטומטיות ולהעדיף הגדרת פונקציות מפורשות לביצוע המרות המרות אוטומטיות עלולות לגרום לדו-משמעות המרות אוטומטיות עלולות לגרום לקוד מוזר להתקמפל 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 } מבוא לתכנות מערכות - 234122

המרות אוטומטיות ניתן להוסיף את המילה השמורה 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 } מבוא לתכנות מערכות - 234122

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

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

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

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(); מבוא לתכנות מערכות - 234122

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 - חפשו תחילה פתרון המשתמש בתכן אחר לבעיה מבוא לתכנות מערכות - 234122

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; } מבוא לתכנות מערכות - 234122

העמסת אופרטור הפלט אופרטור הפלט >> מועמס כדי לאפשר הדפסה נוחה של עצמים לא נוכל להגדיר את אופרטור >> כמתודה של מחלקה שכתבנו, מדוע? נגדיר את אופרטור >> כפונקציה חיצונית: במקרים רבים נצטרך גישה לשדות פרטיים - במקרים אלו נכריז על האופרטור כ-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); }; מבוא לתכנות מערכות - 234122

מחלקת 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 מבוא לתכנות מערכות - 234122

מחלקת 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. מבוא לתכנות מערכות - 234122

מחלקת 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? מבוא לתכנות מערכות - 234122

מחלקת 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 אינו מדויק וכאי להימנע משימוש בו. מבוא לתכנות מערכות - 234122

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

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

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

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; מבוא לתכנות מערכות - 234122

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); מבוא לתכנות מערכות - 234122

מימוש 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 מבוא לתכנות מערכות - 234122

מימוש 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; מבוא לתכנות מערכות - 234122

מימוש 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; מבוא לתכנות מערכות - 234122

מימוש 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) { פונקציות המחזירות & צריכות בדרך כלל שתי גרסאות - עבור עצמים רגילים ועבור קבועים מבוא לתכנות מערכות - 234122

מימוש 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; מבוא לתכנות מערכות - 234122

מימוש 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; מבוא לתכנות מערכות - 234122

דוגמה - קוד המשתמש ב-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* נתמכות במחלקה החדשה רק כעת אין צורך בניהול זיכרון מפורש מבוא לתכנות מערכות - 234122

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