مجموعه آموزشی پی استور - https://programstore.ir

ارجاع‌ ها و اشاره گر ها در ++C

در اين جلسه به مبحث ارجاع‌ ها و اشاره گر ها در ++C می‌رسیم. بطور کلی در این جلسه با اشاره‌گرها و نحوه کار با آدرس‌های حافظه، ارجاع‌ها، عملگرهای new و delete، آرايه‌ای از اشاره‌گرها، اشاره‌گری به اشاره‌گر ديگر، اشاره‌گر به توابع و آرايه‌های پويا آشنا خواهیم شد.

انتظار می رود پس از مطالعه این جلسه «ارجاع» را تعريف کنيد و با استفاده از عملگر ارجاع به متغيرها دستيابی داشته باشید. «اشاره‌گر» را بشناسيد و بتوانيد اشاره‌گرهایی به انواع مختلف ايجاد کرده و آن‌ها را مقداريابی کنيد. «چپ‌مقدارها» و «راست‌مقدارها» را تعريف کرده و آن‌ها را از يکديگر تمیيز دهيد. طريقه استفاده از عملگرهای new و delete و وظيفه هر يک را بدانيد. «آرايه‌های پويا» را تعريف کرده و مزيت آن‌ها را نسبت به آرايه‌های ايستا ذکر کنيد و در نهایت آرايه‌های پويا را در برنامه‌هايتان ايجاد کرده و مديريت نماييد.

مقدمه

حافظه کامپیوتر [2] را می‌توان به صورت يک آرايه بزرگ در نظر گرفت. برای مثال کامپیوتر‌ی با 256 مگابايت RAM در حقيقت حاوی آرايه‌ای به اندازه 268،435،456 (=228) خانه است که اندازه هر خانه يک بايت است. اين خانه‌ها دارای ايندکس صفر تا 268،435،455 هستند. به ايندکس هر بايت، آدرس حافظه آن می‌گويند.

برای آشنایی بیشتر با آرایه ها می توانید در همین سایت آموزش کامل آرایه ها در ++C [3] را مطالعه فرمایید.

آدرس‌های حافظه را با اعداد شانزده‌دهی یا Hex نشان می‌دهند. پس کامپیوتر مذکور دارای محدوده آدرس 0x00000000 تا 0x0fffffff می‌باشد. هر وقت که متغيری را اعلان می‌کنيم، سه ويژگی اساسی به آن متغير نسبت داده می‌شود: «نوع متغير» و «نام متغير» و «آدرس حافظه» آن.

مثلا اعلان ;int n نوع int و نام n و آدرس چند خانه از حافظه که مقدار n در آن قرار می‌گيرد را به يکديگر مرتبط می‌سازد. فرض کنيد آدرس اين متغير 0x0050cdc0 است. بنابراين می‌توانيم n را مانند شکل مقابل مجسم کنيم:

ارجاع‌ ها و اشاره گر ها در ++Cخود متغير به شکل جعبه نمايش داده شده است. نام متغير n، در بالای جعبه است و آدرس متغير در سمت چپ جعبه و نوع متغير int، در زير جعبه نشان داده شده. در بيشتر کامپیوتر‌ها نوع int چهار بايت از حافظه را اشغال می‌نمايد. بنابراين همان‌طور که در شکل بال نشان داده شده است، متغير n يک بلوک چهاربايتی از حافظه را اشغال می‌کند که شامل بايت‌های 0x0050cdc0 تا 0x0050cdc3 است. توجه کنيد که آدرس شی، آدرس اولين بايت از بلوکی است که شی در آن جا ذخيره شده است.

اگر متغير فوق به شکل ;int n=32 مقداردهی اوليه شود، آنگاه بلوک حافظه به شکل زير خواهد بود. مقدار 32 در چهار بايتی که برای آن متغير منظور شده ذخيره می‌شود.

ارجاع‌ ها و اشاره گر ها در ++C

عملگر ارجاع در ++C

در سی پلاس پلاس برای بدست آوردن آدرس يک متغير می‌توان از عملگر ارجاع 1& استفاده کرد. به اين عملگر «علمگر آدرس» نيز می‌گويند. عبارت n& آدرس متغير n را به دست می‌دهد.

int main()
{  int n=44;
   cout << " n = " << n << endl;
 cout << "&n = " << &n << endl;
}

خروجی برنامه:

n = 44
&n = 0x00c9fdc3

خروجی نشان‌ می‌دهد كه‌ آدرس‌ n در اين اجرا برابر با 0x00c9fdc3 است. می‌توان فهميد که اين مقدار بايد يک آدرس باشد زيرا به شکل شانزده‌دهی نمايش داده شده. اعداد شانزده‌دهی را از روی علامت 0x می‌توان تشخيص داد. معادل دهدهی عدد بالا مقدار 13,237,699 می‌باشد.

ارجاع‌ ها در ++C

يك «ارجاع» يك اسم مستعار يا واژه مترادف برای متغير ديگر است. نحو اعلان يک ارجاع به شکل زير است:

type& ref_name = var_name; 

type نوع متغير است، ref_name نام مستعار است و var_name نام متغيری است که می‌خواهيم برای آن نام مستعار بسازيم. برای مثال در اعلان:

int& rn=n;   // r is a synonym for n

rn يک ارجاع يا نام مستعار برای n است. البته n بايد قبلا اعلان شده باشد.

مثالی از استفاده از ارجاع‌ها در ++C

int main()
{  int n=44;
   int& rn=n;     // rn is a synonym for n
   cout << "n = " << n << ", rn = " << rn << endl;
   --n;
   cout << "n = " << n << ", rn = " << rn << endl;
   rn *= 2;
   cout << "n = " << n << ", rn = " << rn << endl;
}

خروجی به شکل زیر است:

n = 44, rn = 44
n = 43, rn = 43
n = 86, rn = 86

n و rn نام‌های متفاوتی برای يک متغير است. اين دو هميشه مقدار يکسانی دارند. اگر n کاسته شود، rn نيز کاسته شده و اگر rn افزايش يابد، n نيز افزايش يافته است. همانند ثابت‌ها، ارجاع‌ها بايد هنگام اعلان مقداردهی اوليه شوند با اين تفاوت که مقدار اوليه يک ارجاع، يک متغير است نه يک ليترال. بنابراين کد زير اشتباه است:

int& rn=44; // ERROR: 44 is not a variable;

گرچه برخی از کامپايلرها ممکن است دستور بالا را مجاز بدانند ولی با نشان دادن يک هشدار اعلام می‌کنند که يک متغير موقتی ايجاد شده تا rn به حافظه آن متغير، ارجاع داشته باشد. درست است که ارجاع با يک متغير مقداردهی می‌شود، اما ارجاع به خودی خود يک متغير نيست. يک متغير، فضای ذخيره‌سازی و نشانی مستقل دارد، حال آن که ارجاع از فضای ذخيره‌سازی و نشانی متغير ديگری بهره می‌برد.

مثال: ارجاع‌ها متغيرهای مستقل نيستند

int main()
{  int n=44;
   int& rn=n;        // rn is a synonym for n
   cout << "  &n = " << &n << ",  &rn = " << &rn << endl;
   int& rn2=n;      // rn2 is another synonym for n
   int& rn3=rn;     // rn3 is another synonym for n
   cout << "&rn2 = " << &rn2 << ", &rn3 = " << &rn3 << endl;
}

خروجی به این شکل خواهد بود:

&n = 0x0064fde4,  &rn = 0x0064fde4
&rn2 = 0x0064fde4, &rn3 = 0x0064fde4 

در برنامه فوق فقط يک شی وجود دارد و آن هم n است. rn و rn2 و rn3 ارجاع‌هايی به n هستند. خروجی نيز تاييد می‌کند که آدرس rn و rn2 و rn3 با آدرس n يکی است. يک شی می‌تواند چند ارجاع داشته باشد.

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

ارجاع‌ ها و اشاره گر ها در ++C

می‌دانيم که اعداد صحيح را بايد در متغيری از نوع int نگهداری کنيم و اعداد اعشاری را در متغيرهايی از نوع float. به همين ترتيب کاراکترها را بايد در متغيرهايی از نوع char نگهداريم و مقدارهای منطقی را در متغيرهايی از نوع bool. اما آدرس حافظه را در چه نوع متغيری بايد قرار دهيم؟

عملگر ارجاع & آدرس حافظه يک متغير موجود را به دست می‌دهد. می‌توان اين آدرس را در متغير ديگری ذخيره نمود. برای اين‌که يک اشاره‌گر اعلان کنيم، ابتدا بايد مشخص کنيم که آدرس چه نوع داده‌ای قرار است در آن ذخيره شود. سپس از عملگر اشاره * استفاده می‌کنيم تا اشاره‌گر را اعلان کنيم. متغيری که يک آدرس در آن ذخيره می‌شود اشاره‌گر ناميده می‌شود.

برای مثال دستور :

float* px;

اشاره‌گری به نام px اعلان می‌کند که اين اشاره‌گر، آدرس متغيرهايی از نوع float را نگهداری می‌نمايد. به طور کلی برای اعلان يک اشاره‌گر از نحو زير استفاده می‌کنيم:

type* pointername;

که type نوع متغيرهايی است که اين اشاره‌گر آدرس آن‌ها را نگهداری می‌کند و pointername نام اشاره‌گر است. آدرس يک شی از نوع int را فقط می‌توان در اشاره‌گری از نوع *int ذخيره کرد و آدرس يک شی از نوع float را فقط می‌توان در اشاره‌گری از نوع *float ذخيره نمود. دقت کنيد که يک اشاره‌گر، يک متغير مستقل است.

مثال: به کارگيری اشاره‌گرها

برنامه زير يک متغير از نوع int به نام n و يک اشاره‌گر از نوع *int به نام pn  را اعلان می‌کند:

int main()
{  int n=44;
   cout << "n = " << n << ", &n = " << &n << endl;
   int* pn=&n;   // pn holds the address of n
   cout << "       pn = " << pn << endl;
   cout << "&pn = " << &pn << endl;}
n = 44, &n = 0x0064fddc
        pn = 0x0064fddc
&pn = 0x0064fde0

متغير n با مقدار 44 مقداردهی شده و آدرس آن 0x0064fddc می‌باشد. اشاره‌گر pn با مقدار n& يعنی آدرس n مقداردهی شده. پس مقدار درون pn برابر با 0x0064fddc است‌ (خط دوم خروجی اين موضوع را تاييد می‌کند) .

اشارگر‌ها و ارجاع‌هااما pn يک متغير مستقل است و آدرس مستقلی دارد. pn& آدرس pn  را به دست می‌دهد. خط سوم خروجی ثابت می‌کند که متغير pn مستقل از متغير n است. تصوير زير به درک بهتر اين موضوع کمک می‌کند. در اين تصوير ويژگی‌های مهم n و pn نشان داده شده. pn يک اشاره‌گر به n است و n مقدار 44 دارد. وقتی می‌گوييم «pn به n اشاره می‌کند» يعنی درون pn آدرس n قرار دارد.

ارجاع‌ ها و اشاره گر ها در ++Cمقداریابی در ++C

فرض کنيد n دارای مقدار 22 باشد و pn اشاره‌گری به n باشد. با اين حساب بايد بتوان از طريق pn به مقدار 22 رسيد. با استفاده از * می‌توان مقداری که اشاره‌گر به آن اشاره دارد را به دست آورد. به اين کار مقداريابی اشاره‌گر می‌گوييم.

