الفريق العربي للبرمجةأرشيف المنتديات · 2000 – 2023
نسخة أرشيفية للقراءة فقط — التسجيل والمشاركة مغلقان، والمحتوى محفوظ كما كان.

Advanced Function For Bigenners

مغلق
بدأه Wajdy Essam في 9 يناير 2008 · 3 رد · 3,539 مشاهدة · في المواضيع والدروس
مشاركة: واتساب X فيسبوك تيليجرام
#1 صاحب الموضوع

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 ) في كل مره أردنا أن نرجع فيها كائن بالقيمه (ايضا في حاله أستقبلنا كائن بالقيمه) .

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

(تحدث أخ خالد عن هذه المفاهيم ، من هنا :

/index.php?showtopic=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();

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

نتحدث الأن عن الدوال الصديقه 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 :

كود

تم تعديل هذه المشاركة بواسطة romansy في 9 يناير 2008 في 21:23

http://informatic-ar.com منصة تعليمية عربية في علوم الحاسب والبرمجة

https://moalfat.com  للكتب الالكترونية والكورسات التعليمية

Everything we see now is just an engineering solution based on old science

#2

ماشاء الله عليك اخي

بارك الله فيك

mov eax, dword ptr ds:[0xffdf0308]

jmp dword ptr [eax+0xfc]

#3

السلام عليكم ,,

ما شاء الله أخي العزيز, متميز كعادتك B)

:stop:

من الآن وصاعداً يمنع وضع أي سؤال حول موضوع الـ operators overloading, و لكن يتم توجيه السائل إلى الدرس و طرح الأسئلة على الدرس نفسه,

أخ رومانسي, لاحظ أن هذا الموضوع ليس بالهين, و أنت ماشاء الله وضعت مرجعاً شبه متكامل للموضوع, رغم أن الكثير من الكتب لا تفصل في الموضوع كما فعلت أنت :)

اذهب إلى أي منتدى أجنبي و ستجد عشرات الأسئلة حول الموضوع, لأن الكتب المتقدمة هي التي تعطيه حقه فقط..

حقيقة لدي عدة تعليقات على الموضوع,

أولاً أنت ذكرت في بداية الكلام أن الغرض ليس readability فقط, و كلامك صحيح مئة بالمئة, لأن المعامل يمكن أن يرجع الكائنات كما تفضلت و بالتالي نحصل على ما يسمى بالـ chaining,

ثانياً موضوع الـ conversion operators يعتبر من المواضيع التي لا يعرف عنها الكثيرون في عالم الـ ++C, و هذه تمكننا ليس فقط من إسناد الكائن إلى متغير بل يمكن تعريف operators لإسناد كائن إلى كائن آخر!

ثالثاً, هناك معاملات كثيرة يمكن إعادة تعريفها, و إحداها المعامل new !! يمكن إعادة تعريفه لتقوم بحجز الذاكرة لفئتك على كيفك :Dهذا غير موجود في اللغات الأخرى بأي شكل,

بل إن كثير من اللغات لاتوفر إعادة تعريف المعاملات في أبسط أشكالها :blink: و لاداعي لذكر الأسماء :D

تحياتي ,,

#4

السلام عليكم ,,

يبدو أن الأخوة لم يعرفوا كيفية الاستفادة من هذا الدرس,

و لكن بكل بساطة إذا كنت تعلمت مبادئ البرمجة الكائنية, فإنه ينقصك الكثير الكثير من التفاصيل , إذا قمت بدراسة هذا الموضوع على مدى يومين على الاقل,

ستصبح بإذن الله متمكناً من برمجة الكائنات في ++C,

أشكر الأخ romansy, ليس لأنه وفر لنا هذه المادة العلمية فحسب, بل لأنه صرف الكثير من جهده لإعداده بنفسه,

هذا النوع من الدروس لن تجده إلا في الكتب التي تكلفك $$$$$, و لن تجده بهذا التفصيل لأن أخينا عرض الكثير من التطبيقات في هذا الدرس,

يمكنك الذهاب إلى مواقع كـ codeproject أو codeguru و ستجد أن كل فقرة من هذا الدرس وضعت في موضوع منفصل على الأقل,,

نصيحتي لأي طالب أو مبرمج أن يقرأ الدرس و يستوعبه, لأنه بتعلمه للفئات في ++C فهو قد أقدم على أول الطريق فقط, و هذا الدرس مهد معظم الباقي ليصبح مبرمج متوسط على الأقل,

أنا في انتظار أسئلتكم لأخينا romansy بعد قرائتكم للدرس,,

تحياتي ,,

هذا الموضوع مغلق.

مواضيع مشابهة