سؤال

هذا نوع من سؤال المبتدئين ، لكنني لم أفعل C ++ منذ فترة طويلة ، لذلك هنا يذهب ...

لدي فصل يحتوي على صفيف مخصص ديناميكيًا ، على سبيل المثال

class A
{
    int* myArray;
    A()
    {
        myArray = 0;
    }
    A(int size)
    {
        myArray = new int[size];
    }
    ~A()
    {
        // Note that as per MikeB's helpful style critique, no need to check against 0.
        delete [] myArray;
    }
}

لكنني الآن أريد إنشاء مجموعة مخصصة ديناميكيًا من هذه الفئات. هذا هو الكود الحالي الخاص بي:

A* arrayOfAs = new A[5];
for (int i = 0; i < 5; ++i)
{
    arrayOfAs[i] = A(3);
}

لكن هذا ينفجر بشكل رهيب. لأن الجديد A الكائن الذي تم إنشاؤه (مع A(3) يتم تدمير المكالمة) عند for ينتهي تكرار الحلقة ، وهذا يعني أن الداخلية myArray من ذلك A يحصل المثال delete []-ed.

لذلك أعتقد أن بناء الجملة الخاص بي يجب أن يكون خطأ فظيع؟ أعتقد أن هناك بعض الإصلاحات التي تبدو مبالغ فيها ، والتي آمل أن أتجنبها:

  • إنشاء منشئ نسخ ل A.
  • استخدام vector<int> و vector<A> لذلك لا داعي للقلق بشأن كل هذا.
  • عوضا عن الحصول arrayOfAs كن مجموعة من A الكائنات ، هل تكون مجموعة من A* مؤشرات.

أعتقد أن هذا مجرد بعض المبتدئين حيث يوجد بناء جملة يعمل فعليًا عند محاولة تخصيص مجموعة من الأشياء التي لها تخصيص ديناميكي داخلي.

(أيضًا ، تم تقدير انتقادات الأسلوب ، حيث كان منذ فترة منذ أن فعلت C ++.)

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

هل كانت مفيدة؟

المحلول

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

إذا كان كائنك يحتوي على مؤشر خام ، فأنت بحاجة إلى تذكر قاعدة 3 (الآن قاعدة 5 في C ++ 11).

  • البناء
  • المدمرة
  • نسخ مُنشئ
  • مهمة تشغيل
  • نقل مُنشئ (C ++ 11)
  • نقل المهمة (C ++ 11)

هذا لأنه إذا لم يتم تعريفه ، فإن المترجم سيولد نسخته الخاصة من هذه الطرق (انظر أدناه). الإصدارات التي تم إنشاؤها المترجم ليست مفيدة دائمًا عند التعامل مع المؤشرات الأولية.

مُنشئ النسخ هو الصعوبة التي يجب تصحيحها (إنها غير تافهة إذا كنت ترغب في تقديم ضمان استثناء قوي). يمكن تعريف مشغل الواجب من حيث مُنشئ النسخ حيث يمكنك استخدام النسخة ومبادلة المصطلح داخليًا.

انظر أدناه للحصول على التفاصيل الكاملة حول الحد الأدنى المطلق لفصل يحتوي على مؤشر إلى مجموعة من الأعداد الصحيحة.

مع العلم أنه من غير التافهة تصحيحها ، يجب أن تفكر في استخدام ناقل STD :: بدلاً من مؤشر إلى مجموعة من الأعداد الصحيحة. المتجه سهل الاستخدام (والتوسع) ويغطي جميع المشكلات المرتبطة بالاستثناءات. قارن الفئة التالية مع تعريف أدناه.

class A
{ 
    std::vector<int>   mArray;
    public:
        A(){}
        A(size_t s) :mArray(s)  {}
};

بالنظر إلى مشكلتك:

A* arrayOfAs = new A[5];
for (int i = 0; i < 5; ++i)
{
    // As you surmised the problem is on this line.
    arrayOfAs[i] = A(3);

    // What is happening:
    // 1) A(3) Build your A object (fine)
    // 2) A::operator=(A const&) is called to assign the value
    //    onto the result of the array access. Because you did
    //    not define this operator the compiler generated one is
    //    used.
}

يعد مشغل المهمة الذي تم إنشاؤه بواسطة برنامج التحويل البرمجي جيدًا لجميع المواقف تقريبًا ، ولكن عندما تكون المؤشرات الأولية في اللعب ، تحتاج إلى الانتباه. في حالتك ، يسبب مشكلة بسبب نسخة سطحية مشكلة. لقد انتهى بك الأمر مع كائنين يحتويان على مؤشرات لنفس قطعة الذاكرة. عندما يخرج A (3) من النطاق في نهاية الحلقة ، فإنه يدعو إلى حذف [] على مؤشره. وهكذا يحتوي الكائن الآخر (في الصفيف) الآن على مؤشر للذاكرة التي تم إرجاعها إلى النظام.

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

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

لذا فإن الحد الأدنى للفئة التي تحتوي على مؤشر:

class A
{
    size_t     mSize;
    int*       mArray;
    public:
         // Simple constructor/destructor are obvious.
         A(size_t s = 0) {mSize=s;mArray = new int[mSize];}
        ~A()             {delete [] mArray;}

