اشاره گر (Pointer)

همانطور که می دانید، هر متغیر در مکانی از حافظه ذخیره می‌شود. زمانی که یک متغیر را تعریف می‌کنید، بخشی از حافظه برای ذخیره مقدار آن تخصیص داده می‌شود. مکان این بخش از حافظه توسط یک آدرس مشخص می‌شود. این آدرس در حافظه، یک مقدار عددی است و شما می‌توانید با استفاده از نام به آن دسترسی داشته باشید.

ایجاد یک اشاره گر

تعریف یک اشاره گر دقیقاً مانند تعریف یک متغیر از انواع داده دیگر می‌باشد، با این تفاوت که بین نوع داده و نام متغیر یک ستاره (*) قرار می‌گیرد. نوع داده‌ای که برای اشاره گر در نظر می‌گیریم، نوع حافظه‌ای که می‌خواهیم به آن اشاره کنیم را مشخص می‌کند.

//pointer to int
int* p;

//pointer to double
double* d;

//pointer to float
float* f;

ما می‌توانیم آدرس یک متغیر در حافظه را با استفاده از قرار دادن علامت ampersand یا همان & پشت آن متغیر به دست آوریم (خط 7 کد زیر):

  1: #include <iostream>
  2: using namespace std;
  3: 
  4: int main()
  5: {
  6:     int a = 10;
  7:     cout << "Address of \"a\" variable is : " << &a << endl;
  8: 
  9:     int* p;
 10:     p = &a;
 11:     cout << "Address of \"p\" variable is : " <<  p << endl;
 12: 
 13:     cout << "Value   of \"p\" variable is : " << *p << endl;
 14: }
Address of "a" variable is : 0019FDA0
Address of "p" variable is : 0019FDA0
Value   of "p" variable is : 10

به دست آوردن مقدار موجود در یک آدرس

اشاره گر بالا (p) شامل آدرس مکانی در حافظه است که مقدار یک عدد صحیح را نگه داری می‌کند (خط 10). برای به دست آوردن مقدار واقعی ذخیره شده در یک آدرس باید کاراکتر ستاره (*) را قبل از نام اشاره گر قرار دهیم (خط 13). به کاراکتر ستاره در اینجا عملگر dereference گفته می‌شود. زمانی که داخل یک اشاره گر چیزی می‌ریزیم، اگر کاراکتر * را قبل از اشاره گر قرار ندهیم، مقداری که داخل این متغیر می‌ریزیم یک آدرس حافظه در نظر گرفته می‌شود. ولی اگر قبل از آن کاراکتر * را قرار دهیم، مقداری که داخل این متغیر ریخته می‌شود، همان مقدار واقعی ذخیره شده در آن خانه حافظه می‌باشد. اگر دو اشاره گر p و p2 را داشته باشیم، و مقدار p را داخل p2 بریزیم، یک کپی از آدرس مکانی که p به آن اشاره می‌کند را در داخل اشاره گر p2 قرار می‌گیرد:

int* p2 = p;

اشاره به یک اشاره گر (اشاره گرهای چندگانه)

در برنامه نویسی، اشاره کردن به یک اشاره گر دیگر گاهی اوقات مورد استفاده قرار می‌گیرد. برای انجام این کار، زمانی که می‌خواهیم متغیر مربوطه را تعریف کنیم از دو کاراکتر * استفاده می‌کنیم.

int** r = &p;   // pointer to p (assigns address of p)

برای اینکه این موضوع را به خوبی درک کنید در قالب یک مثال برای شما بیان می‌کنیم:

  1: #include <iostream>
  2: using namespace std;
  3: 
  4: int main()
  5: {
  6:     int a = 10;
  7: 
  8:     int* p;
  9:     p = &a;
 10: 
 11:     *p = 20;
 12: 
 13:     int** r = &p;
 14: 
 15:     cout << "Address of \"p\" variable is : " <<   r << endl;
 16:     cout << "Address of \"a\" variable is : " <<  *r << endl;
 17:     cout << "Value   of \"a\" variable is : " << **r << endl;
 18: }
Address of "p" variable is : 002CFABC
Address of "a" variable is : 002CFAC8
Value   of "a" variable is : 20

قبل از هر توضیحی یک نکته را یادآور می شویم و آن این است که، برای تغییر مقدار یک متغیر با استفاده از اشاره گر کافیست قبل از نام یک اشاره گر یک علامت * قرارداده و سپس یک مقدار به اشاره گر اختصاص دهیم کاری که در خط 11 انجام داده ایم. در این کد ما ابتدا در عدد صحیح a مقدار 10 را قرار داده ایم (خط 6). سپس به کمک dereference، مقدار a را به 20 تغییر دادیم (خط 11). بنابراین اکنون در متغیر a عدد صحیح 20 و در اشاره گر p، آدرس مکانی از حافظه (برای مثال 002CFAC8) که مقدار a در آن قرار دارد را داریم.

زمانی که می‌نویسیم &p، با توجه به مطالبی که قبلاً گفته شد، می‌خواهیم آدرس مکانی از حافظه که مقدار p در آن قرار دارد را داشته باشیم. آدرس فعلی p برابر با 002CFABC می‌باشد. بنابراین ما در اینجا می‌خواهیم آدرس مکانی از حافظه که مقدار 002CFABC را دارد را داشته باشیم که برای مثال 002CFAC8 می‌باشد. پس مقدار موجود در متغیر r نیز 002CFAC8 می‌باشد. در خطوط 17-15 مقادیر مختلف r را چاپ کرده ایم. در خط 15، آدرس مکانی از حافظه که دارای مقدار p است یعنی 002CFABC، در خط 16 همانطور که گفتیم، زمانی که * را پشت یک متغیر قرار می‌دهیم، می‌خواهیم مقدار واقعی ذخیره شده در آدرس r را داشته باشیم که همان p می‌باشد. زمانی که دو کاراکتر * قرار می‌دهیم به این معنی است که می‌خواهیم مقدار واقعی ذخیره شده در آدرس r* را داشته باشیم. با توجه به حالت قبل که مقدار r* برابر با p بود. پس انگار نوشته‌ایم p* و می‌خواهیم مقدار واقعی ذخیره شده در آدرس p را داشته باشیم که همان 20 است.

عملیات ریاضی بر روی اشاره گرها

همانطور که گفته شد، اشاره گرها مقادیر عدد هستند. شما می‌توانید با استفاده از عملگرهای –، ++، + و – عملیات ریاضی را بر روی اشاره گرها انجام دهید. عملگر افزایش (++)، مقدار اشاره گر را بر حسب نوع عملگر، افزایش می‌دهد. به عبارت دیگر، اگر نوع داده اشاره گر ما int باشد، از آنجایی که int در C++ چهار بایتی است، بنابراین زمانی که ما یک اشاره گر از نوع int را یک واحد افزایش می‌دهیم، در حافظه چهار بایت به سمت جلو حرکت می‌کند. برای مثال ما یک اشاره گر از نوع آرایه اعداد صحیح داریم:

  1: #include <iostream>
  2: using namespace std;
  3: 
  4: int main()
  5: {
  6:     int* a;
  7:     a = new int[3]{5, 3, 9};
  8: 
  9:     cout << a     << endl;
 10:     cout << ++a   << endl;
 11:     cout << --a   << endl;
 12:     cout << a + 2 << endl;
 13: }
00570380
00570384
00570380
00570388

در ابتدا این اشاره گر، مقدار آدرس شروع در حافظه را ذخیره می‌کند (یعنی آدرس عدد 5). اگر ما a را افزایش دهیم:

++a;

مقدار اشاره گر به آدرس عدد صحیح بعدی در آرایه تغییر پیدا می‌کند. به طور مشابه، عملگر کاهش (–) نیز مقدار اشاره گر را بر حسب نوع عملگر، کاهش می‌دهد و آدرس عنصر قبلی در نوع اشاره گر را ذخیره می‌کند. اگر اشاره گر a را کاهش دهیم:

--a;

آدرس عنصر قبلی در آرایه را ذخیره می کند. شما همچنین می‌توانید یک عدد صحیح را از اشاره گر کم (تفریق) و یا به آن اضافه (جمع) کنید. اگر به یک اشاره گر مقدار n را اضافه کنید، اشاره گر، n عنصر متناسب با نوع مشخص شده به جلو حرکت می‌کند. برای مثال ما می‌توانیم مقدار اشاره گر a را 2 واحد افزایش دهیم:

a = a + 2;

با این کار، اشاره گر a، آدرس عنصر سوم در آرایه را ذخیره می‌کند (خط 12). حال خطوط 12-9 کد بالا را به صورت زیر تغییر داده و برنامه را اجرا و نتیجه را مشاهده کنید:

cout << *(a    ) << endl;
cout << *(++a  ) << endl;
cout << *(--a  ) << endl;
cout << *(a + 2) << endl;

اشاره گر Null

زمانی که در یک اشاره گر آدرس معتبری وجود نداشته باشد، مقدار صفر در آن قرار می‌گیرد که به آن null pointer نیز گفته می‌شود. این ویژگی به شما کمک می‌کند تا بتوانید عمل dereference را با اطمینان انجام دهید. زیرا یک اشاره گر معتبر هرگز مقدار صفر را نخواهد داشت. اگرچه ما حافظه تحصیص داده شده به d را با استفاده از delete آزاد کردیم ولی d همچنان به مکانی از حافظه که غیر قابل دسترس است اشاره می‌کند. اگر تلاش کنیم تا عمل dereference را بر روی d انجام دهیم، باعث بروز خطا در زمان اجرا می‌شود. برای جلوگیری از این مشکل، باید مقدار حافظه حذف شده را صفر قرار دهیم. به این نکته توجه داشته باشید که اگر یک حافظه را با استفاده از delete حذف کنیم و سپس مقدار صفر را در آن قرار دهیم (null pointer) و دوباره آن را حذف کنیم مشکلی به وجود نمی‌آید. ولی اگر مقدار اشاره گر را برابر با صفر قرار نداده باشیم و تلاش کنیم تا آن دوباره را حذف کنیم، ممکن است باعث متوقف شدن برنامه شود.

d = 0; // mark as null pointer 

delete d; // safe

از آنجایی که همیشه نمی‌دانیم که یک اشاره گر معتبر است یا خیر، بنابراین بهتر است قبل از آنکه یک اشاره گر را dereference کنیم، مطمئن شویم که مقدار آن صفر نباشد:

if (d!= 0) 
{ 
   *d = 10; 
} 

ثابت NULL برای مشخص کردن یک null pointer مورد استفاده قرار می‌گیرد. NULL در ++C معمولاً صفر در نظر گرفته می‌شود. این ثابت در فایل کتابخانه استاندارد stdio.h قرار دارد که می‌تواند از طریق iostream در دسترس باشد.

#include <iostream>
if (d!= NULL) { *d = 10; } // check for null pointer

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

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

  1: #include <iostream>
  2: using namespace std;
  3: 
  4: void changeA(int* a)
  5: {                   
  6:     (*a) = 10;      
  7: }                   
  8: 
  9: int main()
 10: {
 11:     int *k = new int;
 12:     (*k) = 2;
 13: 
 14:     cout << "k before calling function " << *k << endl;
 15:     changeA(k);
 16:     cout << "k after calling function "  << *k << endl;
 17: }
k before calling function 2
k after calling function 10

در کد بالا پارامتر تابع int* a می‌باشد که یک اشاره گر است و در داخل تابع می‌توانیم به راحتی مقدار متغیر پاس داده شده را تغییر دهیم. برای تست این که آیا واقعاً مقدار متغیر عوض شده یا خیر، ابتدا یک اشاره گر از نوع int به نام k ایجاد کردیم و مقدار 2 را در آن قرار دادیم (خط 12). در خط 14 مقدار فعلی k را که همان 2 است را چاپ می کنیم و در خط 15 تابع changeA را با ارسال k به عنوان پارامتر فراخوانی کردیم. و در خط 16 دوباره مقدار k را چاپ کردیم.

ارسال آرایه به یک تابع

شما می‌توانید یک آرایه را به عنوان یک اشاره گر به یک تابع پاس دهید. دلیل اینکه این کار را می‌توانید انجام دهید این است که نام یک آرایه، یک اشاره گر به ابتدای آرایه است.

#include <iostream>
using namespace std;

int sum(int* arr, int size)
{
    int sum = 0;
    for (int i = 0; i != size; ++i)
        sum += arr[i];
    return sum;
}

int main()
{
    int myArr[5];

    cout << "Sum of myArr's elements is " << sum(myArr, 5);
}
Sum of myArr's elements is -4

زمانی که شما یک آرایه را به صورت بالا (خط 14) تعریف می‌کنید، myArr یک اشاره گر به ابتدای آرایه است. در خطوط 10-4 یک تابع تعریف کرده ایم که مجموع عناصر یک آرایه را حساب کند. این تابع دو پارامتر دریافت می‌کند. یکی اشاره گر و دیگری سایز آرایه می‌باشد (خط 4). سپس این آرایه را با مقادیر دلخواه پر (خط 7) و در خط 16 با فراخوانی تابع مجموع عناصر آن را حساب کنیم. در اینجا sum(myArr, 5)، آرایه myArr را با استفاده از اشاره گری که ابتدای آرایه را نشان می‌دهد به تابع sum پاس داده ایم.