الدوال الاعتبارية في ++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
ومنها نعلم أن الدالة الخاصة بالفئة الأصلية هي التي تستثار دائما , فالمترجم قد تجاهل محتوي المؤشر , مكتفيا نوعه (أنظر الشكل ) .
شكل استثارة دوال عادية
وأحيانا يكون هذا ما نريده . ولكننا نريد الأن إجابة السؤال المتعلق بموضوعنا , التعامل مع عناصر متحدة الاسم لفئات مختلفة .
استثارة الدوال الاعتبارية بالمؤشرات
سوف نجري تغييرا واحدا علي البرنامج السابق , بإضافة كلمة حاكمة هي 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();
نفذنا عمليتين مختلفتين , بناء علي محتوي المؤشر , وليس مجرد نوعه . (أنظر الشكل )
شكل استثارة دوال اعتبارية
الإلتزام المؤجل
نتساءل الأن كيف يمكن لمحول الصياغة أن يعرف أي دالة يترجم بالأمر المذكور من الكود المصدري لكود الهدف . في حالة الدوال غير الاعتبارية ليس لديه مشكلة فهو دائما يترجم الدالة المنتمية للفئة الأساسية , أما في حالة الدوال الاعتبارية لا يعرف أي فئة تنتمي إليها محتوياته .
إن ما يحدث فعلا هو أم محول الصياغة لا يعرف , ولذا فهو يؤجل قراره بتحويل الصيغة إلي وقت التنفيذ الفعلي للبرنامج ويعرف أي فئة يشير لها المؤشر . ويسمي ذلك الإلتزام المؤجل 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 تتسبب في أن تتشارك فئتي الإبن في نفس النسخة من الإبن , ولن يحدث التباس في الوصول إلي البيان المطلوب منها .
تعليقات
إرسال تعليق