تخصيص الذاكرة الثابتة والديناميكية. العمل مع الذاكرة الديناميكية في ج

04.09.2023

يدعم C++ ثلاثة أنواع رئيسية تسريح(وإلا "التوزيعات") ذاكرةاثنان منها نعرفهما بالفعل:

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

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

تخصيص الذاكرة الديناميكيةهو موضوع هذا الدرس.

تخصيص المتغير الديناميكي

يتميز كل من تخصيص الذاكرة الثابتة والتلقائية بخاصيتين شائعتين:

كيف يعمل تخصيص الذاكرة الديناميكية؟

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

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

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

تحرير الذاكرة

عندما تقوم بتخصيص متغير ديناميكيًا، يمكنك أيضًا تهيئته عبر التهيئة الموحدة (في C++ 11):

int *ptr1 = new int (7); // استخدم التهيئة المباشرة int *ptr2 = new int ( 8 ); // استخدم التهيئة الموحدة

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

// افترض أنه تم تخصيص ptr مسبقًا باستخدام عامل التشغيل newdelete ptr; // إرجاع الذاكرة المشار إليها بواسطة ptr إلى نظام التشغيل ptr = 0; // اجعل ptr مؤشرًا فارغًا (استخدم nullptr بدلاً من 0 في C++ 11)

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

على الرغم من أنه قد يبدو أننا نحذف عاملولكن هذا ليس صحيحا! لا يزال لمتغير المؤشر نفس النطاق كما كان من قبل ويمكن تعيين قيمة جديدة له مثل أي متغير آخر.

لاحظ أن حذف المؤشر الذي لا يشير إلى الذاكرة المخصصة ديناميكيًا يمكن أن يسبب مشاكل.

علامات معلقة

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

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

#يشمل int main() ( int *ptr = new int; *ptr = 8; // ضع القيمة في موقع الذاكرة المخصصة وحذف ptr; // أعد الذاكرة مرة أخرى إلى نظام التشغيل. أصبح ptr الآن مؤشرًا متدليًا std:: cout<< *ptr; // разыменование висячего указателя приведёт к неожиданным результатам delete ptr; // попытка освободить память снова приведёт к неожиданным результатам также return 0; }

#يشمل

إنت الرئيسي ()

int * ptr = int الجديد ; // تخصيص متغير عدد صحيح ديناميكيًا

* بتر = 8 ; // ضع القيمة في خلية الذاكرة المخصصة

حذف بي تي آر؛ // إعادة الذاكرة إلى نظام التشغيل. أصبح ptr الآن مؤشرًا متدليًا

الأمراض المنقولة جنسيا::كوت<< * ptr ; // سيؤدي إلغاء الإشارة إلى المؤشر المتدلي إلى نتائج غير متوقعة

حذف بي تي آر؛ // محاولة تحرير الذاكرة مرة أخرى ستؤدي إلى نتائج غير متوقعة أيضًا

العودة 0 ;

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

يمكن أن تؤدي عملية تحرير الذاكرة أيضًا إلى الإنشاء عديدعلامات معلقة. خذ بعين الاعتبار المثال التالي:

#يشمل int main() ( int *ptr = new int; // تخصيص متغير عدد صحيح ديناميكيًا int *otherPtr = ptr; يشير //otherPtr الآن إلى نفس الذاكرة المخصصة مثل ptr، حذف ptr؛ // إرجاع الذاكرة مرة أخرى إلى نظام التشغيل. أصبح ptr وotherPtr الآن مؤشرين متدليين ptr = 0; // ptr أصبح الآن nullptr // ومع ذلك، لا يزالotherPtr مؤشرًا متدليًا return 0;

#يشمل

إنت الرئيسي ()

int * ptr = int الجديد ; // تخصيص متغير عدد صحيح ديناميكيًا

int *otherPtr = ptr ; يشير //otherPtr الآن إلى نفس الذاكرة المخصصة مثل ptr

حذف بي تي آر؛ // إعادة الذاكرة إلى نظام التشغيل. أصبحت الآن ptr وotherPtr مؤشرات متدلية

بتر = 0 ; // ptr أصبح الآن nullptr

