++C/C

کلاس های ذخیره سازی در C

کلاس های ذخیره سازی در C :

این آموزش در ادامه آموزشهای قبلی پیرامون زبان برنامه نویسی C میباشد. در قسمت قبل در مورد متغیرها در زبان C صحبت کردیم.در این آموزش قصد داریم به بررسی کلاس های ذخیره سازی در C بپردازیم با ما همراه باشید.

قبل از ادامه آموزش ابتدا به سرعت تفاوت بین طول عمر و محدوده یک متغیر را با هم بررسی میکنیم.

محدوده متغیر:

محدوده یک متغیر در C به بخشی از یک برنامه اشاره می کند که در آن متغیر قابل دسترسی و استفاده است. به عبارت ساده، این محدوده قابل مشاهده بودن متغیر را تعریف میکند.

طول عمر متغیر:

در برنامه نویسی C، “طول عمر” یک متغیر به مدت یا وجود آن در طول اجرای یک برنامه اشاره دارد. متغیرها در C بر اساس محدوده و کلاس ذخیره سازی، طول عمر متفاوتی دارند.

دامنه متغیرها :

محدوده تعریف
محدوده فایل از ابتدای فایل (که واحد ترجمه نیز نامیده می شود) شروع می شود و در انتهای فایل به پایان می رسد. این دامنه به شناسه هایی اشاره دارد که خارج از همه توابع اعلام شده اند. شناسه دامنه فایل در کل فایل قابل مشاهده است. متغیرهایی که دامنه فایل دارند متغیرهای سراسری یا گلوبال هستند.
محدوده بلاک با باز کردن { یک بلوک شروع می شود و با بسته شدن مربوط به آن } پایان می یابد. با این حال، محدوده بلوک به پارامترهای تابع در تعریف تابع نیز گسترش می یابد. یعنی پارامترهای تابع در محدوده بلوک یک تابع گنجانده شده است. متغیرهای دارای محدوده بلوک برای بلوک خود محلی یا لوکال هستند.
محدوده پروتو تایپ تابع شناسه های اعلام شده درپروتو تایپ تابع. فقط در پروتو تایپ قابل مشاهده است.
محدوده تابع با باز کردن { یک تابع شروع می شود و با بسته شدن آن } پایان می یابد. دامنه تابع فقط برای برچسب ها اعمال می شود.وقتی یک برچسب به عنوان هدف دستور goto استفاده می شود آن برچسب باید در همان تابع goto باشد.

کلاس های ذخیره سازی در C :

در زبان C، طول عمر و محدوده یک متغیر توسط کلاس های ذخیره سازی در C تعریف می شود.

چهار کلاس ذخیره سازی در زبان c وجود دارد :

  • کلاس اتوماتیک Auto :
  • رجیستر (Register) :
  • کلاس Eternal (extern) :
  • کلاس satic :

کلاس ذخیره سازی auto :

ویژگیها :

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

کلاس های ذخیره سازی auto در C کلاس ذخیره سازی پیش فرض برای همه متغیرهای محلی هستند.

کلاس ذخیره سازی رجیستر :

ویژگیها :


{
int mount;
auto int month;
}

مثال بالا دو متغیر را در یک کلاس ذخیره سازی تعریف می کند. به طور پیش فرض، همه متغیرهای محلی خودکار هستند. «خودکار» فقط در توابع، یعنی متغیرهای محلی قابل استفاده است.

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

یک مقدار ذخیره شده در یک ثبات CPU همیشه سریعتر از مقدار ذخیره شده در حافظه قابل دسترسی است. بنابراین، اگر متغیری در بسیاری از مکان‌های یک برنامه استفاده می‌شود، بهتر است کلاس ذخیره‌سازی آن را ثبات اعلام کنیم.

