التحميل الزائد في C ++. الجزء الثالث. تحميل عبارات جديدة / حذف زائد


نواصل سلسلة "C ++ ، الحفر بعمق". الغرض من هذه السلسلة هو إخبارك بأكبر قدر ممكن عن الميزات المختلفة للغة ، ربما خاصة للغاية. هذه المقالة هي عن التحميل الزائد للمشغل new/delete. هذه هي المقالة الثالثة في السلسلة ، أولها مخصص لوظائف وقوالب التحميل الزائد ، الموجود هنا ، والثاني مخصص لمشغلي التحميل الزائد ، الموجود هنا . تختتم هذه المقالة سلسلة من ثلاث مقالات حول التحميل الزائد في C ++.


جدول المحتويات


جدول المحتويات
  1. new/delete
    1.1.
    1.2.
    1.3.
  2. new/delete
    2.1.
    2.2.
      2.2.1. new/delete
      2.2.2. new/delete
      2.2.3.
      2.2.4.
      2.2.5. operator delete()
  3. new/delete
  4.
  5. -
  

1. النماذج القياسية للعاملين الجدد / الحذف


يدعم C ++ العديد من خيارات المشغل new/delete. يمكن تقسيمها إلى معيار أساسي ومعيار إضافي ومخصص. يناقش هذا القسم والقسم 2 النماذج القياسية ؛ وستتم مناقشة النماذج المخصصة في القسم 3.

1.1. النماذج القياسية الأساسية


الأشكال القياسية الرئيسية للعوامل new/deleteالمستخدمة عند إنشاء وحذف كائن وصفيف من النوع هي Tكما يلي:

new T(/*   */)
new T[/*   */]
delete ptr;
delete[] ptr;

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

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

انتباه! من الضروري استدعاء الشكل الصحيح للعاملdeleteبناءً على ما إذا كان سيتم حذف كائن واحد أو صفيف. يجب مراعاة هذه القاعدة بشكل صارم ، وإلا يمكنك الحصول على سلوك غير محدد ، أي أنه يمكن أن يحدث أي شيء: تسرب الذاكرة ، تحطم ، إلخ. راجع [Meyers1] للحصول على التفاصيل.

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

تقوم وظائف تخصيص الذاكرة القياسية ، عندما يتعذر تلبية الطلب ، برمي استثناء نوع std::bad_alloc. ولكن يمكن set_new_handler()اكتشاف هذا الاستثناء ، لذلك تحتاج إلى تثبيت اعتراض عالمي باستخدام استدعاء دالة ، لمزيد من التفاصيل انظر [Meyers1].

أي شكل من أشكال المشغلdeleteتطبيق بأمان على مؤشر فارغ.

عند إنشاء مصفوفة بعامل ، new[]يمكن ضبط الحجم على صفر. يسمح

كلا النموذجين newباستخدام عامل التهيئة بالأقواس.

new int{42}
new int[8]{1,2,3,4}

1.2. نماذج قياسية إضافية


عند توصيل ملف الرأس <new>، تتوافر 4 نماذج مشغل قياسية أخرى new:

new(ptr) T(/*  */);
new(ptr) T[/*   */];
new(std::nothrow) T(/*   */);
new(std::nothrow) T[/*   */];

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

يسمى newالخياران الثانيان عامل التشغيل لا يرمي الاستثناءات (nothrow new) ويختلفان في أنه إذا كان من المستحيل تلبية الطلب ، فإنهما يعودان nullptr، لكنهما لا يرميان استثناء من النوع std::bad_alloc. يحدث حذف كائن باستخدام عامل التشغيل الرئيسي delete. تعتبر هذه الخيارات قديمة ولا يوصى باستخدامها.

1.3. تخصيص الذاكرة ووظائف مجانية


new/deleteتستخدم الأشكال المعيارية للمشغلين وظائف التخصيص والتوزيع التالية:

void* operator new(std::size_t size);
void operator delete(void* ptr);
void* operator new[](std::size_t size);
void operator delete[](void* ptr);
void* operator new(std::size_t size, void* ptr);
void* operator new[](std::size_t size, void* ptr);
void* operator new(std::size_t size, const std::nothrow_t& nth);
void* operator new[](std::size_t size, const std::nothrow_t& nth);

يتم تعريف هذه الدالات في مساحة الاسم العمومية. وظائف تخصيص الذاكرة لعبارات المضيف newلا تفعل شيئًا وتعود ببساطة ptr.

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

void* operator new (std::size_t size, std::align_val_t al);
void* operator new[](std::size_t size, std::align_val_t al);

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

struct alignas(32) X { /* ... */ };

2. التحميل الزائد على النماذج القياسية للمشغلين الجدد / الحذف


