زبان برنامه نویسی سی/اشاره‌گر

ویکی‎کتاب، کتابخانهٔ آزاد
پرش به ناوبری پرش به جستجو
Gnome-go-last.svg
Gnome-go-first.svg

اشاره گر Pointer[ویرایش]

در دانش برنامه‌نویسی ، اشاره‌گر به نوعی از داده می‌گویند که به محل ذخیره داده‌ای دیگر بر روی حافظه اشاره می‌کند و به محتویات آن داده دسترسی دارد . از اشاره‌گرها به صورت عمده ، برای تسریع در روند برنامه ، مخصوصاً تغییر محتوای داده‌های دیگر ، تخصیص حافظه به داده‌ها به صورت هوشمند و پویا و دسترسی و تغییر محتویات داده‌های تابع استفاده می‌شود . ایجاد و استفاده از اشاره‌گرها ، شیوه‌ای در زبان C می‌باشد که آن را به زبان های سطح پائین ، نزدیک می‌نماید و از این روی درک مفهوم اشاره‌گر کمی نسبت به مباحث دیگر ، زمان بیشتری می‌برد . اما فراموش نکنید که اگر مفهوم اشاره‌گر را درک نکنید و نخواهید از آن در برنامه‌نویسی خود استفاده کنید ، در واقع قادر به نوشتن برنامه های روزمرّه کامپیوتری نخواهید بود و تنها قابلیت‌های کوچکی از زبان C را می‌توانید به کار ببندید

نحوه اعلان اشاره‌گر بدین شکل می‌باشد :

type *name;

ابتدا نوع داده را می‌نویسیم ، سپس یک علامت استریسک یا ستاره ( * ) می‌گذاریم و سپس بدون فاصله یا با فاصله ، نام اشاره‌گر خود را می‌نویسیم . هر اشاره‌گر ، پیش از استفاده باید به داده‌ای اشاره نماید و اگر پیش از اشاره دادن ، آن را استفاده نمائید برنامه شما دچار اختلال ، توقف و یا شکست ( crash ) خواهد شد . اگر در هنگام ایجاد اشاره‌گر ، هنوز نمی‌دانید که باید به کجا اشاره کند و یا هنوز مصمم به اشاره دادن آن به داده‌ای در برنامه خود نیستید ؛ بهتر است مقدار آن را تهی یا نول ( 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];

