سؤال

حسنًا، أعتقد أننا جميعًا متفقون على أن ما يحدث للكود التالي غير محدد، اعتمادًا على ما تم تمريره،

void deleteForMe(int* pointer)
{
     delete[] pointer;
}

يمكن أن يكون المؤشر كل أنواع الأشياء المختلفة، وبالتالي يؤدي أداء غير مشروط delete[] عليه غير محدد.ومع ذلك، لنفترض أننا قمنا بالفعل بتمرير مؤشر مصفوفة،

int main()
{
     int* arr = new int[5];
     deleteForMe(arr);
     return 0;
}

سؤالي هو، في هذه الحالة حيث المؤشر يكون مصفوفة، من هو الذي يعرف هذا؟أعني، من وجهة نظر اللغة/المترجم، ليس لديه أي فكرة عما إذا كان ذلك أم لا arr هو مؤشر صفيف مقابل مؤشر إلى int واحد.هيك، فإنه لا يعرف حتى ما إذا كان arr تم إنشاؤه ديناميكيًا.ومع ذلك، إذا قمت بما يلي بدلاً من ذلك،

int main()
{
     int* num = new int(1);
     deleteForMe(num);
     return 0;
}

نظام التشغيل ذكي بما يكفي لحذف عدد صحيح واحد فقط وعدم الاستمرار في نوع من "فورة القتل" عن طريق حذف بقية الذاكرة بعد تلك النقطة (على النقيض من ذلك مع strlen وغير\0-سلسلة منتهية - ستستمر حتى تصل إلى 0).

إذن من وظيفته أن يتذكر هذه الأشياء؟هل يحتفظ نظام التشغيل بنوع من السجلات في الخلفية؟(أعني، أدرك أنني بدأت هذا المنشور بالقول إن ما يحدث غير محدد، ولكن الحقيقة هي أن سيناريو "فورة القتل" لا يحدث، لذلك في العالم العملي شخص ما يتذكر .)

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

المحلول

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

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

نصائح أخرى

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

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

هذا إذا كان الكود الخاص بك يفعل ذلك ببساطة

Foo* foo = new Foo;

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

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

delete[] bar;

بدلا من مجرد

delete bar;

إذا كان الشريط مؤشرًا إلى مصفوفة.

بالنسبة لمعظمنا (وأنا منهم)، فإن هذا الاهتمام بشأن بضعة بايتات إضافية من الذاكرة يبدو غريبًا هذه الأيام.ولكن لا تزال هناك بعض المواقف التي قد يكون فيها حفظ بضع بايتات (من عدد كبير جدًا من كتل الذاكرة) أمرًا مهمًا.

نعم ، يحتفظ نظام التشغيل ببعض الأشياء في "الخلفية". على سبيل المثال ، إذا قمت بتشغيل

int* num = new int[5];

يمكن لنظام التشغيل تخصيص 4 بايتات إضافية، وتخزين حجم التخصيص في أول 4 بايت من الذاكرة المخصصة وإرجاع مؤشر إزاحة (أي أنه يخصص مساحات الذاكرة من 1000 إلى 1024 لكن المؤشر أعاد النقاط إلى 1004، مع المواقع 1000- 1003 تخزين حجم التخصيص).بعد ذلك، عند استدعاء الحذف، يمكنه النظر إلى 4 بايت قبل تمرير المؤشر إليه للعثور على حجم التخصيص.

أنا متأكد من أن هناك طرقًا أخرى لتتبع حجم التخصيص، ولكن هذا أحد الخيارات.

هذا مشابه جدًا لـ هذا سؤال ويحتوي على العديد من التفاصيل التي تبحث عنها.

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

يمكن عرض هذا في بعض التطبيقات عن طريق تنفيذ التعليمات البرمجية التالية

int* pArray = new int[5];
int size = *(pArray-1);

delete أو delete[] من المحتمل أن يحرر كلاهما الذاكرة المخصصة (تشير الذاكرة)، ولكن الفرق الكبير هو ذلك delete على صفيف لن يستدعي المدمر لكل عنصر من عناصر الصفيف.

على أية حال، الخلط new/new[] و delete/delete[] ربما هو UB.

إنه لا يعرف أنها مصفوفة، ولهذا السبب عليك توفيرها delete[] بدلا من القديم العادي delete.

كان لدي سؤال مماثل لهذا.في لغة C، يمكنك تخصيص الذاكرة باستخدام malloc() (أو وظيفة أخرى مماثلة)، وحذفها باستخدام free().يوجد malloc() واحد فقط، والذي يخصص ببساطة عددًا معينًا من البايتات.لا يوجد سوى free() واحد فقط، والذي يأخذ ببساطة المؤشر كمعلمة.

فلماذا في لغة C يمكنك فقط تسليم المؤشر مجانًا، ولكن في لغة C++ يجب عليك معرفة ما إذا كان مصفوفة أم متغيرًا واحدًا؟

لقد تعلمت أن الإجابة تتعلق بمدمري الفصل.

إذا قمت بتخصيص مثيل لفئة MyClass...

classes = new MyClass[3];