هیچ تضمینی وجود ندارد که ما متغیری را به عنوان ثبات اعلام کرده باشیم و در ثبات CPU ذخیره شود! چرا؟ دلیل آن این است که رجیسترهای CPU محدود هستند و ممکن است مشغول انجام کارهای دیگری باشند. در آن صورت، آن متغیر به عنوان کلاس ذخیره سازی پیش فرض در C یعنی کلاس ذخیره سازی خودکار کار می کند.

نکته: اینکه آیا هر متغیری در رجیستر CPU ذخیره می شود یا نه به ظرفیت ریزپردازنده بستگی دارد. به عنوان مثال، اگر ریزپردازنده دارای یک ثبات 16 بیتی باشد، نمی تواند مقدار شناور یا مقدار دوگانه را نگه دارد که به ترتیب به 4 و 8 بایت نیاز دارند. با این حال، اگر از کلاس ذخیره سازی register برای متغیر float یا double استفاده کنید، هیچ پیام خطایی دریافت نمی کنید زیرا کامپایلر آن را به عنوان کلاس ذخیره سازی پیش فرض یعنی کلاس ذخیره سازی خودکار در نظر می گیرد.

در حلقه هایی که از یک متغیر بارها و بارها استفاده میشود بهتر است آن متغیر از نوع کلاس register تعریف شود.

کلاس ذخیره سازی extern :

ویژگیها :

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

کلمه کلیدی extern ، کلاس ذخیره سازی extern متغیر اعلام شده را نشان میدهد. استفاده اصلی از extern این است که نشان میدهد متغیر در جای دیگری از برنامه اعلان شده است. برای درک اینکه چرا این نکته مهم است، لازم است تفاوت بین یک اعلان و یک تعریف را درک کنیم. یک اعلان نام و نوع یک متغیر یا تابع را اعلام می کند. یک تعریف باعث می‌شود که فضای ذخیره‌سازی برای متغیر یا بدنه تابع تعریف شده تخصیص داده شود. یک متغیر یا تابع ممکن است اعلان های زیادی داشته باشد، اما تنها یک تعریف برای آن متغیر یا تابع می تواند وجود داشته باشد.

هنگامی که از کلمه کلیدی extern با اعلان متغیر استفاده می شود، هیچ فضای ذخیره ای به آن متغیر اختصاص داده نمی شود و فرض می شود که متغیر قبلاً در جای دیگری از برنامه تعریف شده است. وقتی از کلمه کلیدی extern استفاده می کنیم، متغیر نمی تواند مقداردهی اولیه شود، زیرا با کلمه کلیدی extern ، متغیر اعلان می شود، نه تعریف.

در نمونه برنامه C زیر، اگر extern int x را حذف کنید. با خطای «Undeclared identifier ‘x’» مواجه خواهید شد زیرا متغیر x دیرتر از زمانی که در printf استفاده شده است تعریف شده است. در این مثال، extern به کامپایلر می‌گوید که متغیر x قبلاً تعریف شده است و در اینجا برای اطلاعات کامپایلر اعلان می‌شود.


#include <stdio.h>

extern int x;

int main()
{
printf("x: %d\n", x);
}

int x=10;

همچنین، اگر عبارت extern int x را تغییر دهید. به ;extern int x = 50 شما دوباره با خطای “تعریف مجدد “x” مواجه خواهید شد زیرا با استفاده از extern متغیر اگر در جای دیگری تعریف شده باشد نمی تواند مقداردهی اولیه شود.

نوشته های مشابه

اغلب کلمه کلیدی extern زمانی استفاده می شود که بخواهیم متغیر سراسری را در دو یا چند فایل .c به اشتراک بگذاریم.

توجه داشته باشید که extern را می توان برای یک اعلان تابع نیز اعمال کرد، اما انجام این کار تاثیری ندارد زیرا همه اعلان های تابع به طور ضمنی خارجی هستند.

متغیرهای static :

ویژگیها

محل ذخیره سازی حافظه
محدوده بلاک
طول عمر مقدار متغیر بین فراخوانی های تابع مختلف باقی می ماند
مقدار اولیه صفر

