استهلال التخصيص والنسخ في ++C

c

استهلال التخصيص والنسخ

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

لقد استخدمنا مؤثر التخصيص مرارا دون أن نشغل بالنا به . فإذا كان لدينا كائنين a1,a2 ولم نأمر المترجم بشئ أخر , فإنه سوف ينفذ العبارة :

A2=a1;

ينسخ بيانات الكائن a1 في عنصر بيانات الكائن a2 إن هذا هو العمل التلقائي للمؤثر = .

كما أننا علي معرفة بعملية استهلال الكائنات , وإن استهلال كائن بكائن أخر , علي غرار العبارة :

Alpha a2(a1);

تفعل نفس الشئ فيقوم المترجم بإنشاء كائن a2 من الفئة alpha وينسخ فيه بيانات الكائن a1 هذا هو العمل التلقائي للبادئة .

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

زيادة تحميل مؤثر التخصيص – الدالة operator()

لننظر المثال التالي الذي يبين التكنيك المتبع لزيادة تحميل مؤثر التخصيص :

assign.cpp


 


// assign.cpp


// overloads assignment operator (=)


// UCS Laboratories


 


#include <iostream.h>


 


class alpha


   {


   private:


      int data;


   public:


      alpha()                      // no-arg constructor


     { }


      alpha(int d)                 // one-arg constructor


     { data = d; }


      void display()               // display data


     { cout << data; }


      alpha operator = (alpha& a)  // overloaded = operator


     {


     data = a.data;            // not done automatically


     cout << "\n...and UCS lives again.";


     return alpha(data);       // return copy of this alpha


     }


   };


 


void main()


   {


   alpha a1(37);


   alpha a2;


 


   a2 = a1;                        // invoke overloaded =


   cout << "\na2="; a2.display();  // display a2


 


   alpha a3 = a2;                  // does NOT invoke =


   cout << "\na3="; a3.display();  // display a3


   }




الجديد في هذه الفئة هو الدالة operator() التي بها يزاد تحميل المؤثر =. وتحتوي الدالة علي ثلاثة أوامر , الأول منها هو تنفيذ عملية التساوي , أما الأمر الثاني فهو أن تظهر العبارة ''assignment operator overloaded'' في كل مرة يستثار فيها مؤثر التخصيص .



التخصيص و الإستهلال



نلاحظ من خرج البرنامج أن العبارة assignment operator overloaded قد ظهرت مرة واحدة رغم أننا استخدمنا المؤثر = مرتين , فهي قد ظهرت عند تنفيذ التساوي بين a1 و a2 , ولم تظهر عند التساوي بين a2 و a3 وتفسير ذلك أن العبارة :



A2=a1;// (تخصيص)



هي عبارة تخصيص , ( لكون a2 قد عرف بدون قيمة , ثم خصصت قيمته بالعبارة المذكورة ) , أما العبارة :



Alpha a3 = a2;// (استهلال)



فهي عبارة استهلال (التساوي عند التعريف ) , أي نفس تأثير العبارة :



Alpha a3(a2);



مسئولية المبرمج في زيادة التحميل



والمبدأ المستفاد من ذلك أن استخدام عملية زيادة التحميل يلقي عبئا علي المترجم أن يكون واعيا بكافة الفروق الدقيقة لطبيعة المؤثرات التي يتولي تغيير طبيعتها .



الإمرار بالتعامل المباشر



لاحظ أننا استخدمنا مؤثر التعامل المباشر & وليس هذا لازما بالضرورة ولكنها عادة فكرة طيبة فلماذا ؟ لأنه كما ذكرنا لك سابقا , يترتب علي إمرار معامل بالقيمة نسخه في الدالة المستدعاه والمعامل المرسل إلي الدالة operator =() ليس استثناء من ذلك ونسخ الكائنات كلما استخدمت هذه الدالة يمثل إهدارا لمساحة الذاكرة خاصة إذا كانت الكائنات كبيرة .



من جهة أخري إذا كنا نتبع عدد الكائنات المنشأة فإن نسخها مع تخصيص الأرقام لها يسبب خطأ في عد هذه الكائنات .



الإعادة بالقيم



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



وفي دالة زيادة تحميل المؤثر =, تعيد هذه الدالة القيمة بإنشاء كائن مؤقت من الفئة alpha تستهله بالقيمة data عن طريق البادئة ذات المعامل الواحد الموجود بالعبارة :



Return alpha(data);



وما يعاد للدالة المستدعية هو نسخة وليس نفس الكائن الذي أنشأته دالة زيادة التحميل . بهذه الطريقة يمكن وضع عبارة تخصيص متسلسل :



A3 = a2 = a1;



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



Alpha & operator = (alpha & a)



