اشتباهات رایج در C: آزاد کردن حافظه‌ای که نگرفتیم!

دز این مطلب می‌خوام بپردازم به یه ایرادی که توی سی خیلی اوقات برنامه‌نویس‌ها بهش مشکل می‌خورن و منم طبق تجربه می‌دونم چیه مشکل و توی سرچ سخت می‌شه پیدا کرد پس چه بهتر که با شما هم به اشتراک بذارم.

فرض این مطلب اینه که شما با پوینتر‌ها آشنایی مختصری دارید و با malloc یا new و free یا delete هم کار کردید. از عنوان مطلب هم مشخصه که به اینا احتیاج داریم در مجموع.




خب مشکل ساده‌س (البته سخت پیدا می شه)، مشکل اینه که یه بلوک حافظه رو که با malloc دریافت کردیم، free می‌کنیم ولی برنامه runtime error می‌ده و خاتمه پیدا می‌کنه.

اما این مشکل چرا پیدا میشه و چطوری رفعش کنیم؟

در یک جمله بخوام بگم، همون چیزی هست که توی عنوان مطلب اشاره شد، اگه پوینتر به حافظه‌ای رو به عنوان آرگومان به free بدید که قبلا به عنوان malloc برنگشته، حالا یا پوینتر به یه خونه که اصلا مال ما نیست، یا پوینتر به یه خونه‌ای غیر از اول بلوک حافظه که توسط malloc ریترن می‌شه، تابع free نمی‌تونه این دستور رو هندل کنه و باعث ران‌تایم ارور می‌شه.

اما بیاید یکم عمیق‌تر جزئیات این اتفاق رو بررسی کنیم.

وقتی که شما malloc رو صدا می‌زنید (ما صدا می‌زنیم، همه صدا می‌زنن، اکثر سیستم‌عامل‌ها با سی نوشته شدن و صدا می‌زنن، پایتون هم اگر توسط C پیاده‌شده باشه که ۹۹درصد شده، اونم malloc رو صدا می‌زنه:) ) یه پوینتر به اولین خونه‌ی یک بلوک حافظه برای شما بر می‌گرده. همچنین جدولی توی پس‌زمینه شکل می‌گیره که آدرس خونه اول حافظه و در مقابلش مقداری که allocate شده رو می‌نویسه که بعدا موقع free کردن استفاده کنه.

موقع free کردن همونطور که می‌دونید تنها و تنها چیزی که لازمه پاس بدیم، آدرس اولین خونه‌ی حافظه هست و حجمش (تعداد بایت) رو لازم نیست ذکر کنیم. چرا؟ چون خود سی توی پس‌زمینه این رو یادداشت کرده. حالا میاد و این آدرس حافظه رو توی جدول سرچ می‌کنه و می‌بینه که چند بایت بوده و به سیستم‌عامل میگه این رو فری کن. (شاید هم یکم نادقیق دارم می‌گم و سهم سیستم‌عامل بیشتر یا کمتره ولی این جدول وجود داره)

حالا چرا آدرس همه‌ٔ خونه‌های حافظه رو توی حدول نمی‌نویسه؟ چون اولا جدول خیلی بزرگ می‌شه و باید نصف رم رو بدیم به این جدوله (منطقیه دیگه، هر خونه آدرس، خودش یه value خواهد داشت، و خونه معادل هم توی اون جدول) پس این کار عبثیه و امکان‌پذیر نیست، تازع عملیات free کردن هم خیلی پرهزینه میشه.

اما اگر ادرسی که ما free رو باهاش صدا می‌کنیم توی جدول نباشه، برنامه ارور میده چون نمی‌دونه چی رو باید free کنه.




این مشکل هم به ۳ دلیل اتفاق می‌افته:

  1. آزاد کردن حافظه‌ای که اصلا allocate نکردیم و مال ما نیست.
  2. آزاد کردن مجدد حافظه‌ای که قبلا free کردیم و باز هم مال ما نیست.
  3. آزاد کردن پوینتر به یه خونه‌ای غیر از خونه اول حافظه مثلا خونه دوم یا سوم.

از مورد سوم، نمونه کد خیلی ساده هم بیبنیم:

نمونه کد مشکل‌دار
نمونه کد مشکل‌دار

همونطور که می‌بینید خود linter من که از clang استفاده می‌کنه هم بهم یه وارنینگ داده.

عبارت دقیق وارنینگش اینه:

Argument to free() is offest by 4 bytes from the start of memory allocated by malloc()

و می‌تونید سوال مربوط بهش رو اینجا بخونید:

https://stackoverflow.com/questions/4744774/can-i-free-by-referencing-an-offset-pointer


و موقع اجرا اتفاقی که می‌افته:

اروری که مشاهده میکنیم
اروری که مشاهده میکنیم



در نهایت به عنوان راه حلی که بخوام برای حل تمام سوالات پوینتری شما ارائه بدم، می‌تونم پیشنهاد کنم اگه درک کمی از پوینتر دارید از یه زبان سطح بالاتر استفاده کنید یا بیشتر درموردش مطالعه کنید.

برای حل همین یه مشکل، می‌تونیم پوینتر رو جلو و عقب نبرید و اگه نیاز داشتید اندازه ش عوض بشه یا خونه‌های اولش غیر قابل دسترس بشه، یه حافظه جدید allocate کنید و هر مقدار از محتوای قبلی رو که می‌خواید بریزید توی حافظه جدید. یا اینکه پوینتر به اول حافظه رو برای زمان free کردن نگه دارید.


نکته: توی متن اشاره نشد ولی لازمه اینجا بهش اشاره کنم، free کردن یه پوینتر با مقدار NULL بدون مشکل است. در واقع تنها پوینتر‌هایی که می‌توانید free کنید، خروجی‌های malloc و calloc و realloc هستند و NULL.


شاد باشید و خندون