מס' שאלות:

מס' שאלות:

1) ידוע לי שבהעמסת אופרטור, יש את האובייקט המפעיל ואת הפרמטר, אבל לא ברור לי כיצד זה מתבטא בסינטקס. למשל בכיתוב הבא:
Point operator+(const Point& other) const

2) באילו מקרים חייבים להגדיר פונקציה מסוימת כפונקציה חברה?
3) לא ברור לי לגמרי המושג "העמסת פונקציות". האם מדובר בפונקציות שנושאות את אותו שם ורק הפרמטר שלהן שונה? ומתי קיימת סכנה שהקומפיילר לא ידע להבחין לאיזו פונקציה אנחנו קוראים במקרה כזה?
4) לגבי אופרטור השמה : האם יש לשחרר בתוכו זיכרון דינאמי? או שמשחררים רק בדסטרקטור?
5) במה שונה יצירת אובייקט חדש באמצעות קונסטרקטור לעומת הקצאה דינאמית לאובייקט כזה? האם הקצאה דינאמית לא משתמשת בקונסטרקטור?
 

BravoMan

Active member
מס' תשובות:

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

בדוגמה שלך, זה נראה כך:
קוד:
class Point {
public:
    Point operator+(const &Point);
}

Point a, b, c;

c = a + b;
\\מה שקורה למעשה זה:
c = a.operator+(b);


2. במקרים שממש רוצים לשבור "הכלה" (incapsulation) ולתת לפונקציה זרה למחלקה לגעת באיבריה הפנימיים.
כמו שנאמר - "C++ the only language where your friends can touch your privates"


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

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

3. תלוי במימוש, רוב הסיכויים שלא. זו שאלה ממש לא ברורה.

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

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

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

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

1) נניח שיש לי מחלקה class. במה שונה הקצאה דינאמית של אובייקט כזה, לדוגמה
class* c = new class;

לעומת האופן בו אני פשוט ארשום:
class c;
?

מדוע הקצאה דינאמית עשויה להיות עדיפה על הפעולה השניה כנ"ל?

2) נניח שיש לנו מחלקה של מטריצה דו מימדית. להלן הקוד הבא:
קוד:
Matrix  Matrix::operator +(const Matrix &other)const  
{
    Matrix temp(rows,cols);  
    if (rows!=other.rows ||cols!=other.cols)
    {
       for(int i=0;i<rows;i++)
        for(int j=0;j<cols;j++)
            temp.Mat[i][j]=Mat[i][j];
       return temp;
    }
    else
     {      
         for(int i=0;i<rows;i++)
             for(int j=0;j<cols;j++)
                 temp.Mat[i][j]+=other.Mat[i][j]+Mat[i][j];
     }
    return temp; 
}
א) האם נכון לומר שהקוד הבא מהווה העמסת אופרטור + לחיבור של שתי מטריצות?
ב) האם השורה
Matrix temp(rows,cols);
מתוך הקוד לא מהווה יצירה של אובייקט חדש ואתחולו ע"י הקונסטרקטור?
ג) בשורה
temp.Mat[i][j]+=other.Mat[i][j]+Mat[i][j];
מה מהווה כאן ה-
Mat[i][j]
לעומת ה-
other.Mat[i][j]
?
3) האם זוהי בניה נכונה של אופרטור השמה של מטריצה ?
קוד:
void Matrix::operator=(const Matrix &other )   // overloading operator =
{
    if(Mat !=other.Mat && cols==other.cols && rows==other.rows)
     {
       for(int i=0;i<rows;i++)
        for(int j=0;j<cols;j++)
            Mat[i][j]=other.Mat[i][j];
     }
}
4) נניח שאני מעוניין לבנות אופרטור של כפל מטריצה במספר שלם מימין ומשמאל. האם לצורך כך יש לבנות אופרטור של כפל מטריצה במספר שלם מימין, ופונקציה חברה של כפל מטריצה במספר שלם משמאל? מדוע למעשה אופציה זו תקינה? האם יש אלטרנטיבה אחרת?
 

BravoMan

Active member
נחלק את התשובה, ונתחיל מ-1:

אתה לא יכול ליצור מחלקה class כי זו מילה שמורה ב-++C.

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

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

אתה שואל את המשתמש כמה מכוניות יש לו, ומקצה כמות אובייקטים בהתאם ככה:
קוד:
class Car {
}

int numberOfCars;
cout << "How many cars do you have?" << endl;
cin >> numberOfCars;

Car *cars = new Car[numberOfCars];

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

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

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

לכל סוג הקצעה יש שימוש מתאים שתלוי בצרכים של התוכנה שלך.
רוב התוכנות ישתמשו בשני סוגי הקצעות במקומות שונים.
 

BravoMan

Active member
סיף 2:

א) נכון לומר.
ב) אכן, מהווה יצירה ואתחול.
ג) זו גישה לאיבר של המחלקה.
נראה שלמחלקה Matrix יש איבר בשם Mat, שהוא סוג של מערך דו ממדי (או אובייקט שמתנהג כמו מערך דו ממדי), שהוא זה שמחזיק בפועל את המספרים של המטריצה.
&nbsp
אז ניגשים לאיבר של האובייקט עליו הפעילו את פונקציית האופרטור (צד שמאל של ה-+, כמו שהראיתי לך בתשובה קודמת), ולאותו איבר של אובייקט אחר (צד ימין) אותו קיבלנו בתור פרמטר בשם other.
 

BravoMan

Active member
סעיף 3

