الدوال الاعتبارية في ++C

الأن , وبعد أن عرفنا شيئا عن المؤشرات يمكننا الغوص في موضوعات أكثر تقدما متعلقة بلغة السي ++ وسوف نعرض في هذا الفصل لموضوعات ضعيفة فيما بينها , الدوال الاعتبارية , الدوال الصديقة , الدوال الإستاتيكية , المؤثر = زائد التحميل , بادئة النسخ زائدة التحميل , والمؤشر this كل هذه الموضوعات تمثل خصائص متقدمة غير مطلوبة لكل برامج السي ++ إلا أنها واسعة الانتشار ولازمة في بعض المواقف , كما أن دراستها تلقي الضوء علي أسباب العمل الداخلية لهذه اللغة .

الدوال الاعتبارية

الشئ الاعتباري هو الشئ الموجود بتأثير بوجوده الرسمي . فالدوال الاعتبارية إذن هي دوال غير موجودة واقعا , إلا أن أثرها يظهر في أجزاء من البرنامج .

ما هو الاحتياج لمثل هذه الدوال ؟ لنفرض أن لديك عدة كائنات تنتمي لفئات مختلفة , ولكنك تريد ضمها في قائمة واحدة وتجري عليها عملية باستدعاء دالة واحدة مثلا , لنفرض أن لديك برنامجا رسوميا يضم عدة أشكال , كالبرنامج multshape.cpp من الفصل الحادي عشر . كافة الفئات تحتوي علي الدالة draw() المسئولة عن رسم الأشكال علي الشاشة , لنفرض أنك أردت أن تجمع هذه الأشكال في شكل واحد , أحد الوسائل أن تنشئ مصفوفة مؤشرات لهذه الأشكال , قد تعرف بشئ من هذا القبيل :

Shape* ptrarr[100];

فإذا ما وضعت في المصفوفة مؤشرات لجميع الأشكال , يمكنك رسم المصفوفة عن طريق الأمر التالي :

