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

الناقل : elmasry | الكاتب الأصلى : 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 :