אני חושב שלא, אבל אי אפשר לדעת בלי לראות את המימוש המלא של המחלקה Matrix.
&nbsp
ראשית, מה משמעות הבדיקה Mat != other.Mat בתנאי?
שנית, מה קורה אם גודל המטריצות אינו זהה?
&nbsp
כרגע לא קורה כלום, אבל האם זו התנהגות רצויה?
 

BravoMan

Active member
סעיף 4:

השאלה בסעיף לא ברורה:
&nbsp
אין הבדל מתמטי מבחינת אם המספר השלם מופיע מימין או משמאל.
אם אתה רק מחפש תירוץ להתאמן בכתיבת פונקציות דריסת אופרטור, ניחה, אבל אם אתה חושב שצריך באמת פונקציונליות שונה אז אני לא בטוח למה אתה מתכוון.
&nbsp
על איזו אופציה בדיוק אתה שואל?
ולמה לדעתך אופציה כזו לא צריכה להיות תקינה?
&nbsp
בהנחה שאתה דורס אופרטור ע"י פונקציה חיצונית למחלקה, היא לא בהכרח צריכה להיות "חברה" של המחלקה.
למשל, האם אפשר לגשת לאיברים של המטריצה מבחוץ, נניח דרך מתודה פומבית (public)?
&nbsp
אם כן, אפשר לממש אופרטור חיבור בלי פונקציה חברה.
&nbsp
אם אי אפשר לגשת (מטריצה די לא שימושית?), אז חייבים פונקציה חברה.
 
לגבי סעיף 4

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

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

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

BravoMan

Active member
כי האובייקט temp מוקצה סטטית

אובייקטים שמוקצים סטטית ממוקמים על המחסנית - זיכרון מקומי של פונקציה.
ברגע שהפונקציה מסתיימת, הזיכרון הזה משוחרר, וכל האובייקטים עליו מושמדים אוטומטית.
&nbsp
אם היית מחזיר רפרנס ( & ) ל-temp, הרפרנס היה לא חוקי מחוץ לפונקציה, כי האובייקט אליו הוא מתייחס כבר לא קיים.
&nbsp
לכן, מחזירים את האובייקט by value, כלומר למעשה נוצר עותק שלו שממשיך להתקיים גם אחרי שהפונקציה הסתיימה ו-temp מקורי הושמד.
&nbsp
ניתן להחזיר רפרנס ( & ) רק לאובייקטים שממשיכים להתקיים.
&nbsp
בכל מקרה, אופרטור חיבור אמור להחזיר ערך, לא רפרנס ולא מצביע, לכן אין לך ברירה אלא להחזיר ערך.
&nbsp
זכור! העמסת אופרטורים נועדה לאפשר לך לתת משמעות לאופרטור עבור טיפוס נתונים שיצרת בעצמך (מחלקה שלך).
היא לא מאפשרת לשנות את ההתנהגות של האופרטורים, כלומר, אם אופרטור מקבל 2 פרמטרים ומחזיר ערך, זה מה שהוא ימשיך לעשות.
&nbsp
אם אופרטור אמור להחזיר רפרנס (כמו במקרה של הצבה) זה מה שהוא ימשיך לעשות.
 

BravoMan

Active member
זוכר ששאלת למה צריך this?

זו דוגמה מעולה!

אופרטור השמה צריך להראות כך:
קוד:
Matrix& Matrix::operator=(const Matrix &other){
    ...
    return *this;
}

למה להחזיר רפרנס לאובייקט עצמו?
כדי שניתן יהיה לשרשר פעולות נוספות כפי שחוקי לעשות ב-++C, למשל כך:
קוד:
int a, b, c;
c = 42;
a = b = c;

Matrix m1, m2, m3;
m1 = m2 = m3;
כמובן שאתה לא צריך ליצור שום אובייקט חדש, כי זה לא מה שאופרטור השמה עושה. הוא מציב ערך בתוך אובייקט קיים.

לגבי מה צריך לקרות אם המטריצות אינן בגודל זהה?
זה תלוי בך! אתה קובע את המימוש.

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

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

BravoMan

Active member
אז, כל מה שאתה רוצה זה לתמוך ב-2 צורות כתיבה?

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

BravoMan

Active member
פונקציה צריכה להיות "חברה" רק אם היא ניגשת לאיברי

מחלקה שהם private או protected בצורה ישירה.

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

אם כבר ממשת בתוך המחלקה פונקציית כפל שמקבלת מצד שמאל אובייקט Matrix ומצד ימין מספר, המימוש של הפונקציה הפוכה יהיה טריוויאלי, ואין צורך שהיא תהיה חברה:
קוד:
class Matrix {
    public:
        Matrix operator*(const int right) { ... }
};

Matrix operator*(const int left, const Matrix& right) {
    return right * left;
}
 
מדוע מימשת את הפונקציה הנ"ל מחוץ למחלקה?

אם לא כותבים אותה מחוץ למחלקה, אז היכן למעשה כותבים אותה? בתוך ה-main בלבד? לא כותבים בקובץ h ובקובץ cpp של המחלקה ?
ואם כבר מימשת את הפונקציה שמבצעת כפל של מטריצה בקבוע משמאל מחוץ למחלקה, אז מדוע לא לממש גם מחוץ למחלקה את הפונקציה שמבצעת את הכפל מימין??
האם יש קשר לכך שהאובייקט נמצא תמיד מצד שמאל והפרמטר מצד ימין?
 

BravoMan

Active member
תסתכל בהודעה הראשונה שלי בשרשור.

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