یکی از مهمترین مباحث کاربردی هر زبان برنامه نویسی، اشاره گر و مفهوم آن است، که کاربرد گسترده ای در شاخه ساختمان داده ها نیز دارد. در این فرصت با مفهوم اشاره گر و همینطور روش تعریف آن در زبان ++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 با هم عوض شده اند، و خروجی به این صورت است:

m = 10 , n = 15

**
اشاره گرها و آرایه های پویا**

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

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 هم توجه داشته باشید. نبود این پرانتزها تعبیر دیگری را برای کامپایلر تداعی می کند که در نهایت به خطا منجر می شود.

پس از تعریف چنین اشاره گری، با دستور انتساب های زیر می توانید آدرس تابعی را در آن قرار دهید:

p = func;

p = &func;

هر دو دستور آدرس تابع func را در اشاره گر p قرار می دهند.

پس از این مقداردهی می توانید از اشاره گر برای فراخوانی تابع استفاده کنید:

cout << p( 6, 7.5 );

این خط معادل دستور زیر است:

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 قرار می گیرد.

aachp.ir