تخصيص الذاكرة الديناميكية للسلسلة ج. بنيات التحكم في لغة C

27.06.2020

آخر تحديث: 28/05/2017

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

أرقام مزدوجة = (1.0، 2.0، 3.0، 4.0، 5.0)؛

لمثل هذه المصفوفة، الذاكرة المخصصة هي 5 * 8 (حجم مزدوج) = 40 بايت. بهذه الطريقة نعرف بالضبط عدد العناصر الموجودة في المصفوفة ومقدار الذاكرة التي تستهلكها. ومع ذلك، هذا ليس مناسبًا دائمًا. في بعض الأحيان يكون من الضروري أن يتم تحديد عدد العناصر، وبالتالي حجم الذاكرة المخصصة للمصفوفة، ديناميكيًا وفقًا لشروط معينة. على سبيل المثال، يمكن للمستخدم إدخال حجم المصفوفة بنفسه. وفي هذه الحالة، يمكننا استخدام تخصيص الذاكرة الديناميكية لإنشاء المصفوفة.

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

    مالوك (). لديه نموذج أولي

    باطل * مالوك (غير موقعة)؛

    يخصص ذاكرة بطول البايت ويعيد مؤشرًا إلى بداية الذاكرة المخصصة. إرجاع NULL إذا لم تنجح

    كالوك (). لديه نموذج أولي

    Void *calloc(unsigned n, unsigned m);

    يخصص ذاكرة لعدد n من العناصر يبلغ عددها m بايت ويعيد مؤشرًا إلى بداية الذاكرة المخصصة. إرجاع NULL إذا لم تنجح

    ريلوك () . لديه نموذج أولي

    Void *realloc(void *bl, unsigned ns);

    يقوم بتغيير حجم كتلة الذاكرة المخصصة مسبقًا والمشار إليها بواسطة المؤشر bl إلى حجم ns بايت. إذا كان مؤشر bl يحتوي على قيمة NULL، أي أنه لم يتم تخصيص ذاكرة، فإن إجراء الدالة يشبه إجراء malloc

    حر() . لديه نموذج أولي

    باطل *مجاني(باطل *بل);

    يحرر كتلة الذاكرة المخصصة مسبقًا، والتي تتم الإشارة إلى بدايتها بواسطة المؤشر bl.

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

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

#يشمل #يشمل int main(void) ( int *block; // مؤشر إلى كتلة الذاكرة int n; // عدد عناصر المصفوفة // أدخل عدد العناصر printf("Size of array="); scanf("%d", &n); // تخصيص ذاكرة للمصفوفة // ترجع الدالة malloc مؤشرًا من النوع void* // والذي يتم تحويله تلقائيًا إلى النوع int* block = malloc(n * sizeof(int)); صفيف لـ (int i=0;i)

مخرجات وحدة التحكم للبرنامج:

حجم المصفوفة=5 كتلة=23 كتلة=-4 كتلة=0 كتلة=17 كتلة=81 23 -4 0 17 81

هنا، يتم تعريف مؤشر كتلة من النوع int لإدارة ذاكرة المصفوفة. عدد عناصر المصفوفة غير معروف مسبقًا، ويمثله المتغير n.

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

Block = malloc(n * sizeof(int));

أولاً، تجدر الإشارة إلى أن الوظائف الثلاث المذكورة أعلاه تُرجع مؤشرًا من النوع void * كنتيجة لعالمية القيمة المرجعة. ولكن في حالتنا، يتم إنشاء مصفوفة من النوع int، والتي يتم التحكم فيها بواسطة مؤشر من النوع int * ، وبالتالي يتم تحويل نتيجة الدالة malloc ضمنيًا إلى النوع int * .

يتم تمرير عدد البايتات للكتلة المخصصة إلى الدالة malloc نفسها. من السهل جدًا حساب هذا الرقم: ما عليك سوى ضرب عدد العناصر بحجم عنصر واحد n * sizeof(int) .

بعد الانتهاء من جميع الإجراءات، يتم تحرير الذاكرة باستخدام الدالة free():

مجاني(كتلة);

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