مثال‌:  مقداريابی يك اشاره‌گر:

اين‌ برنامه‌ همان‌ برنامه مثال قبلی است. فقط يک خط کد بيشتر دارد:

int main()
{  int n=44;
   cout << "n = " << n << ", &n = " << &n << endl;
   int* pn=&n;    // pn holds the address of n
   cout << "        pn = " << pn << endl;
   cout << "&pn = " << &pn << endl;
   cout << "*pn = " << *pn << endl;
}

خروجی:

n = 44, &n = 0x0064fdcc
        pn = 0x0064fdcc
&pn = 0x0064fdd0
*pn = 44

ظاهرا *pn يک اسم مستعار برای n است زيرا هر دو يک مقدار دارند.

 مثال:  اشاره‌گری به اشاره‌گرها

يک اشاره‌گر به هر چيزی می‌تواند اشاره کند، حتی به يک اشاره‌گر ديگر. به مثال زير دقت کنيد:

int main()
{  int n=44;
   cout << "    n = " << n << endl;
   cout << "   &n = " << &n << endl;
   int* pn=&n;      // pn holds the address of n
   cout << "   pn = " << pn << endl;
   cout << "  &pn = " << &pn << endl;
   cout << "  *pn = " << *pn << endl;
   int** ppn=&pn;   // ppn holds the address of pn
   cout << "  ppn = " << ppn << endl;
   cout << " &ppn = " << &ppn << endl;
   cout << " *ppn = " << *ppn << endl;
   cout << "**ppn = " << **ppn << endl;
}
n = 44
   &n = 0x0064fd78
   pn = 0x0064fd78
  &pn = 0x0064fd7c
  *pn = 44
  ppn = 0x0064fd7c
 &ppn = 0x0064fd80
 *ppn = 0x0064fd78
**ppn = 44

در برنامه بالا متغير n از نوع int تعريف شده. pn اشاره‌گری است که به n اشاره دارد. پس نوع pn بايد *int باشد. ppn اشاره‌گری است که به pn اشاره می‌کند. پس نوع ppn بايد **int باشد. همچنين چون ppn به pn اشاره دارد، پس ppn* مقدار pn را نشان می‌دهد و چون pn به n اشاره دارد، پس pn* مقدار n را می‌دهد.

ارجاع‌ ها و اشاره گر ها در ++Cعملگر مقداريابی * و عملگر ارجاع & معکوس يکديگر رفتار می‌کنند. اگر اين دو را با هم ترکيب کنيم، يکديگر را خنثی می‌نمايند. اگر n يک متغير باشد، n& آدرس آن متغير است. از طرفی با استفاده از عملگر * می‌توان مقداری که در آدرس n& قرار گرفته را به دست آورد. بنابراين n&* برابر با خود n خواهد بود. همچنين اگر p يک اشاره‌گر باشد، p* مقداری که p به آن اشاره دارد را می‌دهد. از طرفی با استفاده از عملگر & می‌توانيم آدرس چيزی که در p* قرار گرفته را بدست آوريم.

پس p*& برابر با خود p خواهد بود. ترتيب قرارگرفتن اين عملگرها مهم است. يعنی n&* با n*& برابر نيست. عملگر * دو کاربرد دارد. اگر پسوندِ يک نوع باشد (مثل *int) يک اشاره‌گر به آن نوع را تعريف می‌کند و اگر پيشوندِ يک اشاره‌گر باشد (مثل p*) آنگاه مقداری که p به آن اشاره می‌کند را برمی‌گرداند. عملگر & نيز دو کاربرد دارد. اگر پسوند يک نوع باشد (مثل &int) يک نام مستعار تعريف می‌کند و اگر پيشوند يک متغير باشد (مثل n&) آدرس آن متغير را می‌دهد.

چپ مقدارها، راست مقدارها در ++C

يک دستور جايگزينی دو بخش دارد: بخشی که در سمت چپ علامت جايگزينی قرار می‌گيرد و بخشی که در سمت راست علامت جايگزينی قرار می‌گيرد. مثلا دستور ;n = 55  متغير n در سمت چپ قرار گرفته و مقدار 55 در سمت راست. اين دستور را نمی‌توان به شکل زیر نوشت:

55=n;

زيرا مقدار 55 يک ثابت است و نمی‌تواند مقدار بگيرد. پس هنگام استفاده از عملگر جايگزينی بايد دقت کنيم که چه چيزی را در سمت چپ قرار بدهيم و چه چيزی را در سمت راست.

چيزهايی که می‌توانند در سمت چپ جايگزينی قرار بگيرند «چپ‌مقدار» خوانده می‌شوند و چيزهايی که می‌توانند در سمت راست جايگزينی قرار بگيرند «راست‌مقدار» ناميده می‌شوند. متغيرها (و به طور کلی اشيا) چپ‌مقدار هستند و ليترال‌ها (مثل 15 و “ABC”) راست مقدار هستند. يک ثابت در ابتدا به شکل يک چپ‌مقدار نمايان می‌شود:

const int MAX = 65535;     // MAX is an lvalue

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

MAX = 21024;    // ERROR: MAX is constant

به اين گونه چپ‌مقدارها، چپ‌مقدارهای «تغيير ناپذير» گفته می‌شود. مثل آرايه‌ها:

int a[] = {1,2,3};   // O.K
a[] = {1,2,3};       // ERROR

مابقی چپ‌مقدارها که می‌توان آن‌ها را تغيير داد، چپ‌مقدارهای «تغيير پذير» ناميده می‌شوند. هنگام اعلان يک ارجاع به يک چپ‌مقدار نياز داريم:

int& r = n;            // O.K. n is an lvalue

اما اعلان‌های زير غيرمعتبرند زيرا هيچ‌کدام چپ‌‌مقدار نيستند:

int& r = 44;           // ERROR: 44 is not an lvalue
int& r = n++;          // ERROR: n++ is not an lvalue
int& r = cube(n);      // ERROR: cube(n) is not an lvalue1 – L_values	2- R_values

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

بازگشت از نوع ارجاع در ++C

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

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

m = f();

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

f() = m;

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

مثال‌: بازگشت از نوع ارجاع:

int& max(int& m, int& n) 
{  return (m > n ? m : n);}
int main()
{  int m = 44, n = 22;
   cout << m << ", " << n << ", " << max(m,n) << endl;
   max(m,n) = 55; 
cout << m << ", " << n << ", " << max(m,n) << endl;
}
4455, 22, 55
, 22, 44

تابع ()max از بين m و n مقدار بزرگ‌تر را پيدا کرده و سپس ارجاعی به آن را باز می‌گرداند. بنابراين اگر m از n بزرگ‌تر باشد، تابع max(m,n)  آدرس m را برمی‌گرداند. پس وقتی می‌نويسيم ;max(m,n) = 55 مقدار 55 در حقيقت درون متغير m قرار می‌گيرد (اگر m>n باشد). به بيانی ساده، فراخوانی max(m,n) خود m را بر می‌گرداند نه مقدار آن را.

اخطار: وقتی يک تابع پايان می‌يابد، متغيرهای محلی آن نابود می‌شوند. پس هيچ وقت ارجاعی به يک متغير محلی بازگشت ندهيد زيرا وقتی کار تابع تمام شد، آدرس متغيرهای محلی‌اش غير معتبر می‌شود و ارجاع بازگشت داده شده ممکن است به يک مقدار غير معتبر اشاره داشته باشد. تابع ()max در مثال بالا يک ارجاع به m يا n را بر می‌گرداند. چون m و n خودشان به طريق ارجاع ارسال شده‌اند، پس محلی نيستند و بازگرداندن ارجاعی به آن‌ها خللی در برنامه وارد نمی‌کند.

به اعلان تابع ()max دقت کنيد:

int& max(int& m, int& n)

نوع بازگشتی آن با استفاده از عملگر ارجاع & به شکل يک ارجاع درآمده است.

مثال‌: به کارگيری يك تابع به عنوان عملگر زيرنويس آرايه:

float& component(float* v, int k)
{  return v[k-1];}
int main()
{  float v[4];
   for (int k = 1; k <= 4; k++)
      component(v,k) = 1.0/k;
   for (int i = 0; i < 4; i++)
      cout << "v[" << i << "] = " << v[i] << endl;
}

خروجی:

v[0] = 1
v[1] = 0.5
v[2] = 0.333333
v[3] = 0.25

تابع‌ ()component باعث می‌شود که ايندکس آرايه v از «شماره‌گذاری از صفر» به «شماره‌گذاری از يک» تغيير کند. بنابراين component(v,3) معادل v[2] است. اين کار از طريق بازگشت از طريق ارجاع ممکن شده است.

آرايه‌ها و اشاره‌گر ها در ++C

گرچه اشاره‌گرها از انواع‌ عددی صحيح‌ نيستند اما بعضی از اعمال حسابی را می‌توان روی اشاره‌گرها انجام داد. حاصل اين می‌شود که اشاره‌گر به خانه ديگری از حافظه اشاره می‌کند. اشاره‌گرها را می‌توان مثل اعداد صحيح افزايش و يا کاهش داد و می‌توان يک عدد صحيح را به آن‌ها اضافه نمود يا از آن کم کرد. البته ميزان افزايش يا کاهش اشاره‌گر بستگی به نوع داده‌ای دارد که اشاره‌گر به آن اشاره دارد.

مثال‌: پيمايش آرايه با استفاده از اشاره‌گر

اين‌ مثال‌ نشان‌ می‌دهد كه‌ چگونه‌ می‌توان از اشاره‌گر برای پيمايش يک آرايه استفاده نمود:

int main()
{  const int SIZE = 3;
   short a[SIZE] = {22, 33, 44};
   cout << "a = " << a << endl;
   cout << "sizeof(short) = " << sizeof(short) << endl;
   short* end = a + SIZE; // converts SIZE to offset 6
   short sum = 0;
   for (short* p = a; p < end; p++)
   {  sum += *p;
      cout << "\t p = " << p;
      cout << "\t *p = " << *p;
      cout << "\t sum = " << sum << endl;
   }
   cout << "end = " << end << endl;
}
a = 0x3fffd1a
sizeof(short) = 2
               p = 0x3fffd1a       *p = 22       sum = 22
               p = 0x3fffd1c       *p = 33       sum = 55
               p = 0x3fffd1e       *p = 44       sum = 99
      end = 0x3fffd20

اين مثال نشان می‌دهد که هر‌گاه يک اشاره‌گر افزايش يابد، مقدار آن به اندازه تعداد بايت‌های شئ که به آن اشاره می‌کند، افزايش می‌يابد. مثلا اگر p اشاره‌گری به double باشد و sizeof(double) برابر با هشت بايت باشد، هر گاه که p يک واحد افزايش يابد، اشاره‌گر p هشت بايت به پيش می‌رود. مثلا کد زير :

float a[8];
float* p = a;    // p points to a[0]
++p; // increases the value of p by sizeof(float)