متغیرهای استاتیک هم بر طول عمر و هم بر دامنه تأثیر می گذارند.

تاثیر بر روی طول عمر :

متغیرهای ثابت آن دسته از متغیرهایی هستند که طول عمر آنها مانند متغیرهای سراسری برابر با طول عمر برنامه باقی می ماند. هر متغیر محلی یا سراسری را می توان بسته به آنچه که منطق از آن متغیر انتظار دارد، ثابت کرد.مثال زیر را در نظر بگیرید :


#include <stdio.h>

char** func_Str();

int main(void)
{
char **ptr = NULL;
ptr = func_Str();
printf("\n [%s] \n",*ptr);
return 0;
}

char** func_Str()
{
char *p = "Linux";
return &p;
}

در کد بالا، تابع “()func_str” آدرس اشاره گر “p” را به تابع فراخوانی برمی گرداند که از آن برای چاپ رشته “Linux” برای کاربر از طریق “()printf” استفاده می کند. بیایید به خروجی نگاه کنیم:


$ ./static
[Linux]
$

خروجی بالا مطابق انتظار است. خب، همه چیز به نظر خوب است؟ اما، یک مشکل پنهان در کد وجود دارد. به طور خاص، مقدار بازگشتی اشاره گر، کاراکتر محلی (char *p) در تابع “()func_Str” است. مقداری که برگردانده می شود آدرس متغیر اشاره گر محلی «p» است. از آنجایی که ‘p’ برای تابع محلی است، بنابراین به محض بازگشت تابع، طول عمر این متغیر به پایان می رسد و از این رو مکان حافظه آن برای سایر عملیات آزاد می شود.

البته به این نکته هم دقت کنید که کد بالا در IDE اجرا نمیشود و با هشدار روبرو میگردد.

ولی برای اینکه این نکته رو متوجه بشید یک مثال دیگه هم میزنم :


#include <stdio.h>

char** func1_Str();
char** func2_Str();

int main(void)
{
char **ptr1 = NULL;
char **ptr2 = NULL;

ptr1 = func1_Str();
printf("\n [%s] \n",*ptr1);

ptr2 = func2_Str();
printf("\n [%s] \n",*ptr2);

printf("\n [%s] \n",*ptr1);

return 0;
}

char** func1_Str()
{
char *p = "Linux";
return &p;
}

char** func2_Str()
{
char *p = "Windows";
return &p;
}

در کد بالا، اکنون دو تابع “()func1_Str” و “()func2_Str” وجود دارد. مشکل منطقی در اینجا نیز به همان شکل باقی می ماند. هر یک از این توابع آدرس متغیر محلی خود را برمی گرداند. در تابع ()main، آدرس برگردانده شده توسط ()func1_Str برای چاپ رشته “Linux” (همانطور که توسط متغیر اشاره گر محلی آن اشاره شده است) و آدرس برگردانده شده توسط تابع ()fuc2_Str برای چاپ رشته استفاده می شود. Windows’ (همانطور که توسط متغیر اشاره گر محلی آن اشاره شده است). یک قدم جلوتر به سمت انتهای تابع ()main و با استفاده مجدد از آدرسی که توسط ()func1_Str برای چاپ رشته “Linux” بازگردانده شده ()printf را اجرا میکنیم.

خروجی رو ببینیم با هم :


$ ./static
[Linux]
[Windows]
[Windows]

خروجی بالا مطابق انتظار نیست. چاپ سوم باید «لینوکس» به جای «ویندوز» باشد. خوب، بهتر است بگویم که خروجی بالا مورد انتظار بود.با این سناریوی مشکل کد را بهتر متوجه شدیم.

حالا اجازه دهید کمی عمیق تر برویم تا ببینیم پس از بازگشت آدرس متغیر محلی چه اتفاقی افتاده است.

کد زیر را ببینید:


#include <stdio.h>

char** func1_Str();
char** func2_Str();

