زبان برنامه نویسی سی/کلاسهای ذخیره
تعیین کنندههای کلاس ذخیره Class-Storage Specifiers
در این مبحث میپردازیم به کاربرد پنج کلیدواژه زبان C ، یعنی کلیدواژههای auto و static و extern و register و همچنین کلیدواژه volatile که جزء کلاسهای ذخیره محسوب نمیشود اما جایی بهتر برای مطرح نمودن آن وجود ندارد . پیشتر ، کمی با کلیدواژه volatile که به معنی داده فرّار است و باعث میشود تا مقداردهی یا دسترسی به مقدار و موجودی داده به سختافزار ( و یا سیستم عامل ) واگذار شود آشنا شدید که مقداری به کلاسهای ذخیره مشابه است . پیش از آغاز معرفی کلیدواژهها ، لازم است با مفاهیم حوزه دید (view scope) که به صورت مختصر حوزه (scope) نیز خوانده میشود و طول عمر (lifetime) آشنا شوید
حوزه و طول عمر
ویرایشدر برنامهنویسی ، حوزه دید view scope یا visibility به حوزه و محدودهای گفته میشود که یک داده در متن برنامه ، قابل رؤیت توسط بقیه دادهها و کدهای نوشته شده میباشد و به آن دسترسی دارند تا مقدار و موجودی داخل آن را بخوانند یا تغییر دهند . بنابراین در بسیاری از زبانهای برنامهنویسی از جمله زبان سی ، برخی از دادهها از دید دادهها و کدهای دیگر پنهان هستند . کاربرد حوزه در برنامهنویسی ، جدا کردن بخشهایی از برنامه است که جدای از یکدیگر عمل کنند که مهمترین مزیّت آن این است که تغییر در بحشی از برنامه ، قسمتهای دیگر را تحت تأثیر قرار نمیدهد و قسمتهای دیگر ، مجزا و بدون تحت تأثیر قرار گرفتن نسبت به آن کد به کار خود ادامه میدهند . در زبان C هر متغیّری که داخل بلوکی از هر تابع یا هر دستور ( مثل حلقه while ) قرار بگیرد ، یک متغیّر محلّی ( local variable ) محسوب میشود . بدین معنا که آن متغیر ، خارج از محدوده بلوکی که در آن اعلان یا تعریف شده است ، غیرقابل دسترسی میباشد مگر به واسطه و با کمک یک متغیر سراسری برای خواندن و به صورت اشارهگر ( اشارهگر سراسری ) مقدار متغیر محلی را خارج از بلوک ، تغییر دهیم . در بین کلیدواژههایی که از آنها نام بردیم کلیدواژه auto به ندرت به کار میرود . این کلیدواژه باعث محلی شدن یک متغیر میشود ؛ یعنی متغیر را خودکار یا automatic میکند تا در داخل بلوک قابل دسترسی و خارج از آن غیرقابل دسترس باشد ( آن را محلی میکند ) . علاوه بر این باعث از بین رفتن مقدار و موجودی ذخیره شده در داخل متغیر و آزاد شدن فضای اشغال شده توسط آن متغیر در حافظه ، پس از به پایان رسیدن بلوک میشود . به این مسئله طول عمر یک متغیر میگوئیم : یعنی مدت زمانی که در حین اجرای برنامه نوشته شده ، متغیر در داخل حافظه موقّت کامپیوتر باقی میماند . تمام متغیرهای محلی پس از به پایان رسیدن بلوکِ خود از بین میروند . بنابراین حتی بدون قید کردن کلیدواژه auto در داخل بلوک برای متغیرها ، متغیرهای ما خود به خود محلی هستند . اما در کنار کلیدواژه auto کلیدواژههای دیگری وجود دارند که شامل static که دو کاربرده است و دو مفهوم مجزا دارد که یکی مربوط به دائمی کردن طول عمر متغیرهای محلی و کاربرد دیگر آن در اختصاصی کردن متغیر در فایلهای برنامه نوشته شده میباشد و کلیدواژه extern که اجازه دسترسی به داده را خارج از متن برنامه نوشته شده ، فراهم میکند و یا مقدار دهی به آن را در قسمتی دیگر از فایلهای برنامه ، امکانپذیر مینماید و همچنین کلیدواژه register که باعث ذخیره شدن متغیر در cache سیپییو به جای حافظه موقت در کامپیوتر میشود می باشند و البته کلیدواژه volatile که با آن آشنایی دارید و در قسمت ابتدایی مبحث نیز بدان اشاره نمودیم
در زبان C هر جفت آکولاد باز و بسته از هر تابع یا دستوراتی شامل for و while و do while و switch و if و else if و else یک بلوک نامیده میشود . هر متغیری که خارج از هر بلوکی ( که عموماً در ابتدای برنامه و پس از پیشپردازندهها نوشته میشوند ) اعلان شود ، حوزه سراسری دارد ؛ بدین معنا که پس از اعلان ، در سرتاسر برنامه نوشته شده قابل دسترسی و استفاده است . متغیرهای سراسری میتوانند در هر جای برنامه از جمله بلوکها مقدار جدیدی بگیرند . یعنی مقداری که یک متغیر سراسری در بلوک تابع func1 دارد میتواند پس از رسیدن به تابع func2 تغییر کند و مقدار متغیر سراسری ، آخرین مقداری است که در برنامه به آن داده شده است . متغیرهای سراسری دارای طول عمر دائمی میباشند و در تمام طول اجرای برنامه ، متغیر وجود خواهد داشت و مقدار آن محفوظ است . با نسبت دادن کلیدواژه extern متغیر ما میتواند حتی خارج از فایل فعلی نوشته شده برنامه نیز قابل دسترسی باشد که در قسمت مربوطه ( extern ) در همین مبحث به آن میپردازیم
هر متغیری که داخل بلوک تابع یا دستورات مذکور در بند بالا ( مثل for ) ، اعلان شود فقط در داخل همان بلوک ( و البته پس از اعلان ) قابل دسترسی خواهد بود و مقداری که دارد همان مقداری است که داخل بلوک خواهد یافت . این متغیرها ، محلی هستند و پس از اعلان ، ایجاد شده و داخل بلوک خود مقدار دهی میشوند و پس از به پایان رسیدن بلوک ، متغیر از بین میرود و واضح است که مقدار آن نیز از بین رفته است ( یعنی از حافظه موقت پاک میشود ) . دستورهای مذکور را میتوان تو در تو استفاده نمود و بلوکهای تو در تو تعریف نمود ، اما تابعها چنین قابلیتی ندارند . یعنی شما نمیتوانید تابعی را داخل تابع دیگری تعریف کنید . با فراخوانی تابع در هر جای برنامه متغیرهای محلی دوباره ایجاد میشوند و با پایان رسیدن عمل تابع ، متغیرها از بین میروند . همچنین دادههای بلوکهای داخلیتر به دادههای بلوکهای خارجی تر دسترسی دارند ، اما عکس آن صادق نیست ؛ یعنی بلوکهای خارجیتر به بلوکهایی که داخل آنها تعریف شدهآند ( بلوکهای داخلیتر ) دسترسی ندارند که همان طور که گفته شد قابلیت نوشتن بلوکهای تو در تو تنها برای دستورهای مذکور قابل استفاده است ، نه برای تابعها . همچنین تابعها میتوانند دستورات تو در تو داشته باشند ، اما نمیتوانیم یک تابع را داخل تابعی دیگر اعلان یا تعریف کنیم
علاوه بر این متغیرهایی که به عنوان پارامتر تابع یا متغیر شرطی و یا کنترلی دستورهای مذکور ، اعلان شوند ، فقط داخل بلوک همان تابع یا دستور ، قابل دسترسی هستند و خارج از آن تابع یا دستور ، قابل رؤیت توسط مابقی کدهای برنامه نیستند . در مورد این متغیرها باید بدانید که تنها دستورهای داخلیتر هستند که به متغیرهای دستورهای خارجیتر دسترسی دارند و البته که متغیرهای شرطی و یا کنترلی به پارامترهای تابعی که در آن تعریف شده باشند نیز دسترسی دارند . همچنین متغیری که داخل تابع قبل از دستورات داخل بلوک تابع ( بیرون از بلوکهای دستورها ) اعلان شود توسط تمام بلوکهای دستورهای بعدی قابل دسترسی میباشد ( به حوزه آنها فرمال Formal Scope گفته می شود )
نکته : شما توسط متغیرهای سراسری میتوانید مقدار پارامترهای تابعها و یا متغیرهای شرطی و کنترلی دستورها را بخوانید و با اشاره کردن به آنها با کمک اشارهگرهای سراسری میتوانید دسترسی کامل به آنها داشته باشید و حتی مقدارشان را نیز خارج از بلوک تغییر دهید ؛ گرچه این ترفند در برنامهنویسی به ندرت به کار میآید
با چند مثال و تشریح آنها مطالب بالا را به صورت عملی بیان میکنیم
#include<stdio.h>
void func1(void);
void func2(void);
int i = 7;
int main()
{
func1();
func2();
func1();
return 0;
}
void func1(void)
{
i = 24;
printf("%d\n", i);
}
void func2(void)
{
i = 16;
printf("%d\n", i);
}
در مثال بالا ابتدا با دستور مستقیم و پیشپردازنده include فایل سرآیند stdio ( که مخفف standard input/output است ) را ضمیمه فایل برنامه نمودیم ( تا بتوانیم از تابع کتابخانهای printf استفاده کنیم ) و دو تابعی را که قصد استفاده از آنها را داشتیم ، اعلان نمودیم ( یعنی func1 و func2 ) . یک متغیر سراسری با نام i ایجاد نمودیم و به آن مقدار 7 را اختصاص دادیم که همان طور که ملاحظه میکنید ، متغیر خارج از بلوک تابعها یا دستورها قرار دارد ( خارج از آکولادهای باز و بسته ) سپس تابع main که بخشی از زبان C است را نوشتهایم ( هنوز به این مبحث نرسیدهایم ) که تابع اصلی برنامه میباشد و تابعهای دیگر را باید در آن فراخوانی کنیم تا عمل کنند و برنامه پس از کامپایل از طریق همین تابع به اجرا در خواهد آمد . داخل تابع main ، دو تابع اعلان شده را فراخوانی نمودهایم تا عمل کنند ( یک بار func1 و یک بار func2 و سپس دوباره func1 ) که البته بعد از تابع main تعریف شدهاند . در پایان تابع main نیز دستور return را با مقدار 0 استفاده نمودهایم تا مشخص شود تابع با موفقیت به پایان رسیده است . تابع func1 مقدار متغیر سراسری i را که به آن دسترسی دارد تغییر داده و به آن مقدار 24 را تخصیص داده است و مقدار آن را توسط تابع کتابخانهای printf ( مخفف print formatted که برای نمایش متن در محیطهای خط دستوری به کار میرود ) به خروجی خط دستوری ارسال میکند و در نهایت نمایش داده خواهد شد . سپس تابع func2 را همانند func1 تعریف نمودیم با این تفاوت که مقدار 16 را به متغیر سراسری i تخصیص دادیم که مقدار متغیر به روز خواهد شد . در صورت کامپایل و اجرای قطعه کد بالا شما اعداد 24 و 16 و دوباره 24 را در محیط خطدستوری مشاهده خواهید نمود چرا که وقتی تابع func1 فراخوانی میشود مقدار 24 را در i قرار میدهد و سپس func2 مقدار 16 را داخل آن میگذارد و دوباره که func1 فراخوانده شده و مقدار 24 را در داخل i قرار میدهد . بنابراین آخرین مقدار باقی مانده در i عدد 24 خواهد بود و پس از اتمام اجرای برنامه ، مقدار آن و خودش از بین خواهد رفت اما نه در طول برنامه ( چرا که یک متغیر سراسری است ) . شما میتوانید متن همین برنامه را کپی کرده و داخل یک IDE مثل Visual Studio از مایکروسافت یا Code::Blocks بچسانید ( paste کنید ) متن را داخل یک سند از قالب c ذخیره نمائید ( مثل test.c ) و سپس از طریق کلیک بر روی منوی Build گزینه Compile and Build را و سپس Run را کلیک کنید تا نتیجه را ببینید . اگر IDE ندارید ، کامپایلر ریز C را دانلود کرده و سپس از طریق رابط خط دستوری در سیستم عامل خود ( cmd در ویندوز و Terminal در لینوکس و مکینتاش ، به طور مثال ) دستور زیر را وارد کنید :
در ویندوز:
compiler-directory/tcc.exe -o test.exe address-of/test.c
که address-of و compiler-directory به ترتیب آدرس فایل برنامه نوشته شده شما و آدرس فولدر کامپایلر ریز سی ( یعنی tcc ) میباشد
در لینوکس:
sudo tcc -o test address-of/test.c
سپس آدرس test ( که فایل قابل اجرا میباشد ) را در محیط خط دستوری وارد کنید تا اجرا شود و نتیجه را ببنید
مثال دوم:
#include<stdio.h>
void func1(void)
{
int i = 8;
printf("%d\n", i);
}
int main()
{
printf("%d\n", i);
func1();
return 0;
}
در این مثال تابع func1 را همزمان، اعلان و تعریف نمودهایم . داخل بلوک func1 یک متغیر با نام i اعلان و تعریف نمودهایم ؛ بنابراین محلی است و خارج از دسترس بقیه کدها که بیرون از بلوک func1 هستند . در داخل تابع main تلاش کردهایم تا مقدار i را همانند تابع func1 نمایش دهیم . اما اگر کد بالا را بخواهید کامپایل کنید ، دیباگر ( debugger ، اشکال زدا ) از شما ایراد خواهد گرفت که i چیزی تعریف نشده و ناشناخته است myt.c:12: error: 'i' undeclared
( نام فایل myt.c است و کد نوشته شده در ۱۲ ـمین خط خطا دارد )
چرا که دسترسی به آن امکانپذیر نیست. حال اگر خط ;printf("%d\n", i) را توسط دو ممیز ( دو اسلش / به صورت // ) به کامنت تبدیل کنیم و سپس دوباره بخواهیم آن را کامپایل کنیم، بدون ایراد کامپایل شده و سپس میتوانیم آن را اجرا کنیم تا در خروجی برنامه عدد 8 را در محیط خط دستوری مشاهده کنیم
مثال سوم:
#include<stdio.h>
void func1(void);
void func2(void);
int main()
{
func1();
func2();
return 0;
}
void func1(void)
{
int m = 63;
printf("%d\n", m);
}
void func2(void)
{
int m = 24;
printf("%d\n", m);
}
در این مثال تابع func1 و func2 هر دو متغیری را اعلان و تعریف نمودهاند با نام m که در هر تابع مجزا تعریف شده است . در تابع func1 مقدار 63 و در تابع func2 مقدار 24 را دارد . دقت کنید که اگر در تابع func2 متغیر m را اعلان و تعریف نکنید در واقع متغیر m وجود ندارد چرا که متغیر m که در داخل func1 اعلان و تعریف شده ، محلی است و خارج از بلوک func1 وجود ندارد و اگر متغیری با نام m در داخل بلوکی دیگر تعریف کنیم و یا اینکه به صورت سراسری ، پس از تابع func1 آن را اعلان و تعریف کنیم ، متغیر m داخل بلوک func1 مجزا عمل میکند ( ولی اگر پیش از تعریف تابع func1 آن را سراسری اعلان و یا تعریف کنیم ، آنگاه متغیر m داخل بلوک func1 نیز همان متغیر سراسری خواهد بود و نمیتواند نمونه مجزایی بسازد مگر با کمک کلیدواژه auto که در ادامه آن را بررسی میکنیم )
مثال چهارم:
#include<stdio.h>
void func1(int i);
void func2(void);
int num = 5;
int main()
{
func1(num);
func2();
return 0;
}
void func1(int i)
{
for(i=0; i<num; i++)
{
printf("%d\n", i);
}
}
void func2(void)
{
printf("%d\n", i);
}
در مثال چهارم تابع func1 یک پارامتر دارد با نام i که بر روی آن پردازش انجام میدهد . برخلاف func1 تابع func2 هیچ پارامتری ندارد و بنابراین آرگومانی نمیپذیرد . یک متغیر سراسری با نام num داریم که مقدار 5 را به آن اختصاص دادهایم . در داخل تابع اصلی برنامه یعنی main دو تابع خود را که در ادامهٔ main تعریف نمودهایم ، فراخواندهایم که مقدار متغیر num را به عنوان آرگومان به func1 فرستادهایم تا بر روی آن پردازش انجام دهد . تابع func1 پارامتر i را دارد که توسط دستور حلقهای for ( هنوز به مبحث آن نرسیدهایم و اجمالاً آن را توضیح میدهیم ) از i به مقدار 0 تا زمانی که به مقدار num نرسیده ، i را یکی یکی اضافه میکند و مقدار آن در خروجی خط دستوری نمایش میدهد . اما تابع func2 در تلاش است تا در بدنه خود و داخل بلوک خود به متغیر i که پارامتر تابع func1 است دسترسی پیدا کند که مسلماً مجاز نیست و در صورت تلاش برای کامپایل برنامه بالا ، دیباگر به ما خواهد گفت که متغیر i در خط بیست و پنجم ناشناخته و اعلاننشده است . ولی با حذف کردن اعلان ، فراخوانی و تعریف تابعِ func2 در کد بالا و کامپایل مجدد برنامه ، پس از اجرا در محیط خطدستوری ، خروجی :
0
1
2
3
4
را مشاهده خواهید نمود
مثال پنجم:
#include<stdio.h>
void func1(void);
int main()
{
func1();
return 0;
}
void func1(void)
{
int k=8;
for(int i=0; i<5; i++)
{
for(int m = 0; m<k; m++)
{
printf("%d\n", i);
}
printf("%d\n", m);
}
}
در مثال بالا، تابع func1 را میبینید که در تعریف خود داخل بلوک خود یک متغیر قابل دسترسی برای تمام بلوکهای داخل خود با نام k دارد. حلقه for اولی که بیرونیتر است یک متغیر را اعلان و تعریف نموده است تا از مقدار 0 تا قبل از 5 که میشود ۴ حلقه for داخلیتر خود را تکرار کند ( که میشود 5 بار ) و در هر بار تکرار مقدار متغیر m را که داخل حلقه for داخلیتر تعریف شده است در خروجی خط دستوری نمایش دهد ؛ اما همین مسئله باعث میشود تا کامپایلر از ما خطا بگیرد که m اعلاننشده است . پس خط ۲۱ را که شامل کد ;printf("%d\n", m) میشود حذف نمائید و سپس کد را کامپایل کنید. خواهد دید که حلقه for داخلیتر ۸ بار به اجرا در میاید ( که خود این عمل ۵ بار تکرار میشود ) و از m مساوی 0 شروع میکند و تا m مساوی 7 ادامه میدهد و در هر بار i را نمایش میدهد منتها خود for داخلیتر ۵ بار اجرا و تکرار میشود و در هر بار مقدار i از 0 تا 4 افزایش مییابد پس عددهای 0 تا 4 هر کدام ۸ بار نمایش داده میشوند . هنوز به مباحث دستورها نرسیدهایم . این مثال تنها برای یاد گیری حوزه دید در زبان C میباشد . همان طور که در مثال بالا مشاهده نمودید متغیر k در داخل بلوکها قابل دسترسی بود ولی حلقه for بیرونیتر به متغیر حلقه for داخل خود دسترسی نداشت ( یعنی متغیر m ) اما حلقه for داخلی به متغیر i که داخل حلقه بیرونی تعریف شده است دسترسی دارد
دقت داشته باشید که متغیرهای سراسری پس از اعلان توسط کامپایلر مقدار 0 میگیرند مگر اینکه شما در ادامه به آنها مقدار معینی بدهید که در واقع آنها را تعریف میکنید ؛ همچنین متغیرهای کاراکتری سراسری مقدار پوچ گرفته ( '0\' ) و اشارهگرهای سراسری مقدار تهی میگیرند ( NULL ) اما متغیرهای محلی در صورتی که به آنها مقداری تخصیص ندهید و فقط اعلانشان کنید مقداری آشغال و زباله میگیرند (garbage value) . مقادیر زباله ، اصطلاحاً مقادیری هستند که در حافظه موقت کامپیوتر ، متعلق به برنامههای دیگری هستند ؛ بنابراین زمانی که متغیر محلی را اعلان میکنید ولی آن را تعریف نمیکنید ، متغیر اشاره میکند به یک خانه تصادفی در حافظه موقت که ممکن هست هر مقداری داخل آن موجود باشد . بنابراین فراموش نکنید که متغیرها را باید حتماً تعریف کنید ، متغیرهای سراسری تعریف نشده را استفاده نکنید و باید به آنها مقداری را تخصیص داده و سپس پردازش خود را اعمال کنید
دقت کنید
شما در اعلان و تعریف هر متغیر ، باید به مفهوم تعیینکننده کلاس ذخیرهای که میخواهید استفاده کنید ، توجه داشته باشید . مثلاً وقتی یک متغیر را static میکنید ، دیگر نمیتوانید آن را extern کنید . اما میتوانید یک متغیر ایستا را ( static ) رجیستر کنید ( register ) ، متغیرهای خارجی را ( extern ) نیز میتوانید با register تعریف کنید تا در رجیسترهای کش CPU ذخیره شوند . در هر قسمت از این مبحث بیشتر در مورد هر تعیینکننده کلاس ذخیره توضیح میدهیم
متغیرهای خود کار auto
ویرایشکلیدواژه auto که مخفف automatic میباشد باعث خودکار شدن متغیر میشود تا متغیر ما محلی باشد . بدین معنا که داخل بلوک قابل دسترسی و خارج از آن غیرقابل دسترس باشد و همچنین پس از به پایان رسیدن عمل تابع ، متغیر را از بین میبرد . متغیرهای محلّی به صورت پیشفرض متغیرهایی خودکار ( automatic variables ) هستند اما اگر متغیر سراسریای در متن برنامه خود داشته باشید که بخواهید آن را داخل یک یا چند بلوک به صورت محلی در بیاورید میتوانید داخل بلوک یا بلوکها همان متغیر سراسری را به صورت خودکار اعلان و تعریف نمائید (با نوشتن کلیدواژه auto پیش از نوع داده و شناسه متغیر مورد نظر ) این امر چندان به کار نمیآید و ریسک پذیر است و ممکن است شما را به خطا بیاندازد ؛ اما گاهی برای اینکه یک داده چند صورت داشته باشد و در هر صورت خود یک کار منحصر به فرد خود را انجام دهد از این ترفند استفاده میکنیم مشابه همین مسئله در مورد پیوندسازی داخلی ( internal linkage ) ، توسط کلیدواژه static در مورد متغیرهای سراسری، صادق است. یعنی چنین متغیری با همان نام ( شناسه ) و با همان نوع داده یا نوع دیگری از داده را داخل یک متن کد دیگر اعلان و تعریف میکنید تا کاربرد دیگری داشته باشد ( به عنوان مثال تابع func1 در داخل متن برنامه فعلی با یک روش مرتبسازی میکند - فهرست کردن - و تابع دیگری در متن دیگری از برنامههای پروژه با همان نام یعنی func1 تعریف میکنید تا به نحو دیگر مرتبسازی کند ) که البته به مبتدیها توصیه نمیشود و به راحتی ممکن است باعث اشتباه گرفته شدن دو یا چند داده ( که میتواند تابع نیز باشد ) با همدیگر میشود . برای دسترسی جداگانه به چنین دادههایی که نام یکسانی دارند باید به تعداد متناسب با آنها اشارهگر سراسری تعریف کنید که به آنها اشاره داده شدهاند و در هر جای برنامه که به هر کدام احتیاج داشتید از اشارهگرِ اشارهکننده به آن دادهها استفاده کنید
مثال:
# include <stdio.h>
void func1(void);
void func2(void);
int m = 15;
int main()
{
func1();
func2();
return 0;
}
void func1(void)
{
auto int m = 17;
printf("%d\n", m);
}
void func2(void)
{
printf("%d\n", m);
}
در مثال بالا متغیر m سراسری است و به آن مقدار 15 اختصاص داده شده است . تابع func1 متغیری با همان نام در داخل بلوک خود تعریف نموده است که میدانید محلی است و پس از فراخوانی تابع func1 در تابع main مقدار 17 را در خروجی نمایش خواهد داد . اما با فراخوانی تابع func2 مقداری که نمایش داده میشود ، همان مقدار 15 است که در داخل متغیر سراسری m وجود دارد ؛ بنابراین مقدار m در سرتاسر برنامه به غیر از داخل بلوک تابع func1 ( که داخل آن متغیری با همان نام اما محلی ایجاد کردهایم ) همان مقدار 15 را خواهد داشت اما متغیری که داخل تابع func1 محلی شده است ، مقدار جداگانه 17 را در خود جای داده است . با پایان یافتن تابع func1 که داخل تابع main فراخوانی شده است ، متغیر محلی ما از بین میرود اما متغیر سراسری تا آخرین لحظهای که برنامه در حال اجرا است داخل حافظه موقت باقی میماند
متغیرهای ایستا static
ویرایشکلیدواژه static که به معنی ایستا میباشد دو کاربرد و مفهوم در زبان C دارد:
۱ - ایستا کردن متغیرهای محلی ؛ بدین معنی که مقدار متغیر مورد نظر پس از پایان یافتن بلوک تابع از بین نمیرود و در داخل حافظه موقت تا انتهای زمان اجرای برنامه باقی میماند . بنابراین با نوشتن کلیدواژه static پیش از نوع داده متغیر خود ، به کامپایلر تفهیم میکنید که در فایل اجرایی ، برنامهای را ایجاد کند که در زمان اجرای برنامه ، متغیر شما ایستا باشد تا مقدار آن از بین نرود . متغیرهای ایستا ، همانند متغیرهای سراسری ، در زمان اعلان مقدار 0 میگیرند که در صورت مقداردهی توسط شما ، مقدار داده شده را خواهند گرفت ( که در صورت عدم مقدار دهی ، همانند متغیرهای سراسری در مورد کاراکترها مقدار پوچ و در مورد اشارهگرها مقدار تهی - NULL - میگیرند )
در زبانهایی مثل C متغیرهای ایستا طبق الگوی سیستم عامل در Data Segment یعنی سگمنت به خصوصی از حافظه که به آن Data Segment میگویند ، ذخیره میشوند ( برای کسب اطلاعات بیشتر در مورد الگوی اختصاص حافظه موقت توسط سیستم عامل که مبتنی بر الگوهای سختافزاری نیز میباشد ؛ به صفحه رو به رو مراجعه کنید: https://en.wikipedia.org/wiki/Data_segment ) در نوشتن سیستم عامل نیز باید الگوی مشخصی برای تخصیص حافظه موقت توسط برنامهها داشته باشید که این الگو از دیرباز تا کنون در چارچوب خاصی بوده است که در صفحهای که به آن ارجاع دادهایم و پیوندهای آن مفصلاً توضیح داده شده است
در زبان C ، طبق استاندارد به متغیرهای ایستا ، تنها میتوانید مقادیر صریحی بدهید و نمیتوانید مقدار آنها را یک تابع قرار دهید تا مقدار خروجی آن در متغیر ایستا ذخیره شود
مثال:
#include <stdio.h>
void func1(void);
void func2(void);
int count = 5;
int main()
{
func2();
return 0;
}
void func1(void)
{
static int i = 1;
printf("%d time(s) func1 loaded\n", i);
i++;
}
void func2(void)
{
while(count>0)
{
func1();
count--;
}
}
یکی از کاربردهای متغیرهای ایستا این است که بررسی کنیم تابع چند بار به اجرا در آمده است . در مثال بالا برنامه در صورت کامپایل و اجرا در هر دقعه نشان میدهد که چندمین بار است که تابع func1 به اجرا در آمده است . از آنجایی که هنوز به برخی از مباحث نرسیدهایم به صورت اختصار میگوئیم که تابع func1 متغیر ایستا i را تعریف نموده و آن را در خروجی خطدستوری چاپ می کند و سپس یک واحد به مقدار i اضافه میکند ؛ تابع func2 پنج دفعه تابع func1 را با حلقه while به اجرا در میآورد . اما دقت کنید که اگر متغیر i ایستا نبود در هر دفعه اجرا با پیام :
1 time(s) func1 loaded
مواجه میشدید ، چرا که مقدار آن در هر بار پایان یافتن تابع از بین میرفت و با فراخوانی مجدد تابع ، مقدار 1 میگرفت ؛ به خاطر اینکه متغیر محلی به صورت پیش فرض ، غیرایستا میشد
نکته : شما نمیتوانید همزمان یک متغیر محلی را auto و static اعلان یا تعریف کنید
۲ - دومین کاربرد کلیدواژه static اختصاصی کردن متغیر سراسری در متن فعلی برنامه میباشد . همان طور که پیشتر گفتیم ، در نوشتن برنامهها برای بهتر خوانده شدن آن یا تیمی انجام دادن پروژه ؛ شما ناچارید تا برنامه را در چندین ، چند صد و یا حتی چند هزار فایل متن برنامه مجزا بنویسید . اگر در یکی از این فایلهای متنیِ برنامه خود ، یک متغیر سراسری را ( که میتواند یک تابع باشد ) به صورت static تعریف کنید از حوزه دید بقیه فایلها ، پنهان میماند و اگر از آن متغیر بخواهید استفاده کنید کامپایلر از شما ایراد خواهد گرفت که متغیر مورد نظر اعلان نشده است و شناسه مورد استفاده ناشناس است . کاربرد این نوع متغیرها را در مبحث auto کمی بیان نمودیم . گاهی میخواهید تا از یک نام برای یک تابع یا متغیر ، چند استفاده مختلف بکنید و شناسه شما بیانگر چند نوع عملکرد باشد . بدین ترتیب میتوانید یک نام را برای چند متغیر و یا تابع مجزا در نظر بگیرید و آنها را اعلان و تعریف کنید . البته دقت کنید که این مسئله توسط استاندارد تعریف شده است و بعضاً هم به کار میرود اما استفاده از آن به مبتدیها توصیه نمیشود و ممکن است باعث خطای شما در برنامه شود
اگر بخواهید از همه یا چند تا از این دادههایی که پیوند داخلی شدهاند ( internal linkage ) داخل هر یک از فایلهای برنامه استفاده کنید ، باید از اشارهگرها استفاده کنید تا مثلاً در متن فعلی از متغیر سراسری static خود استفاده کنید و در ادامه از متغیر سراسری static دیگری با همان نام که در فایل متن دیگری تعریف شده است استفاده کنید و یا متغیر غیر تابع شما مقدار خود را به عنوان خروجی یک تابع بازگرداند تا تابع حاوی آن متغیر را فراخوانی کنید
مثال:
cat.c
#include<stdio.h>
static char cat[6] = "pretty";
void pretty(void)
{
printf("The Cat is %s\n", cat);
}
main.c
#include<stdio.h>
extern void pretty();
int main()
{
pretty();
int cat = 9;
printf("%d\n", cat);
return 0;
}
اگر دو فایل بالا را کامپایل کنیم ، در خروجی برنامه اجرا شده متن The cat is pretty و سپس عدد 9 را مشاهده خواهیم نمود . ( دقت کنید که در هنگام کامپایل ، آدرس و نام هر دو فایل را به کامپایلر بدهید یا اینکه اگر از IDE استفاده می کنید هر دو فایل را ضمیمه پروژه کنید ) در دو فایل بالا : در فایل اولی با نام cat متغیر cat یک رشته است که درون خود واژه pretty را ذخیره کرده است و در فایل دوم متغیر cat یک متغیر از نوع صحیح است که مقدار 9 را در خود ذخیره کرده است و جدا از cat در فایل اولی می باشد و ما هر دوی آنها در خروجی خط دستوری نمایش دادهایم . البته در این قطعه کد از کلیدواژه extern استفاده نمودیم که در مبحث بعدی تشریح خواهد شد ؛ اما کلیدواژه extern برای دسترسی به تابع pretty که در فایل دیگری تعریف شده است میباشد
متغیرهای خارجی external
ویرایشاگر در یک فایل برنامه C ، یک متغیر ( که خود میتواند یک تابع باشد ) را با کلیدواژه extern اعلان کنید آنگاه کامپایلر ، در طول فایلهای دیگر پروژه به دنبال تعریف آن متغیر یا تابع خواهد گشت تا بدین ترتیب تعریف آن را در فایلهای دیگر پیدا کند تا در نهایت در فایلی که آن داده را اعلان و استفاده کردهاید بتواند به کار ببندد . بنابراین هر گاه در یک پروژه ، تعریف یک متغیر یا تابع در فایل دیگری قرار داشت ، در فایل فعلی خود پیش از استفاده از آن متغیر یا تابع ، کلیدواژه extern را بنویسید و سپس نوع داده و در نهایت شناسه آن را نوشته تا اعلان شود و سپس از آن استفاده کنید ( که در صورتی که تابع نیز هست به همراه پارامترهای آن بنویسید و آن را اعلان کنید )
مثال:
show.c
#include<stdio.h>
void show(void)
{
int a = 56;
printf("%d\n", a);
}
main.c
#include<stdio.h>
extern void show(void);
int main()
{
show();
return 0;
}
اگر دو فایل بالا را همزمان به کامپایلر خود بدهید تا کامپایل کند و برنامهء کامپایل شده را اجرا کنید عدد 56 را بر روی خروجی خطدستوری مشاهده خواهید نمود . در فایل اول یعنی show ، تابع show را تعریف نمودیم تا با تعریف متغیر a در خود و دادن مقدار 56 به آن ، مقدار متغیر را نمایش دهد . در فایل اصلی خود ، یعنی فایل main تابع show را با کلیدواژه extern اعلان نمودیم ؛ بدین معنا که تابع در فایل دیگری تعریف شده است . سپس آن را داخل تابع main ، فراخوانی نمودیم دقت کنید که در پروژههای خیلی بزرگ فایلهای متعددی برای برنامه که ممکن است وجود داشته باشند که خود ممکن است داخل فولدرهای بسیار متعددی نیز باشند . در یک IDE تنها نوشتن کلیدواژه extern کفایت میکند و IDE خود ، داخل فایلهای پروژه به دنبال تعریف داده ما که یک متغیر یا تابع است خواهد گشت . اما اگر قصد دارید که خودتان بدون IDE تنها فایل اصلی برنامه را کامپایل کنید تا کامپایلر فایلهای دیگر را بیابد ، باید آنها را داخل فایل اصلی برنامه ضمیمه نیز بکنید . برای ضمیمه کردن فایلهای دیگر برنامه در فایل اصلی ، باید آنها را با دستور مستقیم include به همراه یک جفت دابل کوت ( " ) مثل :
#include "show.c"
در ابتدای برنامه خود ( مثل main.c ) ضمیمه کنید که در صورتی که در فولدر دیگری به غیر از از فولدر فایل اصلی برنامه است باید آدرس کامل آن را بنویسید و اگر فایل برنامهای در داخل فولدری در آدرس فایل اصلی برنامه قرار دارد ، با نوشتن نام فولدر و یک اسلش / و نام آن فایل ، به برنامه ضمیمهاش کنید ( مثل "include "progfolder/test.c# ) حتی ممکن است خود فولدر فایل اصلی ، فولدرهای دیگری هم داخل خود داشته باشد که فایلهای برنامه ما در آن جا قرار دارند که باید با کمک عملگر آدرس دهی سامانه فایل بندی ( که معمولاً اسلس است / ) آدرس آنها را بنویسیم ( مثلاً "include "progfolder/myapp/sorting.c# در اینجا فایل اصلی برنامه داخل یک آدرس است که فولدری به نام progfolder دارد که داخل آن فولدر دیگری با نام myapp وجود دارد و فایل برنامه ما یعنی sorting.c در آنجا ذخیره شده است ) همچنین ممکن است در داخل هر فایلِ برنامهای که میخواهیم ضمیمه کنیم ، فایلهای دیگری ضمیمه شده باشند ( به فایلهای برنامه اصطلاحاً ماژول module میگویند )
مثال دوم:
value.c
int g = 91;
main.c
#include<stdio.h>
extern int g;
int main()
{
printf("%d\n", g);
return 0;
}
در مثال بالا متغیر g ، در فایل value تعریف شده است و در فایل اصلی آن را با کلیدواژه extern اعلان نموده و سپس در تابع main از آن استفاده نمودهایم که در صورت کامپایل و اجرا عدد 91 را در خروجی خطدستوری خواهید دید
دقت کنید : طبق استاندراد ، اگر داخل یک تابع برای یک متغیر از کلیدواژه extern استفاده کنید ، مجاز نیستید تا داخل تابع به آن مقداری اختصاص بدهید و البته متغیر همچنان محلی باقی میماند و تنها به کمک اشارهگرها میتوانید به آن متغیر دسترسی پیدا کنید
دقت کنید : شما نمیتوانید یک متغیر را همزمان extern و static اعلان یا تعریف کنید . همچنین اگر متغیر سراسری را حتی خارجی ( external ) و به اصطلاح دارای پیوند خارجی اعلان کنید ، همچنان میتوانید آن را داخل تابعی که میخواهید با کلیدواژه auto محلی کنید و متغیر خود را از متغیر سراسری جدا کنید و اگر متغیری سراسری static باشد ، پیوند آن با فایلهای دیگر پروژه از بین میرود و همان طور که در بحث متغیرهای ایستا گفتیم ، اختصاصی میشود . بنابراین دسترسی به آن حتی با اعلان کردن آن با کمک کلیدواژه extern میسر نمیباشد
متغیرهای ثبتی register
ویرایشاز کلیدواژه register ، تنها مجاز هستید تا در بدنه تابع ( داخل آکولادهای آن ) برای دادهها استفاده کنید که در صورت استفاده ، ممکن است کامپایلر متغیر و داده شما را در برنامه خروجی بر روی رجسیترهای CPU ثبت کند . رجیسترهای CPU ، حافظه نهان یا همان cache ( کش ) واحد پردازنده مرکزی کامپیوتر هستند که دسترسی به آنها برای پردازشگرها ، بسیار راحتتر و سریعتر از حافظه موقت کامپیوتر ( رم RAM ) میباشد . گفتیم : ممکن است ! چرا که استاندارد خاصی برای آن وجود ندارد و هر نویسنده بهکارگیرنده زبان C آن را به صورت دلخواه نوشته است . بنابراین در مواقعی ممکن است کامپایلر کلیدواژه register را نادیده بگیرد و ممکن است بدون نوشتن register آن را داخل کش CPU ثبت کند . عموماً در برنامههای سطح پائین مثل کرنل و ماژولهای کرنل ( در سیستم عاملی مثل لینوکس ) یا درایورها ( که در سیسم عاملهای ویندوز از مایکروسافت ، متدوال هستند ) کلیدواژه ، ترتیب اثر داده میشود و همچنین در متغیرهایی که نقش شمارنده را دارند و به هر نحوی ممکن است نیاز به دسترسی سریعتر داشته باشد ، کامپایلر آن را در کش ، ثبت میکند . برای اطلاعات دقیقتر به راهنمای کامپایلر خود مراجعه کنید
مثال:
# include<stdio.h>
int main()
{
register int i;
for(i=0; i<6; i++)
{
printf("%d\n", i);
}
return 0;
}
دقت کنید : از آنجایی که متغیرهای register در کش CPU ذخیره میشوند ؛ دسترسی به آنها از طریق یک اشارهگر و یا عملگر آدرس ( یعنی & ) امکانپذیر نیست ، پس از اشارهکردن و تلاش برای به دست آوردن آدرس متغیرهایی که ثبت شدهاند ( register شدهاند ) بپرهیزید . در صورتی که کامپایلر از شما خطا نگیرد ، برنامه شما دچار اختلال خواهد شد
متغیرهای فرّار volatile
ویرایشدر بحث اشارهگرها کمی به مبحث متغیرهای فرّار پرداختیم . مبحث کاملاً سادهای است . زمانی که میخواهید برنامهنویسی سطح پائین انجام بدهید و در محلی که سختافزار از دادهها استفاده میکند ، دادهای را ذخیره کنید ؛ اگر لازم است که دسترسی به آن توسط سختافزار صورت بپذیرد و اگر شما آن را volatile یا همان فرار تعریف نکرده باشید ، اختلال نرمافزاری رخ می دهد و باید آن را فرار تعریف کنید . همچنین در نوشتن برنامههایی مثل RAM Editor یا در هر جایی که بخواهید به خانههای خاصی از حافظه موقت دسترسی پیدا کنید ، مخصوصاً اگر خانههای ابتدایی باشند و بخواهید مقدار آن را تغییر بدهید ، بهتر است آن را volatile تعریف کنید تا در صورتی که با سیستمعامل یا سختافزار تداخل ایجاد میکند ؛ آنها خود به خود آن را اصلاح کنند . کلیدواژه volatile را میتوانید پیش از نوع داده یا پس از نوع داده و پیش از شناسه بنویسید . این کلیدواژه در برنامهنویسیهای سطح پائین و در اشارهگرهایی که به خانههای خاصی از حافظه موقت اشاره میکنند و همین طور برخی از تابعهای کتابخانهای که مربوط به پردازش سیگنالها میشود ، کاربرد زیادی دارد
مثال:
volatile int *ptr = (int *)0x1af4e6;
در این مثال اشارهگر ptr از نوع صحیح فرّار به خانه ۱٫۷۶۶٫۶۳۰ ـُم حافظه موقت اشاره می کند _______________
نکته : در استاندارد C11 نوع جدیدی از کلاس ذخیره با نام Thread_local_ در فایل سرآیند threads.h تعریف شده است که در مبحث مربوطه ( یعنی فایل سرآیند threads ) به آن میپردازیم