اگر floatها 4 بايت را اشغال‌ كنند آنگاه p++ مقدار درون p  را 4 بايت افزايش می‌دهد و  ;p += 5 مقدار درون p را 20 بايت افزايش می‌دهد. با استفاده از خاصيت مذکور می‌توان آرايه را پيمايش نمود: يک اشاره‌گر را با آدرس اولين عنصر آرايه مقداردهی کنيد، سپس اشاره‌گر را پی‌در‌پی افزايش دهيد. هر افزايش سبب می‌شود که اشاره‌گر به عنصر بعدی آرايه اشاره کند. يعنی اشاره‌گری که به اين نحو به کار گرفته شود مثل ايندکس آرایه عمل می‌کند.

همچنين با استفاده از اشاره‌گر می‌توانيم مستقيما به عنصر مورد نظر در آرايه دستيابی کنيم:

float* p = a;       // p points to a[0]
p += 5;             // now p points to a[5]

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

کد زير نشان می‌دهد که چطور اين اتفاق رخ می‌دهد:

float a[8];
float* p = a[7]; // points to last element in the array
++p; //now p points to memory past last element!
*p = 22.2;           // TROUBLE!

مثال‌ بعدی نشان‌ می‌دهد كه‌ ارتباط تنگاتنگی بين‌ آرايه‌ها و اشاره‌گرها وجود دارد. نام آرايه در حقيقت يک اشاره‌گر ثابت (const) به اولين عنصر آرايه است. همچنين خواهيم ديد که اشاره‌گرها را مانند هر متغير ديگری می‌توان با هم مقايسه نمود.

مثال‌: پيمايش عناصر آرايه از طريق آدرس‌:

int main()
{  short a[] = {22, 33, 44, 55, 66};
   cout << "a = " << a << ", *a = " << *a << endl;
   for (short* p = a; p < a +5; p++)
     cout << "p = " << p << ", *p = " << *p << endl;
}

خروجی:

a = 0x3fffd08, *a = 22
       p = 0x3fffd08, *p = 22
       p = 0x3fffd0a, *p = 33
       p = 0x3fffd0c, *p = 44
       p = 0x3fffd0e, *p = 55
       p = 0x3fffd10, *p = 66
       p = 0x3fffd12, *p = 77

در نگاه اول‌، a و p مانند هم هستند: هر دو به نوع short اشاره می‌کنند و هر دو دارای مقدار 0x3fffd08 هستند. اما a يک اشاره‌گر ثابت است و نمی‌تواند افزايش يابد تا آرايه پيمايش شود. پس به جای آن p را افزايش می‌دهيم تا آرايه را پيمايش کنيم. شرط (p < a+5)  حلقه را خاتمه می‌دهد. a+5 به شکل زير ارزيابی می‌شود:

0x3fffd08 + 5*sizeof(short) = 0x3fffd08 + 5*2 = 0x3fffd08 + 0xa = 0x3fffd12

پس حلقه تا زمانی که p < 0x3fffd12  باشد ادامه می‌يابد. عملگر زيرنويس ‌[]  مثل عملگر مقداريابی *  رفتار می‌کند. هر دوی اين‌ها می‌توانند به عناصر آرايه دسترسی مستقيم داشته باشند.

a[0] == *a
a[1] == *(a + 1)
a[2] == *(a + 2)
    ...
    ...

پس با استفاده از کد زير نيز می‌توان آرايه را پيمايش نمود:

for (int i = 0; i < 8; i++)
        cout << *(a + i) << endl;

مثال‌: مقايسه الگو

در اين مثال، تابع ()loc در ميان n1 عنصر اول آرايه a1 به دنبال n2 عنصر اول‌ آرایه a2 می‌گردد. اگر پيدا شد، يک اشاره‌گر به درون a1 برمی‌گرداند که a2 از آن‌جا شروع می‌شود وگرنه اشاره‌گر NULL را برمی‌گرداند.

short* loc(short* a1, short* a2, int n1, int n2)
{  short* end1 = a1 + n1;
   for (short* p1 = a1; p1 <end1; p1++)
      if (*p1 == *a2)
int main()
{  short a1[9] = {11, 11, 11, 11, 11, 22, 33, 44, 55};
   short a2[5] = {11, 11, 11, 22, 33};
   cout << "Array a1 begins at location\t" << a1 << endl;
   cout << "Array a2 begins at location\t" << a2 << endl;
   short* p = loc(a1, a2, 9, 5);
   if (p)
   {  cout << "Array a2 found at location\t" << p << endl;
      for (int i = 0; i < 5; i++)
         cout << "\t" << &p[i] << ": " << p[i] << "\t" 
              << &a2[i] << ": " << a2[i] << endl; }
   else cout << "Not found.\n";}
{ for (int j = 0; j < n2; j++) if (p1[j] != a2[j]) break; if (j == n2) return p1; } return 0; }

خروجی:

Array a1 begins at location       0x3fffd12
       Array a2 begins at location       0x3fffd08
       Array a2 found at location         0x3fffd16
              0x3fffd16: 11       0x3fffd08: 11
              0x3fffd18: 11       0x3fffd0a: 11
              0x3fffd1a: 11       0x3fffd0c: 11
              0x3fffd1c: 22       0x3fffd0e: 22
              0x3fffd1e: 33       0x3fffd10: 33

عملگر new در ++C

وقتی يك‌ اشاره‌گر شبيه‌ اين‌ اعلان‌ شود:

float* p;    // p is a pointer to a float

يک فضای چهاربايتی به p تخصيص داده می‌شود (معمولا sizeof(float) چهار بايت است). حالا p ايجاد شده است اما به هيچ جايی اشاره نمی‌کند زيرا هنوز آدرسی درون آن قرار نگرفته. به چنين اشاره‌گری اشاره‌گر سرگردان می‌گويند. اگر سعی کنيم يک اشاره‌گر سرگردان را مقداريابی يا ارجاع کنيم با خطا مواجه می‌شويم.

مثلا دستور:

p = 3.14159;     // ERROR: no storage has been allocated for *P