مجاني(كتلة); ل(int i=0;i

وإذا حاولنا القيام بذلك، فسنحصل على قيم غير محددة.

بدلًا من الدالة malloc، يمكننا بالمثل استخدام الدالة calloc()، التي تأخذ عدد العناصر وحجم العنصر الواحد:

Block = calloc(n, sizeof(int));

أو يمكنك أيضًا استخدام وظيفة realloc() :

Int *block = NULL; block = realloc(block, n * sizeof(int));

عند استخدام Realloc، من المرغوب فيه (في بعض البيئات، على سبيل المثال، في Visual Studio، إلزامي) تهيئة المؤشر إلى NULL على الأقل.

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

Block = malloc(n * sizeof(int)); block = calloc(n, sizeof(int)); block = realloc(block, n * sizeof(int));

الآن دعونا نلقي نظرة على مشكلة أكثر تعقيدًا - تخصيص الذاكرة ديناميكيًا لمصفوفة ثنائية الأبعاد:

#يشمل #يشمل int main(void) ( int **table; // مؤشر إلى كتلة ذاكرة لمجموعة من المؤشرات int *rows; // مؤشر إلى كتلة ذاكرة لتخزين معلومات الصف introwscount; // عدد الصفوف int d; / / رقم الإدخال // أدخل عدد الصفوف printf("Rows count="); scanf("%d", &rowscount // تخصيص الذاكرة لجدول مصفوفة ثنائية الأبعاد = calloc(rowscount, sizeof(int*); ); )*rowscount); // تكرار الصفوف لـ (int i = 0; i

يمثل متغير الجدول مؤشرًا لمجموعة من المؤشرات من النوع int* . يمثل كل مؤشر جدول [i] في هذه المصفوفة مؤشرًا لمصفوفة فرعية من عناصر int، أي صفوف جدول فردية. ويمثل متغير الجدول في الواقع مؤشرًا لمجموعة من المؤشرات إلى صفوف الجدول.

لتخزين عدد العناصر في كل مصفوفة فرعية، يتم تعريف مؤشر صفوف من النوع int. يقوم في الواقع بتخزين عدد الأعمدة لكل صف من الجدول.

أولا، يتم إدخال عدد الصفوف في متغير عدد الصفوف. عدد الصفوف هو عدد المؤشرات في المصفوفة التي يشير إليها مؤشر الجدول. علاوة على ذلك، فإن عدد الصفوف هو عدد العناصر الموجودة في المصفوفة الديناميكية التي يشير إليها مؤشر الصفوف. لذلك، تحتاج أولاً إلى تخصيص ذاكرة لجميع هذه المصفوفات:

Table = calloc(rowscount, sizeof(int*)); rows = malloc(sizeof(int)*rowscount);

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

Scanf("%d", &rows[i]); table[i] = calloc(rows[i], sizeof(int));

ثم يتم إدخال العناصر الخاصة بكل سطر.

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

مجاني(الجدول[i]);

وبالإضافة إلى ذلك، يتم تحرير الذاكرة المخصصة لمؤشرات الجدول والصفوف:

مجاني (الجدول)؛ مجانا (الصفوف)؛

مخرجات وحدة التحكم للبرنامج:

عدد الصفوف=2 عدد الأعمدة لـ 1=3 الجدول=1 الجدول=2 الجدول=3 عدد الأعمدة لـ2=2 الجدول=4 الجدول=5 1 2 3 4 5

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

إنت إيفال = 1024؛

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

إنت ival2 = ival + 1;

ثم نصل إلى القيمة الموجودة في المتغير ival: نضيف إليها 1 ونقوم بتهيئة المتغير ival2 بهذه القيمة الجديدة، 1025. كيف يمكننا الوصول إلى العنوان الذي يوجد به المتغير؟
يحتوي C++ على نوع مؤشر مدمج يُستخدم لتخزين عناوين الكائنات. للإعلان عن مؤشر يحتوي على عنوان المتغير ival يجب أن نكتب:

إنت * باينت؛ // مؤشر إلى كائن من النوع int

هناك أيضًا عملية خاصة لأخذ عنوان يُشار إليه بالرمز &. والنتيجة هي عنوان الكائن. تقوم العبارة التالية بتعيين مؤشر نصف لتر لعنوان المتغير ival:

إنت * باينت؛ باينت = // باينت يحصل على قيمة العنوان ival

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

*باينت = *باينت + 1; // يزيد ضمنيًا ival

هذا التعبير يفعل بالضبط نفس الشيء كما

إيفال = إيفال + 1؛ // يزيد بشكل صريح ival

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

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

المشغل الجديد له شكلين. يخصص النموذج الأول الذاكرة لكائن واحد من نوع معين:

Int *pint = new int(1024);

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

Int *pia = new int;

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

// تحرير كائن واحد حذف باينت؛ // تحرير المصفوفة، حذف بيا؛

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

التمرين 2.3

اشرح الفرق بين الكائنات الأربعة:

(أ) إنت إيفال = 1024؛ (ب) int *pi = (ج) int *pi2 = new int(1024); (د) int *pi3 = int الجديد؛

تمرين 2.4

ماذا يفعل مقتطف الكود التالي؟ ما هي المغالطة المنطقية؟ (لاحظ أن عملية الفهرس() يتم تطبيقها بشكل صحيح على مؤشر pia. ويمكن العثور على شرح لهذه الحقيقة في القسم 3.9.2.)

Int *pi = new int(10); int *pia = new int;
بينما (* بي< 10) {
بيا[*pi] = *pi; *بي = *بي + 1;
) حذف بي؛ حذف بيا؛

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

1. تخصيص الذاكرة الديناميكية والثابتة (الثابتة). الاختلافات الرئيسية

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

1. ثابتة (مُثَبَّت) تخصيص الذاكرة. في هذه الحالة، يتم تخصيص الذاكرة مرة واحدة فقط في وقت الترجمة. حجم الذاكرة المخصصة ثابت ولا يتغير حتى نهاية تنفيذ البرنامج. مثال على هذا التحديد هو الإعلان عن مجموعة من 10 أعداد صحيحة:

كثافة العمليات م؛ // يتم تخصيص ذاكرة المصفوفة مرة واحدة، ويتم إصلاح حجم الذاكرة

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

2. مزايا وعيوب استخدام أساليب تخصيص الذاكرة الديناميكية والثابتة

يوفر تخصيص الذاكرة الديناميكي المزايا التالية مقارنة بتخصيص الذاكرة الثابتة:

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

مزايا طريقة تخصيص الذاكرة الثابتة:

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

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

3. كيفية تخصيص الذاكرة باستخدام العامل الجديد لمتغير واحد؟ الشكل العام.

الشكل العام لتخصيص الذاكرة لمتغير واحد باستخدام العامل الجديد هو كما يلي:

ptrName= نوع جديد؛
  • ptrName- اسم المتغير (المؤشر) الذي سيشير إلى الذاكرة المخصصة؛
  • يكتب– النوع المتغير . يتم تخصيص حجم الذاكرة بشكل كافٍ لوضع قيمة متغير من هذا النوع فيها. يكتب .
4. كيفية تحرير الذاكرة المخصصة لمتغير واحد باستخدام عامل الحذف؟ الشكل العام

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

في لغات البرمجة Java وC#، ليست هناك حاجة لتحرير الذاكرة بعد التخصيص. ويتم ذلك عن طريق "جامع القمامة".

الشكل العام لعامل الحذف لمتغير واحد هو:

حذف ptrName؛

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

5. أمثلة على تخصيص (جديد) وتحرير (حذف) الذاكرة لمؤشرات الأنواع الأساسية

توضح الأمثلة استخدام عوامل التشغيل الجديدة والحذف. تم تبسيط الأمثلة.

مثال 1.مؤشر لكتابة int. أبسط مثال

// تخصيص الذاكرة باستخدام العامل الجديدكثافة العمليات * ص؛ // مؤشر إلى كثافة العملياتع = كثافة العمليات الجديدة؛ // تخصيص الذاكرة للمؤشر*ع = 25؛ // اكتب القيم في الذاكرة // استخدام الذاكرة المخصصة للمؤشركثافة العمليات د؛ د = *ص؛ // د = 25 // تحرير الذاكرة المخصصة للمؤشر - إلزاميحذف ص؛

مثال 2.مؤشر لكتابة مزدوج

// تخصيص الذاكرة لمضاعفة المؤشرمزدوج * pd = NULL؛ pd = مزدوج جديد؛ // تخصيص الذاكرةإذا (pd!=NULL) ( *pd = 10.89; // كتابة القيممزدوج د = *pd; // د = 10.89 - يستخدم في البرنامج // ذاكرة متاحةحذف pd؛ )
6. ما هو "تسرب الذاكرة"؟

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

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

7. كيفية تخصيص الذاكرة باستخدام المشغل الجديد، واعتراض الموقف الحرج الذي قد لا يتم فيه تخصيص الذاكرة؟ استثناء Bad_alloc. مثال

عند استخدام المشغل الجديد، من الممكن ألا يتم تخصيص الذاكرة. قد لا يتم تخصيص الذاكرة في الحالات التالية:

  • إذا لم يكن هناك ذاكرة حرة؛
  • حجم الذاكرة الخالية أقل من الحجم المحدد في المشغل الجديد.

في هذه الحالة، يتم طرح استثناء bad_alloc. يمكن للبرنامج اعتراض هذا الموقف والتعامل معه وفقًا لذلك.

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

انت مين() ( // قم بتعريف مصفوفة من المؤشرات لتطفوتعويم * ptrArray؛ يحاول ( // حاول تخصيص الذاكرة لعشرة عناصر عائمة ptrArray = تعويم جديد؛ ) قبض على (bad_alloc ba) ( cout<< << endl; cout << ba.what() << endl; return -1; // وظيفة الخروج } // إذا كان كل شيء على ما يرام، فاستخدم المصفوفةل (int i = 0؛ i< 10; i++) ptrArray[i] = i * i + 3; int d = ptrArray; cout << d << endl; delete ptrArray; // الذاكرة الحرة المخصصة للمصفوفةالعودة 0؛ )
8. تخصيص الذاكرة لمتغير مع التهيئة المتزامنة. الشكل العام. مثال

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

بشكل عام، يبدو تخصيص الذاكرة لمتغير مع التهيئة المتزامنة

ptrName= نوع جديد( قيمة)
  • ptrName- اسم متغير المؤشر الذي تم تخصيص الذاكرة له؛
  • يكتب- النوع الذي يشير إليه المؤشر ptrName ;
  • قيمة– القيمة التي تم ضبطها لمنطقة الذاكرة المخصصة (قيمة المؤشر).

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

#تتضمن "stdafx.h" #تتضمن استخدام اسم للمحطة؛ انت مين() ( // تخصيص الذاكرة مع التهيئة المتزامنةتعويم * الجبهة الوطنية؛ كثافة العمليات * بي. شار * بي سي؛ يحاول ( // حاول تخصيص الذاكرة للمتغيرات مع التهيئة المتزامنةالجبهة الوطنية = تعويم جديد (3.88)؛ // *pF = 3.88 pI = new int (250); // *pI = 250 pC = new char("M"); // *pC = "M" ) Catch (bad_alloc ba) ( cout<< "الاستثناء: لم يتم تخصيص الذاكرة" << endl; cout << ba.what() << endl; return -1; // وظيفة الخروج } // إذا تم تخصيص الذاكرة، فاستخدم المؤشرات pF، pI، pCتعويم f = *pF; // و = 3.88 int i = *pI; // أنا = 250؛ شار ج؛ ج = *pC; // ج = "م" // طباعة القيم المبدئية cout<< "*pF = " << f<< endl; cout << "*pI = " << i << endl; cout << "*pC = " << c << endl; // الذاكرة الحرة المخصصة مسبقًا للمؤشراتحذف الجبهة الوطنية؛ حذف بي. حذف جهاز الكمبيوتر. العودة 0؛ )

لذا. النوع الثالث، الأكثر إثارة للاهتمام بالنسبة لنا في هذا الموضوع، هو النوع الديناميكي من الذاكرة.

كيف عملنا مع المصفوفات من قبل؟ كثافة العمليات كيف نعمل الآن؟ نحن نخصص قدر الحاجة:

#يشمل < stdio.h> #يشمل < stdlib.h> int main() (size_t size; // قم بإنشاء مؤشر إلى int // - في الأساس مصفوفة فارغة.كثافة العمليات * قائمة؛ سكانف( "%لو"، &مقاس)؛ // تخصيص الذاكرة لعناصر الحجم بالحجم int // وتشير "الصفيف الفارغ" لدينا الآن إلى هذه الذاكرة. list = (int *)malloc(size * sizeof(int)); من أجل (int i = 0؛ i< size; ++i) { scanf (" ٪د " < size; ++i) { printf (" ٪د ", *(list + i)); ) // لا تنس التنظيف بعد نفسك!مجانا(قائمة); ) // *

باطل * malloc(size_t size);

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

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

إذا لم تنجح - فارغة. كما أن errno سيكون مساويا لـ ENOMEM (سننظر إلى هذا المتغير الرائع لاحقا). أي أنه الأصح أن نكتب:

#يشمل < stdio.h> #يشمل < stdlib.h> int main () (size_t size; int *list; scanf ( "%لو"، &مقاس)؛ list = (int *)malloc(size * sizeof(int)); إذا (list == NULL) (goto error;) for (int i = 0 ; i< size; ++i) { scanf (" ٪د "، قائمة + أنا)؛ ) لـ (int i = 0؛ i< size; ++i) { printf (" ٪د ", *(list + i)); ) مجانا(قائمة); العودة 0 ; خطأ: إرجاع 1؛ ) // *

ليست هناك حاجة لمسح مؤشر NULL

#يشمل < stdlib.h> إنت الرئيسي () (مجاني (NULL)؛)

- في نفس الرنين، كل شيء سيكون على ما يرام (لن يتم فعل أي شيء)، ولكن في الحالات الأكثر غرابة، قد يؤدي ذلك إلى تعطل البرنامج.

بجوار malloc ومجاني في مانا يمكنك أيضًا رؤية:

    باطلة * calloc(size_t count, size_t size);

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

    باطلة * إعادة تخصيص (باطلة *ptr، حجم size_t)؛

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

    إذا كانت قيمة ptr NULL، فإن إعادة التخصيص تكون مماثلة لاستدعاء malloc.

    إذا كان الحجم صفرًا وptr ليس NULL، فسيتم تخصيص جزء من الذاكرة بالحد الأدنى للحجم ويتم تحرير الجزء الأصلي.

    باطلة * reallocf (void *ptr, size_t size);

    فكرة من FreeBSD API. مثل realloc، ولكن إذا فشل في إعادة التخصيص، فإنه يمسح المؤشر المقبول.

    باطلة * valloc(size_t size);

    مثل malloc، ولكن الذاكرة المخصصة تتم محاذاة الصفحة.

غالبًا ما يكون العمل باستخدام الذاكرة الديناميكية بمثابة عنق الزجاجة في العديد من الخوارزميات، ما لم يتم استخدام حيل خاصة.

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

0. هل نحتاج إلى عمل يدوي بالذاكرة؟

أولاً، دعونا نتحقق من مدى قدرة المُخصص الذكي على تسريع عمل الذاكرة.

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

عقدة الفئة (عامة: Node* next; ); // ... لـ (int i = 0; i< 10000000; i++) { Node* v = new Node(); }

عقدة الفئة (العقدة العامة التالية؛) // ... for (int l = 0; l< 10000000; l++) { var v = new Node(); }

على الرغم من طبيعة "الفراغ الكروي" للمثال، كان الفارق الزمني 10 مرات (62 مللي ثانية مقابل 650 مللي ثانية). بالإضافة إلى ذلك، تم الانتهاء من مثال C#، ووفقًا لقواعد الأخلاق الحميدة في C++، يجب حذف الكائنات المحددة، مما سيؤدي إلى زيادة الفجوة (حتى 2580 مللي ثانية).

1. تجمع الكائنات

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

نظرًا لأن الهدف هو الحد الأدنى من التدخل في البرنامج، فكل ما يمكن فعله هو إضافة مزيج BlockAlloc إلى فئة Node:
عقدة الفئة: BlockAlloc العامة

بادئ ذي بدء، نحتاج إلى مجموعة من الكتل الكبيرة (الصفحات)، التي نأخذها من نظام التشغيل OS أو C-runtime. يمكن تنظيمها فوق وظائف malloc والحرة، ولكن لزيادة الكفاءة (لتخطي المستوى الإضافي من التجريد)، نستخدم VirtualAlloc/VirtualFree. تقوم هذه الوظائف بتخصيص الذاكرة بمضاعفات كتل 4K وتحتفظ أيضًا بمساحة عنوان العملية بمضاعفات كتل 64K. ومن خلال تحديد خيارات الالتزام والحجز في الوقت نفسه، نقفز إلى مستوى آخر من التجريد، ونحجز مساحة العنوان، ونخصص صفحات الذاكرة في مكالمة واحدة.

فئة PagePool

inline size_t align(size_t x, size_t a) ( return ((x-1) | (a-1)) + 1; ) //#define align(x, a) ((((x)-1) | ( (أ)-1)) + 1) القالب فئة PagePool ( public: void* GetPage() ( void* page = VirtualAlloc(NULL, PageSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); pages.push_back(page); صفحة الإرجاع; ) ~PagePool() ( لـ (المتجه) ::iterator i = pages.begin(); i != pages.end(); ++i) ( VirtualFree(*i, 0, MEM_RELEASE); ) ) خاص: Vector الصفحات؛ );

ثم نقوم بتنظيم مجموعة من الكتل بحجم معين

فئة بلوك بول

نموذج فئة BlockPool: PagePool ( public: BlockPool() : head(NULL) ( BlockSize = align(sizeof(T), Alignment); count = PageSize / BlockSize; ) void* AllocBlock() ( // todo: lock(this) if (!head) FormatNewPage(); void* tmp = head = *(void**)head void FreeBlock(void* tmp) ( // todo: lock(this) *(void**)tmp = head; head = tmp; ) خاص: void* head; size_t BlockSize; void FormatNewPage();< count-1; i++) { void* next = (char*)tmp + BlockSize; *(void**)tmp = next; tmp = next; } *(void**)tmp = NULL; } };

تعليق // ما يجب القيام به: قفل (هذا) يتم وضع علامة على الأماكن التي تتطلب مزامنة بين مؤشرات الترابط (على سبيل المثال، استخدم EnterCriticalSection أو Boost::mutex).

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

من أجل (size_t i = 0؛ i< PageSize; i += BlockSize) FreeBlock((char*)tmp+i);

بعد ذلك، سيتم ترميز الصفحة، باستخدام مبدأ FIFO، "في الاتجاه المعاكس":

العديد من الكتل المطلوبة من التجمع على التوالي سيكون لها عناوين تنازلية. لكن المعالج لا يحب الرجوع للخلف، وهذا يكسر الجلب المسبق ( محدث: غير مناسب للمعالجات الحديثة). إذا قمت بالترميز في حلقة
for (size_t i = PageSize-(BlockSize-(PageSize%BlockSize)); i != 0; i -= BlockSize) FreeBlock...
ثم تعود دورة الترميز إلى العناوين.

الآن وبعد الانتهاء من الاستعدادات، يمكننا وصف فئة mixin.
نموذج فئة BlockAlloc (عام: عامل التشغيل static void* new(size_t s) ( if (s != sizeof(T)) ( return::operator new(s); ) returnpool.AllocBlock(); ) عامل التشغيل static void حذف(void * m, size_t s) ( if (s != sizeof(T)) ( ::operatorحذف(m); ) else if (m != NULL) (pool.FreeBlock(m); ) ) // todo: تنفيذ nothrow_t الحمولة الزائدة، وفقًا لبوريسكو" تعليق // http://habrahabr.ru/post/148657/#comment_5020297 // تجنب إخفاء الموضع الجديد الذي تحتاجه حاويات stl... عامل التشغيل static void* new(size_t, void * m) (return m;) // ... والتحذير بشأن حذف الموضع المفقود... حذف عامل التشغيل void(void*, void*) () خاص: static BlockPool حمام سباحة؛ ); نموذج BlockPool BlockAlloc ::حمام سباحة؛

سأشرح سبب الحاجة إلى الشيكات إذا (ق != sizeof(T))
متى يعملون؟ ثم، عندما يتم إنشاء/حذف فئة موروثة من القاعدة T.
سيستخدم الورثة الإجراء الجديد/الحذف المعتاد، ولكن يمكن أيضًا دمج BlockAlloc معهم. بهذه الطريقة، يمكننا بسهولة وأمان تحديد الفئات التي يجب أن تستخدم المجمعات دون خوف من تعطل أي شيء في البرنامج. يعمل الميراث المتعدد أيضًا بشكل رائع مع هذا المزيج.

مستعد. نحن نرث العقدة من BlockAlloc ونعيد تشغيل الاختبار.
وقت الاختبار الآن 120 مللي ثانية. 5 مرات أسرع. ولكن في C# لا يزال المُخصص أفضل. ربما لا تكون مجرد قائمة مرتبطة. (إذا قمنا باستدعاء الحذف مباشرة بعد جديد، وبالتالي لا نضيع الكثير من الذاكرة، عند وضع البيانات في ذاكرة التخزين المؤقت، نحصل على 62 مللي ثانية. غريب. تمامًا مثل .NET CLR، كما لو أنه يعيد المتغيرات المحلية المحررة على الفور إلى التجمع المقابل، دون انتظار GC)

2. الحاوية ومحتوياتها الملونة

هل تصادف غالبًا فصولًا تخزن الكثير من الكائنات الفرعية المختلفة، بحيث لا يكون عمر الأخير أطول من عمر الوالد؟

على سبيل المثال، يمكن أن تكون هذه فئة XmlDocument مملوءة بفئات العقدة والسمة، بالإضافة إلى سلاسل c (char*) مأخوذة من النص الموجود داخل العقد. أو قائمة بالملفات والأدلة الموجودة في مدير الملفات والتي يتم تحميلها مرة واحدة عند إعادة قراءة الدليل ولا تتغير مرة أخرى أبدًا.

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

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

فئة PointerBumpAllocator

نموذج class PointerBumpAllocator ( public: PointerBumpAllocator() : free(0) ( ) void* AllocBlock(size_t block) ( // todo: lock(this) block = align(block, Alignment); if (block > free) ( free = align (block, PageSize head = GetPage(free void* tmp = head = (char*)head + block; ::iterator i = pages.begin(); i != pages.end(); ++i) ( VirtualFree(*i, 0, MEM_RELEASE); ) ) خاص: void* GetPage(size_t size) ( void* page = VirtualAlloc(NULL, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); pages.push_back(page) صفحة العودة؛ الصفحات؛ رأس فارغ*؛ size_t مجاني؛ ); typedef PointerBumpAllocator<>DefaultAllocator;

أخيرًا، دعنا نصف مزيج ChildObject مع التحميل الزائد الجديد والحذف للوصول إلى مُخصص معين:

نموذج بناء ChildObject (عامل الفراغ الثابت* الجديد (size_t s, A& allocator) (مخصص الإرجاع.AllocBlock(s);) عامل التشغيل void* الثابت الجديد(size_t s, A* المخصص) (مخصص الإرجاع->AllocBlock(s);) ثابت عامل التشغيل void حذف(void*, size_t) ( ) // *1 عامل التشغيل void static حذف(void*, A*) ( ) عامل التشغيل static void حذف(void*, A&) ( ) خاص: عامل التشغيل static void* new(size_t s );

في هذه الحالة، بالإضافة إلى إضافة mixin إلى الفصل الفرعي، ستحتاج أيضًا إلى تصحيح جميع الاستدعاءات إلى جديد (أو استخدام نمط "المصنع"). سيكون بناء جملة المشغل الجديد كما يلي:

جديد (...معلمات للمشغل...) ChildObject (...معلمات للمنشئ...)

لتسهيل الأمر، قمت بتحديد عاملين جديدين يقبلان A& أو A*.
إذا تمت إضافة المُخصص إلى الفئة الأصل كعضو، فسيكون الخيار الأول أكثر ملاءمة:
العقدة = جديد(المخصص) XmlNode(nodename);
إذا أضيف المخصص كسلف (ميكسين)، فإن الثاني يكون أكثر ملاءمة:
العقدة = جديد(هذا) XmlNode(nodename);

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

إذا حدث استثناء في مُنشئ ChildObject (أو سليله)، فسيتم استدعاء الحذف بتوقيع يتوافق مع توقيع العامل الجديد المستخدم لإنشاء هذا الكائن (سيتم استبدال المعلمة الأولى size_t بـ void*).

يؤدي وضع المشغل الجديد في القسم الخاص إلى الحماية من الاتصال بجديد دون تحديد مخصص.

فيما يلي مثال كامل لاستخدام زوج Allocator-ChildObject:

مثال

فئة XmlDocument: DefaultAllocator العام ( public: ~XmlDocument() ( لـ (المتجه) ::iterator i =nodes.begin(); i!=nodes.end(); ++i) (حذف (*i);​)) ) void AddNode(char* content, char* name) ( char* c = (char*)AllocBlock(strlen(content)+1); strcpy(c, content ); char* n = (char*)AllocBlock(strlen(name)+1); strcpy(n, content); (عام: XmlNode(char* _content, char* _name) : content(_content), name(_name) ( ) خاص: char* content; char* name; ); خاص: ناقل العقد. );

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