وحذفه باستخدام الحذف، قد تحصل فقط على أداة التدمير للمثيل الأول من MyClass الذي تم استدعاؤه.إذا كنت تستخدم حذف []، فيمكنك التأكد من أنه سيتم استدعاء المدمر لجميع المثيلات في المصفوفة.

هذا هو الفرق المهم.إذا كنت تعمل ببساطة مع الأنواع القياسية (على سبيل المثال،int) لن ترى هذه المشكلة حقًا.بالإضافة إلى ذلك، يجب أن تتذكر أن سلوك استخدام الحذف على الجديد[] والحذف[] على الجديد غير محدد - فقد لا يعمل بنفس الطريقة على كل مترجم/نظام.

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

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

إحدى طرق المترجمين هي تخصيص المزيد من الذاكرة وتخزين عدد العناصر في عنصر الرأس.

مثال كيف يمكن القيام بذلك:هنا

int* i = new int[4];

سيخصص المترجم sizeof(int)*5 بايت.

int *temp = malloc(sizeof(int)*5)

سوف يخزن 4 في البداية sizeof(int) بايت

*temp = 4;

وحدد i

i = temp + 1;

لذا i يشير إلى مجموعة من 4 عناصر، وليس 5.

و

delete[] i;

سيتم معالجتها بالطريقة التالية

int *temp = i - 1;
int numbers_of_element = *temp; // = 4
... call destructor for numbers_of_element elements if needed
... that are stored in temp + 1, temp + 2, ... temp + 4
free (temp)

من الناحية الدلالية، يمكن لكلا الإصدارين من عامل الحذف في لغة C++ "أكل" أي مؤشر؛ومع ذلك، إذا تم إعطاء مؤشر إلى كائن واحد delete[], ، فسينتج عن ذلك UB، مما يعني أن أي شيء قد يحدث، بما في ذلك تعطل النظام أو لا شيء على الإطلاق.

يتطلب C++ من المبرمج اختيار الإصدار المناسب من عامل الحذف اعتمادًا على موضوع إلغاء التخصيص:صفيف أو كائن واحد.

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

توافق على أن المترجم لا يعرف ما إذا كان مصفوفة أم لا.الأمر متروك للمبرمج.

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

للحصول على المواصفات الكاملة عند تخصيص مساحة تخزين إضافية، يرجى الرجوع إلى C++ ABI (كيفية تنفيذ المجمعات): إيتانيوم C++ أبي:مشغل المصفوفة ملفات تعريف الارتباط الجديدة

لا يمكنك استخدام يمسح لمصفوفة، ولا يمكنك استخدامها يمسح [] لغير المصفوفة.

"السلوك غير المحدد" يعني ببساطة أن مواصفات اللغة لا تقدم ضمانات بشأن ما سيحدث.وهذا لا يعني بالضرورة أن شيئًا سيئًا سيحدث.

إذن من وظيفته أن يتذكر هذه الأشياء؟هل يحتفظ نظام التشغيل بنوع من السجلات في الخلفية؟(أعني أنني أدرك أنني بدأت هذا المنشور بالقول إن ما يحدث غير محدد، ولكن الحقيقة هي أن سيناريو "فورة القتل" لا يحدث، لذلك في العالم العملي هناك شخص يتذكر.)

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

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

لن يتذكر تطبيق C++ عمومًا حجم المصفوفة إلا إذا كان بحاجة إلى القيام بذلك لأغراضه الخاصة، عادةً لأن النوع يحتوي على أداة تدمير غير تافهة.

لذلك، بالنسبة للأنواع التي تحتوي على أداة تدمير تافهة، يكون تنفيذ "delete" و"delete []" هو نفسه عادةً.يقوم تطبيق C++ ببساطة بتمرير المؤشر إلى مدير الذاكرة الأساسي.شيء مثل

free(p)

من ناحية أخرى، بالنسبة للأنواع ذات أداة التدمير غير التافهة "حذف" و"حذف []" من المحتمل أن تكون مختلفة.سيكون "الحذف" شيئًا مثل (حيث T هو النوع الذي يشير إليه المؤشر)

p->~T();
free(p);

في حين أن "حذف []" سيكون شيئًا من هذا القبيل.

size_t * pcount = ((size_t *)p)-1;
size_t count = *count;
for (size_t i=0;i<count;i++) {
  p[i].~T();
}
char * pmemblock = ((char *)p) - max(sizeof(size_t),alignof(T));
free(pmemblock);