For (int j=0; j<N; j++_

Ptrarr[j] ->draw();

إنها إمكانية مثيرة للتعجب , دوال مختلفة تماما تستدعي بنفس الأمر , فإذا كان المؤشر يشير إلي كرة رسمت وإذا كان لمثلث . إنها حالة من حالات تعدد الأشكال polymorphism , أو إعطاء نفس الشئ أسماء مختلفة .

إلا أن تعدد الأشكال يحتاج لبعض الشروط ليحقق النجاح فأولا , يجب أن تكون كافة الكائنات منتمية لنفس الفئة ( الفئة المسماة shape في البرنامج multishpe) .

وثانيا , يجب أن تعرف الدالة drow() علي أنها "اعتبارية virtual في الفئة الأساسية .

 

استثارة الدوال المعتادة بالمؤشرات

مثالنا الأول يتعلق بحالة اشتراك دالة في الفئة الأساسية والفئة المشتقة في نفس الأسم , حين تتعامل معها بأسلوب المؤشرات , ولكن دوال اعتبارية .

notvirt.cpp


 


// notvirt.cpp


// normal functions accessed from pointer


#include <iostream.h>


 


class Base                       // base class


   {


   public:


      void show()                // normal function


     { cout << "\nBase"; }


   };


class Derv1 : public Base        // derived class 1


   {


   public:


      void show()


     { cout << "\nDerv1"; }


   };


class Derv2 : public Base        // derived class 2


   {


   public:


      void show()


     { cout << "\nDerv2"; }


   };


void main()


   {


   Derv1 dv1;           // object of derived class 1


   Derv2 dv2;           // object of derived class 2


   Base* ptr;           // pointer to base class


 


   ptr = &dv1;          // put address of dv1 in pointer


   ptr->show();         // execute show()


 


   ptr = &dv2;          // put address of dv2 in pointer


   ptr->show();         // execute show()


   }




الفئتان Derv1, Derv2 مشتقتان من الفئة الأساسية Base ولكل من الفئات الثلاث دالة باسم show() ومؤشر ptr يشير لكائنات الفئة الأساسية وقد وضعنا في هذا المؤشر عنوانا لكائن منتم للفئة المشتقة الأولي بالأمر :



Ptr->&dv1;



وقد كررنا نفس المحاولة مع الفئة المشتقة الثانية .



الإجابة تبدو من خرج البرنامج :



Base



Base



ومنها نعلم أن الدالة الخاصة بالفئة الأصلية هي التي تستثار دائما , فالمترجم قد تجاهل محتوي المؤشر , مكتفيا نوعه (أنظر الشكل ) .



Object-Oriented Programming in C   _Page_0533_Image_0001



شكل استثارة دوال عادية



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



استثارة الدوال الاعتبارية بالمؤشرات



سوف نجري تغييرا واحدا علي البرنامج السابق , بإضافة كلمة حاكمة هي virtual أما أسم الدالة في موضع الإعلان عنها :






virt.cpp


 


// virt.cpp


// virtual functions accessed from pointer


#include <iostream.h>


 


class Base                        // base class


   {


   public:


      virtual void show()         // virtual function


     { cout << "\nBase"; }


   };


class Derv1 : public Base         // derived class 1


   {


   public:


      void show()


     { cout << "\nDerv1"; }


   };


class Derv2 : public Base         // derived class 2


   {


   public:


      void show()


     { cout << "\nDerv2"; }


   };


void main()


   {


   Derv1 dv1;           // object of derived class 1


   Derv2 dv2;           // object of derived class 2


   Base* ptr;           // pointer to base class


 


   ptr = &dv1;          // put address of dv1 in pointer


   ptr->show();         // execute show()


 


   ptr = &dv2;          // put address of dv2 in pointer


   ptr->show();         // execute show()


   }




وتري أن الخرج هو ما نريده :



Derv1



Derv2



لقد أستثيرت الدوال المنتمية للفئة المشتقة لا الأصلية فنحن بنفس الأمر :



Ptr->show();



نفذنا عمليتين مختلفتين , بناء علي محتوي المؤشر , وليس مجرد نوعه . (أنظر الشكل )



Object-Oriented Programming in C   _Page_0535_Image_0001



شكل استثارة دوال اعتبارية



الإلتزام المؤجل



نتساءل الأن كيف يمكن لمحول الصياغة أن يعرف أي دالة يترجم بالأمر المذكور من الكود المصدري لكود الهدف . في حالة الدوال غير الاعتبارية ليس لديه مشكلة فهو دائما يترجم الدالة المنتمية للفئة الأساسية , أما في حالة الدوال الاعتبارية لا يعرف أي فئة تنتمي إليها محتوياته .



إن ما يحدث فعلا هو أم محول الصياغة لا يعرف , ولذا فهو يؤجل قراره بتحويل الصيغة إلي وقت التنفيذ الفعلي للبرنامج ويعرف أي فئة يشير لها المؤشر . ويسمي ذلك الإلتزام المؤجل late binding او الإلتزام الديناميكي dynamic binding ] يقصد بذلك إلتزام محول الصياغة بتعريف معين وضع في البرنامج [ في حين يسمي اختيار الدالة وقت الترجمة الإلتزام المبكر early binding أو الاستاتيكي static وتعني إمكانية الإلزام المتأخر عبءا إضافيا علي الصياغة ولكنها تعطي مرونة كبيرة .



وسوف ندمج هذه الإمكانيات معا بعد أن تبلور أفكارنا بعض الشئ .



الدوال الاعتبارية الخالصة



قد لا نحتاج للدالة المنتمية للفئة الأصلية طوال البرنامج , فيكون دورها مقصورا علي الاشتقاق منها وفي هذه الحالة قد لا نعطي لها موضوعا تنفذه وتسمي مثل هذه الدوال "الدوال الاعتبارية الخالصة pure virtual " ., وتتميز في الإعلان عن الفئة بأنها تعطي القيمة 0 , كما يبين المثال التالي :






virtpure.cpp


 


// virtpure.cpp


// pure virtual function


#include <iostream.h>


 


class Base                        // base class


   {


   public:


      virtual void show() = 0;    // pure virtual function


   };


 


class Derv1 : public Base         // derived class 1


   {


   public:


      void show()


     { cout << "\nDerv1"; }


   };


 


class Derv2 : public Base         // derived class 2


   {


   public:


      void show()


     { cout << "\nDerv2"; }


   };


 


void main()


   {


   Base* list[2];       // list of pointers to base class


   Derv1 dv1;           // object of derived class 1


   Derv2 dv2;           // object of derived class 2


 


   list[0] = &dv1;      // put address of dv1 in list


 


   list[1] = &dv2;      // put address of dv2 in list


 


   list[0]->show();     // execute show() in both objects


   list[1]->show();


   }




وليس للرمز = أي علاقة بعملية التخصيص , فالقيمة 0 ليست مخصصة لشئ ما فالصيغة =0 دورها هو إختيار المترجم بأن الدالة المقصودة اعتبارية خالصة .



ولعلك تتساءل عن جدوي تعريف دالة لا أوامر لها , إن عدم تعريفها لن يجعل التعرف علي الدوال المشتقة منها ممكنا .



تطبيق : الدوال الاعتبارية والفئة person



بعد أن فهمنا معني الدوال الاعتبارية , لنضعها الأن في مثال تطبيقي , وسوف يكون تطويرا لمثالنا ptrobj.cpp المعطي في الفصل السابق , بأن نضيف إليه فئتين مشتقتين , student, professor . كل فئة تحتوي علي دالة تسمي isoutstanding() تجمع أسماء الأساتذة والطلبة المرشحين لحفلة التفوق أخر العام .







virtpers.cpp


 


// virtpers.cpp


// virtual functions with person class


// UCS Laboratories


 


#include <iostream.h>


enum boolean { false, true };


 


class person                         // person class


   {


   protected:


      char name[40];


   public:


      void getName()


     { cout << "   Enter name: "; cin >> name; }


      void putName()


     { cout << "Name is: " << name << endl; }


      virtual void getData() = 0;           // pure virtual func


      virtual boolean isOutstanding() = 0;  // pure virtual func


   };


 


class student : public person        // student class


   {


   private:


      float gpa;              // grade point average


   public:


      void getData()          // get student data from user


     {


     person::getName();


     cout << "   Enter student's GPA: "; cin >> gpa;


     }


      boolean isOutstanding()


         { return (gpa > 3.5) ? true : false; }


   };


 


class professor : public person      // professor class


   {


   private:


      int numPubs;             // number of papers published


   public:


      void getData()           // get professor data from user


     {


     person::getName();


     cout << "   Enter number of professor's publications: ";


     cin >> numPubs;


     }


      boolean isOutstanding()


     { return (numPubs > 100) ? true : false; }


   };


 


void main(void)


//UCS Laboratories


   {


   person* persPtr[100];     // list of pointers to persons


   int n = 0;                // number of persons on list


   char choice;


 


   do


      {


      cout << "Enter student or professor (s/p): ";


      cin >> choice;


      if(choice=='s')                  // put new student


     persPtr[n] = new student;     //    in array


      else                             // put new professor


     persPtr[n] = new professor;   //    in array


      persPtr[n++]->getData();         // get data for person


      cout << "   Enter another (y/n)? ";  // do another person?


      cin >> choice;                    


      }  while( choice=='y' );         // cycle until not 'y'


 


      for(int j=0; j<n; j++)           // print names of all


     {                             // persons, and


     persPtr[j]->putName();        // say if outstanding


     if( persPtr[j]->isOutstanding()==true )


        cout << "   This person is outstanding\n";


     }


   }  // end main()





تضيف الفئتان student, professor بيانات جديدة للفئة الأصلية . تحتوي فئة الطلبة علي متغير gpa يختزن درجة نجاح الطالب , وتحتوي فئة الأساتذة علي متغير numpubs يحتوي علي عدد ما نشر له من أبحاث . ويعتبر الطالب مرشحا لحفلة التفوق في حالة حصوله علي درجة 3.5 فأكثر (من 5) , ويعتبر الأستاذ مرشحا في حالة نشره 100 بحث فأكثر ( لن نعلق علي مدي سلامة هذه المعايير من الناحية التعليمية ) .



والدالة isoutstanding() معلنة في الفئة الأساسية كدالة اعتبارية صرفة , وفي فئة الطلبة تعيد حالة تحقق شرط تفوق الطالب , وفي فئة الأساتذة تعيد حالة تحقق تفوق الأستاذ , والشرطان مختلفان في الحالتين كما تعلم .



مثال رسومي



نقدم لك مثالا لاستخدام الدوال الاعتبارية في الأشكال الرسومية , وذلك لاستخدام نفس الدالة draw() في رسم الأشكال .






virtshap.cpp


 


// virtshap.cpp


// virtual functions with shapes


// UCS Laboratories


#include <graphics.h>         // for graphics functions


#include <conio.h>            // for getche()


 


const int W = 50;             // size of images


 


class shape                   // base class


   {


   protected:


      int xCo, yCo;           // coordinates of center


      int linecolor;          // color of outline


      int fillcolor;          // color of interior


   public:


      shape()                 // no-arg constructor


     { xCo=0; yCo=0; linecolor=WHITE; fillcolor=WHITE; }


      void set(int x, int y, int lc, int fc)  // set data


     { xCo=x; yCo=y; linecolor=lc; fillcolor=fc; }


      void colorize()         // set colors


     {


     setcolor(linecolor);                      // line color


     setlinestyle(SOLID_LINE, 0, THICK_WIDTH); // line width


     setfillstyle(SOLID_FILL, fillcolor);   // set fill color


     }


      virtual void draw() = 0;     // pure virtual draw function


   };


 


class ball : public shape


   {


   public:


      ball() : shape()        // no-arg constr


     { }


      void set(int x, int y, int lc, int fc)    // set data


      { shape::set(x, y, lc, fc); }


      void draw()             // draw the ball


     {


     shape::colorize();                     // set colors


     circle(xCo, yCo, W);                   // draw circle


     floodfill(xCo, yCo, linecolor);        // fill circle


     }


   };


 


class rect : public shape


   {


   public:


      rect() : shape()        // no-arg constr


     { }


      void set(int x, int y, int lc, int fc)    // set data


      { shape::set(x, y, lc, fc); }


      void draw()             // draw the rectangle


     {


     shape::colorize();                     // set colors


     rectangle(xCo-W, yCo-W, xCo+W, yCo+W); // draw rectangle


     floodfill(xCo, yCo, linecolor);        // fill rectangle


     moveto(xCo-W, yCo+W);                  // draw diagonal


     lineto(xCo+W, yCo-W);                  //    line


     }


   };


 


class tria : public shape


   {


   public:


      tria() : shape()        // no-arg constr


     { }


      void set(int x, int y, int lc, int fc)    // set data


      { shape::set(x, y, lc, fc); }


      void draw();            // draw the triangle


   };


 


void tria::draw()             // draw the triangle


   {


   shape::colorize();                    // set colors


   int triarray[] = { xCo,   yCo-W,      // top


              xCo+W, yCo+W,      // bottom right


              xCo-W, yCo+W };    // bottom left


   fillpoly(3, triarray);                // draw triangle


   }


 


void main()


//UCS Laboratories


   {


   int driver, mode;


   driver = DETECT;            // set to best graphics mode


   initgraph(&driver, &mode, "\\bc45\\bgi");


 


   shape* ptrarr[3];           // array of pointers to shapes


 


   ball b1;                    // define three shapes


   rect r1;


   tria t1;


                   // set values in shapes


   b1.set(100, 100, WHITE, BLUE);


   r1.set(100, 210, WHITE, RED);


   t1.set(100, 320, WHITE, GREEN);


 


   ptrarr[0] = &b1;            // put addresses in array


   ptrarr[1] = &r1;


   ptrarr[2] = &t1;


 


   for(int j=0; j<3; j++)      // draw all shapes


      ptrarr[j]->draw();


 


   getche();                   // wait for keypress


   closegraph();               // close graphics system


   }




تنشئ الدالة الأصلية مصفوفة تضم ثلاثة مؤشرات تستخدمها في دوارة لرسم الأشكال .



ويعطينا هذا أداة قوية لضم الأشكال معا .



الفئات المجردة



الفئة المجردة هي الفئة التي لا تنشأ كائنات منتمية لها , وتكون لمجرد اشتقاق الفئات منها والفئة الأساسية مجردة أن تحتوي علي دالة اعتبارية علي الأقل . مما يؤكد صفتها المجردة .



الفئة الأساسية الاعتبارية



قبل أن نترك موضوع العناصر الاعتبارية في البرمجة , نعرض لمفهوم الفئة الأساسية الاعتبارية . ويثور الموضوع حين يشتق كائن من فئتين مشتقتين .



في تنظيم كهذا نطلق علي الفئة الأساسية "الأب " , وكل فئة مشتقة منه "إبن " , وعلي الفئة المشتقة من الابنين "حفيد" لو أراد كائن منتم لفئة الحفيد أن يتوصل لبيان من فئة الأب , فلن يمكنه ذلك , أنظر للهيكل التالي :



// normbase



// ambiguous refrence to base class





Class parent



{



Protected:



Int basedata;



};





Class Child : public Parent



{};





Class Cild2 : public Parent



{};



Class Grandcild : public Child1, public Cild2



}



Int getdata()



{ return basedata; } // Error: ambiguous غموض



};



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



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



// virtbase



// virtual base class





Class Parent



{



Protected:



Int basedate;



};





Class Child : virtual public Parent



{};





Class Cild2 : virtual public Parent



{};





Class Grandcild : public Child1, public Cild2



{



Int getdata()



{ return basedata; } // OK. Only one copy of parent



};



وتري أن التعديل حدث في الإعلان عن كل فئة من فئتي الإبن , حيث كان الاشتقاق من الدالة الأساسية باعتبارها اعتبارية . فالكلمة الحاكمة virtual تتسبب في أن تتشارك فئتي الإبن في نفس النسخة من الإبن , ولن يحدث التباس في الوصول إلي البيان المطلوب منها .

التسميات: