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 :