درک کارکرد حافظه در نرم افزار — به زبان ساده
زمانی که در زبانهایی مانند C یا ++C برنامهنویسی میکنید، میتوانید با حافظه در سطوحی بسیار پایینتر تعامل داشته باشید. در برخی موارد این مسئله مشکلات زیادی مانند segfaults-ها ایجاد میکند که در زبانهای مدرنتر مشاهده نمیشوند. این خطاها آزاردهنده هستند و میتوانند موجب بروز دردسر زیادی شوند و در اغلب موارد نشانگر استفاده از حافظهای هستند که مجاز نبوده است. یکی از رایجترین مسائل، دسترسی به حافظهای است که قبلاً آزاد شده است. این حافظه یا به وسیله free آزاد شده است یا حافظهای است که برنامه شما به طور خودکار (برای نمونه از یک پشته) آزاد کرده است. درک کارکرد حافظه آسان است و قطعاً باعث میشود که برنامهتان را به روشی بهتر و هوشمندانهتر مدیریت کنید.
حافظه چگونه تقسیم میشود؟
حافظه به چندین قطعه تقسیم میشود که دو مورد از مهمترین آنها (دست کم در این نوشته) پشته و هیپ هستند. پشته یک محل درج ترتیبی است در حالی که هیپ کاملاً تصادفی است و شما میتوانید حافظه را از هر جایی که دوست دارید تخصیص دهید.
حافظه پشته مجموعه روشها و عملیاتی برای کار خود دارد و جایی است که برخی از اطلاعات رجیسترهای پردازنده و همچنین اطلاعاتی در رابطه با برنامه شما ذخیره میشوند، تابعها فراخوانی میشوند و تابعهایی که ایجاد کردهاید و برخی از اطلاعات دیگر نیز در آن قرار دارند. این حافظه از سوی برنامه مدیریت میشود و نه از سوی توسعهدهنده.
هیپ در اغلب موارد برای تخصیص مقادیر بالایی از حافظه که تصور میشود تا هر زمان توسعهدهنده بخواهد باید دوام بیاورند استفاده میشود. به بیان دیگر کنترل استفاده از حافظه روی هیپ در اختیار توسعهدهنده است. هنگام ساخت برنامههای پیچیده در اغلب موارد لازم است که بخشهای بزرگی از حافظه تخصیص یابند و این همان جایی است که از هیپ استفاده میکنیم. این نوع حافظه به نام «حافظه دینامیک» (Dynamic Memory) نیز نامیده میشود.
ما هر بار که از malloc برای تخصیص حافظه برای چیزی استفاده میکنیم آن شیء را در هیپ قرار میدهیم. هر نوع فراخوانی دیگری مانند ;int i باعث ورود شیء به حافظه پشته میشود. دانستن این نکته اهمیت بالایی دارد، زیرا میتوان به سادگی خطاها را در برنامه یافت و جستجوی خطای Segfault را بهینهتر ساخت.
درک پشته
با این که ما پشته را نمیشناسیم؛ اما برنامه ما به طور مداوم حافظه پشته را برای کارکرد خود تخصیص میدهد. هر متغیر محلی و هر تابعی که فراخوانی میشود در آن جا قرار دارد. بدین ترتیب با پشته میتوان کارهای زیادی انجام داد که اغلب آنها کارهایی هستند که نمیخواهیم رخ بدهند، مانند سرریز بافر، دسترسی نادرست به حافظه و غیره.
پشته واقعاً چگونه کار میکند؟
پشته یک ساختمان داده LIFO (ورودی آخر- خروجی اول) است که آن را میتوان مانند یک جعبه کاملاً پر از کتاب تصور کرد. آخرین کتابی که در این جعبه قرار گیرد، نخستین کتابی است که باید از آن برداشت. برنامه با استفاده از این ساختمان داده میتواند به سهولت همه عملیات خود و حیطههای تعریف را با استفاده از دو عملیات ساده push و pop مدیریت کند. این دو عملیات دقیقاً کارکردی متضاد هم دارند، چون push مقداری را در بالای پشته درج میکند؛ در حالی که pop مقداری را از بخش فوقانی پشته برمیدارد.
برای این که سابقه وضعیت کنونی مکانهای حافظه نگهداری شود، یک رجیستر خاص پردازنده به نام «اشارهگر پشته» (Stack Pointer) وجود دارد. هر بار که لازم باشد چیزی مانند یک متغیر یا آدرس بازگشتی از یک تابع) را ذخیره کنید، این اشارهگر push میکند و اشارهگر پشته را به بالا میراند. هر بار که از یک تابع خارج میشوید این اشارهگر همه چیز را از اشارهگر پشته pop میکند تا این که به آدرس بازگشتی ذخیره شده تابع برسد. فرایند کار به همین سادگی است.
برای این که دانستههای خود را بیازمایید از مثال زیر استفاده کنید. شما باید باگ کد زیر را پیدا کنید:
#include <stdio.h> int * createArray(int size) { int arr[size]; return arr; } int main() { int s = 10; int * arr = createArray(s); for (int i = 0; i < s; i++) arr[i] = i; return 0; }
اگر کد فوق را اجرا کنید، برنامه دچار خطا میشود. اما دلیل این مسئله چیست؟ همه چیز درست به نظر میرسد به جز پشته!
زمانی که تابع createArray فراخوانی میشود، پشته آدرس بازگشتی را ذخیره میکند و یک arr در حافظه پشته ایجاد میکند و آن را بازگشت میدهد؛ اما از آنجا که از malloc استفاده نکردهایم در حافظه پشته ذخیره شده است. پس از این اشارهگر را بازگشت دادیم، به این جهت که هیچ کنترلی روی عملیات پشته نداریم، برنامه اطلاعات را از پشته pop میکند و در صورت نیاز از آن استفاده میکند. زمانی که تلاش میکنیم آرایه را پس از آن که از تابع بازگشت مورد استفاده قرار دهیم، حافظه را تخریب کردهایم و باعث بروز خطا در برنامه شدهایم.
درک هیپ
هیپ معکوس پشته است و هنگامی که میخواهیم چیزی برای مدتی مستقل از تابعها و حیطه تعریف آنها وجود داشته باشد از هیپ استفاده میکنیم. برای استفاده از این نوع حافظه در زبان C از stdlib استفاده میکنیم که دو تابع مناسب به این منظور به نامهای malloc و free دارد.
Malloc که اختصاری برای عبارت «تخصیص حافظه» (memory allocation) است، از سیستم میخواهد که مقداری از حافظه را به آن اختصاص دهد و یک اشارهگر برای آدرس آغاز این حافظه بازگشت میدهد. Free به سیستم اعلام میکند که حافظه خواسته شده دیگر مورد نیاز نیست و میتواند برای وظایف دیگر مورد استفاده قرار گیرد. این ساختار تا زمانی که اشتباهی نکنید، ساده به نظر میرسد.
از آنجا که سیستم آن چه را که توسعهدهندگان میخواهند بازنویسی میکند؛ این به ما انسانها مربوط است که دو تابع فوق را مدیریت کنیم. بدین ترتیب مسیری برای خطای انسانی به صورت نشت حافظه باز میشود.
نشت حافظه (Memory Leak) به حافظهای گفته میشود که از سوی کاربر درخواست شده و هنگامی که برنامه پایان یافته یا اشارهگرها به مکان آن مفقود شدهاند، آزاد نشده است و بدین ترتیب برنامه از حافظهای به مراتب بیشتر از مقداری که انتظار میرفت استفاده میکند. برای جلوگیری از این وضعیت، هر بار که دیگر به عنصر هیپ تخصیص یافته نیاز نداشتیم، باید آن را آزاد کنیم.
#include <stdlib.h> int main() { //bad int * arr= malloc(sizeof(int) * 10); //array of size 10 arr = malloc(sizeof(int) * 10); //allocates a new array - old one is lost! //good arr = malloc(sizeof(int) * 10); free(arr); //we free the first array, and then we change the value arr = malloc(sizeof(int) * 10); free(arr); return 0; }
در کد فوق، در بخش استفاده بد، حافظه تخصیص یافته هرگز آزاد نمیشود. این امر موجب میشود که 20 × 4 بایت (اندازه int برابر با 64 بیت) یعنی 80 بایت به هدر برود. شاید این مقدار حافظه زیاد به نظر نرسد؛ اما تصور کنید این وضعیت در برنامه بزرگی رخ بدهد، در نهایت ممکن است چندین گیگابایت حافظه به هدر بروند.
مدیریت حافظه هیپ برای اینکه برنامه از نظر حافظه کارآمد باشد؛ امری ضروری محسوب میشود؛ اما باید مراقب باشید که چگونه از آن استفاده میکنید. همانند حافظه پشته، پس از این که حافظه هیپ آزاد شد، دسترسی به آن یا استفاده از آن ممکن است موجب بروز خطا شود.
Struct و هیپ
یکی از اشتباههای رایج هنگام استفاده از Struct ها این است که تنها Struct آزاد میشود که تا زمانی که حافظهای را به اشارهگرهای درون آن تخصیص نداده باشیم، اشکالی ندارد. اگر حافظهای به اشارهگرهای درون Struct تخصیص یافته باشد، باید ابتدا آنها را آزاد کنیم و تنها پس از آن میتوانیم حافظه تخصیص یافته را آزاد کنیم.
#include <stdlib.h> typedef struct { int * values; } Element; int main() Element * el = malloc(sizeof(Element)); //allocate an array of size 10 for the values property el->alues malloc(sizeof(int)* 10): free(el->va1ues); free(e1); }
روش حل مشکل نشت حافظه
در اغلب موارد که در زبان C برنامهنویسی میکنیم، در واقع از Struct-ها استفاده میکنیم، بنابراین همیشه دو تابع اجباری وجود دارند که باید به همراه Struct استفاده کنیم که یکی از آنها «تابع سازنده» (constructor) و دیگری «تخریبکننده» (destructor) است. این 2 تابع تنها توابعی هستند که برای malloc و free روی Struct استفاده میشوند و بدین ترتیب مشکل نشت حافظه تا حدود زیادی رفع میشود.
typedef struct element { char * description; int id; } * Element; Element createElement(int id, char * description) { Element el = malloc(sizeof(struct element)); el->id = id; el->description = malloc(sizeof(char) * (strlen(description) + 1)); strcpy(el->description, description); return el; } void deleteElement(Element el) { free(el->description); free(el); }
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای برنامهنویسی
- مجموعه آموزش های پروژه محور برنامه نویسی
- مجموعه آموزشهای علوم کامپیوتر
- حافظه مجازی چیست و چگونه میتوانید کمبود آن را جبران کنید؟
- حافظه مجازی (Virtual Memory) در سیستم عامل — راهنمای جامع
- آموزش حافظه (الف) در حل تست های معماری کامپیوتر
- مدیریت حافظه در سیستم عامل — راهنمای جامع
==