c++ قاموس - ما هي قاعدة الثلاثة؟




عربي انجليزي (8)

قانون الثلاثة الكبار هو كما هو محدد أعلاه.

مثال سهل ، بلغة إنجليزية بسيطة ، على نوع المشكلة التي يحلها:

غير destructor الافتراضي

قمت بتخصيص الذاكرة في المنشئ الخاص بك وبذلك تحتاج إلى كتابة destructor لحذفها. وإلا فإنك ستتسبب في حدوث تسرب للذاكرة.

قد تعتقد أن هذا هو العمل المنجز.

ستكون المشكلة ، إذا تم إنشاء نسخة من الكائن الخاص بك ، ثم ستشير النسخة إلى نفس الذاكرة مثل الكائن الأصلي.

مرة واحدة ، يحذف أحد هذه الذاكرة في destructor الخاص به ، والآخر سيكون لديك مؤشر للذاكرة غير صالحة (وهذا ما يسمى المؤشر المتدلي) عندما يحاول استخدام الأشياء سوف تحصل على شعر.

لذلك ، تقوم بكتابة مُنشئ نسخة بحيث يقوم بتخصيص كائنات جديدة خاصة بها قطعة من الذاكرة لتدميرها.

عامل التنازل ومنشئ النسخ

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

هذا يعني أن الكائن الجديد والكائن القديم سيشيران إلى نفس الجزء من الذاكرة ، لذا عند تغييره في كائن واحد ، سيتم تغييره لكائن آخر أيضًا. إذا قام أحد الكائنات بحذف هذه الذاكرة ، فسيواصل الآخر محاولة استخدامه - eek.

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

ماذا يعني تقليد كائن ما ؟ ما هو مُنشئ النسخ ومُشغل تخصيص النسخة ؟ متى أحتاج إلى الإعلان عن نفسي؟ كيف يمكنني منع نسخ الكائنات الخاصة بي؟


ماذا يعني تقليد كائن ما؟ هناك بعض الطرق التي يمكنك من خلالها نسخ الأشياء - دعنا نتحدث عن نوعين من المرجح أن تشير إليهما - نسخة عميقة ونسخة ضحلة.

بما أننا في لغة موجهة للكائنات (أو على الأقل نفترض ذلك) ، لنفترض أن لديك جزءًا من الذاكرة المخصصة. نظرًا لأنها لغة OO ، يمكننا بسهولة الرجوع إلى أجزاء الذاكرة التي نخصصها نظرًا لأنها في الغالب متغيرات بدائية (ints أو chars أو bytes) أو فئات حددناها مصنوعة من أنواعنا وأولوياتنا. لنفترض أن لدينا فئة من السيارات على النحو التالي:

class Car //A very simple class just to demonstrate what these definitions mean.
//It's pseudocode C++/Javaish, I assume strings do not need to be allocated.
{
private String sPrintColor;
private String sModel;
private String sMake;

public changePaint(String newColor)
{
   this.sPrintColor = newColor;
}

public Car(String model, String make, String color) //Constructor
{
   this.sPrintColor = color;
   this.sModel = model;
   this.sMake = make;
}

public ~Car() //Destructor
{
//Because we did not create any custom types, we aren't adding more code.
//Anytime your object goes out of scope / program collects garbage / etc. this guy gets called + all other related destructors.
//Since we did not use anything but strings, we have nothing additional to handle.
//The assumption is being made that the 3 strings will be handled by string's destructor and that it is being called automatically--if this were not the case you would need to do it here.
}

public Car(const Car &other) // Copy Constructor
{
   this.sPrintColor = other.sPrintColor;
   this.sModel = other.sModel;
   this.sMake = other.sMake;
}
public Car &operator =(const Car &other) // Assignment Operator
{
   if(this != &other)
   {
      this.sPrintColor = other.sPrintColor;
      this.sModel = other.sModel;
      this.sMake = other.sMake;
   }
   return *this;
}

}

والنسخة العميقة هي إذا أعلننا عن كائن ثم أنشأنا نسخة منفصلة تمامًا من الكائن ... فنحن في نهاية الأمر نحصل على كائنين في مجموعتين من الذاكرة تمامًا.

Car car1 = new Car("mustang", "ford", "red");
Car car2 = car1; //Call the copy constructor
car2.changePaint("green");
//car2 is now green but car1 is still red.

الآن دعونا نفعل شيئا غريبا. دعنا نقول أن car2 إما مبرمجة خاطئة أو المقصود عمدا لمشاركة الذاكرة الفعلية التي يتم car1 من. (عادة ما يكون من الخطأ القيام بذلك ، وفي الفصول الدراسية عادة ما تكون هي الباقة التي يتم مناقشتها في إطار.) تدعي أنه في أي وقت تسأل فيه عن car2 ، فأنت بالفعل تحلّل مؤشرًا إلى مساحة ذاكرة car1 ... وهذا أكثر أو أقل ما هي نسخة ضحلة هو.