int main(void)
{
char **ptr1 = NULL;
char **ptr2 = NULL;

ptr1 = func1_Str();
printf("\n [%s] :: func1_Str() address = [%p], its returned address is [%p]\n",*ptr1,(void*)func1_Str,(void*)ptr1);

ptr2 = func2_Str();
printf("\n [%s] :: func2_Str()address = [%p], its returned address is [%p]\n",*ptr2,(void*)func2_Str,(void*)ptr2);
printf("\n [%s] [%p]\n",*ptr1,(void*)ptr1);

return 0;
}

char** func1_Str()
{
char *p = "Linux";
return &p;
}

char** func2_Str()
{
char *p = "Windows";
return &p;
}

کد بالا برای چاپ آدرس توابع و آدرس متغیرهای اشاره گر محلی مربوطه اصلاح شده است.

خروجی به صورت زیر خواهد بود :


$ ./static
[Linux] :: func1_Str() address = [0x4005d5], its returned address is [0x7fff705e9378]
[Windows] :: func2_Str()address = [0x4005e7], its returned address is [0x7fff705e9378]
[Windows] [0x7fff705e9378]

خروجی بالا روشن می کند که زمانی که طول عمر متغیر محلی تابع “()func1_Str” به پایان برسد، همان آدرس حافظه برای متغیر اشاره گر محلی تابع “()func2_Str” استفاده میشود از این رو چاپ سوم  “ویندوز” است و نه “لینوکس”.

بنابراین، اکنون می بینیم که ریشه مشکل طول عمر متغیرهای اشاره گر است. اینجاست که کلاس ذخیره سازی «استاتیک» به کمک می آید. همانطور که قبلاً بحث شد، کلاس های ذخیره سازی استاتیک طول عمر یک متغیر را برابر با عمر برنامه می کند. بنابراین، بیایید متغیرهای اشاره گر محلی را ثابت کنیم و سپس خروجی را ببینیم:


#include <stdio.h>

char** func1_Str();
char** func2_Str();

int main(void)
{
char **ptr1 = NULL;
char **ptr2 = NULL;

ptr1 = func1_Str();
printf("\n [%s] :: func1_Str() address = [%p], its returned address is [%p]\n",*ptr1,(void*)func1_Str,(void*)ptr1);

ptr2 = func2_Str();
printf("\n [%s] :: func2_Str()address = [%p], its returned address is [%p]\n",*ptr2,(void*)func2_Str,(void*)ptr2);
printf("\n [%s] [%p]\n",*ptr1,(void*)ptr1);

return 0;
}

char** func1_Str()
{
static char *p = "Linux";
return &p;
}

char** func2_Str()
{
static char *p = "Windows";
return &p;
}

توجه داشته باشید که در کد بالا، اشاره گرها  با کلاس static ساخته شده اند. در این حالت خروجی به صورت زیر است :


$ ./static
[Linux] :: func1_Str() address = [0x4005d5], its returned address is [0x601028]
[Windows] :: func2_Str()address = [0x4005e0], its returned address is [0x601020]
[Linux] [0x601028]

تاثیر بر دامنه :

در مواردی که کد در چندین فایل پخش می شود، می توان از نوع ذخیره سازی استاتیک برای محدود کردن دامنه یک متغیر به یک فایل خاص استفاده کرد. به عنوان مثال، اگر در یک فایل یک متغیر ‘count’ داشته باشیم و بخواهیم متغیر دیگری با همین نام در فایل دیگری داشته باشیم، در آن صورت، یکی از متغیرها باید ثابت شود.

مثال زیر این موضوع را نشان می دهد:

static.c


#include <stdio.h>

int count = 1;

int main(void)
{
printf("\n count = [%d]\n",count);
return 0;
}

static_1.c


#include <stdio.h>

int count = 4;

int func(void)
{
printf("\n count = [%d]\n",count);
return 0;
}

اکنون، زمانی که هر دو فایل کامپایل شده و به یکدیگر پیوند داده می شوند تا یک فایل اجرایی واحد تشکیل دهند،  خطایی وجود دارد که توسط GCC اعلان می شود:


$ gcc -Wall static.c static_1.c -o static
/tmp/ccwO66em.o:(.data+0x0): multiple definition of `count'
/tmp/ccGwx5t4.o:(.data+0x0): first defined here
collect2: ld returned 1 exit status
$

بنابراین، می بینیم که GCC از اعلان های متعدد متغیر «شمارش» اشکال میگیرد.

به عنوان یک اقدام اصلاحی، این بار یکی از متغیرهای “count” از نوع static تعریف میشود:

static.c


#include <stdio.h>

static int count = 1;

int main(void)
{
printf("\n count = [%d]\n",count);
return 0;
}

static_1.c


#include <stdio.h>

int count = 4;

int func(void)
{
printf("\n count = [%d]\n",count);
return 0;
}

اکنون، هر دو فایل کامپایل شده و با هم پیوند داده می شوند:


$ gcc -Wall static.c static_1.c -o static
$

بنابراین، می بینیم که این بار هیچ خطایی ایجاد نمی شود زیرا static دامنه متغیر “count” در فایل static.c را به خود فایل محدود کرده است.

توابع استاتیک :

به طور پیش فرض هر تابعی که در یک فایل C تعریف شده باشد extern است. این بدان معنی است که تابع را می توان در هر فایل منبع دیگری از همان کد/پروژه (که به عنوان یک واحد ترجمه جداگانه کامپایل می شود) استفاده کرد. حال، اگر شرایطی وجود داشته باشد که دسترسی به یک تابع به فایلی که در آن تعریف شده است محدود شود یا اگر تابعی با همان نام در فایل دیگری از همان کد/پروژه مورد نظر باشد،  آن توابع را در C می توان استاتیک کرد.

با بسط دادن همان مثالی که در بخش قبل استفاده شد، فرض کنید دو فایل داریم:

static.c


#include <stdio.h>

void func();

int main(void)
{
func();
return 0;
}

void funcNew()
{
printf("\n Hi, I am a normal function\n");
}

static_1.c


#include <stdio.h>

void funcNew();

int func(void)
{
funcNew();
return 0;
}

اگر کد بالا را کامپایل و اجرا کنیم خواهیم داشت :


$ gcc -Wall static.c static_1.c -o static
$ ./static
Hi, I am a normal function
$

بنابراین، می بینیم که تابع ()funcNew در یک فایل تعریف شده و با موفقیت از فایل دیگر فراخوانی شده است. حال، اگر فایل static_1.c بخواهد ()funcNew خودش را داشته باشد، یعنی:

static_1.c


#include <stdio.h>

void funcNew();

int func(void)
{
funcNew();
return 0;
}

void funcNew()
{
printf("\n Hi, I am a normal function\n");
}

حال، اگر هر دو فایل کامپایل شده و با هم پیوند داده شوند:


$gcc -Wall static.c static_1.c -o static
/tmp/ccqI0jsP.o: In function `funcNew':
static_1.c:(.text+0x15): multiple definition of `funcNew'
/tmp/ccUO2XFS.o:static.c:(.text+0x15): first defined here
collect2: ld returned 1 exit status
$

بنابراین، می بینیم که کامپایلر از تعاریف متعدد تابع ()fucNew  ارور میگیرد. بنابراین،

حالا ()funcNewرا در static_1.c به صورت static اعلان میکینم:


#include <stdio.h>

static void funcNew();

int func(void)
{
funcNew();
return 0;
}

static void funcNew()
{
printf("\n Hi, I am also a normal function\n");
}

حالا اگر کد را کامپایل کنیم، می بینیم که کامپایلر هرگز خطایی نمیگیرد :


$ gcc -Wall static.c static_1.c -o static
$ ./static
Hi, I am also a normal function
$

نوشته های مشابه

دیدگاهتان را بنویسید

همچنین ببینید
بستن
دکمه بازگشت به بالا