یکی از مهمترین مباحث کاربردی هر زبان برنامهنویسی، اشارهگر و مفهوم آن است، که کاربرد گستردهای در شاخه ساختمان دادهها نیز دارد. در این فرصت با مفهوم اشارهگر و همینطور روش تعریف آن در زبان ++C آشنا میشوید. باید توجه داشته باشید که سوای روش تعریف اشارهگر در این زبان، کلیت مفهوم آن در بین تمام زبانها مشترک است. فبل از شروع بحث دو مطلب مهم را یادآوری میکنم:
1- تک تک بایتهای حافظه برای خود آدرسی دارند، که یک عدد صحیح و مثبت است. این آدرس دقیقا مانند کد پستی عمل میکند. یعنی کاملا منحصربفرد بوده، و میتوان از آن برای ارجاع به بایت استفاده کرد.
2- هر متغیری پس از تعریف، متناسب با نوع خود چند بایت از حافطه را اشغال میکند. این بایتها همگی متوالی بوده و در حافظه پشت سر هم قرار دارند.
اشارهگر
به زبان ساده، اشارهگر نوعی متغیر است که محتوای آن آدرس یکی از خانههای حافظه کامپیوتر است. به این مثال توجه کنید:
کد:
long int a;
long int *b;
b = &a;
دو خط اول متغیر a را به عنوان متغیر صحیح و b را یک اشارهگر صحیح معرفی میکنند. این تعریف اشارهگر به کامپایلر میگوید که آدرس موجود در اشارهگر b مربوط به یک متغیر صحیح است. توسط خط سوم آدرس متغیر a در b قرار میگیرد. مثلا اگر متغیر a بایتهای شماره 1000 تا 1003 را در اختیار داشته باشد، مقدار 1000 برای متغیر b در نظر گرفته میشود. اصطلاحا گفته میشود که اشارهگر b به a اشاره میکند. اینجا ممکن است دو سوال پیش بیاید: چرا فقط آدرس بایت اول در اشارهگر قرار میگیرد؟ و چرا برای اشارهگر نوع تعیین میشود؟ مگر نه اینکه آنها حاوی آدرس حافظه هستند؟ به چه دلیل نوع محلی که اشارهگر به آن اشاره دارد مشخص میشود؟
به مثال زیر توجه کنید:
کد:
long int a, *b, c;
a = 69355;
b = &a;
c = *b;
عبارت b* را بخوانید: "محتوای محلی که متغیر b به آن اشاره دارد". پس خط آخر مقدار 69355 را به c اختصاص میدهد (به کاربردهای متفاوت اپراتورهای & و * دقت کنید). فرض کنیم بایتهای شماره 1000 تا 1003 برای ذخیره کردن مقدار متغیر a استفاده شده باشند (فرض کردهایم متغیر از نوع long int چهار بایت اندازه دارد). پس اشارهگر b مقدار 1000 را دارد. وقتی برنامه به خط آخر میرسد، باید محتوای محلی را که b به آن اشاره میکند، در متغیر c قرار دهد. اما از کجا متوجه میشود که علاوه بر بایت شماره 1000 باید سه بایت بعدی را هم استفاده کند؟ و چطور متوجه میشود که باید این چهار بایت را به صورت عدد صحیح تفسیر کند؟ ممکن است این چهار بایت مربوط به یک عدد اعشاری چهار بایتی float باشد. به همین خاطر است که نوع اشارهگر تعیین میشود. وقتی اشارهگر b از نوع long int مشخص شده است، برنامه متوجه میشود که باید از چهار بایت به صورت عدد صحیح استفاده کند. اگر تعیین نوع برای اشارهگر انجام نمیشد، مشکلات زیادی به وجود میآمد. البته زبان برنامهنویسی ++C اشارهگرهای بدون نوع (void) هم دارد، که کاربرد اختصاصی خود را دارند.
اشارهگرها و توابع
مطمئنا کاربرد اشارهگرها تنها محدود به مثال بالا نمیشود. یکی از کاربردهای مهم اشارهگر مربوط به انتقال دادهها بین توابع مختلف در برنامه است. متغیرهایی که در حالت عادی به عنوان پارامتر به یک تابع ارسال میشوند، از تغییر پیدا کردن توسط تابع مصون هستند. چرا که تابع یک کپی از آنها را دریافت میکند. مثلا:
کد:
void func( int n )
{
n++;
}
void main( )
{
int n = 10;
func( n );
cout << n;
}
متغیر n مربوط به تابع func یک واحد افزایش پیدا میکند. اما این تغییر تاثیری در متغیر n در تابع اصلی ندارد. پس عدد 10 توسط cout به خروجی ارسال میشود. اما مواقعی هست که ما نیاز داریم بتوانیم مقدار متغیر را تغییر دهیم. مانند تابعی که دو متغیر را دریافت کرده و مقدار آنها را با هم عوض میکند. اینجا اشارهگر به کمک میآید:
کد:
void swap( int *a, int *b )
{
int t;
t = *a;
*a = *b;
*b = t;
}
void main( )
{
int m = 15, n = 10;
swap( &m, &n );
cout << "m = " << m << " , n = " << n;
}
به جای محتویات متغیرهای m و n، آدرس حافظه آنها به تابع ارسال میشود. پس اشارهگر a به m و اشارهگر b به n اشاره میکنند. حال به مراحل مختلف تابع swap توجه کنید:
خط دوم: محتوای محلی که a به آن اشاره دارد (یعنی مقدار m) در t قرار میگیرد. پس t = 15.
خط سوم: محتوای محلی که b به آن اشاره دارد (یعنی مقدار n) در محلی که a به آن اشاره دارد (یعنی m) ریخته میشود. پس m = 10.
خط چهارم: محتوای t به محلی که b به آن اشاره دارد (یعنی n) وارد می شود. پس n = 15.
بعد از این که کنترل به تابع اصلی باز میگردد، مقادیر m و n با هم عوض شدهاند، و خروجی به این صورت است:
اشارهگرها و آرایههای پویا
مهمترین و اصلیترین کاربرد اشارهگرها مربوط به کار با متغیرهای پویا است. متغیرهای عادی که در برنامهها مورد استفاده قرار میگیرند ایستا هستند. یعنی حافظه آنها توسط خود برنامه اختصاص داده شده و در پایان نیز توسط خود برنامه آزاد میشوند. متغیرهای پویا عکس این حالت هستند. یعنی باید خودتان حافظه بگیرید و خودتان آزاد کنید. این نوع متغیر توسط اشارهگر کنترل میشود. به مثال ساده زیر توجه کنید:
کد:
void main( )
{
int *p;
p = new int;
*p = 5;
cout << *p;
delete p;
}
توسط دستور new حافظهای به عنوان عدد صحیح رزرو شده، و آدرس آن در اشارهگر p قرار میگیرد. بعد از انجام دادن هر عملیات دلخواه روی مقدار ذخیره شده در این حافظه، با استفاده از دستور delete حافظه آزاد میشود.
تخصیص حافظه نیز دو کاربرد بسیار مهم دارد: آرایههای پویا و پیادهسازی ساختارهای مبجث ساختمان دادهها.
اشارهگر به نابع
زمانی که یک برنامه اجرا میشود، کدهای مورد نیاز آن در حافظه اصلی کامپیوتر بارگذاری میشوند. بنابراین کدهای برنامه نیز همانند متغیرهای مورد استفاده در آن شامل آدرسی هستند. هر تابع بلوکی از حافظه را در اختیار میگیرد که آدرس شروع آن به عنوان آدرس تابع در نظر گرفته میشود. یک اشاره گر امکان نگهداری چنین آدرسی را نیز دارد.
تعریف چنین اشارهگری را با یک مثال نشان میدهم. فرض کنید تابعی به صورت زیر داریم:
کد:
long int func( int m, float n );
اشارهگر به چنین تابعی اینگونه تعریف میشود:
کد:
long int (*p)( int, float );
این تعریف مشخص میکند که p یک اشارهگر به مجموعه توابعی است که دو پارامتر به ترتیب از نوع int و float دارند و یک متغیر صحیح از نوع long int بر میگردانند. آدرس هر تابعی که چنین ساختاری داشته باشد میتواند در اشارهگر p قرار بگیرد. به قرار دادن پرانتز در دو طرف تعریف اشارهگر p هم توجه داشته باشید. نبود این پرانتزها تعبیر دیگری را برای کامپایلر تداعی میکند که در نهایت به خطا منجر میشود.
پس از تعریف چنین اشارهگری، با دستور انتسابهای زیر میتوانید آدرس تابعی را در آن قرار دهید:
هر دو دستور آدرس تابع func را در اشارهگر p قرار میدهند.
پس از این مقداردهی میتوانید از اشارهگر برای فراخوانی تابع استفاده کنید:
این خط معادل دستور زیر است:
کد:
cout << func( 6, 7.5 );
اشارهگر به توابع کاربردهای ویژهای دارد که بحث جداگانهای را میطلبد.
اشارهگر به ساختمان و کلاس
متغیرهای تعریف شده از ساختمانها و کلاسها نیز همچون متغیرهای عادی فضایی در حافظه کامپیوتر در اختیار میگیرند که میتوان اشارهگر به آنها تعریف کرد. فرض کنید ساختمانی با تعریف زیر داریم:
کد:
struct student
{
char name[ 100 ];
float average;
int age;
};
با داشتن چنین ساختاری دستورات زیر معتبر هستند:
کد:
student st, *p;
p = &st;
(*p).age = 14;
در خط اول متغیر st از نوع ساختمان student و اشارهگر p به این نوع ساختمان تعریف شده است. در خط بعدی آدرس متغیر st در اشارهگر p قرار گرفته و در خط آخر محتوی فیلد age مربوط به محلی که p به آن اشاره دارد (در اینجا st) برابر 14 میشود.
در زبان برنامهنویسی ++C روش دیگری نیز برای دسترسی به فیلدهای یک ساختمان از طریق اشارهگر وجود دارد. دو عبارت زیر معادل هم هستند:
(*p).age = 14;
p->age = 14;
استفاده از عملگر <- خوانایی برنامه را بیشتر میکند.
اشارهگر به اشارهگر
اشارهگر متغیری است که محتوای آن آدرس خانهای از حافظه است. پس خود این اشارهگر هم در خانهای از حافظه قرار دارد. اشارهگر به اشارهگر متغیری است که آدرس خانه حافظهای را در خود نگه میدارد که محتوای آن خود آدرس یکی از خانههای حافظه است. به عبارت دیگر، محتوای اشارهگر معمولی آدرس خانه حافظه متغیرهایی از نوع متغیرهای استاندارد غیر اشارهگر زبان برنامهنویسی ++C، و ساختمانها و کلاسها و توابع است. اما اشارهگر به اشارهگر آدرس خانه حافظه متغیری از نوع اشارهگر معمولی را نگه میدارد.
برای تعریف چنین اشارهگری از ** استفاده میکنیم:
کد:
int a;
int *p1 = &a;
int **p2 = &p1;
اشارهگر به اشارهگر کاربرهای مهمی مانند تعریف آرایههای پویای دو بعدی دارد.
عملیات ریاضی روی اشارهگرها
محتوای یک اشارهگر آدرس خانه حافظه است که یک عدد صحیح است. روی این عدد صحیح میتوان دو عمل جمع و تفریق را انجام داد. اما این عملیات محدودیتها و نکاتی را شامل میشود.
آدرس خانه حافظه را میتوان به شماره پلاک منازل تشبیه کرد. ما هیچ دو شماره پلاک را با هم جمع یا از هم کم نمیکنیم. در مورد اشارهگرها هم اینگونه است و نمیتوان دو اشارهگر را جمع و یا یکی را از دیگری کم کرد. اما میتوان عدد صحیحی را به آن اضافه نمود یا از آن کم کرد:
کد:
long int a = 100, *b, *c;
b = &a;
c = b + 1;
c++;
فرض کنیم متغیر a از خانه شماره 1000 شروع شده باشد. پس مقدار b با توچه به دستورات فوق 1000 خواهد بود. در خط بعدی b را با عدد یک جمع زده و در c قرار میدهیم. اما مقدار اشارهگر c بر خلاف حالت عادی 1001 نخواهد شد. متغیر c یک اشارهگر به عدد صحیح بزرگ است. زمانی که آن را با عدد یک جمع میزنیم، این اشارهگر به اندازه فضای مصرفی عدد صحیح بزرگ (در حالت استاندارد چهار بایت) پیش رفته و به خانه 1004 اشاره خواهد کرد. به همین ترتیب اگر از b یک واحد کم میکردیم به جای 999 به خانه شماره 996 اشاره میکرد. دلیل این مساله هم مشخص است. این چهار بایت در کنار هم معنی عدد صحیح بزرگ را دارند و حرکت در داخل بایتهای آن معنی ندارد. اگر اشارهگر به نوع دیگری تعریف شده بود، دقیقا به میزان فضای مصرفی همان نوع حرکت به جلو یا عقب صورت میگرفت. در خط آخر قطعه کد بالا هم باز یک پیشروی به جلو صورت گرفته و مقدار 1008 در خود اشارهگر c قرار میگیرد.