//Shallow copy example
//Assume we're in C++ because it's standard behavior is to shallow copy objects if you do not have a constructor written for an operation.
//Now let's assume I do not have any code for the assignment or copy operations like I do above...with those now gone, C++ will use the default.

 Car car1 = new Car("ford", "mustang", "red"); 
 Car car2 = car1; 
 car2.changePaint("green");//car1 is also now green 
 delete car2;/*I get rid of my car which is also really your car...I told C++ to resolve 
 the address of where car2 exists and delete the memory...which is also
 the memory associated with your car.*/
 car1.changePaint("red");/*program will likely crash because this area is
 no longer allocated to the program.*/

لذا بغض النظر عن اللغة التي تكتب بها ، كن حذرًا جدًا بشأن ما تعنيه عندما يتعلق الأمر بنسخ الكائنات نظرًا لأن معظم الوقت تريد نسخة عميقة.

ما هو مُنشئ النسخ ومُشغل تخصيص النسخة؟ لقد استخدمتها بالفعل أعلاه. يتم استدعاء مُنشئ النسخة عند كتابة تعليمة برمجية مثل Car car2 = car1; أساسا إذا قمت بتعريف متغير وتعيينه في سطر واحد ، وهذا هو عندما يتم استدعاء المنشئ نسخة. مشغّل المهمة هو ما يحدث عندما تستخدم علامة مساوية - car2 = car1; . لا يتم الإعلان عن car2 في نفس العبارة. من المحتمل أن تكون قطعتا الرمز التي تكتبها لهذه العمليات متشابهة للغاية. في الواقع ، يحتوي نمط التصميم النموذجي على وظيفة أخرى تتصل بها لتعيين كل شيء بمجرد اقتناعك بأن النسخة الأولية / المهمة مشروعة - إذا نظرت إلى الشفرة المميتة التي كتبتها ، فإن الوظائف متطابقة تقريبًا.

متى أحتاج إلى الإعلان عن نفسي؟ إذا كنت لا تقوم بكتابة الكود المطلوب مشاركته أو لإنتاجه بطريقة ما ، فإنك تحتاج فقط إلى الإعلان عنه عندما تحتاج إليه. يجب أن تكون على دراية بما تقوم به لغة برنامجك إذا اخترت استخدامه "عن طريق الصدفة" ولم تقم بإنشاء واحدة - أي أنك تحصل على برنامج التحويل البرمجي الافتراضي. نادراً ما أستخدم نسخ المنشئات على سبيل المثال ، لكن تجاوزات عامل التخصيص شائعة جدًا. هل تعلم أنه بإمكانك تجاوز ما الإضافة أو الطرح أو ما إلى ذلك؟

كيف يمكنني منع نسخ الكائنات الخاصة بي؟ تجاوز كل الطرق التي يسمح لك بتخصيصها للذاكرة الخاصة بك مع وظيفة خاصة هو بداية معقولة. إذا كنت لا تريد حقا أن يقوم الناس بنسخها ، يمكنك جعلها عامة وتنبيه المبرمج عن طريق رمي استثناء وكذلك عدم نسخ الكائن.


قاعدة ثلاثة في C ++ هي مبدأ أساسي في تصميم وتطوير ثلاثة متطلبات أنه إذا كان هناك تعريف واضح في إحدى وظائف الأعضاء التالية ، فيجب على المبرمج أن يحدد وظيفتي الأعضاء الآخرين معًا. وهي وظائف العضو الثلاثة التالية لا غنى عنها: destructor ، منشئ نسخة ، مشغل مهمة نسخ.

نسخ منشئ في C ++ هو منشئ خاص. يتم استخدامه لإنشاء كائن جديد ، وهو كائن جديد مكافئ لنسخة من كائن موجود.

عامل تشغيل نسخ المهمة هو عامل تخصيص خاص يستخدم عادةً لتحديد كائن موجود للآخرين من نفس نوع الكائن.

هناك أمثلة سريعة:

// default constructor
My_Class a;

// copy constructor
My_Class b(a);

// copy constructor
My_Class c = a;

// copy assignment operator
b = a;

متى أحتاج إلى الإعلان عن نفسي؟

تنص قاعدة الثلاثة أنه إذا أعلنت أيًا من

  1. منشئ نسخة
  2. مشغل تخصيص نسخة
  3. المدمر

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

  • أيا كانت إدارة الموارد التي يجري القيام بها في عملية نسخ واحدة ربما هناك حاجة إلى القيام به في عملية النسخ الأخرى و

  • كما سيشارك دستور الطبقة في إدارة الموارد (عادة ما يطلقها). كان المورد الكلاسيكي الذي يتم إدارته هو الذاكرة ، وهذا هو السبب في أن جميع فئات "ستاندرد للمكتبة" التي تدير الذاكرة (على سبيل المثال ، حاويات STL التي تقوم بإدارة الذاكرة الديناميكية) تعلن جميعها "الثلاثة الكبار": كل من عمليات النسخ والمدمرة.