// ومع ذلك، لا يزالotherPtr مؤشرًا متدليًا!

العودة 0 ;

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

ثانيًا، عندما تقوم بحذف مؤشر، وإذا لم يخرج فورًا بعد الحذف، فيجب جعله خاليًا، أي. قم بتعيين القيمة 0 (أو في C++ 11). من خلال "الخروج خارج النطاق فور الحذف" نعني أنك قمت بحذف المؤشر الموجود في نهاية الكتلة التي تم الإعلان عنها.

القاعدة: اضبط المؤشرات المحذوفة على 0 (أو nullptr في C++11) ما لم تخرج عن النطاق فورًا بعد حذفها.

المشغل جديد

عند طلب الذاكرة من نظام التشغيل، في حالات نادرة قد لا تكون متاحة (أي قد لا تكون متاحة).

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

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

int *value = new (std::nothrow) int; // سيصبح مؤشر القيمة فارغًا إذا فشل التخصيص الديناميكي لمتغير عدد صحيح

في المثال أعلاه، إذا لم يُرجع new مؤشرًا بذاكرة مخصصة ديناميكيًا، فسيتم إرجاع مؤشر فارغ.

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

int *value = new (std::nothrow) int; // طلب تخصيص ذاكرة ديناميكية لقيمة عددية صحيحة if (!value) // التعامل مع الحالة عند إرجاع قيمة جديدة فارغة (أي لم يتم تخصيص الذاكرة) ( // التعامل مع هذه الحالة std::cout<< "Could not allocate memory"; }

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

مؤشرات فارغة وتخصيص الذاكرة الديناميكية

تعد المؤشرات الخالية (المؤشرات ذات القيمة 0 أو nullptr) مفيدة بشكل خاص أثناء عملية تخصيص الذاكرة الديناميكية. وكأن وجودهم يخبرنا: "لم يتم تخصيص ذاكرة لهذا المؤشر". وهذا بدوره يمكن استخدامه لإجراء تخصيص الذاكرة الشرطية:

// إذا لم يتم تخصيص ذاكرة ptr بعد، فقم بتخصيصها if (!ptr) ptr = new int;

إزالة المؤشر الفارغ ليس له أي تأثير. لذلك ليس من الضروري ما يلي:

إذا (ptr) حذف ptr؛

إذا (ptr)

حذف بي تي آر؛

بدلا من ذلك يمكنك ببساطة أن تكتب:

حذف بي تي آر؛

إذا لم تكن قيمة ptr فارغة، فسيتم حذف المتغير المخصص ديناميكيًا. إذا كانت قيمة المؤشر فارغة، فلن يحدث شيء.

تسرب الذاكرة

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

باطلة doSomething() ( int *ptr = new int; )

قبل أن نتعمق في التطوير الموجه للكائنات، نحتاج إلى إجراء استطراد قصير حول العمل مع الذاكرة في برنامج 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.
عندما لا تكون هناك حاجة إلى كائن ديناميكي، يجب علينا تحرير الذاكرة المخصصة له بشكل صريح. يتم ذلك باستخدام عامل الحذف، والذي، مثل الجديد، له نموذجان - لكائن واحد وللمصفوفة:

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

