سؤال

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

هل يمكن لأي شخص أن يريني (مع أمثلة التعليمات البرمجية) مثالاً على الوقت الذي يتعين عليك فيه القيام ببعض "إدارة الذاكرة"؟

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

المحلول

هناك مكانان حيث يمكن وضع المتغيرات في الذاكرة.عندما تقوم بإنشاء متغير مثل هذا:

int  a;
char c;
char d[16];

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

ستستخدم العديد من الأمثلة المبتدئة متغيرات المكدس فقط.

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

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

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

int size;
// ...
// Set size to some value, based on information available at run-time. Then:
// ...
char *p = (char *)malloc(size);

(لاحظ أن المتغيرات الموجودة في الكومة لا يتم التعامل معها بشكل مباشر، ولكن عبر المؤشرات)

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

free(p);

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

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

(لقد قمت بتغطية العديد من التفاصيل لإعطاء إجابة أبسط، ولكن نأمل أن تكون أكثر توازناً)

نصائح أخرى

هنا مثال.لنفترض أن لديك وظيفة strdup() التي تكرر سلسلة:

char *strdup(char *src)
{
    char * dest;
    dest = malloc(strlen(src) + 1);
    if (dest == NULL)
        abort();
    strcpy(dest, src);
    return dest;
}

وأنت تسميها هكذا:

main()
{
    char *s;
    s = strdup("hello");
    printf("%s\n", s);
    s = strdup("world");
    printf("%s\n", s);
}

يمكنك أن ترى أن البرنامج يعمل، ولكنك قمت بتخصيص الذاكرة (عبر malloc) دون تحريرها.لقد فقدت المؤشر الخاص بك إلى كتلة الذاكرة الأولى عندما اتصلت بـ strdup للمرة الثانية.

هذا ليس بالأمر الكبير بالنسبة لهذه الكمية الصغيرة من الذاكرة، ولكن ضع في اعتبارك الحالة:

for (i = 0; i < 1000000000; ++i)  /* billion times */
    s = strdup("hello world");    /* 11 bytes */

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

لإصلاح ذلك، تحتاج إلى الاتصال بـ free()‎ لكل ما يتم الحصول عليه باستخدام malloc() بعد الانتهاء من استخدامه:

s = strdup("hello");
free(s);  /* now not leaking memory! */
s = strdup("world");
...

نأمل أن يساعد هذا المثال!

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

 char *string = malloc(stringlength); // stringlength is the number of bytes to allocate

 // Do something with the string...

 free(string); // Free the allocated memory

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

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

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

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

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

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

مثال:

int main() {
    char* myString = (char*)malloc(5*sizeof(char));
    myString = "abcd";
}

في هذه المرحلة، قمت بتخصيص 5 بايت لـ myString وملأتها بـ "abcd\0" (السلاسل تنتهي بـ null - \0).إذا كان تخصيص السلسلة الخاص بك

myString = "abcde";

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

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

قد ترغب أيضًا في استخدام تخصيص الذاكرة الديناميكية عندما تحتاج إلى تحديد مصفوفة ضخمة، على سبيل المثال int[10000].لا يمكنك وضعها في كومة لأنه حينها، حسنًا...سوف تحصل على تجاوز سعة المكدس.

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

(أنا أكتب لأنني أشعر أن الإجابات حتى الآن ليست في محلها تمامًا).

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

typedef struct listelem { struct listelem *next; void *data;} listelem;

listelem * create(void * data)
{
   listelem *p = calloc(1, sizeof(listelem));
   if(p) p->data = data;
   return p;
}

listelem * delete(listelem * p)
{
   listelem next = p->next;
   free(p);
   return next;
}

void deleteall(listelem * p)
{
  while(p) p = delete(p);
}

void foreach(listelem * p, void (*fun)(void *data) )
{
  for( ; p != NULL; p = p->next) fun(p->data);
}

listelem * merge(listelem *p, listelem *q)
{
  while(p != NULL && p->next != NULL) p = p->next;
  if(p) {
    p->next = q;
    return p;
  } else
    return q;
}

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

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

احصل على مصحح أخطاء جيد... حظ سعيد!

@يورو ميشيلي

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

@تيد بيرسيفال:
...لست بحاجة إلى إرسال القيمة المرجعة لـ malloc().

انت على حق، بالطبع.أعتقد أن هذا كان صحيحًا دائمًا، على الرغم من أنني لا أملك نسخة منه ك&ر للتأكد.

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

هذا محتمل بشكل خاص إذا كان المترجم الخاص بك يفهم التعليقات ذات النمط C++.

نعم...لقد أمسكت بي هناك.أقضي وقتًا أطول بكثير في لغة C++ مقارنة بـ C.شكرا لملاحظة ذلك.

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

أ.تريد أن يظل المتغير أطول من الوظائف، ولا تريد أن يكون لديك متغير عام.السابق:

struct pair{
   int val;
   struct pair *next;
}

struct pair* new_pair(int val){
   struct pair* np = malloc(sizeof(struct pair));
   np->val = val;
   np->next = NULL;
   return np;
}

ب.تريد أن يكون لديك ذاكرة مخصصة ديناميكيًا.المثال الأكثر شيوعًا هو المصفوفة بدون طول ثابت:

int *my_special_array;
my_special_array = malloc(sizeof(int) * number_of_element);
for(i=0; i

ج.تريد أن تفعل شيئًا قذرًا حقًا.على سبيل المثال، أرغب في أن تمثل البنية العديد من أنواع البيانات ولا أحب الاتحاد (يبدو الاتحاد فوضويًا جدًا):

struct data{ int data_type; long data_in_mem; }; struct animal{/*something*/}; struct person{/*some other thing*/}; struct animal* read_animal(); struct person* read_person(); /*In main*/ struct data sample; sampe.data_type = input_type; switch(input_type){ case DATA_PERSON: sample.data_in_mem = read_person(); break; case DATA_ANIMAL: sample.data_in_mem = read_animal(); default: printf("Oh hoh! I warn you, that again and I will seg fault your OS"); }

انظر، القيمة الطويلة كافية للاحتفاظ بأي شيء.فقط تذكر أن تحرره، وإلا فسوف تندم.هذه من بين الحيل المفضلة لدي لقضاء وقت ممتع في C :D.

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

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

class MyClass
{
   SomeOtherClass *myObject;

   public MyClass()
   {
      //The object is created when the class is constructed
      myObject = (SomeOtherClass*)malloc(sizeof(myObject));
   }

   public ~MyClass()
   {
      //The class is destructed
      //If you don't free the object here, you leak memory
      free(myObject);
   }

   public void SomeMemberFunction()
   {
      //Some use of the object
      myObject->SomeOperation();
   }


};

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

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

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