نتيجة "القاعدة الثالثة" هي أن وجود مدمر مستخدم معلن يشير إلى أنه من غير المحتمل أن تكون نسخة العضو البسيطة الحكيمة مناسبة لعمليات النسخ في الفصل. وهذا ، بدوره ، يشير إلى أنه إذا صرحت إحدى الفئات بدمار ، فمن المحتمل ألا يتم إنشاء عمليات النسخ تلقائيًا ، لأنهم لن يفعلوا الشيء الصحيح. في الوقت الذي تم فيه تبني C ++ 98 ، لم تكن أهمية هذا الخط من الاستدلال موضع تقدير كامل ، لذا في C ++ 98 ، لم يكن لوجود مستخدم معلن مدمر أي تأثير على رغبة المجمعين في توليد عمليات النسخ. يستمر هذا الوضع في C ++ 11 ، ولكن فقط لأن تقييد الشروط التي يتم بموجبها إنشاء عمليات النسخ يؤدي إلى كسر الكثير من التعليمات البرمجية القديمة.

كيف يمكنني منع نسخ الكائنات الخاصة بي؟

إعلان منشئ نسخة ومشغل تخصيص نسخة كمعيّن وصول خاص.

class MemoryBlock
{
public:

//code here

private:
MemoryBlock(const MemoryBlock& other)
{
   cout<<"copy constructor"<<endl;
}

// Copy assignment operator.
MemoryBlock& operator=(const MemoryBlock& other)
{
 return *this;
}
};

int main()
{
   MemoryBlock a;
   MemoryBlock b(a);
}

في C ++ 11 فصاعدًا ، يمكنك أيضًا الإعلان عن حذف مُنشئ نسخة ومهمة

class MemoryBlock
{
public:
MemoryBlock(const MemoryBlock& other) = delete

// Copy assignment operator.
MemoryBlock& operator=(const MemoryBlock& other) =delete
};


int main()
{
   MemoryBlock a;
   MemoryBlock b(a);
}

قاعدة الثلاثة هي قاعدة أساسية لـ C ++ ، تقول أساساً

إذا كان صفك يحتاج إلى أي من

  • منشئ نسخة ،
  • مشغل الاحالة ،
  • أو مدمر ،

تعريف explictly ، فمن المرجح أن تحتاج كل ثلاثة منهم .

تكمن الأسباب وراء ذلك في أن الثلاثة يستخدمون عادةً لإدارة مورد ، وإذا كانت صفك يدير موردًا ، فعادة ما يحتاج إلى إدارة النسخ بالإضافة إلى تحريره.

إذا لم يكن هناك دلالة جيدة لنسخ المصدر الذي تديره فصلك ، فاحرص على حظر النسخ عن طريق التصريح (وليس defining ) بأن مُنشئ النسخ ومشغل المهمة كخاص.

(لاحظ أن الإصدار الجديد القادم من معيار C ++ (وهو C ++ 11) يضيف دلالات نقل إلى C ++ ، والتي من المحتمل أن تغير قاعدة الثلاثة .ومع ذلك ، لا أعرف سوى القليل عن هذا الأمر لكتابة قسم C ++ 11 حول قاعدة الثلاثة.)


أساسا إذا كان لديك destructor (وليس destructor الافتراضي) فهذا يعني أن الفئة التي قمت بتعريف بعض تخصيص الذاكرة. افترض أن يتم استخدام الفئة خارج بواسطة بعض رمز العميل أو بواسطتك.

    MyClass x(a, b);
    MyClass y(c, d);
    x = y; // This is a shallow copy if assignment operator is not provided

إذا كان لدى MyClass بعض الأعضاء الذين تم كتابتهم بشكل أولي فقط ، فستعمل مشغِّل التعيين الافتراضي ولكن إذا كان يحتوي على بعض أعضاء المؤشر والكائنات التي ليس بها مشغلات مهمة ، فإن النتيجة ستكون غير متوقعة. لذلك يمكننا القول أنه إذا كان هناك شيء ما لحذفه في طبقة مدمرة ، فقد نحتاج إلى مشغل نسخ عميق مما يعني أنه يجب علينا توفير مُنشئ نسخة ومشغل عامل.


العديد من الإجابات الموجودة تلمس بالفعل منشئ نسخة ، مشغل التعيين و destructor. ومع ذلك ، في مرحلة ما بعد C ++ 11 ، قد يؤدي تطبيق التحريك الدلالية إلى توسيع هذا الرقم إلى ما بعد 3.

قدم مؤخرا مايكل كلايس محاضرة تلمس هذا الموضوع: http://channel9.msdn.com/events/CPP/C-PP-Con-2014/The-Canonical-Class


كتاب واحد قرأته (لا أتذكر بشكل صحيح أي كتاب) ذكر: يحاول Compilers تحليل التعبيرات إلى الرمز المميز الأكبر باستخدام القاعدة اليمنى اليسرى.

في هذه الحالة ، التعبير:

x-->0

يوزع على أكبر الرموز:

token 1: x
token 2: --
token 3: >
token 4: 0
conclude: x-- > 0

تنطبق القاعدة نفسها على هذا التعبير:

a-----b

بعد التحليل:

token 1: a
token 2: --
token 3: --
token 4: -
token 5: b
conclude: (a--)-- - b

آمل أن يساعد هذا على فهم تعبير معقد ^^





c++ copy-constructor assignment-operator c++-faq rule-of-three