اشارهگرها یکی از قدرتمندترین ابزارهای سیپلاسپلاس است که در استفاده از آن باید دقت زیادی به خرج داد. از اشاره گر در c++ (پوینتر در c++) برای دسترسی به حافظه و نگهداری آدرس استفاده میشود. برای اینکه اشارهگرها را بهتر یاد بگیرید باید کمی حوصله به خرج دهید تا آن را به صورت کامل توضیح دهیم. در آموزشِ پیشرو چنانچه قسمتی از آموزش را متوجه نشدید دوباره و با دقت بیشتری آن مبحث را مطالعه کنید.
شاید بد نباشد برای شروع کمی در مورد حافظه کامپیوتر صحبت کنیم:
متغیرها کجا ذخیره میشوند؟
برای اینکه اشارهگرها را بهتر متوجه شوید باید در مورد نحوه ذخیره متغیرها صحبت کنیم. شما هر متغیری که در برنامه تان استفاده میکنید در مکانی از حافظه ذخیره میشود که این مکان یک آدرس دارد و با این آدرس است که میتوان به محتوای آن خانه دسترسی پیدا کرد. قطعه کد ساده زیر را در نظر بگیرید:
#include <iostream.h> void main( ) { int data = 100; float value = 56.47; cout<< &data<<" -> " << data << endl; cout << &value<<" -> " << value << endl; }
در کد بالا از دو متغیر data و value استفاده شده است. همانطور که گفتیم این متغیرها در مکانی از حافظه ذخیره خواهند شد که این مکان ها یک آدرس دارد. عملگر & (شما بخوانید ampersand یا اَمپِرسَند) وقتی که دردر کنار نام متغیر قرار میگیرد، آدرس متغیر را برمیگرداند. یعنی value& در واقع آدرس متغیر value را برمیگرداند. برای اینکه درک بهتری از موضوع داشته باشید در تصویر زیر نمونه ساده ای از حافظه را به تصویر کشیدیم. در تصویر هر خانه از حافظه یک آدرس مشخص دارد که دو متغیر value و data در حافظه ذخیره شده است:
با توجه به کد و تصویر بالا قابل تصور است که خروجی برنامه به صورت زیر باشد:
FFF4 -> 100
FFF0 -> 56.47
حال که با مفهوم آدرس متغیر آشنا شدید میتوانیم به تعریف اشاره گر بپردازیم: اشاره گر يك متغير است كه آدرس يك متغير ديگر را در خود نگه ميدارد. قالب تعریف اشاره گر به صورت زیر است:
; متغیر* نوع داده
مثلا:
int *ptr;
در کد بالا یک اشاره گر تعریف شده است که نوع تعریف اشاره گر نشان میدهد که قرار است در این اشاره گر آدرس یک متغیر از نوع int ذخیره شود.
قبل از اینکه گیج شوید بیاید با هم مرور کنیم! گفتیم هر متغیر در مکانی از حافظه ذخیره میشود. این مکان یک آدرس دارد. ما میتوانیم این آدرس را ذخیره کنیم (آدرس متغیر) برای ذخیره آدرس به مفهوم اشاره گر نیاز داریم. یعنی اشاره گر را تعریف میکنیم که در آن آدرس یک متغیر دیگر ذخیره کنیم. علت نامگذاری اشاره گر هم همین است. در واقع اشاره گر به یک متغیر دیگر اشاره میکند.( یعنی با استفاده از آدرسی که در خود دارد به آن متغیر دسترسی خواهد داشت. )
حال فرض کنید که بخواهیم آدرس متغیر data را در ptr ذخیره کنیم. به نظرتان کد این دستور چیست؟
همانطور که بالاتر گفتیم با استفاه از عملگر & میتوان به آدرس متغیر دسترسی داشت:
ptr = $data;
پس تا اینجا با مفهوم اشاره گر و نحوه مقداردهی آن آشنا شدید.
حال باید بتوانیم از اشاره گر استفاده کنیم و به محتوای جایی که به آن اشاره میکند دسترسی داشته باشیم.به این معنی که مثلا در مثال ما ptr به متغیر data اشاره میکند و ما میخواهیم با استفاده از اشاره گر ptr به مقداری که در data ذخیره شده است دسترسی داشته باشیم. برای این کار کافی است از عملگر * استفاده کنیم. یعنی ptr* . کد زیر را ببینید:
#include <iostream> using namespace std; int main(){ int data,*ptr; data = 5; ptr = &data; cout<< *ptr; }
توضیح کد:
در خط 6 دو متغیر تعریف شده است. data و ptr. متغیر data از نوع int و متغیر ptr از جنس اشاره گر به int. یعنی ptr مکانی از حافظه است که در آن آدرس یک متغیر از جنس int ذخیره میشود. در خط 8 در متغیر data عدد 5 ذخیره شده است و در خط بعدی آدرس متغیر data در ptr ذخیره شده است. حال اگر بخواهیم با استفاده از ptr به 5 دسترسی داشته باشیم باید مانند خط 11 از ptr* استفاده کنیم. در واقع ptr* به محتوای جایی که ptr به آن اشاره میکند دسترسی دارد.
چرا به اشاره گر نیاز داریم؟!
سوالی که ممکن است به ذهنتان برسد این است که وقتی میتوان به راحتی با متغیر data کار کرد (مثال قبل) چرا نیاز است متغیری (اشاره گر ptr) تعریف کنیم که و آدرس data را در آن ذخیره کنیم و به جای اینکه با data کار کنیم با ptr* کار کنیم؟ متغیر data چه محدودیتی دارد که به ptr نیاز پیدا میکنیم؟
برای پاسخ به این پرسش باید در مورد متغیرهای محلی و عمومی صحبت کنیم. متغیرهای عمومی متغیرهایی هستند که در همه جای برنامه قابل استفاده هستند. منظور از همه جا این است که هم در تابع اصلی (یا همان ()int main) میتوانیم استفاده کنیم و هم در توابعی که خودمان تعریفمیکنیم. اما متغیرهای محلی فقط در محلی که تعریف میشوند قابل استفاده است. برای درک بهتر موضوع ابتدا کد زیر را نگاه کنید:
#include <iostream> using namespace std; void my_function(int x); int main(){ int x = 5; my_function(x); cout<<x; } void my_function(int x){ x++; }
در کد بالا چه عددی چاپ میشود؟ 5 یا 6؟ اگر فکر میکنید 6 چاپ میشود مطلب زیر را به دقت بخوانید:
در خط 8 متغیر x تعریف شده است. ما به این متغیر متغیر محلی میگوییم. به این معنی که این متغیر فقط در تابع اصلی (()int main) قابل استفاده است. یعنی یعنی خط 8 تا 10. شاید بگویید در تابع my_function نیز از x استفاده شده است. توجه داشته باشید که در پارامتر تابع در خط 13 دوباره x تعریف شده است و xای که در تابع my_function استفاده میشود با xای که در int main استفاده شده است دو متغیر جدا از هم هستند. اگر دقیق تر بخواهیم به این موضوع نگاه کنیم به این شکل میتوانیم کد بالا را بررسی کنیم:
متغیر x در خط 8 با عدد 5 مقداردهی میشود. در خط 9 یک کپی از این متغیر به تابع my_function ارسال میشود. پس در خط 13 x با مقدار 5 دریافت میشود و در خط 14 مقدار آن 6 میشود . نکته مهم این است که مقدار x در تایع int main همچنان برابر 5 است. زیرا هر دو متغیر محلی هستند و فقط در حوزه تعریف خودشان قابل استفاده هستند. برای اینکه مطمئن شوید که متغیر محلی را متوجه شده اید کد زیر را نگاه کنید:
#include <iostream> using namespace std; void my_function(int x); int main(){ int x = 5,y=6; my_function(x); } void my_function(int x){ cout<<y; }
در کد بالا چه عددی چاپ میشود؟ 6؟
این کد اصلا اجرا نمیشود! زیرا متغیر y در خط 13 تعریف نشده است. متغیر y که در خط 8 تعریف شده است یک متغیر محلی است که فقط در تابع int main قابل استفاده است.
از بحث اصلی دور نشویم. سوال اصلی این بود که چرا به اشاره گر نیاز داریم؟ فرض کنید بخواهیم دو متغیر به یک تابع ارسال کنیم و در تابع بر روی این دو متغیر تغییراتی انجام دهیم و این تغییرات در جایی که فراخوانی شده است نیز دیده شود.
#include <iostream> using namespace std; void my_function(int x,int y); int main(){ int x = 5,y=6; my_function(x,y); cout<<x<<y; } void my_function(int x,int y){ x++; y++; }
همانطور که قبلا بحث شد مقدار 5 و 6 در خروجی چاپ میشود نه 6 و 7! برای حل این مشکل از اشاره گر ها استفاده میکنیم. به چه صورت؟ اگر به یاد داشته باشید بالاتر از اصطلاح کپی استفاده کردیم. مثلا در کد بالا در خط 9 یک کپی از x و y به تابع mu_function ارسال میکنیم. وقتی کپی ارسال میشود هر تغییری در کپی انجام دهیم تغییری بر مقدار اصلی ندارد. با استفاده از اشاره گرها ما به جای اینکه یک کپی از x و y ارسال کنیم آدرس متغیرهای x و y را به تابع ارسال میکنیم. بنابراین با این آدرسی که تابع در اختیار دارد به مقدار اصلی متغیر دسترسی پیدا میکند و میتواند مقدار اصلی را مستقیما تغییر دهد. کد زیر را ببینید:
#include <iostream> using namespace std; void my_function(int *x,int *y); int main(){ int x = 5,y=6; my_function(&x,&y); cout<<x<<y; } void my_function(int *x,int *y){ (*x)++; (*y)++; }
توضیح کد:
1- به الگوی تابع در خط 5 دقت کنید. پارامترهای ورودی اشاره گر هستند. بنابراین باید آدرس متغیر به این توابع ارسال شود.
2- همانطور که در مورد 1 گفتیم، در خط 9 آدرس دو متغیر x و y ارسال میشود
3- در تابع mu_function آدرس دو متغیر x و y دریافت شده است. توجه داشته باشید که در تابع my_function دو متغیر x و y اشاره گر هستند. به هیچ وجه درگیر اسامی مشابه هم نشوید! در تابع int main متغیرهای x و y دو متغیر هستند که در آنها اعداد 5 و 6 ذخیره شده است و در تابع my_function متغیرهای x و y دو متغیر هستند که در آنها آدرس دو متغیر دیگر ذخیره شده است.
اگر همه مباحث مطرح شده تا اینجا را یاد گرفته اید به شما تبریک میگوییم! مفاهیمی که تا اینجا مطرح شد مفاهیم سخت و پیچیده ای نبود اما برای کسی که برای اولین بار این مفاهیم را میخواند ممکن است کمی گیج کننده باشد. اگر هنوز حس میکنید که مباحث را کامل متوجه نشده اید دوباره برگردید و مطالب را با دقت بیشتری بخوانید.
جمع بندی
مباحث تکمیلی و مزیتهای اشاره گرها را در پست بعدی کامل خواهیم کرد اما تا اینجا با یک مزیت بزرگ از اشاره گرها آشنا شدید. وقتی تابعی دارید که نیاز به برگرداندن بیشتر از یک مقدار دارد میتوانید روی قدرت اشاره گرها حساب باز کنید. کافیست کد آخر را دوباره مرور کنید تا متوجه بشوید که در واقع در آن مثال دو مقدار x و y را برگرداندیم. در آموزش بعدی با مزیت دیگر اشاره گر آشنا خواهید شد.