خطاست. زيرا p به هيچ آدرسی اشاره نمی‌کند و سيستم عامل نمی‌داند که مقدار 3.14159 را کجا ذخيره کند. برای رفع اين مشکل می‌توان اشاره‌گرها را هنگام اعلان، مقداردهی کرد:

float x = 0;    // x cintains the value 0
float* p = &x   // now p points to x
*p = 3.14159;   // O.K. assigns this value to address that p points to

در اين حالت می‌توان به p* دستيابی داشت زيرا حالا p به x اشاره می‌کند و آدرس آن را دارد. راه حل ديگر اين است که يک آدرس اختصاصی ايجاد شود و درون p قرار بگيرد. بدين ترتيب p از سرگردانی خارج می‌شود. اين کار با استفاده از عملگر new صورت می‌پذيرد:

float* p;	
p = new float; // allocates storage for 1 float
*p = 3.14159; // O.K. assigns this value to that storage

دقت کنيد که عملگر new فقط خود p را مقداردهی می‌کند نه آدرسی که p به آن اشاره می‌کند. می‌توانيم سه خط فوق را با هم ترکيب کرده و به شکل يک دستور بنويسيم:

float* p = new float(3.141459);

با اين دستور، اشاره‌گر p از نوع *float تعريف می‌شود و سپس يک بلوک خالی از نوع float منظور شده و آدرس آن به p تخصيص می‌يابد و همچنين مقدار 3.14159 در آن آدرس قرار می‌گيرد. اگر عملگر new نتواند خانه خالی در حافظه پيدا کند، مقدار صفر را برمی‌گرداند. اشاره‌گری که اين چنين باشد، «اشاره‌گر تهی» يا NULL می‌نامند. با استفاده از کد هوشمند زير می‌توانيم مراقب باشيم که اشاره‌گر تهی ايجاد نشود:

double* p = new double;
if (p == 0) abort();     // allocator failed: insufficent memory
else *p = 3.141592658979324;

در اين قطعه کد، هرگاه اشاره‌گری تهی ايجاد شد، تابع ()abort فراخوانی شده و اين دستور لغو می‌شود. تاکنون دانستيم که به دو طريق می‌توان يک متغير را ايجاد و مقداردهی کرد. روش اول:

float x = 3.14159;               // allocates named memory

و روش دوم:

float* p = new float(3.14159);   // allocates unnamed memory

در حالت اول، حافظه مورد نياز برای x هنگام کامپايل تخصيص می‌يابد. در حالت دوم حافظه مورد نياز در زمان اجرا و به يک شئ بی‌نام تخصيص می‌يابد که با استفاده از p* قابل دستيابی است.

عملگر delete در ++C

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

float* p = new float(3.14159);
delete p;           // deallocates q
*p = 2.71828; // ERROR: q has been deallocated

وقتی اشاره گر p در کد بالا آزاد شود، حافظه‌ای که توسط new به آن تخصيص يافته بود، آزاد شده و به ميزان sizeof(float) به حافظه آزاد اضافه می‌شود. وقتی اشاره‌گری آزاد شد، به هيچ چيزی اشاره نمی‌کند، مثل متغيری که مقداردهی نشده است. به اين اشاره‌گر، اشاره‌گر سرگردان می‌گويند. اشاره‌گر به يک شیء ثابت را نمی‌توان آزاد کرد:

const int* p = new int;
delete p; // ERROR: cannot delete pointer to const objects

علت اين است که «ثابت‌ها نمی‌توانند تغيير کنند». اگر متغيری را صريحا اعلان کرده‌ايد و سپس اشاره‌گری به آن نسبت داده‌ايد، از عملگر delete استفاده نکنيد. اين کار باعث اشتباه غير عمدی زير می‌شود:

float x =3.14159; // x contains the value 3.14159
float* p = &x; // p contains the address of x
delete p; // WARNING: this will make x free

کد بالا باعث می‌شود که حافظه تخصيص‌ يافته برای x آزاد شود. اين اشتباه را به سختی می‌توان تشخيص داد و اشکال‌زدايی کرد.

آرايه‌های پويا در ++C

نام آرايه در حقيقت يك اشاره‌گر ثابت است كه‌ در زمان‌ كامپايل‌، ايجاد و تخصيص‌ داده‌ می‌شود:

float a[20]; //a is a const pointer to a block of 20 floats
float* const p = new float[20];    // so is p

هم a و هم p اشاره‌گرهای ثابتی هستند که به بلوکی حاوی 20 متغير float اشاره دارند. به اعلان a بسته‌بندی ايستا1 می‌گويند زيرا اين کد باعث می‌شود که حافظه مورد نياز برای a در زمان کامپايل تخصيص داده شود. وقی برنامه اجرا شود، به هر حال حافظه مربوطه تخصيص خواهد يافت حتی اگر از آن هيچ استفاده‌ای نشود.

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

float* p = new float[20];

دستور بالا، 20 خانه خالی حافظه از نوع float را در اختيار گذاشته و اشاره‌گر p را به خانه اول آن نسبت می‌دهد. به اين آرايه، «آرايه پويا2» می‌گويند. به اين طرز ايجاد اشيا بسته‌بندی پويا3 يا «بسته‌بندی زمان اجرا» می‌گويند.

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

delete [] p;

برای آزاد کردن آرايه پويای p براکت‌ها [] قبل از نام p بايد حتما قيد شوند زيرا p به يک آرايه اشاره دارد.

مثال‌: استفاده‌ از آرايه‌های پويا

تابع‌ ()get در برنامه زير يک آرايه پويا ايجاد می‌كند:

void get(double*& a, int& n)
{  cout << "Enter number of items: ";  cin >> n;
   a = new double[n];
   cout << "Enter " << n << " items, one per line:\n";
   for (int i = 0; i < n; i++)
   {  cout << "\t" << i+1 << ": ";
 cin >> a[i];
   }}