ماذا يحدث إذا نسينا تحرير الذاكرة المخصصة؟ سيتم إهدار الذاكرة، ولن يتم استخدامها، ولكن لا يمكن إعادتها إلى النظام لأنه ليس لدينا مؤشر إليها. وقد حصلت هذه الظاهرة على اسم خاص تسرب الذاكرة. في نهاية المطاف سوف يتعطل البرنامج بسبب نقص الذاكرة (إذا كان يعمل لفترة كافية، بطبيعة الحال). قد يكون من الصعب اكتشاف تسرب صغير، ولكن هناك أدوات مساعدة يمكن أن تساعدك على القيام بذلك.
ربما أثارت نظرة عامة مكثفة حول تخصيص الذاكرة الديناميكية واستخدام المؤشر أسئلة أكثر مما أجابت. سيغطي القسم 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;

) حذف بي؛ حذف بيا؛ذاكرة ثابتة

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

  • هناك نوعان من المتغيرات الثابتة:المتغيرات العالمية يتم تعريف المتغيراتخارج الوظيفة , ووصف الذي لا يحتوي على كلمة ثابتة . عادةالأوصاف يتم وضع المتغيرات العامة التي تتضمن الكلمة extern في ملفات الرأس (ملفات h). تعني الكلمة extern أنه تم الإعلان عن المتغير، ولكن لم يتم إنشاؤه في هذه المرحلة من البرنامج.المتغيرات العالمية، أي يتم وضع الأوصاف التي لا تحتوي على كلمة extern في ملفات التنفيذ (ملفات c أو ملفات cpp). مثال: تم الإعلان عن المتغير العامmaxind مرتين:
    • في ملف h باستخدام السطر

      كثافة العمليات الخارجية؛

      يُبلغ هذا الإعلان عن وجود مثل هذا المتغير، لكنه لا يُنشئ هذا المتغير!
    • في ملف CPP باستخدام السطر

      كثافة العمليات = 1000؛

      هذا هو الوصف يخلقالمتغيرmaxind ويعين له القيمة الأولية 1000 . لاحظ أن معيار اللغة لا يتطلب التعيين الإلزامي للقيم الأولية للمتغيرات العالمية، ولكن مع ذلك، من الأفضل القيام بذلك دائمًا، وإلا فإن المتغير سيحتوي على قيمة غير متوقعة (قمامة، كما يقول المبرمجون). إنه أسلوب جيد لتهيئة جميع المتغيرات العامة عند تعريفها.
    تسمى المتغيرات العامة بهذا الاسم لأنها متوفرة في أي مكان في البرنامج بجميع ملفاته. لذلك، يجب أن تكون أسماء المتغيرات العامة طويلة بما يكفي لتجنب الأسماء غير المقصودة لمتغيرين مختلفين. على سبيل المثال، الأسماء x أو n غير مناسبة لمتغير عام؛
  • المتغيرات الثابتة- هذه هي المتغيرات التي يحتوي وصفها على كلمة static . عادة، يتم وصف المتغيرات الثابتة يتم تعريف المتغيرات. تشبه هذه المتغيرات الثابتة في كل شيء المتغيرات العامة، مع استثناء واحد: يقتصر نطاق المتغير الثابت على الملف الوحيد الذي تم تعريفه فيه - وعلاوة على ذلك، لا يمكن استخدامه إلا بعد الإعلان عنه، أي. أدناه في النص. لهذا السبب، عادةً ما يتم وضع إعلانات المتغيرات الثابتة في بداية الملف. على عكس المتغيرات العالمية، المتغيرات الثابتة أبداًلم يتم وصفها في ملفات h (تتعارض المعدلات الخارجية والثابتة مع بعضها البعض). نصيحة: استخدم المتغيرات الثابتة إذا كنت تريد أن تكون متاحة فقط للوظائف الموضحة بالداخل نفس الملف. إذا أمكن، لا تستخدم المتغيرات العامة في مثل هذه المواقف، فهذا سوف يتجنب تعارض الأسماء عند تنفيذ مشاريع كبيرة تتكون من مئات الملفات.
    • يمكن أيضًا وصف المتغير الثابت داخل دالة، على الرغم من أنه لا أحد يفعل ذلك عادةً. لا يوجد المتغير على المكدس، ولكن في الذاكرة الثابتة، أي. لا يمكن استخدامه في العودية، ويتم تخزين قيمته بين المدخلات المختلفة للوظيفة. يقتصر نطاق هذا المتغير على نص الوظيفة التي تم تعريفه فيها. وإلا فإنه يشبه متغير ثابت أو عالمي. لاحظ أن الكلمة الأساسية الثابتة في لغة C تُستخدم لغرضين مختلفين:
      • كإشارة إلى نوع الذاكرة: المتغير موجود في الذاكرة الثابتة، وليس على المكدس؛
      • كوسيلة لتقييد نطاق المتغير بملف واحد (في حالة وصف متغير خارج الوظيفة).
  • يمكن أن تظهر الكلمة الثابتة أيضًا في رأس الوظيفة. ومع ذلك، يتم استخدامه فقط لتقييد نطاق اسم الدالة إلى ملف واحد. مثال:

    ثابت int gcd(int x, int y); // النموذج الأولي للوظيفة. . . ثابت int gcd(int x, int y) (// التنفيذ...)

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

