بتـــــاريخ : 2/22/2011 9:05:34 PM
الفــــــــئة
  • الحـــــــــــاسب
  • التعليقات المشاهدات التقييمات
    0 2584 0


    Advanced Function For Bigenners Operator Overloading , Copy Constructor , Assigment Operator

    الناقل : elmasry | العمر :42 | الكاتب الأصلى : Wajdy Essam | المصدر : www.arabteam2000-forum.com

    كلمات مفتاحية  :

    Operator OverLoading
    اعاده تعريف المعاملات أو كما يطلق عليها البعض التحميل الزائد للمعاملات .


    كما مر علينا من قبل عند دراستنا للدوال Function وعرفنا أنه يمكن للداله أن نعيد تعريفها (تحميل زائد ) Overload بحيث تستقبل وسائط مختلفه من حيث العدد أو النوع ، فإن هذا المفهوم نطبقه أيضا على المعاملات العاديه + ، - ، * ، < الخ ، حيث نعيد تعريف هذه المعاملات حتى تتعامل مع الكائنات وليس مع أنواع بيانات عاديه Primitive .

    لنوضح أكثر قليلا ، عندما نريد أن نجمع متغيرين من نوع int ، نقوم بتطبيق علامه الجمع + بالشكل التالي :

     
    int x = a+ b;


    لاحظ هنا جمعنا المتغير a مع المتغير b .

    الان في حاله أردنا جمع كائنين ، فاننا لا نستطيع تطبيق (سينتج خطأ عند الترجمه) :

     
    Object x = ObjectA + ObjectB;


    لأن المعامل + لا يستطيع معرفه ما الذي يجمعه أو كيف سيجمع كائن مع كائن ؟
    لذلك علينا (المبرمج) أن يعيد تعريف المعامل + بحيث يتعامل مع البيانات التي بداخل الكائن ويقوم بجمعها أو طرحها أو العمليه التي نريد .

    هذه هي الفكره من موضوع الـ Operator Overloading أي نجعل هذه الأشارات تتعامل مع الكائنات ، فقط ، طبعا جميع المعاملات نستطيع اعاده تعريفها مثل + ، - ، [] ، << ، ++ ، -- ، الخ ....

    لنبدأ في البدايه بالمعامل ++ وهي كما هو معروف لزياده واحد . وهذا المعامل يمكن أن يكون prefix أو Postfix أي يكون قبل المتغير (أو الكائن) ، أو بعد المتغير .


     
    int x = ++s; // this is prefix , add one first to s and then assign to x 
    int x = s++; // this is postfix , assign s to x first and then add one to s


    نبدأ بمثال بسيط أولا ، ونرى كيف يمكن تحقيق هذا المثال بدون استخدام مفهوم الoperator overloading ، وبعدها نقوم بتطبيق المفهوم لنرى ماذا يقدمه لنا .

    ليكن لدينا كلاس اسمه Counter هذا الكلاس يحتوي على متغير x ، ونريد أن نزيد كل مره 1 الى هذا المتغير .

    شاهد الكود التالي ، وفيه سنستخدم الداله add وهي التي تزيد 1 على قيمه المتغير .

     
    // solution without using operator overloading
    #include <iostream>
    using namespace std;
    class Counter
    {
     private :
       int x;
       
     public :
       Counter (int c ) : x(c)
       { }
       
       // this function increment x by 1
       void add ()
       { x = x+1; }
       
       int getX ()
       { return x; }  
    };
    int main ()
    {
     Counter c(1); // x now is 1
     c.add(); // x increment by 1
     cout << c.getX() << endl; // print x i.e 2
    }



    الان لدينا الداله Add وهي تؤدي الغرض المطلوب (زياده 1 ) ، ولكن ألقى نظره على المثال التالي :

     
    // solution using operator overloading
    #include <iostream>
    using namespace std;
    class Counter
    {
     private :
       int x;
       
     public :
       Counter (int c ) : x(c)
       { }
       
       // this function increment x by 1
       void operator++ ()
       { x++; }
       
       int getX ()
       { return x; }  
    };

    int main ()
    {
     Counter c(1); // x now is 1
     ++c; // x increment by 1
     cout << c.getX() << endl; // print x i.e 2
    }



    هنا في هذا المثال استخدمنا مفهوم التحميل الزائد Operator Overlaoding ، ونرى في المثال السابق أن البرنامج أصبح أكثر مقروئيه Readability ، حيث أصبح بمكاننا استخدام ++ مباشره مع الكائن ومن دون أي دوال أخرى .

    ربما تتسائل الان ، هل الفائده فقط هي هذه المقروئيه ؟؟
    بالطبع لا ، ولكن في مثل هذه البرامج البسيطه Toys Example لن تتضح الفائده الكبيره من وراء هذا المفهوم ، ولكنها ستتضح في عندما تحاول تحل مسأله أو مشكله ما ، وسوف نأخذ في نهايه الموضوع بعض الأمثله على هذا .

    اذاً حاليا بشكل عام عليك أن تعرف كيفيه استخدام هذا المفهوم ، بعدها متى تستخدمه يعود على حسب المسألة التي تحلها .

    الشكل العام للـ Operator Overlaoding Function هو :

     
    returnType Operator op (parameters);


    op هنا تدل على المعامل الذي نود استخدامه ( ++ ، -- ، + ، * ، ==)
    (هناك المعامل = وهو يتم تعريفه مباشره عند عملك للكلاس ، أي يقوم الكلاس بتزويده لك مباشره default assignment operator ، سنتكلم عنه بعد قليل )

    تبقى شيء واحد في المثال السابق ، وهو أن المثال السابق إعتمدنا مفهوم الزياده القبليه Prefix increment ، فلو قمت بتغيير هذه الزياده من prefix الى postfix ، بدون تغيير الداله الموجوده في الكلاس ، فسوف ينتج خطأ (في حاله استخدمت مترجم حديث مثل gcc ، أو مترجم فيجول سي++ تقريبا اسمه make ) أما اذا استخدمت مترجم قديم فسوف يتنج تحذير warring فقط (مترجم قديم أقصد به Trbuo c++ ، ولا أعلم لماذا الكثير من الجامعات (وخاصه هنا) تحب هذه البيئه مع انه أكل عليها الدهر وشرب ) .

    لذلك علينا عمل داله للPrefix وداله لل postfix ( طبعا سواء مع الجمع أو الطرح ، الفكره نفسها) .

     
    void operator ++ (); // this prefix increment
    void operator ++ (int); // this postfix increment


    هنا لاحظ أن في الزياده البعديه postfix نرسل لها متغير ما (أي متغير) ، وفي الحقيقه سي++ تدعم مفهوم عمل داله تستقبل نوع بيانات معين ولكن من غير تحديد اسم له .

    الان هكذا أصبحنا نفرق بين prefix و postfix ، ولكن ماذا أذا أردنا أن تكون العمليه مشابه لما يحصل مع المتغيرات :

     
    int x = ++s; // this is prefix , add one first to s and then assign to x 
    int x = s++; // this is postfix , assign s to x first and then add one to s


    أي تصبح :

     
    Counter x = ++s; // this is prefix , add one first to s and then assign to x 
    Counter x = s++; // this is postfix , assign s to x first and then add one to s


    فلو غيرنا في المثال السابق الجمله ++c وجعلناها ترجع قيمه بعد الزياده ، فسوف يكون هناك خطأ وهو اننا عرفنا المعامل :

     
    void operator ++ (); // this prefix increment


    بحيث أنه لا يرجع قيمه ... ولذلك علينا بتعديله بحيث يرجع القيمه التي زدناها .

    وهنا :

     
    void operator ++ (int); // this postfix increment


    نقوم بارجاع القيمه الحاليه للكائن ، بعدها يتم زيادته بواحد .
    (عن طريق عمل كائن مؤقت نحفظ فيه قيمه الكائن الحالي ، ونجمع واحد للكائن الحالي ، ومن ثم نرجع الكائن المؤقت) .

    قبل أن ننتقل الى المثال ، علينا بمعرفه كيفيه التعامل مع المؤشر this ، وسوف يكون له الكثير من الأستخدامات في الأمثله القادمه ،

    أولا علينا أن نعرف أن this هي مؤشر للكائن الحالي (كلام مهم ) .
    ما المقصود بالكائن الحالي ؟
    الكائن الحالي هو الكائن الذي انشأته في الداله main ، وقمت باستدعاء داله ما من الكلاس ، الان في داخل هذه الداله أنا لن أستطيع أن أعرف ما هو الكائن الذي أستدعى هذه الداله ، لذلك اذا اردت أن اشير للكائن الحالي استخدم this ،

    اي this هي تأخذ نفس عنوان الكائن الذي استدعى الداله ... نأخذ مثال بسيط يبين ذلك :

     
    #include <iostream>
    using namespace std;
    class Simple
    {
     public :
       void printAddress ()
       { cout << this << endl; }
    };
    int main ()
    {
     Simple c;
     
     cout << &c << endl;
     c.printAddress();
    }


    الان هنا ، قمنا بعمل كائن من الكلاس Simple ،
    بعدها قمنا بطباعه عنوان الكائن (باستخدام معامل Addrss of Operator & ) .
    بعدها تأتي النقطه الأهم ، استدعينا الداله printAddress ، وفيه داخل هذه الداله اردنا أن نطبع عنوان الكائن الحالي ، كيف نطبع العنوان ؟ بالطبع عن طريق this .
    جرب تنفيذ البرنامج السابق وسترى المخرج هو عباره عن عنون الكائن c .
    اذا باختصار this هي مؤشر للكائن الحالي .

    وطبعا جميعنا يعلم اننا اذا اردنا أن نطبع القيمه الموجوده داخل المؤشر يجب أن نستخدم علامه * وتسمى Derefrence ، أي اننا اذا كتبنا *this هنا معناه أننا أردنا قيمه الكائن الحالي . مفهوم ؟

    طبعا هناك أستخدام أخر لل this ، هو عندما يكون لدي مثلا متغير اسمه x في الكلاس ، وفي داله البناء لنفس الكلاس ارسلنا متغير اسمه x ، فكيف نفرق بين x الموجوده داخل الكلاس عن الأخرى المرسله ؟ وذلك باستخدام this مع المتغير الموجود داخل الكلاس this->x ;

    (انظر في داله البناء في المثال القادم ، وهو يشرح هذه الطريقه) .

    نخرج من this الان ، ونعود الى المثال السابق ، حيث كنا نريد أن المعامل ++ (سواء prefix or postfix) يقوم بارجاع القيمه التي تم زيادتها .

    قم بتشغيل المثال السابق ، وسوف ترى أن هذه المعاملات أصبحت تعمل كما هو المطلوب تماما :

     
    #include <iostream>
    using namespace std;
    class Counter
    {
     private :
      int x;
     
     public :
      Counter (int x)
      { this->x = x; } // here another usage for this  
     
      Counter operator++ ()
      {
       x++;
       return *this;
      }
     
      Counter operator++ (int)
      {
       Counter tmp(*this);
       x++;
       return tmp;
      }
     
      int getX()
      { return x; }
     
    };
    int main ()
    {
     Counter c(1);
     cout << "c = " << c.getX() << endl;
     
     c++;
     cout << "c++ : " <<  c.getX() << endl;
     
     ++c;
     cout << "++c : " <<  c.getX() << endl;
     
     Counter d = c; // here call to copy Constructor , we will explain it later
     d = c++;
     cout << "d = c++ \n";
     cout << "c   : " <<  c.getX() << endl;
     cout << "d   : " <<  d.getX() << endl;
     
     d = ++c;
     cout << "d = ++c \n";
     cout << "c   : " <<  c.getX() << endl;
     cout << "d   : " <<  d.getX() << endl;
     
     return 0;
    }


    الان البرنامج تمام وما فيه مشكله ، ولكن (وأه من لكن :) ) في الداله :

     
    Counter operator++ () 
      {
       x++;
       return *this;
      }


    هنا أرجعنا قيمه الكائن الحالي بعد الزياده ، الطريقه صحيحه ، ولكن الإرجاع هنا تم بالقيمه !! كلنا نعلم انه يمكن استقبال متغيرات بالقيمه أو بالمؤشر Pointer أو بالمرجع Reference ، وأيضا يمكن أن نرجع القيمه بأحد هذه الطرق !

    المشكله أن الإرجاع بالقيمه (والإستقبال بالقيمه أيضا) مكلف من ناحيه أنه يتم انشاء نسخه جديده من المتغير أو الكائن الذي نريد أن نرجعه أو نستقبله ، وليس كما هو الحال مع الاستقبال أو الارجاع بواسطه المؤشر أو المرجع .

    كيف يكون تأثير الإرجاع بالقيمه مكلف ؟ ولذلك لأنه يقوم أولا بانشاء نسخه جديده من الكائن الذي نريد استدعائه (يقوم هنا باستدعاء copy constructor ) في كل مره أردنا أن نرجع فيها كائن بالقيمه (ايضا في حاله أستقبلنا كائن بالقيمه) .

    لذلك الحل الأفضل هو ، اذا لم يكن هناك انشاء لكائن داخل الداله وأردنا أن نرجع قيمه الكائن الحالي فقط ، كما هو الحال مع الداله الأولى فيفضل دائما ارجاع هذه القيمه بواسطه المرجع ويفضل أن تكون ثابته !

    (تحدث أخ خالد عن هذه المفاهيم ، من هنا :
    http://www.arabteam2...howtopic=145287
    ).

    اذا الداله أصبحت بهذا الشكل الأن :

     
    const Counter& operator++ () 
      {
       x++;
       return *this;
      }


    وهنا أرجعنا قيمه الكائن بعد زياده بواسطه المرجع Reference .

    نعود الأن الى الداله postfix ونرى طرق وسبل تحسينها Optimization :

     
     Counter operator++ (int)
      {
       Counter tmp(*this);
       x++;
       return tmp;
      }


    هنا نحن في البدايه كتبناها بشكل محسن ، حيث أننا دائما اذا أنشأنا كائن داخل داله ما ، فيفضل دائما إرجاع قيمه الكائن بواسطه القيمه ، لماذا ؟ حتى لا تحصل مشاكل خروج الكائن من الحياه ونتسبب في وفاته وبعدين يحاكمونا في غواتناماو .
    (ارجع للرابط السابق لمعرفه السبب) .

    نأخذ معامل أخر مثلا اشاره الجمع ، أي مثل :

     
    Object 1 = Object2 + Object3;


    أي نجمع قيمه الكائن 3 مع الكائن 2 ونضع الناتج في 1 .

    هنا داله التحميل الزائد ، سوف تكون بالشكل :

     
    Counter operator + (const Counter& rhs) :


    أي أنها تستقبل القيمه ( وهي هنا تمثل الكائن الذي يأتي بعد عمليه + ) ، وتجمعه مع الكائن الحالي ، وترجع الناتج . (ولأننا سوف ننشيء كائن مؤقت داخل هذه الداله سوف يكون الإرجاع بالقيمه) .

    والمثال التالي يبين ذلك :

     
    #include <iostream>
    using namespace std;
    class Counter
    {
     private :
      int x;
     
     public :
      Counter (int x)
      { this->x = x; } // here another usage for this  
     
     
      Counter operator + (const Counter& rhs)
      {
       Counter tmp(*this);
       tmp.x = tmp.x + rhs.x;
       return tmp;
      }
       
      int getX()
      { return x; }
     
    };
    int main ()
    {
     Counter c(1);
     Counter d = c;
     Counter a(3);
     
     cout << "c = " << c.getX() << endl;
     cout << "d = " << d.getX() << endl;
     cout << "a = " << a.getX() << endl;
     
     c = d + a;
     
     cout << "c = " << c.getX() << endl;
     cout << "d = " << d.getX() << endl;
     cout << "a = " << a.getX() << endl;
     
     
     return 0;
    }


    ويمكن أن تكون الداله بهذا الشكل ، ويعمل البرنامج أيضا :

     
    Counter operator + (const Counter& rhs)
      {
       return Counter(x + rhs.x);
      }


    وهنا عملنا كائن جديد ومرننا فيه قيمه x للكائن الحالي + قيمه x للكائن الأخر ، والسبب في ذلك أن لدينا داله بناء تأخذ عدد ، فذلك العمليه صحيح

    تبقي مفهوم أو داله Conversion Operator وهي غير ضروريه ، ولكن المغزى منها هو اسناد كائن الى متغير . أي :

     
    int x = Object1;


    وهنا يكون تعريف داله التحميل بهذا الشكل :

     
    operator int();


    وكل ما عليك هو ارجاع قيمه المتغير (الموجود داخل الكلاس) والذي تريد ان تسنده للمتغير .


    [color="#FF0000"]نتحدث الأن عن الدوال الصديقه Friend Function


    أولا الدوال الصديقه Friend Function يعتبرها الكثير أمر غير محبذ الا في بعض الأوقات وللضروه القصوى ، حيث تهز مبدأ الكبسله ، لأنها كما ذكرت لها القابليه لتغيير جميع متغيرات الكلاس سواء عامه public أو خاصه private أو محميه Protected .

    بالنسبه لاستخدامها فصراحه أشهر استخدام لها هو في اعاده تعريف المعاملات Operator Overloading ، وفي بعض الأحيان يستخدم لزياده المقروئيه Readability ،غير ذلك لم أر له أستخدام من قبل الا في شرح وظيفته فقط ، أما في أمثله كبيره فلم أرى أبد .

    المهم نأخذ هذا المثال ونرى ماذا تقدم لنا الدوال الصديقه في Operator Overloading ، ولنأخذ كلاس Counter الذي كنا نأخذه قبل قليل .

     
    #include <iostream>
    using namespace std;
    class Counter
    {
    private :
      int x;

    public :
      Counter (int u) : x(u)
      { }

      int getX() const
      { return x; }

      Counter operator + (const Counter& rhs);

    };
    Counter Counter :: operator+ (const Counter& rhs)
    {
    int x = getX() + rhs.getX();
    Counter tmp(x);
    return tmp;
    }
    int main ()
    {
    Counter a(1);
    Counter b(4);

    Counter c(0);

    c = a+b;
    cout << c.getX() << endl;
    return 0;
    }


    الان هنا جمع 1+4 والقيمه الناتجه هنا 5 ، هنا في هذه العمليه قمنا بجمع الكائن a مع الكائن b ولأننا لدينا داله قمنا باعاده تعريفها لجمع المتغير الذي يكون بداخل الكائن فان العمليه صحيحه .
    الأن في حاله قمنا بجمع كائن مع متغير ، فان العمليه أيضا صحيحه ، جرب واكتب :

     
                                                                                    c = a+ 7;  
    cout << c.getX() << endl;


    في الداله main ، وشاهد التنفيذ والمخرج يكون هو 8 .
    كيف عمل الكود السابق ، أولا بما أن هناك عمليه جمع مع الكائن ، اذا فسوف يذهب المترجم ليرى هل هناك داله + قمنا باعاده تعريفها ، فاذا كانت لا توجد فسوف يطبع خطأ ، المهم في حالتنا الداله موجوده ، وسوف يرسل ما بعد علامه + الى الداله كوسيط ، المهم هنا انه الوسيط الذي ارسلناه هو رقم وليس كائن ، لكن العمليه تعتبر صحيحه وذلك لأن هناك داله بناء تأخذ عدد صحيح ، أي كأن العدد 7 ارسل الى كائن جديد ، وتم استدعاء داله البناء الخاصه بهذا الكائن .

    حسنا ، الى هنا الأمر جميل ، ولكن ماذا اذا أردنا أن نكتب العكس أي :

     
                                                                                    c = 7+a;  
    cout << c.getX() << endl;


    هنا سوف يصرخ المترجم ويعلن عن خطأ ، لأن 7 ليست كائن ، اذا علامه الجمع سوف يتعبرها علامه عاديه ، بعدها سوف يذهب للوسيط الثاني ليرى أنه كائن ، ولن يستطيع جمع عدد مع كائن .

    الحل هنا عمل داله صديقه ( والدوال الصديقه دائما تأخذ وسيط يمثل الكلاس الذي تريد أن تصل الى متغيراته) ، وبما أنه هنا لدينا كائنين ، فاذا الداله الصديقه سوف تأخذ كائنين ، وتقوم بعمليه الجمع ، سواء الكائن الأول كان رقم أم كائن ، فسوف تتم عمليه الجمع .

    وها هو المثال بعد استخدام freind Function :

     
    #include <iostream>
    using namespace std;
    class Counter
    {
    private :
      int x;

    public :
      Counter (int u) : x(u)
      { }

      int getX() const
      { return x; }

      friend Counter operator + ( Counter , Counter);
    };

    Counter operator + ( Counter rhs , Counter rhs2)
    {
    int x = rhs.getX() + rhs2.getX();
    Counter tmp(x);
    return tmp;
    }

    int main ()
    {
    Counter a(1);
    Counter b(4);

    Counter c(0);

    c = a+b;
    cout << c.getX() << endl;

    c = a+7;
    cout << c.getX() << endl;

    c = 3+a;
    cout << c.getX() << endl;

    c = 3+4;
    cout << c.getX() << endl;

    return 0;
    }


    وسوف تلاحظ هنا اختلاف طريقه كتابه الداله + عندما تكون موجوده داخل الكلاس Member Function ، وعندما تكون داله صديقه Friend Fucntion (معرفه بالخارج حيث جميع الدوال الصديقه تعرف بالخارج) .

    قد لا تبدوا الداله الصديقه منطقيه ، حيث ربما تتسائل لماذا أخذت وسيطين ، لكن عليك باعتبارها داله عاديه اسمها + ، اذا أردت استخدامها أرسل الوسيط الأول ومن ثم + ومن ثم الوسيط الثاني . فقط .

    الأستخدام الأخر له وهو يكون في هذه الحاله أكثر وضوحا من الدوال Member Function .
    مثلا نأخذ نفس الكلاس Counter وليكن فيه داله تربيع Square ترجع قيمه x*x :

    كلمات مفتاحية  :

    تعليقات الزوار ()