new/deleteيتكون التحميل الزائد للأشكال القياسية للمشغلين من تحديد الوظائف المعرفة من قبل المستخدم لتخصيص وتحرير الذاكرة التي تتوافق توقيعاتها مع التواقيع القياسية. يمكن تعريف هذه الدالات في مساحة الاسم العمومية أو في فئة ، ولكن ليس في مساحة اسم غير العمومي. newلا يمكن تعريف وظيفة تخصيص الذاكرة لعبارة مضيف قياسية في مساحة الاسم العامة. بعد هذا التعريف ، new/deleteستستخدمها عوامل التشغيل المقابلة ، وليس العوامل القياسية.

2.1. الزائد في مساحة الاسم العالمية


لنفترض ، على سبيل المثال ، في وحدة نمطية في مساحة اسم عمومية أن الوظائف المعرفة من قبل المستخدم محددة:

void* operator new(std::size_t size)
{
// ...
}

void operator delete(void* ptr)
{
// ...
}

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

دالة تعريف مساحة الاسم العامة

void* operator new(std::size_t size, const std::nothrow_t& nth)
{
// ...
}

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

نفس الوضع مع وظائف المصفوفات.

العبارات الزائدة new/deleteفي مساحة الاسم العالمية غير محبذة بشدة.

2.2. الزائد الطبقة


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

class X
{
// ...
public:
    void* operator new(std::size_t size)
    {
        std::cout << "X new\n";
        return ::operator new(size);
    }

    void operator delete(void* ptr)
    {
        std::cout << "X delete\n";
        ::operator delete(ptr);
    }

    void* operator new[](std::size_t size)
    {
        std::cout << "X new[]\n";
        return ::operator new[](size);
    }

    void operator delete[](void* ptr)
    {
        std::cout << "X delete[]\n";
        ::operator delete[](ptr);
    }
};

في هذا المثال ، تتم إضافة التتبع ببساطة إلى العمليات القياسية. الآن، من حيث new X()و new X[N]سوف تستخدم هذه الوظائف لتخصيص وتحرير الذاكرة.

هذه الوظائف ثابتة رسميًا ويمكن الإعلان عنها على أنها static. ولكن في الأساس هم مثال ، مع استدعاء الوظيفة operator new()، يبدأ إنشاء المثيل ، operator delete()ويكمل استدعاء الوظيفة حذفها. لا يتم استدعاء هذه الوظائف مطلقًا لمهام أخرى. علاوة على ذلك ، كما هو موضح أدناه ، فإن الوظيفة operator delete()هي في الأساس افتراضية. لذا من الأصح الإعلان عنها بدون static.

2.2.1. الوصول إلى النماذج القياسية من عوامل التشغيل الجديدة / الحذف


مشغلي new/deleteيمكن استخدامها مع عامل إضافي قرار نطاق، على سبيل المثال ::new(p) X(). في هذه الحالة ، operator new()سيتم تجاهل الوظيفة المحددة في الفئة ، وسيتم استخدام المعيار المقابل. بنفس الطريقة ، يمكنك استخدام عامل التشغيل delete.

2.2.2. إخفاء أشكال أخرى من عوامل التشغيل الجديدة / الحذف


إذا Xحاولنا الآن في الفصل استخدام الرمي أو عدم رمي الاستثناءات new، نحصل على خطأ. والحقيقة هي أن الوظيفة operator new(std::size_t size)ستخفي أشكالًا أخرى operator new(). يمكن حل المشكلة بطريقتين. في البداية ، تحتاج إلى إضافة الخيارات المناسبة للفئة (يجب أن تفوض هذه الخيارات ببساطة تشغيل الوظيفة القياسية). في الثانية ، تحتاج إلى استخدام عامل newمع عامل دقة النطاق ، على سبيل المثال ::new(p) X().

2.2.3. حاويات قياسية


إذا حاولنا وضع المثيلات Xفي بعض الحاوية القياسية ، على سبيل المثال std::vector<X>، فسوف نرى أن وظائفنا لا تُستخدم لتخصيص الذاكرة وتحريرها. والحقيقة هي أن جميع الحاويات القياسية لديها آلية خاصة بها لتخصيص وتحرير الذاكرة (فئة مخصصة خاصة ، وهي معلمة قالب للحاوية) ، ويستخدمون عامل وضع لتهيئة العناصر new.

2.2.4. ميراث


يتم تخصيص وظائف تخصيص الذاكرة وتحريرها. إذا تم تحديد هذه الوظائف في الفئة الأساسية ، ولكن ليس في الفئة المشتقة ، فسيتم تحميل عوامل التشغيل بشكل زائد على الفئة المشتقة new/deleteوسيتم استخدام الوظائف المحددة والمخصصة في الفئة الأساسية لتخصيص الذاكرة وتحريرها.

ضع في اعتبارك الآن التسلسل الهرمي للفئة متعددة الأشكال ، حيث تزيد كل فئة من عوامل التشغيل new/delete. الآن ، اسمح بحذف مثيل الفئة المشتقة باستخدام عامل التشغيل من deleteخلال مؤشر إلى الفئة الأساسية. إذا كان المدمر للفئة الأساسية ظاهريًا ، فإن المعيار يضمن أن المدمر لهذه الفئة المشتقة يسمى. في هذه الحالة operator delete()، يتم أيضًا ضمان استدعاء الوظيفة المحدد لهذه الفئة المشتقة. وبالتالي ، فإن الوظيفة هي في operator delete()الواقع افتراضية.