المكدس أو الذاكرة المحلية

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

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

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

الذاكرة الديناميكية أو الكومة

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

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

عنوان محتويات الذاكرة

كود البرنامج والبيانات،

مقاومة للعبث

...

المتغيرات الثابتة

البرامج

الذاكرة الديناميكية

الأعلى. العنوان (2 32 -4)

كومة

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

في لغة C، يتم استخدام وظائف malloc القياسية والحرة للحصول على الذاكرة الديناميكية وتحريرها؛ وترد أوصاف نماذجها الأولية في ملف الرأس القياسي "stdlib.h". (الاسم malloc هو اختصار ل تخصيص الذاكرة- "التقاط الذاكرة".) تبدو النماذج الأولية لهذه الوظائف كما يلي:

باطلة *malloc(size_t n); // التقاط منطقة ذاكرة // بحجم n بايت void free(void *p); // حرر قسمًا من // الذاكرة بالعنوان p

هنا n هو حجم المنطقة الملتقطة بالبايت، وsize_t هو اسم أحد أنواع الأعداد الصحيحة التي تحدد الحد الأقصى لحجم المنطقة الملتقطة. يتم تحديد النوع size_t في ملف الرأس القياسي "stdlib.h" باستخدام عامل تشغيل typedef (انظر ص 117). وهذا يضمن استقلال نص برنامج C عن البنية المستخدمة. في بنية 32 بت، يتم تعريف النوع size_t كعدد صحيح غير موقّع:

typedef unsigned int size_t;

تقوم الدالة malloc بإرجاع عنوان موقع الذاكرة المخصصة، أو صفر إذا فشلت (عندما لا يكون هناك موقع مجاني كبير بما فيه الكفاية). تعمل الوظيفة المجانية على تحرير جزء من الذاكرة بعنوان معين. لتعيين العنوان، يتم استخدام مؤشر من النوع العام void*. بعد استدعاء الدالة malloc، يجب توجيهها إلى مؤشر إلى نوع معين باستخدام عملية تحويل النوع، راجع القسم 3.4.11. على سبيل المثال، المثال التالي يلتقط قطعة من ذاكرة الكومة بحجم 4000 بايت ويعين عنوانها لمؤشر لمصفوفة مكونة من 1000 عدد صحيح:

كثافة العمليات * أ؛ // مؤشر إلى مجموعة من الأعداد الصحيحة. . . a = (int *) malloc(1000 * sizeof(int));

التعبير في وسيطة الدالة malloc هو 4000 لأن حجم العدد الصحيح sizeof(int) هو أربعة بايت. لتحويل مؤشر، يتم استخدام عملية الإرسال (int *) من مؤشر نوع عام إلى مؤشر إلى عدد صحيح.

مثال: طباعة الأعداد الأولية n الأولى

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

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

#يشمل #يشمل #يشمل int main() ( int n; // العدد المطلوب من الأعداد الأولية int k; // العدد الحالي للأعداد الأولية التي تم العثور عليها int *a; // مؤشر إلى مصفوفة من الأعداد الأولية التي تم العثور عليها int p; // الرقم التالي للتحقق منه int r; / / الجذر التربيعي للجزء الصحيح من p int i; // فهرس المقسوم الأولي bool prime;<= 0) // Некорректное значение =>العودة 1؛ // الخروج برمز خطأ // احصل على الذاكرة لمجموعة من الأعداد الأولية a = (int *) malloc(n * sizeof(int));< n) { // Проверяем число p на простоту r = (int)(// Целая часть корня sqrt((double) p) + 0.001); i = 0; prime = true; while (i < k && a[i] <= r) { if (p % a[i] == 0) { // p делится на a[i] prime = false; // =>أ = 2؛ ك = 1؛ // أضف اثنين إلى المصفوفة printf("%d ", a); // واطبعها p = 3;

