C/C++ מדריכי
הצגה מזורזת :C++ הבנת
C++ הקדמה להיררכית מחלקות ב C הקדמה לתכנות ב דף הבית

שיעור 8 - פונקציות וירטואליות

בשיעורים אלה ראינו דוגמאות רבות של ירושה, כיוון שירושה היא חשובה מאוד בתכנות מונחה עצמים. ראינו שירושה מאפשרת להוסיף data members ופונקציות חברות במחלקה הנגזרת. ראינו גם מספר דוגמאות בהן השתמשנו בירושה כדי לשנות את ההתנהגות של פונקציה. לדוגמא, בשיעור 3 ראינו דוגמא שבה פונקצית ה Insert של מחלקת בסיס List נדרסה כדי ליישם תכונה של סיכום. הירארכיה דומה מוצגת להלן, תוך שימוש במחלקת בסיס בשם List ומחלקה נגזרת בשם TotalingList:

#include <iostream.h> 

class List
{
    int array[100];
    int count;
public:
    List(): count(0) {}
    void Insert(int n) { array[count++]=n; }
    int Get(int i) { return array[i]; }
    int Size() { return count; }
};

void ManipList(List list)
{
    // do things to the list
    list.Insert(100);
    list.Insert(200);
    // do things to the list
}

class TotalingList: public List
{
    int total;
public:
    TotalingList(): List(), total(0) {}
    void Insert(int n)
    {
        total += n;
        List::Insert(n);
    }
    int GetTotal() { return total; }
};

void main()
{
    TotalingList list;
    int x;
    list.Insert(10);
    list.Insert(5);
    cout << list.GetTotal() << endl;
    ManipList(list);
    cout << list.GetTotal() << endl;
    for (x=0; x < list.Size(); x++)
        cout << list.Get(x) << ' ';
    cout << endl;
}

בקוד זה, המחלקה List מיישמת את מחלקת הרשימה הפשוטה ביותר האפשרית עם שלושת ה ,Get ,Insert :member functions ו Size נוסף על הבנאי. הפונקציה ManipList היא דוגמא לפונקציה שרירותית שמשתמשת במחלקה List, והיא קוראת לפונקצית ה Insert פעמיים רק כדוגמא.

המחלקה TotalingList יורשת את המחלקה List ומוסיפה לה data member בשם total. ה member הזה מחזיק את הסכום הנוכחי של כל המספרים המוחזקים ברשימה. פונקצית ה Insert נדרסת כך ש total מעודכן בכל הכנסה.

פונקצית ה main מצהירה על מופע של המחלקה TotalingList. היא מכניסה 10 ו 5, ומדפיסה את הסכום. לאחר מכן היא קוראת ל ManipList. זה עשוי להפתיע אותך שזה באמת עובר הידור -- אם תתבונן באב הטיפוס של ManipList תוכל לראות שהוא מצפה לפרמטר מטיפוס List ולא TotalingList. אבל ++C מבינה מספר דברים לגבי מחלקות שעברו בירושה, אחד מהם הוא שפרמטר מטיפוס של מחלקת בסיס צריך גם לקבל כל מחלקה שנגזרה ממחלקת הבסיס הזו. לפיכך, כיוון ש TotalingList נגזרת מהמחלקה List, ManipList תקבל אותו. זוהי אחת התכונות של ++C שעושות את הירושה לכל כך רבת עצמה -- ניתן ליצור מחלקות נגזרות ולהעביר אותן לפונקציות קיימות שיודעות רק על מחלקת הבסיס.

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

15
15
10 5

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

void ManipList

כעת הפלט יראה כך:

15
15
10 5 100 200

זה חינוכי לעבור צעד צעד על ה ManipList ולראות מה קורה. כאשר הקריאות לפונקצית ה Insert מתבצעות, הן מנתבות את עצמן ל List::Insert במקום ל TotalingList::Insert.

אולם, ניתן לפתור גם בעיה זו. ניתן ב ++C ליצור פונקציה עם הסיומת virtual, וזה גורם ל ++C לקרוא לגרסה של הפונקציה במחלקה הנגזרת. כלומר, כאשר מחלקה מוגדרת כ virtual, המהדר יכול לקרוא לגרסאות של הפונקציה שאפילו לא היו קיימות כאשר נכתב הקוד שקורא לפונקציה. כדי לראות זאת, הוסף את המילה virtual לפני פונקציות ה Insert הן במחלקה List והן במחלקה TotalingList, כפי שמוצג להלן:

class List
{
    int array[100];
    int count;
public:
    List(): count(0) {}
    virtual void Insert(int n) { array[count++]=n; }
    int Get(int i) { return array[i]; }
    int Size() { return count; }
};

void ManipList(List&  list)
{
    // do things to the list
    list.Insert(100);
    list.Insert(200);
    // do things to the list
}

class TotalingList: public List
{
    int total;
public:
    TotalingList(): List(), total(0) {}
    virtual void Insert(int n)
    {
        total += n;
        List::Insert(n);
    }
    int GetTotal() { return total; }
};

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

עכשיו כאשר תריץ את התוכנית, תקבל את הפלט הנכון:

15
315
10 5 100 200

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

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

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

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

סיכום

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

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

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




לדף הראשון

<< לדף הקודם