داشته باشیم ، آنگاه اگر بنویسیم (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 آن اشاره کند ، باید با کمک عملگرها به خانه‌های بعدی اشاره کنید . اما اگر یک اشاره‌گر به صورت آرایه‌ای تعریف کنید ( یعنی آرایه‌ای از داده‌های اشاره‌گر ) می‌توانید همان طور که از آرایه به عنوان اشاره‌گر استفاده می‌کردید ( که در قسمت‌های بالا نوشتیم ) ، نام اشاره‌گر را به جای نام آرایه بنویسید

برای اشاره‌گری از نوع آرایه ، اشاره‌گر را به صورت زیر تعریف می‌کنید :

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;

جهت دسترسی به اعضای ساختمان یا اجتماع نیز از دو روش می توانید استفاده نمائید . روش اول ، روش معمول استفاده از اشاره گر می باشد که به عنوان یک نمونه از ساختمان با آن رفتار می شود و روش دوم روش ابداعی در استاندارد زبان سی می باشد و روش راحت تر و سریع تری خواهد بود . به مثال زیر دقت کنید :

(*ptr).a = 5;
ptr->a = 7;
ptr->b = 'g';

روش دوم ، روش استفاده از عملگر <- می باشد که استفاده ما از اعضای ساختمان را تسهیل می نماید

نکته دیگری که در قسمت باقی می ماند ، روش ساخت لیست پیوندی می باشد که از آنجایی که ما در فصول مقدماتی ، فقط مبانی زبان سی را بیان می کنیم و به روش ها و ترفند ها نمی پردازیم ؛ واکاوی این مبحث را به فصل تمرین ها و ترفند های زبان سی واگذار می نمائیم . اما بد نیست در اینجا با لیست پیوندی آشنا شوید ، چرا که بدون استفاده از اشاره گر ها ، ایجاد لیست پیوندی میسر نخواهد شد

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

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 تغییر دهید

کاراکتر اشاره گر[ویرایش]

اگر یک متغیر از نوع کاراکتر را به عنوان اشاره گر ایجاد کنید ( اعلان کرده و یا تعریف کنید ) به عنوان مقدار و موجودی آن کاراکتر می توانید به جای یک کاراکتر ( حرف ، عدد یا کاراکتر گرافیکی ) ، یک رشته را در آن ذخیره نمائید . گرچه باید دقت داشته باشید که به صورتی ، می توان گفت که از حالت متغیر خارج می شود و شما قادر به تغییر محتوای رشته خود نخواهید بود . این عمل ( یعنی تغییر مقدار و موجودی رشته ) در استاندارد زبان C تعریف نشده است و هر کامپایلر برای خود رویه خاصی را دنبال می کند ولی به عنوان مثال GCC از شما خطا خواهد گرفت .

مثال :

#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(variable 1, variable 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 ** ptr2 = &ptr;

در مثال بالا در نهایت مقدار ptr و ptr2 بعد از بازارجاع برابر با 812 خواهد بود . اما دقت داشته باشید که در اشاره‌گر های تو در تو وقتی اشاره‌گر بیرونی تر ( یعنی با ستاره‌های بیشتر ) را میخواهیم بازارجاع کنیم ، به ازای هر ستاره‌ای که در بازارجاع برای آن قرار می دهیم به همان اندازه عمیق می شود و به اشاره‌گر و یا داده غیراشاره‌گر داخلی اشاره خواهد نمود . مثال :

int k = 5, m = 8;
int * ptr = &k;
int ** ptr2 = &ptr;

**ptr2 = 12;
*ptr2 = &m;

در مثال بالا ، در اولین بازارجاعِ ptr2 ، مقدار k به 12 تغییر پیدا کرد و در بازارجاع دوم ، مقداری که ptr به آن اشاره می‌کند به متغیر m تغییر یافت . پس دقت کنید که وقتی می‌خواهید از اشاره‌گرهای تو در تو استفاده کنید ، به تعداد استریسک هایی که می نویسید توجه لازم را مبذول دارید . استفاده از اشاره‌گرهای تو در تو در جایی مطرح می‌شود و ضرورت می‌یابد که امکان دسترسی و تغییر دادن یک اشاره‌گر وجود ندارد و باید از اشاره‌گر دیگری برای تغییر دادن اشاره‌گر اولی استفاده نمود . در تمام بلوک‌های تابع‌ها و همین طور انتقال یک اشاره‌گر به عنوان آرگومان به پارامتر یک تابع باید از یک اشاره‌گر به اشاره‌گر استفاده نمود . یعنی برای تغییر دادن در متغیر خود از داخل بلوک تابع که متغیر در خارج از بلوک تعریف شده و یا تغییر متغیر خود از خارج از بلوک که متغیر در داخل بلوک تعریف شده و همین طور دریافت یک اشاره‌گر در پارامتر تابع باید از یک اشاره‌گر به اشاره‌گر استفاده کنید

مثال :

int a = 7;

int func(int j , int k)
{
//some codes
int * ptr = &a;
ptr = 12;

return 0;
}
int m = 9;
int n = 16;
int * ptr = &m;

int function(int ** ptr2)
{
ptr2 = &ptr;
*ptr2 = &n;
}

return 0;
}

در مثال اول با اشاره‌گر ptr مستقیماً به متغیر a دسترسی پیدا کردیم و مقدار آن را تغییر دادیم که در صورت عدم استفاده از اشاره‌گر مقدار a خارج از بلوک همان 7 باقی خواهد ماند . در مثال دوم نیز قصد داشتیم تا یک اشاره‌گر را به عنوان پارامتر یک تابع تعریف کنیم ( که مطابق با استاندارد C تابع‌ها برای پارامترهای خود یک نمونه از روی متغیر می‌سازند و هر تغییری که روی آن اعمال می‌کنند فقط در داخل تابع قرار می‌گیرد و خارج از تابع مقدار قبلی خود را خواهند داشت ) که برای این منظور باید یک اشاره‌گر به اشاره‌گر مورد نظرمان ایجاد کنیم تا تغییرات بر روی آن اعمال شوند ؛ بنابراین در این مثال ptr در ابتدا به متغیر m اشاره می‌کرد که پس از اجرای تابع به متغیر n اشاره خواهد نمود

اشاره به آدرسی خاص[ویرایش]

موضوع اشاره‌گر را با مبحث اشاره کردن به آدرسی خاص به عنوان آخرین مبحث به پایان می‌بریم . ما می‌توانیم به جای اینکه به یک داده اشاره کنیم که داده ما را کامپایلر به واسطه سیستم عامل در حافظه موقت جای می‌دهد ؛ خودمان به آدرسی که می‌خواهیم و نیاز داریم اشاره کنیم تا مقدار درون آن را بخوانیم و یا مقداری در آن بنویسیم . دقت کنید ! این عمل تنها در نوشتن برنامه‌های سطح پائین به کار می‌آید ؛ مثلاً در نوشتن یک 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 می‌باشد ) را می‌گیرید ( البته مورد آخر بستگی به خود شما دارد و ما آن را فقط به عنوان یک مثال نوشتیم )