مرحبًا، حسنًا، يعتمد الأمر على ما تخصصه باستخدام تعبير [] جديد عندما تقوم بتخصيص مجموعة من الأنواع المبنية أو الفئة/البنية ولا تقدم المنشئ والمدمر الخاص بك، فسيتعامل معه المشغل على أنه حجم "sizeof(object)* numObjects" بدلاً من مصفوفة الكائنات، لذلك في هذه الحالة لن يتم تخزين عدد الكائنات المخصصة في أي مكان، ولكن إذا قمت بتخصيص مصفوفة كائنات وقمت بتوفير مُنشئ ومدمر في كائنك بدلاً من تغيير السلوك، فسيقوم التعبير الجديد بتخصيص 4 بايتات أكثر وتخزين عدد من كائنات في أول 4 بايتات بحيث يمكن استدعاء المدمر لكل واحد منهم وبالتالي سيعيد التعبير [] الجديد المؤشر مزاحًا بمقدار 4 بايت للأمام، وعندما يتم إرجاع الذاكرة، سوف يستدعي تعبير الحذف [] قالب دالة أولاً، ويكرر من خلال مجموعة من الكائنات واستدعاء المدمر لكل واحد منهم.لقد قمت بإنشاء هذا الكود البسيط الذي يقوم بتحميل تعبيرات جديدة [] وحذف [] ويوفر وظيفة قالب لإلغاء تخصيص الذاكرة واستدعاء المدمر لكل كائن إذا لزم الأمر:

// overloaded new expression 
void* operator new[]( size_t size )
{
    // allocate 4 bytes more see comment below 
    int* ptr = (int*)malloc( size + 4 );

    // set value stored at address to 0 
    // and shift pointer by 4 bytes to avoid situation that
    // might arise where two memory blocks 
    // are adjacent and non-zero
    *ptr = 0;
    ++ptr; 

    return ptr;
}
//////////////////////////////////////////

// overloaded delete expression 
void static operator delete[]( void* ptr )
{
    // decrement value of pointer to get the
    // "Real Pointer Value"
    int* realPtr = (int*)ptr;
    --realPtr;

    free( realPtr );
}
//////////////////////////////////////////

// Template used to call destructor if needed 
// and call appropriate delete 
template<class T>
void Deallocate( T* ptr )
{
    int* instanceCount = (int*)ptr;
    --instanceCount;

    if(*instanceCount > 0) // if larger than 0 array is being deleted
    {
        // call destructor for each object
        for(int i = 0; i < *instanceCount; i++)
        {
            ptr[i].~T();
        }
        // call delete passing instance count witch points
        // to begin of array memory 
        ::operator delete[]( instanceCount );
    }
    else
    {
        // single instance deleted call destructor
        // and delete passing ptr
        ptr->~T();
        ::operator delete[]( ptr );
    }
}

// Replace calls to new and delete
#define MyNew ::new
#define MyDelete(ptr) Deallocate(ptr)

// structure with constructor/ destructor
struct StructureOne
{
    StructureOne():
    someInt(0)
    {}
    ~StructureOne() 
    {
        someInt = 0;
    }

    int someInt;
};
//////////////////////////////

// structure without constructor/ destructor
struct StructureTwo
{
    int someInt;
};
//////////////////////////////


void main(void)
{
    const unsigned int numElements = 30;

    StructureOne* structOne = nullptr;
    StructureTwo* structTwo = nullptr;
    int* basicType = nullptr;
    size_t ArraySize = 0;

/**********************************************************************/
    // basic type array 

    // place break point here and in new expression
    // check size and compare it with size passed 
    // in to new expression size will be the same
    ArraySize = sizeof( int ) * numElements;

    // this will be treated as size rather than object array as there is no 
    // constructor and destructor. value assigned to basicType pointer
    // will be the same as value of "++ptr" in new expression
    basicType = MyNew int[numElements];

    // Place break point in template function to see the behavior
    // destructors will not be called and it will be treated as 
    // single instance of size equal to "sizeof( int ) * numElements"
    MyDelete( basicType );

/**********************************************************************/
    // structure without constructor and destructor array 

    // behavior will be the same as with basic type 

    // place break point here and in new expression
    // check size and compare it with size passed 
    // in to new expression size will be the same
    ArraySize = sizeof( StructureTwo ) * numElements;

    // this will be treated as size rather than object array as there is no 
    // constructor and destructor value assigned to structTwo pointer
    // will be the same as value of "++ptr" in new expression
    structTwo = MyNew StructureTwo[numElements]; 

    // Place break point in template function to see the behavior
    // destructors will not be called and it will be treated as 
    // single instance of size equal to "sizeof( StructureTwo ) * numElements"
    MyDelete( structTwo );

/**********************************************************************/
    // structure with constructor and destructor array 

    // place break point check size and compare it with size passed in
    // new expression size in expression will be larger by 4 bytes
    ArraySize = sizeof( StructureOne ) * numElements;

    // value assigned to "structOne pointer" will be different 
    // of "++ptr" in new expression  "shifted by another 4 bytes"
    structOne = MyNew StructureOne[numElements];

    // Place break point in template function to see the behavior
    // destructors will be called for each array object 
    MyDelete( structOne );
}
///////////////////////////////////////////

ما عليك سوى تحديد أداة تدمير داخل الفصل الدراسي وتنفيذ التعليمات البرمجية الخاصة بك بكلتا الصيغتين

delete pointer

delete [] pointer

وفقا للإخراج يمكنك العثور على الحلول

الاجابة:

int* pArray = new int[5];

int size = *(pArray-1);

ما تم نشره أعلاه غير صحيح وينتج قيمة غير صالحة.تُحسب "-1" عناصر على نظام التشغيل Windows 64 بت.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top