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

שיעור 7 - עבודה עם מצביעים

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

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

#include <iostream.h> 

class Stack
{
    int stk[100];
    int top;
public:
    Stack(): top(0) {}
    ~Stack() {}
    void Clear() {top=0;}


    void Push(int i) {if (top < 100) stk[top++]=i;}
    int Pop()
    {
        if (top > 0) return stk[--top]; 
        else return 0;
    }
    int Size() {return top;}
};

void main()
{
    Stack stack1, stack2;

    stack1.Push(10);
    stack1.Push(20);
    stack1.Push(30);
    cout << stack1.Pop() << endl;
    stack2=stack1;
    cout << stack1.Size() << endl;
    cout << stack2.Size() << endl;
    cout << stack2.Pop() << endl;
    cout << stack2.Pop() << endl;
}

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

typedef struct node
{
    int data;
    node *next;

} node; class Stack { node *top; public: Stack(): top(0) {} ~Stack() { Clear(); } void Clear() { node *p=top; while (p) { top = top->next; delete p; p = top; } } void Push(int i) { node *p = new node; p->data = i; p->next = top; top = p; } int Pop() { if (top != 0) { int d = top->data; node *p=top; top = top->next; delete p; return d; } else return 0; } int Size() { int c=0; node *p=top; while (p) { c++; p = p->next; } return c; } };

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

stack1 = stack2;

התרשים הבא מדגים מה קורה. כאשר פעולת השמה מתבצעת, היא פשוט מעתיקה את ה data members מ stack2 ל stack1 תוך השארת העתק אחד של הנתונים בערמה עם שני מצביעים שניגשים אליו:

לאחר ההשמה, המצביעים stack1.top ו stack2.top מצביעים שניהם לאותה שרשרת של גושי זיכרון. אם אז אחת המחסניות מנוקה, או אם אחת מבצעת Pop, המצביע השני יצביע לזיכרון שכבר אינו תקף. במכונות רבות הקוד יעבור הידור והכל יראה בסדר לזמן מה במהלך הביצוע. אבל ככל שהמערכת רצה הבעיה משתרשת בה והדברים הולכים ומתפוררים בלי שום סיבה נראית לעין עד שלבסוף התוכנית מתרסקת.

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

7.1 פונקציות ברירת מחדל

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

· בנאי ברירת המחדל

· בנאי ההעתקה של ברירת המחדל

· אופרטור ההשמה של ברירת המחדל

· הורס ברירת המחדל

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

Sample s:

ההצהרה המאותחלת הבאה של s2 מפעילה את בנאי ההעתקה:

Sample s1;
Sample s2 = s1;

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

בקוד להלן ניתן להשתמש כדי לרכוש הבנה של מה שהבנאי וההורס עושים:

#include <iostream.h> 

class Class0
{
    int data0;
public:
    Class0 () { cout << "class0 constructor" << endl; }
    ~Class0 () { cout << "class0 destructor" << endl; }
};

class Class1
{
    int data1;
public:
    Class1 () { cout << "class1 constructor" << endl; }
    ~Class1 () { cout << "class1 destructor" << endl; }
};

class Class2: public Class1
{
    int data2;
  Class0 c0;
};

void main()
{
    Class2 c;
}

למחלקה Class2 אין לא בנאי ולא הורס, אבל כאשר מריצים קוד זה הפלט הבא נוצר:

class1 constructor
class0 constructor
class0 destructor
class1 destructor

מה שקרה זה שהמהדר יצר בנאי והורס של ברירת מחדל ל Class2. הפעילות של הורס ברירת המחדל היא לקרוא לבנאי ברירת המחדל של מחלקת הבסיס בנוסף על הורס ברירת המחדל עבור כל ה data members שהם מחלקות. הורס ברירת המחדל קורא להורסים של מחלקת הבסיס ושל ה data members של המחלקה.

נניח שאתה יוצר בנאי חדש ל Class2 שמקבל מספר שלם. המהדר עדיין יקרא לבנאי ברירת מחדל הנחוצים למחלקת הבסיס ול data members של המחלקה. הקוד הבא מדגים את התהליך:

class Class2: public Class1
{
    int data2;
    Class0 c0;
public:
    Class2(int i) 
    { 
        cout << "class2 constructor" << endl; 
    }
};

void main()
{
    Class2 c(1);
}

גם זה פועל, ומייצר את הפלט הבא:

class1 constructor
class0 constructor
class2 constructor
class0 destructor
class1 destructor

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

Class2 c(1);    // OK
Class2 e;    // not OK--no default constructor

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

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

void Copy(const Stack&  s)
    {
        node *q=0;
        node *p=s.top;

        while (p)
        {
            if (top==0)
            {
                top = new node;
                q=top;
            }
            else
            {
                q->next = new node;
                q = q->next;
            }

            q->data = p->data;
            p = p->next;
            q->next=0;
        }
    }
    Stack&  operator= (const Stack&  s) //assignment
    {
        if (this == & s)
            return *this;
        Clear();
        Copy(s);
        return *this;
    }
    Stack(const Stack&  s): top(0) // copy constructor
    {
        Copy(s);
    }

הפונקציה לאופרטור ההשמה מתחילה בבדיקה למקרה של השמה שקולה, כמו ב:

s = s;

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

Stack s1;
s1.Push(10);
s1.Push(20);
Stack s2(s1);        // copy constructor invoked
Stack s3 = s1;    // copy constructor invoked

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

7.2 סיכום

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

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




לדף הראשון

<< לדף הקודם

לדף הבא >>