زبان برنامه نویسی سی/اشارهگر
اشاره گر Pointer
ویرایشدر دانش برنامهنویسی ، اشارهگر به نوعی از داده میگویند که به محل ذخیره دادهای دیگر بر روی حافظه اشاره میکند و به محتویات آن داده دسترسی دارد . از اشارهگرها به صورت عمده ، برای تسریع در روند برنامه ، مخصوصاً تغییر محتوای دادههای دیگر ، تخصیص حافظه به دادهها به صورت هوشمند و پویا و دسترسی و تغییر محتویات دادههای تابع استفاده میشود . ایجاد و استفاده از اشارهگرها ، شیوهای در زبان C میباشد که آن را به زبان های سطح پائین ، نزدیک مینماید و از این روی درک مفهوم اشارهگر کمی نسبت به مباحث دیگر ، زمان بیشتری میبرد . اما فراموش نکنید که اگر مفهوم اشارهگر را درک نکنید و نخواهید از آن در برنامهنویسی خود استفاده کنید ، در واقع قادر به نوشتن برنامه های روزمرّه کامپیوتری نخواهید بود و تنها قابلیتهای کوچکی از زبان C را میتوانید به کار ببندید
نحوه اعلان اشارهگر بدین شکل میباشد :
data-type *name;
ابتدا نوع داده را مینویسیم ، سپس یک علامت استریسک یا ستاره ( * ) میگذاریم و سپس بدون فاصله یا با فاصله ، نام اشارهگر خود را مینویسیم . هر اشارهگر ، پیش از استفاده باید به دادهای اشاره نماید و اگر پیش از اشاره دادن ، آن را استفاده نمائید برنامه شما دچار اختلال ، توقف و یا شکست ( crash ) خواهد شد و این فقط یکی از عوارض استفاده بد از اشارهگرها است ، یکی دیگر از عوارض استفاده بد که ناشی از عدم تطابق اندازه اشارهگر و داده مورد شده است ، نشتی حافظه (memory leak) است . اگر در هنگام ایجاد اشارهگر ، هنوز نمیدانید که باید به کجا اشاره کند و یا هنوز مصمم به اشاره دادن آن به دادهای در برنامه خود نیستید ؛ بهتر است مقدار آن را تهی یا نول ( NULL ) قرار دهید
int *ptr = NULL
بدین شکل ، اشارهگر شما به خانهای از حافظه اشاره مینماید که توسط کامپیوتر یا سیستم عامل شما از قبل اشغال شده است . دقت کنید که این روش فقط برای مقدار دهی اولیه اشارهگر میباشد و در صورتی که بخواهید از اشارهگر خود استفاده نمائید ، حتماً باید آن را به دادهای مشخص ( تعریف شده ) اشاره دهید ، چرا که مجاز به تغییر در محتوای آن نیستید
نکته :
NULL یک مقدار تعریف شده ثابت در فایل سرآیند stdio.h میباشد ، بنابراین برای استفاده از آن باید فایل سرآیند مذکور را به برنامه خود ضمیمه کنید . NULL به معنی تهی میباشد و در برنامهنویسی به عنوان مقدار تهی به کار میرود ، اما اگر به عنوان مقدار یک اشاره گر تعیین شود ؛ به خانه ای از حافظه اشاره میکند که غیر قابل دسترسی است
نحوه اعلان یک اشارهگر را در بالا بیان نمودیم که باید از عملگر متناظر خود که علامت استریسک یا ستاره ( * ) میباشد بهره بگیریم . اما برای مقدار دهی اشارهگر که به یک داده اشاره نماید باید از عملگر دیگری که عملگر آدرس دهی یا آدرس نام دارد و با علامت امپرسند ( & ) نوشته میشود ، بهره جوئیم . برای این منظور ابتدا باید دادهای که پیش از اشاره داده شدن در برنامه موجود باشد را منظور نمائیم ، سپس برای تعریف اشارهگرمان ، آن را به واسطه عملگر آدرس به داده مورد نظر خود اشاره دهیم
مثال :
int a = 5;
int *ptr;
ptr = &a;
دقت کنید که فقط در اعلان اشارهگر از عملگر اشارهگر که علامت ستاره میباشد ، استفاده مینمائیم و زمانی که میخواهیم آن را مقدار دهی نمائیم نباید علامتی برای نام آن بگذاریم . پس تنها ، نام اشارهگر را مینویسیم و بعد از علامت تساوی ، عملگر آدرسدهی را مینویسم و بدون فاصله ، نام دادهای را که میخواهیم اشارهگر به آن اشاره نماید را قرار میدهیم . در اینجا به یک متغیر صحیح اشاره نمودیم . دقت کنید که نوع داده اشارهگر با نوع دادهای که میخواهد به آن اشاره کند باید یکسان باشد
مقداری که به یک اشارهگر توسط عملگر آدرس داده می شود ، شماره یا همان آدرس حافظه موقت دادهای است که به آن اشاره مینماید و در مبنای شانزده شانزدهی یا همان هگزادسیمال میباشد . مثل : 0xbfffdac4 یا 0xbfffdac0
در ادامه اگر بخواهیم به جای استفاده از داده خود ، از اشارهگرِ به آن داده برای فراخوانی داده استفاده نمائیم یا بخواهیم مقدار آن داده را تغییر دهیم ( که به این عمل بازارجاع Derefernce یا غیر مستقیم کردن Indirection میگویند ) باید پس از اشاره دادن اشارهگر به داده خود ( که بدون علامت استریسک بود ) بار دیگر از عملگر اشارهگر ( * ) استفاده نمائیم
مثال :
int *my_pointer;
int barny;
my_pointer = &barny;
*my_pointer = 3;
در خط اوّل یک اشارهگر از نوع صحیح اعلان نمودیم . در خط دوّم یک متغیر از نوع صحیح با نام barny ایجاد نمودیم . در خط سوّم اشاره گر my_pointer را به متغیر barny اشاره دادیم . در خط پایانی نیز با روش indirection یا derefernce که روش غیر مستقیم یا بازارجاع می باشد ، مقدار 3 را به عنوان مقدار و موجودی در barny قرار دادیم ( در واقع به محتویات متغیر barny دسترسی یافته و مقدار 3 را داخل آن گذاشتیم )
نکته :
عمل غیرمستقیم کردن یا بازارجاع ، فقط مقدار و موجودی داده اشاره شده را تغییر میدهد و هیچ تغییری در مقدار اشاره گر ( که آدرس خانه حافظه دادهای که به آن اشاره میکند ، میباشد ) ایجاد نمی نماید
اشاره گر ها به همراه اعمال ریاضی و منطقی
ویرایشبر روی اشارهگرها می توان برخی از اعمال ریاضی و منطقی را نیز انجام داد . این اعمال شامل افزایش ، کاهش ، جمع ، تفریق و مقایسه می شود . از آنجایی که هنوز به مباحث عملگر ها نرسیدهایم ، فعلاً نگاهی به لیست عملگر هایی که می نویسیم بیاندازید ( درک آنها هم بسیار ساده است ) اما پس از رسیدن به فصل عملگر ها و اتمام آن ، اگر در مبحث فعلی مشکلی در درک مطلب داشتید ، بازگردید و نگاهی دوباره بیاندازید . عملگر افزایش ++ و عملگر کاهش -- در هر بار اجرا ، یک بار ، به ترتیب ، مقدار عملوند خود را ( داده ای که عملگر بر روی آن عمل می کند ) افزایش یا کاهش می دهند ( مثلاً اگر متغیر a مقدار 1 داشته باشد و از عملگر افزایشی به صورت ++a استفاده نمائیم ، مقدار a به 2 تغییر خواهد یافت ؛ عکس آن نیز عملگر کاهش می باشد )
لیست عملگر های قابل استفاده بر روی اشاره گر ها :
- ++
- --
- +
- -
- =+
- =-
- ==
- =!
- >
- =>
- <
- =<
علاوه بر عملگر افزایش ( ++ ) و کاهش ( −− ) میتوانیم به وسیله عملگر جمع ( + ) یا تفریق ( - ) مقدار مشخصی را به اشارهگر اضافه ، یا از آن کم کنیم . عملگر =+ مقداری را که در سمت چپ نوشته میشود با سمت راست تساوی جمع میکند و حاصل را در سمت چپ تساوی قرار میدهد و عکس این عمل را =- انجام میدهد ( یعنی سمت جپ را از سمت راست کم می کند و حاصل را در سمت چپ قرار میدهد ) . پنج مورد آخر نیز عملگر های منطقی هستند که وظیفه آنها مقایسه دو سمت تساوی میباشد که اگر سمت چپ کوچکتر باشد یا کوچکتر مساوی سمت راست عملگر باشد ( به ترتیب > و => ) آنگاه شرط ما برقرار است و پاسخ صحیح میباشد که میتوانیم در برنامه خود به کمک دستور های شرطی یا حلقهها از آنها بهره جوئیم تا با بررسی کوچکتر ، کوچکتر مساوی ، تساوی و یا بزرگتر یا بزرگتر مساوی بودن عملوندهای خود و در صورت برقراری شرط ، عملیاتهایی را تعریف کنیم تا انجام شوند و یا در صورت عدم صحت عملیات دیگری انجام شوند یا حلقه متوقف شود ( شکسته شود )
دقت کنید که در عملیات ریاضی به واسطه عملگرها بر روی اشارهگر بر خلاف عملوندهای دیگر مثل متغیرهای پایه ، هر یک واحد که به اشارهگر اضافه گردد ، در واقع به اندازه یک واحد از اندازه نوع داده اشارهگر به آن اضافه خواهد گردید ( کم کردن و تفریق نیز به همین روال میباشد ) یعنی مثلاً اگر یک اشارهگر از نوع صحیح ( int ) داشته باشیم و از عملگر ++ استفاده نمائیم ، در سیستم های 32 بیتی قدیمی 2 بایت و در سیستمهای جدیدتر و 64 بیتی 4 بایت ، اشارهگر ما جا به جا می شود . دقت کنید که این افزایش و یا کاهش دقیقاً در مقدار اشارهگر که آدرس دادهای که به آن اشاره میکند ، میباشد . یعنی اگر به ابتدای خانه 1080 از حافظه موقت اشاره کرده باشد در صورتی که یک واحد به آن اضافه شود به ابتدای خانه 1082 ( در سیستم های 32 بیتی قدیمی ) و یا 1084 ( در سیستم های 64 بیتی و جدید ) اشاره خواهد نمود . از ابتدای خانه تا انتهای ظرفیت اشارهگر که به داده ای اشاره کرده ، در دسترس اشارهگر قرار دارد . به همین شکل اگر در مثال قبل ، اشارهگری به نام h که به خانه 1080 اشاره مینماید ، در متن منبع جهت عملیات محاسباتی یا همان ریاضی ، بنویسیم h-1 ، آنگاه اشاره گر به خانه 1078 ( در سیستم های 32 بیتی قدیمی ) یا 1076 ( در سیستم های 64 بیتی و جدید ) اشاره خواهد نمود و آن خانهها را در سلطه خود خواهد داشت . دقت کنید که این اعدادی که می نویسیم جهت سهولت در درک مطلب می باشد و آدرس های حافظه کامپیوتر به صورت هگزادسیمال ( شانزده شانزدهی ) نام گذاری میشوند و مورد دسترسی قرار می گیرند ( که کمی پیشتر دو مثال از آن را نوشتیم )
در مورد عملگرهای مقایسه ای نیز لازم است بدانید اگر قصد مقایسه اشارهگری به ساختمان ، اجتماع یا آرایهای را دارید ، اشارهگرهای دیگر شما در عملیاتهای مقایسه ، همگی باید به همان ساختمان ، اجتماع یا آرایه اشاره نمایند . بنابراین مقایسه یک اشارهگر به آرایه a با اشارهگری به آرایه b مطابق با استاندارد C خطا میباشد . همچنین مطابق با استاندارد ، شما تنها مجاز به مقایسه اشارهگر های از نوع یکسان میباشید ولی برخی از کامپایلرها اجازه مقایسه چند نوع مختلف از اشارهگرها ( مثلاً یک اشارهگر int با یک اشارهگر long و یا یک اشارهگر char ) را میدهند
اشاره گر ها و آرایه ها
ویرایشدر مبحث پیشین ( یعنی مبحث آرایه ) گفتیم که آرایهها در زبان C به صورت اشارهگر تعریف میشوند و از این روی ارتباط تنگاتنگی با اشارهگرها دارند . بنابراین در مورد آرایهها باید بدانید که : از آنجایی که آرایهها به واسطه اشارهگرها در کامپایلر نوشته میشوند ، اگر یک آرایه تعریف کنید و سپس نام آرایه را بدون اندیس ( و کروشههایش ) بنویسید ، یک اشارهگر ثابت خواهد بود که آدرس اولین خانه آن آرایه را در خود ذخیره میکند ( که میدانیم اولین خانه آرایه می شود عنصر اول و اندیس 0 آن آرایه )
شکل استفاده از آرایه به وسیله اشارهگر به شکل زیر میباشد :
*(array-name + number)
بنابراین اگر آرایه ای به نام a :
int a[5];
با ۵ عنصر آزاد داشته باشیم ( و ۶ عنصر با عنصر آخر که NULL می باشد ) ، آنگاه اگر بنویسیم (a + 2)* به سومین خانه آرایه اشاره خواهد نمود که همانند [2]a میباشد . دقت کنید که علیرغم تعریف آرایه از روی اشارهگر ، شما نمیتوانید از آرایهها به عنوان اشارهگر استفاده کنید ! ( در واقع آرایهها از روی اشاره گرها تعریف میشوند ولی تنها به خانهها و عنصرهای خود اشاره میکنند )
بنابراین هر گاه بخواهیم از نام آرایه به عنوان اشارهگر به عنصرهای آرایه استفاده کنیم ( باز ارجاع یا غیر مستفیم کردن ) به وسیله عملگر اشارهگر و یک جفت پرانتز به شکل باز و بسته استفاده میکنیم که به وسلیه عملگرهای محاسباتی و اعداد به خانههای آرایه دسترسی پیدا میکنیم . مثلاً (a+1)* که به خانه دوم اشارهگر اشاره مینماید ، همانند [1]a خواهد بود ولی برای اندیس 0 آرایه ، نمینویسیم : (a+0)* یا (a)* ؛ بلکه می نویسیم : a*
نکته : در مورد اینکه نام آرایه نیز بدون اندیس ، اشارهگری به اولین خانه همان آرایه میباشد باید بدانید که در ادامه در استفاده از اشارهگرها اگر عملگر اشارهگر را بردارید ، آدرس خانه آرایه مورد اشاره خواهد بود یعنی (a+1) همانند [1]a& خواهد بود
برای اشاره به آرایههای چند بعدی نیز نام آرایه را به همراه عملگر اشارهگر و پرانتزها به اندازه ابعاد آرایه ، تو در تو میکنیم یعنی به جای استفاده از یک جفت پرانتز باز و بسته و یک عملگر اشارهگر ( استریسک * ) ؛ برای یک آرایه دو بعدی ، از دو جفت پرانتز باز و بسته و دو عملگر اشارهگر و برای آرایه سه بعدی از سه جفت پرانتز باز و بسته و سه عملگر اشارهگر ، برای غیر مستقیم کردن و بازارجاع استفاده مینمائیم . مثال :
int array[6][12];
*(*(array+5)+8) = 86;
در اینجا ما به خانه شصت و نهم آرایه اشاره کردیم ، یعنی ردیف 6 و ستون 9 از آرایه که همارز array[5][8] خواهد بود ( ۵ ردیف ۱۲ تایی به علاوه ردیف ۶ ام که به ۹ امین ستون اشاره کرده است ) دقت کنید که خانه اول آرایه همان طور که در مبحث پیشین نیز به آن اشاره کردیم اندیس 0 آرایه خواهد بود و در آرایه های چند بعدی نیز با اندیسهای 0 اُم که عنصر اول آرایه حساب میشود آغاز میشود که در آرایه چند بعدی با تک تکِ اندیس های 0 آن آرایه خواهد بود و در اینجا می شود : [0][0]array . در مبحث پیشین گفتیم که آخرین خانه آرایه توسط کامپایلر مقدار NULL میگیرد ، شما میتوانید اشارهگری به آخرین خانه آرایه اشاره دهید ولی طبق استاندارد مجاز به تغییر مقدار آن نیستید
اگر قصد استفاده از یک اشارهگر بر روی یک آرایه را دارید ، به راحتی میتوانید با تعریف یک اشارهگر که به نام آرایه اشاره میکند به عنصرهای آرایه دسترسی پیدا کنید . برای این کار اگر یک اشارهگر تعریف کنید و اشارهگر را به نام آرایه اشاره بدهید ( بدون علامت امپرسند & ) که در این صورت به عنصر اول آن آرایه که میشود اندیس 0 آن اشاره میکند و در ادامه باید با کمک عملگرهای محاسباتی و اعداد ، به خانههای بعدی اشاره کنید
مثالی از اشاره کردن به آرایه تک بعدی :
#include<stdio.h>
int main()
{
int arr[5] = { 1, 2, 3, 4, 5 };
int *ptr = arr;
printf("%d\n", (*ptr+1));
return 0;
}
در این مثال ساده یک آرایه ۵ عنصری به نام arr داریم که مقادیر نوشته شده را در خانههای خود ذخیره کرده است . سپس یک اشارهگر به نام ptr را تعریف کردهایم که آدرس arr را در خود ذخیره میکند . سپس در تابع کتابخانهای printf که در فایل سرآیند stdio تعریف شده است ( که در ابتدای برنامه آن را ضمیمه نمودیم ) به وسیله بازارجاع و غیرمستقیم کردن به عنصر دوم آرایه arr دسترسی پیدا کرده و مقدار آن در رابط خطدستوری چاپ نمودهایم ( در صورت کامپایل قطعه کد بالا عدد 2 که دومین عنصر آرایه arr است را در خروجی محیط خطدستوری خود خواهید دید )
اما اگر یک اشارهگر به صورت آرایهای تعریف کنید ( یعنی آرایهای از دادههای اشارهگر ) میتوانید همان طور که از آرایه چند بعدی به عنوان اشارهگر استفاده میکردید ( که در قسمتهای بالا نوشتیم ) ، نام اشارهگر را به جای نام آرایه بنویسید . که در مورد اشارهگری که بخواهد به آرایه چند بعدی اشاره کند باید نام اشارهگر را داخل یک جفت پرانتز باز و بسته بگذاریم و تعداد عنصرهای آرایه اشارهگر را به تعداد آخرین اندیس آرایهای که میخواهیم به آن اشاره کنیم ، تعیین نمائیم
ما برای اشارهگری از نوع آرایه ، اشارهگر را به صورت زیر تعریف میکنیم :
int *ptr[number];
اگر به جای number یک عدد که اندیس آرایه میباشد ( مثلاً عدد 5 پنج را ) قرار دهیم یا نام متغیری با مقدار 5 را قرار دهیم ، آنگاه 5 اشارهگر اعلان نمودهایم که میتوانیم بعد از مقدار دهی از آنها استفاده نمائیم
مثالی از استفاده از آرایههای اشارهگر که به آرایههای چند بعدی اشاره میکنند :
#include<stdio.h>
int main()
{
int arr[3][4] = {
{10, 11, 12, 13},
{20, 21, 22, 23},
{30, 31, 32, 33}
};
int (*ptr)[4];
ptr = arr;
printf("%p %p %p\n", ptr, ptr + 1, ptr + 2);
printf("%p %p %p\n", *ptr, *(ptr + 1), *(ptr + 2));
printf("%d %d %d\n", **ptr, *(*(ptr + 1) + 2), *(*(ptr + 2) + 3));
printf("%d %d %d\n", ptr[0][0], ptr[1][2], ptr[2][3]);
return 0;
}
دقت کنید : همان ظوز که گقتیم اگر بخواهیم از نام اشارهگر به جای نام آرایه چند بعدی استفاده کنیم در اعلان اشارهگر خود باید آن را داخل یک جفت پرانتز باز و بسته بگذاریم ( همانند قطعه کد بالا )
در مثال بالا آرایهای با نام arr تعریف کردهایم که ۳ دسته ۴ تایی از دادههای صحیح است ( که برای تعریف ، مقادیر نوشته را اختصاص دادهایم ) . سپس یک اشارهگر از نوع صحیح با ۴ عنصر به شکل آرایه اعلان نمودهایم ( همان طور که گفته شد برای اشاره به آرایههای چند بعدی باید آرایهای از اشارهگر با تعداد عنصرهای -اندیسی- به مقدار آخرین اندیس آرایه چند بعدیمان بسازیم که در اینجا ۴ بود ) . سپس برای تعریف اشارهگر خود آن را به arr بدون علامت امپرسند اشاره دادهایم ( arr یک اشارهگر است و از نوع صحیح ، بنابراین میتوانیم برای تعریف ptr آن را به شکل بالا تعریف کنیم ) پس ما ۴ اشارهگر داریم ( ptr[1] و ptr[2] و ptr[3] و ptr[4] که میتوانیم آنها را به آخرین دستههای عنصرهای arr اشاره بدهیم ) در تابع printf اول ، ptr تنها آدرس آرایه arr را دارد که arr یک اشارهگر است ، بنابراین در printf دوم هم آدرسهای خانههای حافظه را خواهیم داشت چرا که arr دو بعدی است و برای دسترسی به محتوای خانههای حافظه آن باید دو مرجله تو در تو آن را غیر مستقیم کنیم ( به مبحث اشارهگرهای تو در تو که کمی پائینتر است مراجعه کنید ) و ما میتوانیم همانند آرایه که با نام خودش استفاده میشود ، از نام اشارهگر ( به جای آرایه ) برای دسترسی به عنصرهای آرایه اشاره شده استفاده کنیم ( چه به شکل اشارهگر - در printf سوم - و چه به شکل آرایهای - در printf چهارم )
دقت کنید : اگر در اشاره دادن یک اشارهگر به یک آرایه از علامت امپرسند (&) پیش از نام آرایه استفاده کنیم ، اشارهگر ما به کل آرایه اشاره خواهد نمود و جمع و تفریق در آن به اندازه کل آرایه ، اشارهگر را جلوتر یا عقبتر میبرد و ما دیگر نمیتوانیم همانند قطعه کد بالا از آن استفاده کنیم
توجه : بحث اشارهگرهای تو در تو ( مثل اشارهگر به اشارهگر یا اشارهگر به اشارهگر به اشارهگر ) کمی پائینتر نوشته شده است . اگر یک آرایه سه بعدی داشته باشید و تنها دو بار ( دو مرحله ) از پرانتزها و عملگر اشارهگر استفاده کنید ، در واقع مقدار اشارهگر ما آدرس خانههای حافظه آن آرایه ، در مبنای شانزده شانزدهی خواهد بود
اشاره گر ها در مصاف با ساختمان ها و اجتماع ها
ویرایشجهت فرستادن و ارجاع نمونه ساختمانها یا اجتماعها به عنوان آرگومان به پارامتر تابع ، جهت دسترسی و تغییر مقدار و موجودی اعضای آنها نیازمند استفاده از اشارهگر خواهیم بود . علاوه بر سهولت کار با اعضای ساختمان یا اجتماع به وسیله اشارهگرها ، ایجاد لیستهای پیوندی ( که در اینجا فقط به آن اشاره مینمائیم ) نیازمند به کار گیری اشارهگر میباشد . جهت اشاره کردن به یک ساختمان ، یک ساختمان به روش بیان شده در مطلب مربوطه ایجاد مینمائید و به غیر از نمونه یا نمونههای غیر اشارهگر ، یک یا چند نمونه از نوع اشارهگر نیز می سازید و نمونه یا نمونههای اشارهگر را اشاره می دهید به نمونه یا نمونههای معمولی و غیر اشارهگر ساختمان ( به هر تعداد که نیاز دارید ) در مورد اجتماع نیز به همین شکل می باشد
struct sa {
int a;
char b;
} sam, *ptr;
ptr = &sam;
در مثال بالا یک نمونه از ساختمان sa با نام sam ساختیم و البته یک نمونه اشارهگر با نام ptr و سپس ptr را اشاره دادیم به نمونه غیر اشارهگر sam
جهت دسترسی به اعضای ساختمان یا اجتماع نیز از دو روش می توانید استفاده نمائید . روش اول ، روش معمول استفاده از نام نمونه اشارهگر به همراه عملگر نقطه ( . ) میباشد و روش دوم روش ابداعی در استاندارد زبان سی میباشد و روش راحتتر و سریعتری خواهد بود که در مثال زیر میتوانید استفاده از آن را مشاهده کنید :
(*ptr).a = 5;
ptr->a = 7;
ptr->b = 'g';
روش دوم همان طور که در مثال بالا مشاهده مینمائید ، روش استفاده از عملگر <- می باشد که استفاده ما از اعضای ساختمان را تسهیل مینماید
نکته دیگری که در این قسمت باقی می ماند ، روش ساخت لیست پیوندی میباشد که از آنجایی که ما در فصول مقدماتی ، فقط مبانی زبان سی را بیان میکنیم و به روشها و ترفندها نمی پردازیم ، واکاوی و تشریح این مبحث را به فصل ترفندها و تمرینهای زبان سی واگذار مینمائیم . اما بد نیست در اینجا با لیست پیوندی آشنا شوید ، چرا که بدون استفاده از اشارهگرها ، ایجاد لیست پیوندی میسر نخواهد شد
لیست پیوندی به ساختمانی میگویند که نمونه اشارهگری به خود ، در داخل خود ساختمان دارد و سپس به وسیله اعضای دیگر که از نوع اشارهگر میباشند به یکدیگر یا به اعضای غیر اشارهگر اشاره میکنند و گره هایی ( Nodes ) را ایجاد مینمایند که قابل اضافه شدن و حذف شدن و تغییر موجودی و مقدار میباشند . بنابراین هر گره از لیست پیوندی یک متغیر میباشد که به آن اشاره شده است و خود میتواند به عضو دیگری اشاره نماید . اینکه اعضا را از آخر به اول اشاره دهید یا از اول به آخر یا دو طرفه تمام اعضا را به یکدیگر اشاره دهید ، بستگی به نیاز شما خواهد داشت که کدام یک ، برای برنامه شما مناسبتر خواهد بود . هر عضو اشارهگر ( گره ) میتواند حذف شود یا به وسیله اشاره کردن ، گره دیگری را ایجاد نماید یا مقدار آن تغییر یابد . بنابراین لیست های پیوندی به اقسام مختلفی ایجاد می شوند اما همه آنها در یک مورد با یکدیگر مشترکند که دارای یک نمونه اشارهگر از خود ساختمان ، داخل ساختمان میباشند . مثال :
struct list {
int a = 6;
struct list *node;
}
این ترفند باعث میشود تا ساختمان خودش را شامل شود و به این ترتیب میتوانیم به اول یا آخر ، وسط یا هر جای دیگر ساختمان ما که لیست پیوندی شده است ، گره اضافه کنیم و سپس برای آزاد کردن فضای اشغال شده آن را حذف کنیم . از آنجایی که این مبحث نیاز به استفاده از تابع دارد ، فقط بدان اشارهای نمودیم تا با کاربردهای اشارهگر در زبان C و خانواده آن آشنا شوید
اشاره گر ها و ثابت ها
ویرایشاشاره گر های ثابت اگر در تعریف اشارهگر ، نوع داده را ثابت تعریف کنیم ، آنگاه یک اشارهگر ثابت ایجاد نمودهایم . چنین اشارهگری به محلی از حافظه اشاره می کند که در ادامه برنامه نمی توان آن اشارهگر را به محل دیگری از حافظه اشاره داد اما می توان محتوای مورد اشاره را ( خانه یا خانه هایی از حافظه که مورد اشاره قرار گرفتهاند ) را تغییر داد . به مثال زیر توجه کنید :
#include<stdio.h>
int main(void)
{
int var1 = 0, var2 = 0;
int *const ptr = &var1;
ptr = &var2;
printf("%d\n", *ptr);
return 0;
}
در مثال بالا نحوه تعریف یک اشارهگر ثابت بیان شده . یعنی ابتدا نوع داده نوشته می شود ، سپس عملگر اشارهگر نوشته می شود و در ادامه آن کلیدواژه const و سپس نام متغیر خود را مینویسیم ، در پایان نیز آن را مقداردهی مینمائیم . اما در کد بالا ، اشارهگر ثابت را دوباره مفدار دهینمودیم و آن را به متغیر دیگری اشاره دادیم و این کار بر خلاف استاندارد زبان سی می باشد . بنابراین اگر به کامپایلر دستور بدهید که کد بالا را کامپایل کند به شما اخطار خواهد داد که شما مجاز به تغییر مقدار اشارهگر ptr نیستید ؛ چرا که یک اشارهگر ثابت است
اشاره گر به مقدار ثابت
شما میتوانید اشارهگری را به یک داده ثابت ، اشاره دهید ؛ اما مجاز به بازارجاع و غیر مستفیم کردن برای تغییر مقدار و موجودی داده ثابت خود نیستید . اگر به یاد داشته باشید در مباحث پیشین بیان گردید که مقدار یک ثابت پس از تعریف آن در قسمت دیگری از کد قابل تغییر نیست ، این امر در مورد اشارهگرها نیز صادق است . به مثال زیر توجه کنید :
#include<stdio.h>
int main(void)
{
const int var1 = 0;
int* ptr = &var1;
*ptr = 1;
printf("%d\n", *ptr);
return 0;
}
در مثال بالا مقدار متغیر var1 را غیر قابل تغییر نمودیم ، سپس یک اشاره گر تعریف کرده و سعی کردیم تا با اشارهگر خود مقدار ثابت var1 را تغییر دهیم . اگر کد بالا را به کامپایلر بدهید تا کامپایل کند ، از شما خطا خواهد گرفت
اشاره گر به ثابت
اشاره گر به ثابتها ، اشارهگرهایی هستند که میتوانند به محل دیگری از حافظه اشاره کنند ، اما قادر نیستند تا محتوای محلی از حافظه را که بدان اشاره نمودهاند تغییر دهند . مثال :
#include<stdio.h>
int main(void)
{
int var1 = 0;
const int* ptr = &var1;
*ptr = 1;
printf("%d\n", *ptr);
return 0;
}
کد بالا را نیز اگر بخواهید کامپایل کنید با خطا مواجه خواهید شد . چرا که اشارهگر const int* ptr سعی نموده تا مقدار var1 را تغییر دهد
اشاره گر ثابت به یک ثابت
با توجه به مطالب بالا به راحتی می توانید حدس بزنید که این گونه از اشارهگر ، نه قادر به تغییر مقدار و موجودی داده محل حافظه مورد اشاره خود میباشد و نه میتواند به محل دیگری از حافظه اشاره کند
#include<stdio.h>
int main(void)
{
int var1 = 0,var2 = 0;
const int* const ptr = &var1;
*ptr = 1;
ptr = &var2;
printf("%d\n", *ptr);
return 0;
}
در صورتی که بخواهید کد بالا را کامپایل کنید با دو خطا مواجه خواهید شد ؛ اول اینکه سعی بر تغییر دادن مقدار var1 نموده اید و دوم اینکه سعی کرده ایده تا آدرس مورد اشاره اشارهگر ptr را به محل ذخیره var2 در حافظه ، تغییر دهید
کاراکتر اشاره گر
ویرایشاگر یک متغیر از نوع کاراکتر را به عنوان اشارهگر ایجاد کنید ( اعلان کرده و یا تعریف کنید ) به عنوان مقدار و موجودی آن کاراکتر می توانید به جای یک کاراکتر ( حرف ، عدد یا کاراکتر گرافیکی ) ، یک رشته را در آن ذخیره نمائید . گرچه باید دقت داشته باشید که به صورتی ، می توان گفت که از حالت متغیر خارج می شود و شما قادر به تغییر محتوای رشته خود نخواهید بود
مثال :
#include<stdio.h>
int main()
{
char *s = "geeksquiz";
printf("%lu", sizeof(s));
return 0;
}
برای جلوگیری از اشتباه خود که بعد از کد خودتان بخواهید محتوای رشته خود را تغییر دهید بهتر است از کلیدواژه const ، برای ثابت کردن متغیر خود استفاده کنید تا کامپایلر در صورت نوشتن کدی برای تغییر دادن محتوای رشته ، از شما خطا بگرید . اگر این کار را نکنید ، کامپایلر به جای خطا گرفتن ، تصمیم دیگری که نامعلوم است خواهد گرفت و رشته شما را تغییر خواهد داد ( و امکان خراب کردن برنامه شما خیلی زیاد خواهد بود ) . بنابراین هر گاه تصمیم گرفتید برای ذخیره یک رشته از کاراکتر اشارهگر استفاده کیند بنویسید :
const char * name
اشاره گر و تابع
ویرایشتابع در برنامهنویسی از ارکان اصلی نوشتن برنامه محسوب میشود . بسیاری از زبانهای برنامهنویسی ، تابعگرا هستند و بدون نوشتن تابع در آن زبانها ، نمیتوانید برنامهای را بنویسید ؛ یکی از آن زبانها ، همین زبان C میباشد . علاوه بر این ، زبان C از زبانهایی است که تابع در آن به عنوان داده تعریف میشود . پیشتر کمی به مفهوم تابع اشاره نمودیم و مفهوم تابع در یک فصل از این کتاب مفصلاً مورد بررسی قرار خواهد گرفت ، اما از آنجایی که یکی از کاربردهای عمده اشارهگرها در زبان C مربوط به تابعها میباشد ؛ ناچاریم کمی به صورت ابتدایی مفهوم تابع را در اینجا بازگو نمائیم . تابع در زبان C دادهای است که ممکن است داده یا دادههای دیگری را بگیرد و بر روی آنها پردازشی انجام دهد ( به کمک عملگرها و دستورها و ... ) و یک مقدار را به عنوان خروجی باز پس دهد ( اصطلاحاً بازگرداند ) گاهی نیز تابعها هیچ دادهای را نمیگیرند و اعمالی را به انجام میرسانند و مقداری را باز نمیگردانند . ممکن است این تابعها از پیش در فایلهای سرایند تعریف شده باشند که به آنها تابع های کتابخانهای گفته میشود و شما میتوانید در تعریف تابع خود از تابعهای کتابخانه ای نیز بهره جوئید . شکل کلی ایجاد تابع به این شکل است :
data-type name(parameter 1, parameter 2)
{
local variable 1;
local variable 2;
return result;
}
ابتدا نوع داده را معین می کنیم و سپس نامی به تابع میدهیم و بعد داخل یک جفت پرانتز باز و بسته پارامترهای تابع را مینویسیم که در صورت تعیین پارامتر باید آن متغیرها در داخل تابع مورد پردازش قرار بگیرند و تعریف کنیم که تابع ما با آن متغیرها چه میکند ( برای اینکه اگر تابع را در جایی دیگر فراخوانی کردیم و دادههایی را به تابع فرستادیم - اصطلاحاً پاس دادیم - مورد پردازش قرار بگیرند ، که در این صورت ، به دادههای ارجاع داده شده آرگومان میگوئیم )
در داخل بلوک تابع ، یعنی کروشههای باز و بسته ، هر متغیری که تعریف کنید ، محلی می باشد که بحث فعلی ما نیست ( به این مبحث هنوز نرسیدهایم ولی اگر به شکل کلی تابع که در بالا نوشته شد دفت کنید نوشتهایم : local variable که local به معنی محلی میباشد ) یعنی خارج از بلوک و به عبارت دیگر خارج از کروشههای تابع نمیتوانیم به مقدار آن متغیرها دسترسی داشته باشیم که البته میتوان با کمک یک متغیر سراسری مقدار آن را خواند اما تغییر مقدار و موجودی متغیر محلی تنها از طریق یک اشارهگر سراسری امکان پذیر است ( به موضوع کلاسهای ذخیره مراجعه کنید ) . علاوه بر این آرگومانهایی که به تابع فرستاده میشوند ، بازنویسی شده و نسخه دیگری از آنها در حافظه ایجاد میشود و تابع ، پردازش خود را بر روی دادههای بازنویسی شده انجام میدهد و داده یا دادههایی که ارجاع شده بودند ، دست نخورده باقی میمانند . برای دسترسی به دادههای خارج از تابع که بخواهیم مقدار آنها را تغییر دهیم باید پارامترهایی از نوع اشارهگر تعریف کنیم و سپس دادههای خود را به کمک عملگر آدرس ( امپرسند & ) به تابعمان بفرستیم ( اصطلاحاً پاس بدهیم ) تا مورد پردازش قرار بگیرند
علاوه بر این شما میتوانید خود تابع را هم به عنوان اشارهگر تعریف کنید . این عمل دو مزیت دارد ، یکی اینکه می توانید به تابع دیگری اشاره کنید و همچنین در صورتی که پارامتر تابع دیگری را اشارهگر تعریف کنید ، میتوانید تابع اولی را به عنوان آرگومان به تابع دوم ارجاع دهید که مقدار تابع اول به عنوان آرگومان در تابع دوم مورد پردازش قرار میگیرد . کاربرد دیگر تابع اشارهگر ، فراخوانی سریعتر تابع تعریف شده توسط شما خواهد بود . این قابلیت در زبان سی که خود تابع را به عنوان آرگومان در یک تابع دیگر قرار دهید و البته اشاره دادن یک تابع به تابع دیگر کمک میکند تا خاصیت شیئ گرایی ، در زبانهای شیئ گرایی مثل سیپلاسپلاس را شبیه سازی کند و از جمله قدرت های زبان C محسوب میشود
دقت کنید که شکل بالا از تابع ، یک شکل و نمای کلی از تابع بود که با توجه به مطالب گفته شده ، فقط در اینجا بدین شکل مطرح گردید تا موضوع را سادهتر بیابید . مثلاً دو متغیر به نام های a و b برای یک تابع به نام sum تعریف میکنیم که در داخل تابع ، خروجی تابع ، a + b خواهد بود ؛ بدین ترتیب پس از تعریف تابع ، اگر دو متغیر را به تابع خود بفرستیم ، مقدار آن دو متغیر با یکدیگر جمع خواهند شد و نتیجه به عنوان خروجی عرضه خواهد گردید
اشاره گر های تو در تو
ویرایشاشارهگر به هر دادهای میتواند اشاره کند ، یعنی حتی یک اشارهگر میتواند به یک اشارهگر دیگر نیز اشاره کند . برای اینکه یک اشارهگر را بتوانیم به اشارهگر دیگری اشاره دهیم باید از دو استریسک ( یعنی * ) استفاده کنیم . به همین ترتیب برای اشاره کردن به یک اشارهگر به اشارهگر باید از سه استریسک استفاده کنیم مثل :
int *** ptr;
در اشارهگرهای تو در تو ابتدا دادهای توسط اشارهگر مورد اشاره قرار میگیرد ، سپس اشارهگرهای دوبل ( دو ستارهای ) به اشارهگرهای به آن دادهها ، اشاره میکنند . اگر لازم داشته باشیم از اشارهگرهای سه ستارهای برای اشارهکردن به اشارهگرهای به اشارهگر استفاده خواهیم نمود . مثال :
int i = 812;
int *ptr = &i;
int **ptrtwo = &ptr;
در مثال بالا در نهایت مقدار ptr و ptr2 بعد از بازارجاع برابر با 812 خواهد بود . اما دقت داشته باشید که در اشارهگرهای تو در تو وقتی اشارهگر بیرونیتر ( یعنی با ستارههای بیشتر ) را میخواهیم باز ارجاع یا غیر مستقیم کنیم ، به ازای هر ستارهای که در باز ارجاع برای آن قرار میدهیم به همان اندازه عمیقتر می شود و به اشارهگر و در مراحل بعدی به داده داخل اشارهگر اشاره شده ، اشاره خواهد نمود . مثال :
int k = 5, m = 8;
int *ptr = &k;
int **ptrtwo = &ptr;
**ptrtwo = 12;
*ptrtwo = &m;
در مثال بالا ، در اولین بازارجاعِ ptrtwo ، مقدار k به 12 تغییر پیدا کرد و در بازارجاع دوم ptrtwo ، به اشارهگر ptr دسترسی یافته و مقداری که ptr به آن اشاره میکند را به متغیر m تغییر دادیم ( چون فقط یک بار غیر مستقیم گردید و این کار محل مورد اشاره ptr را تغییر میدهد اما در صورتی که با دو ستاره غیر مستقیم کنیم ، به داده مورد اشاره - مثلاً k - دسترسی مییابیم ) . پس دقت کنید که وقتی میخواهید از اشارهگرهای تو در تو استفاده کنید ، به تعداد استریسکهایی که مینویسید توجه لازم را مبذول دارید . استفاده از اشارهگرهای تو در تو در جایی مطرح میشود و ضرورت مییابد که امکان دسترسی و تغییر دادن یک اشارهگر وجود ندارد و باید از اشارهگر دیگری برای تغییر دادن اشارهگر اولی استفاده نمود . در انتقال یک اشارهگر به عنوان آرگومان به پارامتر یک تابع باید از یک اشارهگر به اشارهگر استفاده نمود . یعنی برای دریافت یک اشارهگر به عنوان آرگومان تابع ، باید از یک اشارهگر به اشارهگر استفاده کنید
مثال :
#include<stdio.h>
int a = 852;
int *ptr = &a;
int func(int **ptr2);
int main(void)
{
printf("%d\n", func(&ptr));
return 0;
}
int func(int **ptr2)
{
return (**ptr2 + 2);
}
۱ −در مثال بالا ابتدا فایل سرآیند stdio که یک فایل سرآیند استاندارد از زبان C است را ضمیمه برنامه خود نمودهایم ( تا بتوانیم از تابع کتابخانهای printf استفاده کنیم )
۲ − سپس یک متغیر به نام a تعریف نمودهایم که مقدار 852 را در خود ذخیره کرده است
۳ − بعد از آن یک اشارهگر از نوع صحیح با نام ptr تعریف نمودهایم که به متغیر a اشاره میکند
۴ − بعد از آن تابع func را اعلان نمودهایم که یک پارامتر از نوع صحیح اشارهگر به اشارهگر را میپذیرد
۵ − بعد از تابع اصلی برنامه یعنی تابع main ، تابع func را در انتهای برنامه تعریف نمودهایم که پارامتری از نوع اشارهگر به اشارهگر دارد (ptr2) که میتوان یک اشارهگر را به عنوان آرگومان به آن ارجاع داد ( در اینجا ptr )
که در این صورت به کمک دو عملگر اشارهگر و روش غیر مستقیم کردن به مقدار و موجودی اشارهگری که به آن ارجاع داده شده است ( یعنی ptr ) دسترسی یافته ( که این مقدار که به آن اشاره شده است 852 میباشد ) و دو واحد به آن اضافه میکند
۶ − در تابع main از تابع کتابخانهای printf استفاده نمودهایم تا مقدار بازگردانده شده از تابع func را به عنوان خروجی در رابط خطدستوری چاپ کند و البته که به تابع func اشارهگر ptr را فرستادهایم تا مقدار و موجودیای که به آن اشاره کرده است ( مقدار a که 852 میباشد ) را دو واحد افزایش دهد
توجه کنید : در داخل تابع func ، متغیر اشارهگر به اشارهگر ptr2 با دو استریسک ( ستاره ) بازارجاع و غیر مستقیم شده است
اشاره به آدرسی خاص
ویرایشموضوع اشارهگر را با مبحث اشاره کردن به آدرسی خاص به عنوان آخرین مبحث به پایان میبریم . ما میتوانیم به جای اینکه به یک داده اشاره کنیم که داده ما را کامپایلر مستقیم و یا به واسطه سیستم عامل در حافظه موقت جای دهد ، خودمان به آدرسی که میخواهیم و نیاز داریم اشاره کنیم تا مقدار درون آن را بخوانیم و یا مقداری را در آن بنویسیم . دقت کنید ! این عمل تنها در نوشتن برنامههای سطح پائین به کار میآید ؛ مثلاً در نوشتن یک firmware که میتواند یک درایور باشد یا نوشتن کرنل سیستم عامل . اما زمانی که شما در داخل سیستم عامل قرار دارید ، خود به خود سیستم عاملی مثل ویندوز یا لینوکس یا مکاواس ، کرنل خود را در حافظه موقت بارگذاری نموده است ، همچنین درایورهای سختافزار شما با کمک سیستم عامل با همین ترفند ، اطلاعات خود را داخل حافظه موقت نوشتهاند ، پس به راحتی ممکن و محتمل است که شما در صورت دسترسی به خانههای حافظهای که سیستم عامل و میانافزارهایش ( Firmware ) از آنها استفاده میکنند و یا برنامههای دیگری که در حال اجرا هستند ، به اطلاعات آنها آسیب بزنید . بنابراین این ترفند برای برنامهنویسان حرفهای میباشد و به مبتدیها اصلاً توصیه نمیشود . حتی اگر بخواهید یک کرنل بنویسید ، تنها دانستن زبان C برای شما کافی نیست . شما باید سختافزار و ساز و کار رایانه را نیز بدانید ( که برای این امر میتوانید از منابع آنلاین و کتابهای الکترونیکی در زمینه الکترونیک و سختافزار نوشته شدهاند استفاده کنید ) . در هر صورت ما این روش استفاده از اشارهگر را نیز برای تکیمل این موضوع مینویسیم
پیش از آغاز نیز ناچاریم به یک مبحث دیگر که در موضوع کلاسهای ذخیر بیان شده ، اشارهای بکنیم . کلیدواژه volatile برای مشخص کردن قابلیت تغییر و یا تعیین مقدار یک داده ، توسط سیستم عامل و یا سختافزار میباشد . در برنامهنویسی سطح پائین مثل همین گونه استفاده از اشارهگر ، شماباید بسیاری از دادههای خود را که مقدار و موجودیشان را سختافزار یا سیستم عامل تعیین میکند با کلیدواژه volatile آزاد بگذارید . نحوه تعریف یک اشارهگر به آدرسی خاص بدین شکل میباشد :
int volatile *ptr = (int *)0x123456;
در اینجا ptr دادهای است از نوع صحیح ( int ) که یک اشارهگر میباشد ( و البته با کلاس ذخیره volatile ) که به خانه 123456 ( در مبنای شانزده شانزدهی ) اشاره مینماید نحوه دیگری که متدوالتر و منطقیتر است و برنامهنویسان حرفهای از آن استفاده میکنند با کمک پیشپردازندهها میباشد . به مثال زیر دفت کنید :
#define PORTBASE 0x40000000
unsigned int volatile * const port = (unsigned int *) PORTBASE;
unsigned int abc;
abc = *port;
در مثال بالا با دستور مستقیم define مقدار هگزادسیمال 40000000 را به جای شناسه PORTBASE قرار میدهیم و port یک داده از نوع صحیح بدون علامت است که به صورت یک اشارهگر ثابت تعریف شده است . پس port به آدرس 40000000 اشاره خواهد نمود . همچنین برای اینکه مقدار موجود در خانه 40000000 را بخوانیم و مقدار آن را به دست بیاوریم از داده دیگری که همان abc که صحیح بدون علامت است ، استفاده نمودیم باز هم تکرار میکنیم که این عمل بدون دانستن اینکه به چه آدرسی دسترسی پیدا میکنید و سپس تغییر محتوای آن ، به سیستم عامل و یا دست کم ، در صورت دسترسی به خانههای حافظه که ممکن است برنامههای دیگری مثل Media Player ها از آنها استفاده میکنند باشد باعث اختلال میشود و حتی ممکن است سیستم متوقف شده و قفل کند . اما زمانی که بخواهید یک کرنل و یا یک firmware بنویسید ، مطمئناً خانههای ابتدایی بر خانههای دیگر اولویت دارند و شما میخواهید سریعتر دسترسی پیدا کنید یا مثلاً سختافزار رایانه برای بارگذاری و اجرای یک سیستم عامل و درایورها به خانههای خاصی رجوع میکنند که شما باید از آنها استفاده کنید و یا اینکه در نوشتن یک سیستم عامل ، شما تعیین میکنید که برنامههایی که میخواهند تحت سیستم عامل شما اجرا شوند باید از چه خانههایی استفاده کنند برای همین آدرسهای صریح به آنها میدهید و جلوی دسترسی تصادفی را که در بطن حافظههای موقت ( یعنی RAM که سرآیند Random Access Memory میباشد ) را میگیرید ( البته مورد آخر بستگی به خود شما دارد و ما آن را فقط به عنوان یک مثال نوشتیم )