2.2.5. شكل بديل لوظيفة حذف عامل ()


في الفصل الدراسي (خاصة عند استخدام الوراثة) ، يكون من المناسب في بعض الأحيان استخدام شكل بديل من الوظيفة لتحرير الذاكرة:

void operator delete(void* p, std::size_t size);
void operator delete[](void* p, std::size_t size);

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

3. مستخدم جديد / حذف


يمكن لـ C ++ دعم نماذج عامل التشغيل المخصصة من newالنموذج التالي:

new(/*  */) T(/*   */)
new(/*  */) T[/*   */]

من أجل دعم هذه النماذج ، من الضروري تحديد الوظائف المناسبة لتخصيص وتحرير الذاكرة:

void* operator new(std::size_t size, /* .  */);
void* operator new[](std::size_t size, /* .  */);
void operator delete(void* p, /* .  */);
void operator delete[](void* p, /* .  */);

يجب ألا تكون قائمة المعلمات الإضافية لوظائف تخصيص الذاكرة فارغة ويجب ألا تتكون من واحدة void*أو const std::nothrow_t&، أي أن توقيعها يجب ألا يتزامن مع واحدة من المعايير القياسية. قوائم معلمات إضافية في operator new()و operator delete()يجب أن تتطابق. newيجب أن تتوافق الوسائط التي تم تمريرها إلى العامل مع معلمات إضافية لوظائف تخصيص الذاكرة. operator delete()يمكن أن تكون الوظيفة المخصصة أيضًا في النموذج مع معلمة حجم اختيارية.

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

نماذج عامل التشغيل المعرفة من قبل المستخدم الموضع المعرفة من قبل المستخدم . لا يجب الخلط بينها وبين عامل التنسيب القياسي (غير المخصص) الموصوف في القسم 1.2.newnewnewnew

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

هنا مثال (في النطاق العالمي).

void* operator new(std::size_t size, int a, const char* b)
{
    std::cout << "new " << a << " + " << b << "\n";
    return ::operator new(size);
}
void operator delete(void* p, int a, const char* b)
{
    std::cout << "delete " << a << " + " << b << "\n";
    ::operator delete(p);
}

class X {/* ... */};
X* p = new(42, "meow") X(); // : new 42 + meow
delete p; //   ::operator delete()

4. تعريف وظائف تخصيص الذاكرة


في هذه الأمثلة ، وظائف المستخدم operator new()والعملية operator delete()المفوضة المقابلة للوظائف القياسية. في بعض الأحيان يكون هذا الخيار مفيدًا ، ولكن الهدف الرئيسي من التحميل الزائد new/deleteهو إنشاء آلية جديدة لتخصيص / تحرير الذاكرة. المهمة ليست بسيطة ، وقبل القيام بها ، يجب على المرء أن يفكر مليًا في كل شيء. سكوت مايرز [Meyers1] يناقش الدوافع المحتملة لاتخاذ مثل هذا القرار (بالطبع ، أهمها هو الكفاءة). كما يناقش المشاكل التقنية الرئيسية المرتبطة بالتنفيذ الصحيح للوظائف المعرفة من قبل المستخدم لتخصيص وتحرير الذاكرة (باستخدام الوظيفةset_new_handler()تزامن متعدد المحاذاة). يقدم Guntheroth مثالًا على تنفيذ وظائف تخصيص الذاكرة وإلغاء التخصيص البسيطة نسبيًا المعرفة من قبل المستخدم. قبل إنشاء نسختك الخاصة ، يجب أن تبحث عن حلول جاهزة ، كمثال ، يمكنك إحضار مكتبة البلياردو من مشروع Boost.

5. فئات التخصيص للحاويات القياسية


كما ذكر أعلاه ، تستخدم الحاويات القياسية فئات مخصصة خاصة لتخصيص وتحرير الذاكرة. هذه الفئات هي معلمات قالب للحاويات ويمكن للمستخدم تحديد نسخته من هذه الفئة. الدوافع لمثل هذا الحل هي تقريبًا نفس دوافع مشغلي التحميل الزائد new/delete. يصف [Guntheroth] كيفية إنشاء مثل هذه الفئات.

قائمة المراجع


[جونتروث]
جونتروث ، كيرت. تحسين البرامج في C ++. طرق مثبتة لزيادة الإنتاجية.: Per. من الانجليزية - SPb.: Alpha-book LLC، 2017.
[Meyers1]
Meyers، Scott. الاستخدام الفعال لـ C ++. 55 طريقة أكيدة لتحسين هيكل وشفرة برامجك: Per. من الانجليزية - م: مطبعة DMK ، 2014.

All Articles