للأسف لا يمكننا ذلك فقد سبق أن بينا أن ذلك لا يصلح لمتغيرات الألية , لكونها تفني بفناء دالتها (راجع المقالات السابقة , علي أننا سوف نري أسلوبا لمعالجة ذلك مع المؤثر this لاحقا ) .



لا توارث



الخصيصة التي لمؤثر التخصيص بين غيره من المؤثرات هي أنه لا يقبل التوارث , فإذا ما قمت بزيادة التحميل له في الفئة الأصلية فلا يمكن استخدام نفس دالة زيادة التحميل في الفئات المشتقة .



بادئة النسخ



كما ذكرنا يمكنك استهلال كائن بأخر في نفس عبارة تعريفه بأي من العبارتين :



Alpha a3(a2);



Alpha a3 = a2;



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



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






xofxref.cpp


 


// xofxref.cpp


// copy constructor: X(X&)


#include <iostream.h>


 


class alpha


   {


   private:


      int data;


   public:


      alpha()                    // no-arg constructor


     { }


      alpha(int d)               // one-arg constructor


     { data = d; }


      alpha(alpha& a)            // copy constructor


     {


     data = a.data;


     cout << "\nCopy constructor invoked";


     }


      void display()             // display


     { cout << data; }


      void operator = (alpha& a) // overloaded = operator


     {


     data = a.data;


     cout << "\nAssignment operator invoked";


     }


   };


 


void main()


   {


   alpha a1(37);


   alpha a2;


 


   a2 = a1;                        // invoke overloaded =


   cout << "\na2="; a2.display();  // display a2


 


   alpha a3(a1);                   // invoke copy constructor


// alpha a3 = a1;                  // equivalent definition of a3


   cout << "\na3="; a3.display();  // display a3


   }




يحمل هذا البرنامج كلا من مؤثر التخصيص كالبرنامج السابق , وبادئة النسخ . وتأخذ البادئة معاملا واحدا كائن من الفئة alpha ممرر إليها بالإشارة والصورة العامة للإعلان عنها هي X(X&) (تنطق : X of Xref ) . ويكون خرج البرنامج علي الصورة التالية :



Assignment operator invoked



A2 = 37



Copy constructor invoked



A3= 37



فالعبارة a2 = a1 تستثير مؤثر التخصيص , بينما تستثير العبارة a3 (a1) ومثيلتها العبارة a3=a1 بادئة النسخ .



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



معاملات الدوال



تستثار بادئة النسخ حين يمرر كائن بالقيمة إلي دالة , لإنشاء نسخة من الكائن تنفذ عليه الدالة وظيفتها . فلو أننا في البرنامج السابق أعلنا عن دالة void func(alpha) ثم استدعيت هذه الدالة بالعبارة func(a1) : فإن نسخة من الكائن a1 لكي تعمل عليه الدالة funce() أمل لو إن الإمرار كان بطريقة الإشارة أو كان الإمرار بمؤشرات , فإن الدالة تعمل علي الكائن الأصلي , دون أن تنشئ له نسخة .



القيم المعادة من الدوال



تنشئ بادئة النسخ أيضا نسخة مؤقتة من الكائن عندما تعاد قيمة من دالة . لنفرض أنه كان لدينا دالة علي الصورة alpha func() في برنامجنا السابق وأنها استدعيت بالأمر : a2=finc() فإن البادئة تستثار لكي تنسخ من القيمة المعادة من الدالة , ثم يستثار مؤثر التخصيص لكي تخصص القيمة إلي a2 .



لماذا لا تكون البادئة علي الصورة X(X) ؟



هل لابد أن نمرر المعاملات لبادئة النسخ بالإشارة ؟ ألا يمكننا أن نمررها بالقيمة , لتكون علي الصورة alpha(alpha a) ؟ الإجابة : لا سوف يصدر لك المترجم الرسالة : out of memory لماذا ؟ لأن الإمرار بالقيمة يترتب عليه عملية نسخ تقوم بها بادئة النسخ , فإذا كان الإمرار لها , فإنها تقوم باستدعاء نفسها . وتظل تستدعي نفسها المرة تلو المرة إلي أن يستفيد المترجم الذاكرة المخصصة له . لذلك لابد أن تمرر المعاملات لبادئة النسخ بالإشارة , حيث لا يتطلب أي نسخ .



زيادة تحميل الاثنين معا



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



انتبه لعمل المنهيات



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



منع النسخ



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



Class alpha



{



Private:



Alpha& operator = (alpha&); // private assignment operator



Alpha(alpha&); // private copy constructor



};



فئات عبارات نصية اقتصادية للذاكرة



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



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



فئة لعد العبارات النصية



لنفرض أننا نريد أن نتبع عدد الكائنات التي تشير لعبارة نصية ما , أين تخزن هذه المعلومة ؟



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



