חידה

DadleFish

New member
חידה ../images/Emo13.gif

נא לא לכתוב את התשובה בכותרת בקוד הבא יש זליגת זכרון. איפה היא, ואיך ניתן למנוע אותה?
#include <string> #include <iostream> using namespace std; class CBase { public: CBase(string &s) { m_s = s; } void PrintAndDelete() { Print(); delete this; } protected: virtual void Print() = 0; protected: string m_s; }; class CDerived : public CBase { public: CDerived(int nData, string &sAddedInformation) : CBase(string("Hi")), m_sAddedInformation(sAddedInformation), m_nData(nData) {} virtual void Print() { cout << m_s << ", " << m_nData << ", " << m_sAddedInformation << endl; } private: string m_sAddedInformation; int m_nData; }; int main() { CDerived *pDerived = NULL; for (int i = 0; i < 1000; i++) { pDerived = new CDerived(i, string("Hello!")); pDerived->PrintAndDelete(); } return 0; }​
 

gilad_no

New member
רעיון

הDESTRUCTOR של m_sAddedInformation לעולם לא ייקרא מכיוון שTHIS בPRINTANDELETE מכיר את CBASE ולא את המחלקה היורשת? או בצורה יותר ברורה: DELETE THIS מוחק מצביע של CBASE ולא של CDERIVED ולכן הוא לא ישחרר את m_sAddedInformation שמוגדרת בCDERIVED?
 

vinney

Well-known member
נראה לי

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

voguemaster

New member
למה לא עונים..

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

annefan

New member
אפשר שאלה אחרת?

אני מבין שהגעתי מאוחר מדי, ושנים כבר פתרו. הקריאה ל-delete מריחה לי לא טוב. אבל יש לי שאלה אחרת. למה אתה כותב קוד שלא מתקמפל?? ++g לא מקמפל אותו, וב-VS אתה משתמש ב-language extension כדי לאפשר cast.
 

annefan

New member
שאלה מעצבנת!!

אני לא לגמרי מבין מה הולך כאן. השאלה היא העצם מהו הטיפוס של this. כשאני עוקב אחרי התוכנית בדיבאגר, אחרי הכניסה ל-PrintAndDelete הטיפוס (גם ב-VS וגם ב-gdb) הוא CBase * const. בשורה הבאה, אחרי הכניסה ל-Print, הטיפוס הוא CDerived * const. כשאני חוזר לפונקציה PrintAndDelete, הטיפוס שוב CBase * const, ועליו נקרא delete. הוירטואליות של ה-DTOR לא היתה נראית לי קשורה כי הקריאה לפונקציה נעשית דרך אוביקט מסוג הבן, ולא האב. מצד שני, PrintAndDelete גם היא לא וירטואלית, כך שאולי משם מתחילה הבעיה. מצד שלישי, ה-vtable, בכל מקרה, מכיל מצביע ל-CBase::printAndDelete, בין אם היא וירטואלית או לא. משהו מוזר פה...
 

selalerer

New member
זאת דווקא דוגמא מצויינת לסיבה להשתמ

ש במפרק וירטואלי בכל מחלקה נורשת. אני לא התעצבנתי
 

annefan

New member
תשובה ומשהו הרבה יותר חשוב!

א. בעקרון אתה צודק. מחלקה שיורשים ממנה, כמעט בכל מקרה צריכה virtual dtor. ב. מה זה בעצם קשור לחידה של אלדד? וירטואליות באה לידי ביטוי כשאתה פונה לאוביקט מסוג בן דרך מצביע מסוג של מצביע לאבא. זה לא המקרה כאן. ג. והחשוב! מי אמר שיש פה memory leak???? בדקתי עם _CrtDumpMemoryLeaks ב-Windows והוא לא מזהה leak (לשם ביקורת, הורדתי את הקריאה ל-PrintAndDelete והוא מדווח על leak כמו תנין!) בדקתי עם valgrind בלינוקס ואין leak (שוב, לביקורת הורדתי את הקריאה ל-PrintAndDelete והנזילה הופיעה). אלדד, איך גילית שיש leak?
 

DadleFish

New member
איך גיליתי שיש LEAK ../images/Emo13.gif

מטעמים שלא ניכנס אליהם כרגע נאלצתי לכתוב כל מיני מנגנונים שמסופקים על ידי מערכת ההפעלה בכוחות עצמי. בין השאר כתבתי EVENT HANDLER. לכל THREAD יש QUEUE של EVENT-ים, שהם כמובן היררכיה. אתם יכולים להחליף בדוגמה שלי את PrintAndDelete ב-InvokeAndDelete ותתחילו להבין על מה אני מדבר. אחד האובייקטים שירשו - EVENT של TIMER לצורך העניין - החזיק STRING בתוכו. שמתי לב שכשמנגנון הטיימרים שלי רץ, יש לי LEAK של 4KB כל 10 שניות לכל טיימר (כלומר, אם היו 10 טיימרים, אז כל שניה). אחרי בידוד הבעיה הבנתי שזה בגלל שברוב טמטומי לא עשיתי virtual dtor ב-base class. הקוד שנתתי פה כדוגמה מתקמפל ב-VS, ואם תריץ אותו עם BoundsChecker תראה בבירור שיש לך 1000 זליגות של string אחד - זה הפנימי ב-derived. לגבי CrtDumpMemoryLeaks, אני חייב להודות שאני מפונק ולא השתמשתי בו מעולם (אלא בכלים אוטומטיים כמו purify ו-bc). יכול להיות שמשהו לא בסדר במה שעשית, ויכול להיות שהוא סתם לא מוצלח. אותו דבר לגבי valgrind. לגבי הניתוח שלך בהתחלה, זה פשוט מאוד כשמבינים את הפתרון. תראה את הדוגמה הבאה:
class Base { public: int Init() { ... SpecificInit(); ... } virtual int SpecificInit() = 0; } class Derived { public: virtaul int SpecificInit() { blah; } }​
אם יש לך מצביע ל-Derived ואתה קורא ל-Init, אתה בעצם קורא לקוד של ה-Init שמגיע אליך מה-Base. הקוד הזה מנסה לקרוא לקוד של SpecificInit המקומי, אבל בגלל שזו פונקציה וירטואלית הוא יילך לפי ה-vtbl הנוכחי, ויגיע לפונקציה הנכונה. עכשיו תבדוק את הדוגמה הבאה:
class Base { public: int Init() { ... SpecificInit(); ... } int SpecificInit() { blah1; } } class Derived { public: int SpecificInit() { blah2; } }​
במקרה הזה יתבצע blah1 מה-base, ולא blah2 מה-derived. אותו הדבר קורה עם destructor, אלא ש-destructor זה משהו שלא תמיד רואים, כי הוא נוצר בצורה סמויה.
 

annefan

New member
(גירוד בראש המעיד על חוסר הבנה)

לגבי הכלים, הצרה איתם היא שהם מדווחים יותר מדי, או פחות מדי. CrtDumpMemoryLeaks לא מדווח על כלום. ב-valgrind הוא מדווח על leak אבל לא בקוד שלך, אלא בזה של הספריה הסטנדרטית - go figure... לגבי ההסבר, אני עדיין לא מבין ויש לי שאלה. מה קורה כשאתה עושה את PrintAndDelte וירטואלית? אתה מכניס כאן מושג של context בה הפונקציה נקראת. אני קורא לפונקציה דרך מצביע לבן, כלומר הפונקציה של הבן אמורה להיות נקראת (למרות שהקוד שלה באבא). עדיין לא הבנתי, אבל אני אחשוב, אני אחפש ואולי אתה תסביר לי...
 

gilad_no

New member
הסברים

קודם כל, הנזילה היא *אכן* בסיפרייה הסטנדרטית - ליתר דיוק בSTRING. לגבי הCONTEXT: נראה לי שאתה לא הפנמת עד הסוף את מושג הפונקציות הווירטואליות. להלן הסבר קצר:
class CBase { public: void A() { cout<<"CBase::A"; } virtual void B() { cout<<"CBase::B"; } void C() { A(); B(); } }; class CDerived : public CBase { public: void A() { cout<<"CDerived::A"; } virtual void B() { cout<<"CDerived::B"; } }; int main() { CBase* pBase=new CDerived; pBase->C(); delete pBase; return 0; }​
מה יקרה פה? במחלקה היורשת שלנו, גם A וגם B מוגדרים שוב. לכן נראה הגיוני שהפונקציות שלהם ייקראו - אך לא כך הדבר. C מוגדרת בCBASE. עקב כך, היא נקראת בקונטקסט של CBASE (אשר *לא* מכירה את CDERIVED!!!). כאשר נקרא לA, היא תקרא לA של CBASE!. כאשר נקרא לB, היא תחפש את הגירסה הכי עדכנית של B (בCDERIVED) וזאת מכיוון שהגדרנו אותה כVIRTUAL. או דבר בDESTRUCTOR. כאשר הפונקציה בCBASE קוראת לDELETE, היא קוראת לDELETE של *אותה* המחלקה! רק אם נגדיר את המפרק בתור VIRTUAL, נבטיח שבקריאה לDELETE המפרק הכי עדכני ייקרא (בCDERIVED)
 

ברנדל

New member
נדמה לי שאת זה הוא יודע ../images/Emo8.gif

במקרה שאלדד הציג ה type של המצביע עצמו הוא ממחלקה יורשת. ולכן הוא שאל לגבי קריאה למפרק של מצביע האב.
 

annefan

New member
מה שברנדל אמר

בקוד של אלדד המצביע הוא לבן, לא לאב.
 

annefan

New member
מה שנראה לי

אני כותב את הדברים בהסתייגות שאני לא בטוח ב-100%... הנה קוד דומה, בלי leak אבל עם הדפסות.
#include <string> #include <iostream> #include <typeinfo> using namespace std; class CBase { public: void Print() // Is virtual needed here? { cout << "in CBase::print " << typeid(this).name() << endl; ppp(); } void ppp() // What happens when it's virtual? { cout << "in CBase::ppp " << typeid(this).name() << endl; } }; class CDerived : public CBase { public: void ppp() { cout << "in CDerived::ppp " << typeid(this).name() << endl; } }; int main() { CDerived * pDerived = new CDerived; pDerived->Print(); delete pDerived; return 0; }​
יש פה כמה דברים שצריך להבחין בינהם, בעיקר בין הוירטואליות של פונקציות לבין בחירת פונקציה מתאימה ע"י הקומפיילר. בואו נתחיל. אנחנו קוראים ל-Print עם מצביע לבן. להבנתי, אין כאן שום משמעות לוירטואליות, כיוון שאיננו קוראים עם מצביע לבן. לכן, אין משמעות להכרזת הפונקציה הזו כוירטואלית (כלומר, מה שברנדל כתב שזה פתרון, כנראה לא נכון). הקומפיילר מחפש פונקציות מתאימות ל-CDerived::print. ב-CDerived עצמו אין (וגם אם היה), הוא מחפש באבות של CDerived, במקרה הזה ב-CBase, ומוצא. (יכול להיות שזה נובע מכך שה-prototype של הפונקציה הוא
Print(CBase)​
ו-CDerived הוא CBase כיוון שהוא יורש כ-public ממנו.) טוב, הגענו ל-Print. כאן, צריך לעקוב אחרי הטיפוס המדויק של this. כיוון ש-Print נמצאת באב, הטיפוס שלו הוא CBase (אולי סוג של cast?). עכשיו הגענו לקריאה ל-ppp. במקרה ש-ppp איננה וירטואלית, שוב נעשה חיפוש של הפונקציה המתאימה ביותר, והיחידה שמתאימה ל-ppp היא זו שבאב, ולכן היא נקראת. אם היא כן וירטואלית, ה-vtbl קובע למי לקרוא, כמובן לזו של הבן. המסקנה מהחידה הזו שונה (וחזקה יותר) מהכלל של virtual DTOR לכל מחלקה נורשת, שיש סיכוי שיפנו אליו דרך מצביע לאב, כיוון שאין כאן מצביע לאב בכלל, ובכל זאת התוכנית מתנהגת באופן לא צפוי. הערות? תיקונים?
 
למעלה