در یکی از جلسات مصاحبه، مصاحبه کننده در مورد تفاوت دو تابع ()malloc و ()realloc ازم پرسید و جواب درستی براش نداشتم! 😔 یعنی پیش نیومده بود از realloc استفاده کنم، و خیلی وقت بود از C نوشتنم گذشته بود. در این پست به بررسی چهار تابع کار با حافظه در زبان C یعنی توابع malloc, realloc, calloc و free پرداخته ام.

تابع ()malloc

این تابع به اندازه n بایت از حافظه را تخصیص داده و یک اشاره گر به اولین خانه حافظه را برمی گرداند. اگر خطایی به هنگام تخصیص حافظه رخ دهد مقدار NULL برگردانده می شود. الگوی فراخوانی تابع به شکل زیر است

(type *) malloc(sizeof(type) * n);

مقدار type می تواند یکی از انواع متغیرها در C (اصلی یا تعریف شده توسط برنامه نویس) مثل int, char, float, … باشد. فراخوانی تابع فوق به اندازه n خانه حافظه که هر خانه به اندازه یک متغیر از نوع type است، تخصیص می دهد. برای نمونه کد زیر را اجرا کنید. این کد ده خانه حافظه که هر خانه به اندازه یک int فضا اشغال می کند را تخصیص می دهد.

#include <stdio.h>
#include <stdlib.h>

int main(void) 
{
    int *p;
    
    p = 0x42;
    printf("Pointer before malloc(): %p\n", p);
    
    printf("Size of int: %d Byte\n", sizeof(int));
    
    p = (int *)malloc(sizeof(int) * 10);
    printf("Pointer after malloc(): %p\n", p);    
    return (0);
}

خروجی چیزی شبیه به این خواهد بود:

Pointer before malloc(): 0x42
Size of int: 4 Byte
Pointer after malloc(): 0x5402040

در این کد ابتدا یک اشاره گر به int تعریف، سپس آن را مقدار دهی می کنیم. آدرسی که در آن ذخیره می شود در واقع جایی از حافظه است (که احتمالا در دسترس برنامه قرار ندارد). سپس با فراخوانی تابع malloc به اندازه ۱۰ خانه که هر کدام به اندازه sizeof(int) فضا می گیرد، تخصیص داده می شود. تابع printf دوم اندازه یک خانه حافظه از نوع int (یعنی ۴ بایت) را چاپ می کند. تابع printf سوم آدرس خانه اول حافظه تخصیص یافته را چاپ می کند.

این کد زیاد امن نیست؛ زیرا اگر تابع malloc به هر دلیلی نتواند حافظه درخواستی را تخصیص دهد مقدار NULL بر می گرداند. بنابراین برنامه را به شکل زیر تغییر می دهیم.

#include <stdio.h>
#include <stdlib.h>

#define NUM_ELEM (10)

int main(void)
{
    int *ptr;
    
    ptr = (int *)malloc(sizeof(int) * NUM_ELEM); 
    
    if (ptr == NULL)
    {
       printf("Memory allocation falied!\n");
    }
    else
    {
       printf("%d Byte memory is allocated successfully.\n", (int)(sizeof(int) * NUM_ELEM));
    }    
    
    return (0);
}

کد فوق را اجرا کنید و خروجی باید چیزی شبیه به این باشد:

۴۰ Byte memory is allocated successfully.

کی از تابع malloc استفاده کنیم؟

برای نمونه می توان به موارد زیر اشاره کرد:

  • وقتی که تعداد آیتم های یک لیست از اول (در زمان کامپایل) مشخص نیست و تغییر می کند. (برای مثال لیست های پیوندی)
  • زمانی که نیاز به داده ساختارهایی دارید که اندازه آنها در زمان اجرا مشخص می شود. (مثلا رشته هایی که بر اساس ورودی های نامعلوم ساخته می شود.) این مساله وقتی که در C99 آرایه های با طول متغییر VLA معرفی شد، تا حدی بهبود پیدا کرد.
  • زمانی که طول عمر یک متغیر با حالت عادی آن (که توسط اسکوپ متغیر کنترل می شود) متفاوت است و شما زودتر میخواهید از شر متغیر اضافی خلاص شوید.

تابع ()free

این تابع عکس عمل malloc را انجام می دهد. در برنامه هایی که به زبان C می نویسید مسئول مستقیم مدیریت حافظه برنامه نویس است. بنابراین در صورتی که به حافظه ای نیاز نداشتید باید آن را آزاد کنید. در این صورت می توانید از اتفاق نشت حافظه یا همان Memory Leak جلوگیری کنید. برای آزاد سازی حافظه کافی است اشاره گر به خانه اول حافظه را به عنوان آرگومان ورودی به تابع free بدهید. چیزی شبیه به کد زیر:

int *ptr = malloc(sizeof(int) * NUM_ELEM);
free(ptr);

تابع ()calloc

تمام نکاتی که در مورد malloc گفته شد در مورد این تابع نیز برقرار است با این تفاوت که این تابع خانه های حافظه تخصیص داده شده را به مقدار صفر، مقداردهی اولیه می کند.

malloc calloc
مقایسه malloc و calloc
#include <stdio.h>
#include <stdlib.h>

#define NUM_ELEM (5)

void print_array(int *array, int size)
{
    for (int i = 0; i < size; i += 1)
    {
        if (i > size)
        {
            printf("[%d]", array[i]);
        }
        else
        {
            printf("[%d], ", array[i]);
        }
    }
    putchar('\n');
}

int    main(void)
{
    int *ptr_calloc;
    int *ptr_malloc;

    ptr_calloc = (int*) calloc(NUM_ELEM, sizeof(int));
    ptr_malloc = (int*) malloc(NUM_ELEM * sizeof(int));

    print_array(ptr_calloc, NUM_ELEM);
    print_array(ptr_malloc, NUM_ELEM);

    return (0);
}

برنامه بالا رو کامپایل و اجرا کنید. از طریق این شبیه سازی که لینکش رو گذاشتم که اجراش بکنید برنامه کرش می کنه! ولی وقتی خودتون با gcc کامپایل و اجرا کنید این اتفاق نمی افته! و خروجی یکسان هست. خروجی پس از کامپایل با gcc و اجرا چیزی مثل این میشه:

[۰], [۰], [۰], [۰], [۰],
[۰], [۰], [۰], [۰], [۰],

خوب در واقع malloc مقداردهی اولیه انجام نمیده؛ وقتی شما تابع malloc را فراخوانی می کنید، این تابع چک می کند که تخصیص گر حافظه glibc، حافظه به اندازه درخواست شده را دارد یا خیر. اگر در اختیار داشت حافظه را بر می گرداند (از کجا میاره؟ از آزادسازی حافظه که در همان برنامه انجام داده اید). این حافظه معمولا حاوی مقادیر آشغال (صفر یا غیر صفر) است.

در صورتی که حافظه آزاد در اختیار نداشته باشد با فراخواین توابعی مثل sbrk یا mmap از سیستم عامل درخواست تخصیص حافظه می کند. سیستم عامل به دلایل امنیتی یک صفحه که با مقدار صفر مقداردهی اولیه شده است را برمیگرداند. امنیتی؟ بله، این حافظه ممکن است حاوی مقادیری باشد که توسط برنامه ای دیگری در حافظه قرار داده شده است.

طبیعتا calloc چون زمانی رو صرف پاک کردن حافظه تخصیص یافته می کند، از malloc کندتر است. این تابع در واقع ترکیبی از اجرای malloc و memset است.

int *ptr = malloc(sizeof(int));
memset(ptr, 0, sizeof (int));

البته در برخی پیاده سازی های calloc از امکان سیستم عامل، اشاره شده در بالا، استفاده شده است. در واقع بجای فراخوانی malloc مستقیما از سیستم عامل درخواست حافظه می شود. با این روش حافظه مقداردهی اولیه شده در اختیار قرار می گیرد.

تابع ()realloc

از این تابع برای تغییر اندازه حافظه تخصیص داده شده استفاده می شود. فرض کنید ۱۰ خانه حافظه از نوع int را با دستور زیر تخصیص داده ایم.

int *ptr = malloc(10 * sizeof(int));

حال میخواهیم بدون آن که محتوی موجود تغییر کند اندازه حافظه را به ۲۰ خانه افزایش دهیم. برای این کار تابع realloc را به صورت زیر فراخوانی می کنیم.

ptr = (int *)realloc(ptr, 20 * sizeof(int));

این تابع جایی از حافظه ۲۰ خانه را تخصیص داده، سپس ده خانه موجود را در آن کپی می کند. بعد از آن ۱۰ خانه قبلی را آزاد کرده و اشاره گر به خانه اول ۲۰ خانه جدید را بر میگرداند.

چند نکته:

  1. اگر اشاره گری که به realloc پاس می دهید null باشد، این تابع مشابه malloc عمل می کند.
  2. اگر اندازه صفر و اشاره گر null نباشد، این تابع مشابه تابع free عمل می کند.
  3. اگر به هر دلیلی اجرای تابع realloc موفقیت آمیز نباشد، حافظه قبلی دست نخورده باقی می ماند.

پیوست

در زبان ++C از عملگرهای new و delete بجای malloc و free استفاده می شود.

منابع

مطالب مرتبط