بينما (ك

p ليس أوليًا، ينكسر؛ // الخروج من الحلقة ) ++i; // إلى المقسوم الأولي التالي ) if (prime) ( // إذا وجدنا عددًا أوليًا، a[k] = p; // ثم أضفه إلى المصفوفة ++k; // زيادة عدد الأعداد الأولية printf( "%d"، p ); // اطبع عددًا أوليًا if (k % 5 == 0) ( // انتقل إلى سطر جديد printf("\n"); // بعد كل خمسة أرقام ) ) p += 2؛ // إلى الرقم الفردي التالي ) if (k % 5 != 0) ( printf("\n"); // ترجمة السطر ) // تحرير الذاكرة الديناميكية free(a);

العودة 0؛ )

مثال لكيفية عمل هذا البرنامج:

أدخل عدد الأعداد الأولية: 50 2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 101 103 107 109 113 127 131 137 139 149 1 51 157 163 167 173 179 181 191 193 197 199 211 223 227 229

C++ عوامل التشغيل الجديدة والحذف

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

دع T يكون أحد أنواع لغة C أو C++، p هو مؤشر لكائن من النوع T. ثم يتم استخدام العامل الجديد للاستيلاء على ذاكرة عنصر واحد من النوع T:

تي * ع؛ ع = جديد تي؛

على سبيل المثال، لالتقاط ثمانية بايت لعدد حقيقي من النوع المزدوج، يتم استخدام جزء

مزدوج *ص؛ ع = مزدوج جديد؛

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

العودة 0؛ )

8. تخصيص الذاكرة لمتغير مع التهيئة المتزامنة. الشكل العام. مثال

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

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

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

ج = *pC; // ج = "م"

// طباعة القيم المبدئية

cout

// الذاكرة الحرة المخصصة مسبقًا للمؤشرات حذف الجبهة الوطنية؛حذف بي. حذف جهاز الكمبيوتر.العودة 0؛ ) في لغة C++، كما هو الحال في العديد من اللغات الأخرى، يمكن تخصيص الذاكرة بشكل ثابت (يتم تخصيص الذاكرة قبل بدء تنفيذ البرنامج وتحريرها بعد اكتمال البرنامج) أو ديناميكيًا (يتم تخصيص الذاكرة وتحريرها أثناء تنفيذ البرنامج).يتم إجراء تخصيص الذاكرة الثابتة لجميع المتغيرات العامة والمحلية التي تحتوي على إعلانات صريحة في البرنامج (دون استخدام المؤشرات). في هذه الحالة، يتم تحديد آلية تخصيص الذاكرة من خلال موقع وصف المتغير في البرنامج ومحدد فئة الذاكرة في الوصف. يحدد نوع المتغير حجم مساحة الذاكرة المخصصة، لكن آلية تخصيص الذاكرة لا تعتمد على النوع. هناك آليتان رئيسيتان لتخصيص الذاكرة الثابتة. · يتم تخصيص الذاكرة لكل من المتغيرات العامة والثابتة (المعلن عنها بواسطة المحدد الثابت) قبل بدء تنفيذ البرنامج وفقًا لوصف النوع. من بداية تنفيذ البرنامج إلى نهايته، ترتبط هذه المتغيرات بمساحة الذاكرة المخصصة لها. وبالتالي، فهي تتمتع بعمر عالمي، لكن نطاقها مختلف.للتمييز بين المكدس كنوع بيانات مجردة). يعتمد حجم المكدس على بيئة التطوير، على سبيل المثال، في MS Visual C++، يتم تخصيص 1 ميغا بايت للمكدس افتراضيًا (يمكن تخصيص هذه القيمة). أثناء تنفيذ البرنامج، عند الدخول إلى كتلة معينة، يتم تخصيص الذاكرة على المكدس للمتغيرات المترجمة في الكتلة (وفقًا لوصف نوعها)، عند الخروج من الكتلة، يتم تحرير هذه الذاكرة. يتم تنفيذ هذه العمليات تلقائيًا، ولهذا السبب غالبًا ما يتم استدعاء المتغيرات المحلية في لغة C++ تلقائي.

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

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



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

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

الشكل 2.1 - مخطط توزيع الذاكرة

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



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

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

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