         // Copy constructor needs more work
         A(A const& copy)
         {
             mSize  = copy.mSize;
             mArray = new int[copy.mSize];

             // Don't need to worry about copying integers.
             // But if the object has a copy constructor then
             // it would also need to worry about throws from the copy constructor.
             std::copy(&copy.mArray[0],&copy.mArray[c.mSize],mArray);

         }

         // Define assignment operator in terms of the copy constructor
         // Modified: There is a slight twist to the copy swap idiom, that you can
         //           Remove the manual copy made by passing the rhs by value thus
         //           providing an implicit copy generated by the compiler.
         A& operator=(A rhs) // Pass by value (thus generating a copy)
         {
             rhs.swap(*this); // Now swap data with the copy.
                              // The rhs parameter will delete the array when it
                              // goes out of scope at the end of the function
             return *this;
         }
         void swap(A& s) noexcept
         {
             using std::swap;
             swap(this.mArray,s.mArray);
             swap(this.mSize ,s.mSize);
         }

         // C++11
         A(A&& src) noexcept
             : mSize(0)
             , mArray(NULL)
         {
             src.swap(*this);
         }
         A& operator=(A&& src) noexcept
         {
             src.swap(*this);     // You are moving the state of the src object
                                  // into this one. The state of the src object
                                  // after the move must be valid but indeterminate.
                                  //
                                  // The easiest way to do this is to swap the states
                                  // of the two objects.
                                  //
                                  // Note: Doing any operation on src after a move 
                                  // is risky (apart from destroy) until you put it 
                                  // into a specific state. Your object should have
                                  // appropriate methods for this.
                                  // 
                                  // Example: Assignment (operator = should work).
                                  //          std::vector() has clear() which sets
                                  //          a specific state without needing to
                                  //          know the current state.
             return *this;
         }   
 }

نصائح أخرى

أوصي باستخدام STD :: Vector: شيء مثل

typedef std::vector<int> A;
typedef std::vector<A> AS;

لا حرج في المبالغة في STL ، وستتمكن من قضاء المزيد من الوقت في تنفيذ الميزات المحددة لتطبيقك بدلاً من إعادة اختراع الدراجة.

يخصص مُنشئ كائنك كائنًا آخر ديناميكيًا ويخزن مؤشرًا على هذا الكائن المخصص ديناميكيًا في مؤشر خام.

لهذا السيناريو ، أنت يجب حدد مُنشئ النسخ الخاص بك ومشغل المهام والمدمر. المترجم الذي تم إنشاؤه لن يعمل بشكل صحيح. (هذا نتيجة طبيعية لـ "قانون الثلاثة الكبار": فئة مع أي من Destructor ، مشغل المهام ، يحتاج منشئ النسخ عمومًا إلى 3).

لقد حددت المدمر الخاص بك (وذكرت إنشاء مُنشئ نسخ) ، لكنك تحتاج إلى تحديد كلا من الـ 2 من الثلاثة الكبار.

البديل هو تخزين المؤشر لتخصيصك ديناميكيًا int[] في بعض الأشياء الأخرى التي تعتني بهذه الأشياء لك. شيء مثل vector<int> (كما ذكرت) أو أ boost::shared_array<>.

لتغلي هذا - للاستفادة من RAII إلى أقصى حد ، يجب عليك تجنب التعامل مع المؤشرات الأولية إلى الحد الممكن.

وبما أنك طلبت انتقادات نمط أخرى ، فإن القاصر هو أنه عندما تقوم بحذف المؤشرات الأولية ، لا تحتاج إلى التحقق من 0 قبل الاتصال delete - delete يتعامل مع هذه الحالة من خلال عدم القيام بأي شيء حتى لا تضطر إلى فوضى رمزك بالشيكات.

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

  2. تخزين المؤشرات خلاف ذلك (أو المؤشرات الذكية ، ولكن قد تفي ببعض المشكلات في هذه الحالة).

ملاحظة: دائمًا ما يتم تحديد مُنشئات الافتراضات والنسخ الخاصة

تحتاج إلى مشغل مهام بحيث:

arrayOfAs[i] = A(3);

يعمل كما ينبغي.

لماذا لا يكون لديك طريقة setSize.

A* arrayOfAs = new A[5];
for (int i = 0; i < 5; ++i)
{
    arrayOfAs[i].SetSize(3);
}

يعجبني "النسخ" ولكن في هذه الحالة ، لا يقوم المنشئ الافتراضي بأي شيء. يمكن لمجموعة SetSize نسخ البيانات من m_array الأصلي (إذا كانت موجودة) .. يجب عليك تخزين حجم الصفيف داخل الفصل للقيام بذلك.
أو
يمكن أن يحذف setSize m_array الأصلي.

void SetSize(unsigned int p_newSize)
{
    //I don't care if it's null because delete is smart enough to deal with that.
    delete myArray;
    myArray = new int[p_newSize];
    ASSERT(myArray);
}

باستخدام ميزة التنسيب من new المشغل ، يمكنك إنشاء الكائن في مكانه وتجنب النسخ:

الموضع (3): void* المشغل جديد (STD :: size_t size ، void* ptr) noExcept ؛

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

أقترح ما يلي:

A* arrayOfAs = new A[5]; //Allocate a block of memory for 5 objects
for (int i = 0; i < 5; ++i)
{
    //Do not allocate memory,
    //initialize an object in memory address provided by the pointer
    new (&arrayOfAs[i]) A(3);
}
مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top