void print(double* a, int n)
{  for (int i = 0; i < n; i++)
   cout << a[i] << " " ;
   cout << endl;
}
int main()
{ double* a;// a is simply an unallocated pointer
   int n;
   get(a,n);      // now a is an array of n doubles  print(a,n);
   delete [] a;// now a is simply an unallocated pointer again
   get(a,n); // now a is an array of n doubles   print(a,n);
}

خروجی برنامه به شکل زیر است:

Enter number of items: 4
Enter 4 items, one per line:
       1: 44.4
       2: 77.7
       3: 22.2
       4: 88.8
44.4 77.7 22.2 88.8
Enter number of items: 2
Enter 2 items, one per line:
       1: 3.33
       2: 9.99
3.33 9.99

اشاره‌گر ثابت در ++C

«اشاره‌گر به يک ثابت» با «اشاره‌گر ثابت» تفاوت دارد. اين تفاوت در قالب مثال زير نشان داده شده است.

مثال‌: اشاره‌گرهای ثابت و اشاره‌گرهای به ثابت‌ها

در اين کد چهار اشاره‌گر اعلان شده. اشاره‌گر p، اشاره‌گر ثابت cp، اشاره به يک ثابت pc، اشاره‌گر ثابت به يک ثابت cpc :

int n = 44;                  // an int
int* p = &n;                 // a pointer to an int
++(*p);                      // OK: increments int *p
++p;                         // OK: increments pointer p
int* const cp = &n;          // a const pointer to an int
++(*cp);                     // OK: increments int *cp
++cp;                        // illegal: pointer cp is const
const int k = 88;            // a const int
const int * pc = &k;         // a pointer to a const int
++(*pc);                     // illegal: int *pc is const
++pc;                        // OK: increments pointer pc
const int* const cpc = &k;   // a const pointer to a const int
++(*cpc);                    // illegal: int *pc is const
++cpc;                       // illegal: pointer cpc is const

اشاره‌گر p اشاره‌گری به متغير n است. هم خود p قابل افزايش است (p++) و هم مقداری که p به آن اشاره می‌کند قابل افزايش است ((P*)++). اشاره گر cp يک اشاره‌گر ثابت است. يعنی آدرسی که در cp است قابل تغيير نيست ولی مقداری که در آن آدرس است را می‌توان دست‌کاری کرد. اشاره‌گر pc اشاره‌گری است که به آدرس يک ثابت اشاره دارد. خود pc را می‌توان تغيير داد ولی مقداری که pc به آن اشاره دارد قابل تغيير نيست. در آخر هم cpc يک اشاره‌گر ثابت به يک شیء ثابت است. نه مقدار cpc قابل تغيير است و نه مقداری که آدرس آن در cpc است.

آرايه‌ای از اشاره‌گرها در ++C

می‌توانيم آرايه‌ای تعريف کنيم که اعضای آن از نوع اشاره‌گر باشند. مثلا دستور:

float* p[4];

آرايه p را با چهار عنصر از نوع *float (يعنی اشاره‌گری به float) اعلان می‌کند. عناصر اين آرايه را مثل اشاره‌گر‌های معمولی می‌توان مقداردهی کرد:

p[0] = new float(3.14159);
p[1] = new float(1.19);

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

ارجاع‌ ها و اشاره گر ها در ++Cمثال‌: مرتب‌سازی حبابی غيرمستقيم

void sort(float* p[], int n)
{  float* temp;
   for (int i = 1; i < n; i++)
      for (int j = 0; j < n-i; j++)
         if (*p[j] > *p[j+1])
         {  temp = p[j];
            p[j] = p[j+1];
            p[j+1] = temp;
         }
}

تابع ()sort آرايه‌ای از اشاره‌گرها را می‌گيرد. سپس درون حلقه‌های تودرتوی for بررسی می‌کند که آيا مقاديری که اشاره‌گرهای مجاور به آن‌ها اشاره دارند، مرتب هستند يا نه. اگر مرتب نبودند، جای اشاره‌گرهای آن‌ها را با هم عوض می‌کند. در پايان به جای اين که يک فهرست مرتب داشته باشيم، آرايه‌ای داريم که اشاره‌گرهای درون آن به ترتيب قرار گرفته اند.

اشاره‌گری به اشاره‌گر ديگر در ++C

يك اشاره‌گر می‌تواند به اشاره‌گر ديگری اشاره کند. مثلا:

char c = 't';
char* pc = &c;
char** ppc = &pc;
char*** pppc = &ppc;
***pppc = 'w';   // changes value of c to 'w'

حالا pc اشاره‌گری به متغير کاراکتری c است. ppc اشاره‌گری به اشاره‌گر pc است و اشاره‌گر pppc هم به اشاره‌گر ppc اشاره دارد. مثل شکل زیر:

ارجاع‌ ها و اشاره گر ها در ++Cبا اين وجود می‌توان با اشاره‌گر pppc مستقيما به متغير c رسيد.

اشاره‌گر به توابع در ++C

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

int f(int);      // declares function f
int (*pf)(int);  // declares function pointer pf
pf = &f;         // assigns address of f to pf

اشاره‌گر pf همراه با * درون پرانتز قرار گرفته، يعنی اين که pf اشاره‌گری به يک تابع است. بعد از آن يک int هم درون پرانتز آمده است، به اين معنی که تابعی که pf به آن اشاره می‌نمايد، پارامتری از نوع int دارد. اشاره‌گر pf را می‌توانيم به شکل زير تصور کنيم:

فايده اشاره‌گر به توابع اين است که به اين طريق می‌توانيم توابع مرکب بسازيم. يعنی می‌توانيم يک تابع را به عنوان آرگومان به تابع ديگر ارسال کنيم. اين کار با استفاده از اشاره‌گر به تابع امکان پذير است.

ارجاع‌ ها و اشاره گر ها در ++Cمثال‌: تابع مرکب جمع

تابع‌ ()sum در اين مثال دو پارامتر دارد: اشاره‌گر تابع pf و عدد صحيح n :

int sum(int (*)(int), int);
int square(int);
int cube(int);
int main()
{  cout << sum(square,4) << endl; // 1 + 4 + 9 + 16
   cout << sum(cube,4) << endl; //1 + 8 + 27 + 64
}

تابع ()sum يک پارامتر غير معمول دارد. نام تابع ديگری به عنوان آرگومان به آن ارسال شده. هنگامی که ‌ sum(square,4) فراخوانی شود، مقدار square(1)+square(2)+square(3)+square(4) بازگشت داده می‌شود. چونsquare(k) مقدار k*k را برمی‌گرداند، فراخوانی sum(square,4) مقدار 30=16+9+4+1 را محاسبه نموده و بازمی‌گرداند. تعريف توابع و خروجی آزمايشی به شکل زير است:

int sum(int (*pf)(int k), int n)
{  // returns the sum f(0) + f(1) + f(2) + ... + f(n-1):
   int s = 0;
   for (int i = 1; i <= n; i++)
      s += (*pf)(i);
   return s;
}
int square(int k)
{  return k*k;
}
int cube(int k)
{  return k*k*k;
}
30
100

pf در فهرست پارامترهای تابع ()sum  يک اشاره‌گر به تابع است. اشاره‌گر به تابعی که آن تابع پارامتری از نوع int دارد و مقداری از نوع int را برمی‌گرداند. k در تابع sum اصلا استفاده نشده اما حتما بايد قيد شود تا کامپايلر بفهمد که pf به تابعی اشاره دارد که پارامتری از نوع int دارد. عبار (i)(pf*) معادل با square(i) يا cube(i) خواهد بود، بسته به اين که کدام يک از اين دو تابع به عنوان آرگومان به ()sum ارسال شوند.

نام تابع، آدرس شروع تابع را دارد. پس square آدرس شروع تابع ()squareرا دارد. بنابراين وقتی تابع ()sum به شکل sum(square,4)  فراخوانی شود، آدرسی که درون square است به اشاره‌گر pf فرستاده می‌شود. با استفاده از عبارت (i)(pf*) مقدار i به آرگومان تابعی فرستاده می‌شود که pf به آن اشاره دارد.

NUL  و NULL در ++C

ثابت‌ صفر (0) از نوع‌ int است اما اين مقدار را به هر نوع بنيادی ديگر می‌توان تخصيص داد:

char c = 0;        // initializes c to the char '\0'
short d = 0;       // initializes d to the short int 0
int n = 0;         // initializes n to the int 0
unsigned u = 0; // initializes u to the unsigned int 0
float x = 0;       // initializes x to the float 0.0
double z = 0;      // initializes z to the double 0.0

مقدار صفر معناهای گوناگونی دارد. وقتی برای اشيای عددی به کار رود، به معنای عدد صفر است. وقتی برای اشيای کاراکتری به کار رود، به معنای کاراکتر تهی يا NUL است. NUL معادل کاراکتر ‘O\’ نيز هست. وقتی مقدار صفر براي اشاره‌گر‌ها به کار رود، به معناي «هيچ چيز» يا NULL است. NULL يک کلمه کليدی است و کامپايلر آن را می‌شناسد. هنگامی که مقدار NULL يا صفر در يک اشاره‌گر قرار مي‌گيرد، آن اشاره‌گر به خانه 0x0 در حافظه اشاره دارد. اين خانه حافظه، يک خانه استثنايی است که قابل پردازش نيست. نه می‌توان آن خانه را مقداريابی کرد و نه می‌توان مقداری را درون آن قرار داد. به همين دليل به NULL «هيچ چيز» می‌گويند.

وقتی اشاره‌گری را بدون استفاده از new اعلان می‌کنيم، خوب است که ابتدا آن را NULL کنيم تا مقدار زباله آن پاک شود. اما هميشه بايد به خاطر داشته باشيم که اشاره‌گر NULL را نبايد مقداريابی نماييم:

int* p = 0;    // p points to NULL
*p = 22;       // ERROR: cannot dereference the NULL pointer

پس خوب است هنگام مقداريابی اشاره‌گرها، احتياط کرده و بررسی کنيم که آن اشاره‌گر NULL نباشد:

if (p) *p = 22;   // O.K.

حالا دستور ;p=22* وقتی اجرا می‌شود که p صفر نباشد. می‌دانيد که شرط بالا معادل شرط زير است:

if (p != NULL) *p = 22;

اشاره‌گر‌ها را نمی‌توان ناديده گرفت. آن‌ها سرعت پردازش را زياد می‌کنند و کدنويسی را کم می‌کنند. با استفاده از اشاره‌گرها می‌توان به بهترين شکل از حافظه استفاده کرد. با به کارگيری اشاره‌گرها می‌توان اشيائی پيچيده‌تر و کارآمدتر ساخت.

سخن پایانی

در این جلسه به توضیح مفاهیم ارجاع‌ ها و اشاره گر ها در ++C پرداختیم. هدف این مقاله آموزشی آشنایی شما با اشاره‌گر‌ها و نحوه کار با آدرس‌های حافظه بود. عملگرهای New و Delete را همراه با مثال بیان کردیم. در جلسات بعدی با بخش‌های دیگری از زبان ++C در خدمت شما خواهیم بود. حتماً نظرات و پیشنهادات خود را با ما در میان بگذارید. موفق و پیروز باشید.