ولكي نضمن أن كائنات الفئة string لها إتصال بكائنات الفئة strCount نجعل الأولي صديقة للثانية , وإليك صياغة البرنامج .




strimem.cpp


 


// strimem.cpp


// memory-saving String class


// overloaded assignment and copy constructor


// UCS Laboratories


 


#include <iostream.h>


#include <string.h>               // for strcpy(), etc.


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


 


class strCount                    // keep track of number


   {                              // of unique strings


   private:


      int count;                  // number of instances


      char* str;                  // pointer to string


      friend class String;        // make ourselves available


   public:


      strCount(char* s)           // one-arg constructor


     {


     int length = strlen(s);  // length of string argument


     str = new char[length+1];  // get memory for string


     strcpy(str, s);          // copy argument to it


     count=1;                 // start count at 1


     }


      ~strCount()                 // destructor


     {


     delete[] str;            // delete the string


     }


   };


 


class String                      // String class


   {


   private:


      strCount* psc;              // pointer to strCount


   public:


      String()                    // no-arg constructor


     {


     psc = new strCount("NULL");


     }


      String(char* s)             // 1-arg constructor


     {


     psc = new strCount(s);


     }


      String(String& S)           // copy constructor


     {


     psc = S.psc;


         (psc->count)++;


     }


      ~String()                   // destructor


     {


     if(psc->count==1)      // if we are its last user,


        delete psc;       //    delete our strCount


     else              //    otherwise,


        (psc->count)--;      //    decrement its count


     }


      void display()              // display the String


     {


     cout << psc->str;                 // print string


     cout << " (addr=" << psc << ")";  // print address


     }


      void operator = (String& S) // assign the string


     {


     if(psc->count==1)      // if we are its last user,


        delete psc;       //    delete our strCount


     else              //    otherwise,


        (psc->count)--;      //    decrement its count


     psc = S.psc;             // use argument's strCount


     (psc->count)++;      // increment its count


     }


   };


 


void main()


//UCS Laboratories


   {


   String s3 = "When the fox preaches, look to your geese.";


   cout << "\ns3="; s3.display();   // display s3


 


   String s1;                       // define String


   s1 = s3;                         // assign it another String


   cout << "\ns1="; s1.display();   // display it


 


   String s2(s3);                   // initialize with String


   cout << "\ns2="; s2.display();   // display it


   getch();


   }




في الجزء الأساسي من البرنامج تنشئ الدالة الأصلية العبارة النصية s3 وتملؤها بالعبارة المبينة , ثم تخصصها مرة للعبارة s1 وتستهل بها مرة أخري العبارة s2 , وفي المرة الأولي يستثار مؤثر التخصيص مزاد التحميل وفي الثانية تستثار بادئة النسخ مزادة التحميل . ونخرج العبارات النصية الثلاثة , وأيضا العنوان الذي يشير إليه المتغير psc في كل كائن , ويكون خرج البرنامج علي الوجه التالي :



S3 = When the fox preaches, look to your geese.



(addr=0x8f510c00)



S1 = When the fox preaches, look to your geese.



(addr=0x8f510c00)



S2 = When the fox preaches, look to your geese.



(addr=0x8f510c00)



وبذلك ها علي العنوان الذي يشير إليه كائن الفئة StrCoun , والذي يختزنه المتغير psc في كل كائن من الكائنات الثلاثة , هو نفس العنوان .



أما بقية الأعباء الخاصة بالفئة String فقسمة بينها وبين الفئة الأخري .



الفئة strCount



تحتوي هذه الفئة علي مؤشر للعبارة النصية , وعلي متغير يختزن عد الكائنات التي تشير لهذه العبارة . وبادئته الوحيدة تأخذ مؤشرا لعبارة نصية كمعامل وتخلق مساحة جديدة في الذاكرة للعبارة النصية , وتنسخها في تلك المساحة تضع العداد في العداد 1 . أما دالته المنهية فتمحو الذاكرة التي استغلتها العبارة (استخدمنا delete مع قوسين مربعين لأن العبارة النصية تعتبر مصفوفة ) .



الفئة string



تستخدم هذه الفئة ثلاث بادئات وعند تخليق عبارة جديدة تتولي البادئتان عديمة المعامل ووحيدة المعامل إنشاء كائن منتم للفئة setCount ووضع المؤشر psc ليشير لذلك الكائن وإذا ما كانت العبارة النصية سوف تنسخ فإن مؤثر التخصيص وبادئة النسخ المزادين التحميل يتوليان وضع المؤثر psc في نفس العنوان السابق , وزيادة الرقم في العداد وفي حالة محو العبارات النصية فإن مؤثر التخصيص مزاد التحميل مع الدالة المنهية يتوليان محو كائن الفئة setCount إذا كان العدد في العداد 1 .

التسميات: