در اين جلسه به مبحث ارجاع ها و اشاره گرها در ++C میرسیم. بطور کلی در این جلسه با اشارهگرها و نحوه کار با آدرسهای حافظه، ارجاعها، عملگرهای new و delete، آرايهای از اشارهگرها، اشارهگری به اشارهگر ديگر، اشارهگر به توابع و آرايههای پويا آشنا خواهیم شد.
انتظار میرود پس از مطالعه این جلسه «ارجاع» را تعريف کنيد و با استفاده از عملگر ارجاع به متغيرها دستيابی داشته باشید. «اشارهگر» را بشناسيد و بتوانيد اشارهگرهایی به انواع مختلف ايجاد کرده و آنها را مقداريابی کنيد. «چپمقدارها» و «راستمقدارها» را تعريف کرده و آنها را از يکديگر تمیيز دهيد. طريقه استفاده از عملگرهای new و delete و وظيفه هر يک را بدانيد. «آرايههای پويا» را تعريف کرده و مزيت آنها را نسبت به آرايههای ايستا ذکر کنيد و در نهایت آرايههای پويا را در برنامههايتان ايجاد کرده و مديريت نماييد.
مقدمه
حافظه کامپیوتر را میتوان به صورت يک آرايه بزرگ در نظر گرفت. برای مثال کامپیوتری با 256 مگابايت RAM در حقيقت حاوی آرايهای به اندازه 268،435،456 (=228) خانه است که اندازه هر خانه يک بايت است. اين خانهها دارای ايندکس صفر تا 268،435،455 هستند. به ايندکس هر بايت، آدرس حافظه آن میگويند.
آدرسهای حافظه را با اعداد شانزدهدهی یا Hex نشان میدهند. پس کامپیوتر مذکور دارای محدوده آدرس 0x00000000 تا 0x0fffffff میباشد. هر وقت که متغيری را اعلان میکنيم، سه ويژگی اساسی به آن متغير نسبت داده میشود: «نوع متغير» و «نام متغير» و «آدرس حافظه» آن.
مثلا اعلان ;int n نوع int و نام n و آدرس چند خانه از حافظه که مقدار n در آن قرار میگيرد را به يکديگر مرتبط میسازد. فرض کنيد آدرس اين متغير 0x0050cdc0 است. بنابراين میتوانيم n را مانند شکل مقابل مجسم کنيم:
خود متغير به شکل جعبه نمايش داده شده است. نام متغير n، در بالای جعبه است و آدرس متغير در سمت چپ جعبه و نوع متغير int، در زير جعبه نشان داده شده. در بيشتر کامپیوترها نوع int چهار بايت از حافظه را اشغال مینمايد. بنابراين همانطور که در شکل بالا نشان داده شده است، متغير n يک بلوک چهاربايتی از حافظه را اشغال میکند که شامل بايتهای 0x0050cdc0 تا 0x0050cdc3 است. توجه کنيد که آدرس شی، آدرس اولين بايت از بلوکی است که شی در آن جا ذخيره شده است.
اگر متغير فوق به شکل ;int n=32 مقداردهی اوليه شود، آنگاه بلوک حافظه به شکل زير خواهد بود. مقدار 32 در چهار بايتی که برای آن متغير منظور شده ذخيره میشود.
عملگر ارجاع در ++C
در سی پلاس پلاس برای بدست آوردن آدرس يک متغير میتوان از عملگر ارجاع n& استفاده کرد. به اين عملگر «علمگر آدرس» نيز میگويند. با عبارت n& میتوان آدرس متغير n را به دست آورد. این مفهوم یکی از پایههای اصلی در مبحث ارجاع ها و اشاره گرها در ++C محسوب میشود.
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 به حافظه آن متغیر ارجاع داشته باشد. درست است که ارجاع با یک متغیر مقداردهی میشود، اما ارجاع به خودی خود یک متغیر نیست. یک متغیر، فضای ذخیرهسازی و نشانی مستقل دارد، حال آنکه ارجاع از فضای ذخیرهسازی و نشانی متغیر دیگری بهره میبرد. درک تفاوت میان ارجاع و متغیر مستقل یکی از نکات کلیدی در بحث ارجاع ها و اشاره گرها در ++C است.
مثال: ارجاعها متغيرهای مستقل نيستند
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. اما آدرس حافظه را در چه نوع متغیری باید قرار دهیم؟ پاسخ به این سؤال ما را وارد مبحث مهم ارجاع ها و اشاره گرها در ++C میکند، جایی که نوع دادهای خاصی برای نگهداری آدرس حافظه به کار میرود: اشارهگرها (pointers).
عملگر ارجاع & آدرس حافظه يک متغير موجود را به دست میدهد. میتوان اين آدرس را در متغير ديگری ذخيره نمود. برای اينکه يک اشارهگر اعلان کنيم، ابتدا بايد مشخص کنيم که آدرس چه نوع دادهای قرار است در آن ذخيره شود. سپس از عملگر اشاره * استفاده میکنيم تا اشارهگر را اعلان کنيم. متغيری که يک آدرس در آن ذخيره میشود اشارهگر ناميده میشود.
مثال:
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 را میدهد.
عملگر مقداریابی * و عملگر ارجاع & بهنوعی معکوس یکدیگر عمل میکنند. اگر این دو را با هم ترکیب کنیم، یکدیگر را خنثی مینمایند. اگر n یک متغیر باشد، &n آدرس آن متغیر است. از طرفی با استفاده از عملگر * میتوان مقداری را که در آدرس &n قرار گرفته، به دست آورد. بنابراین، *(&n) برابر با خود n خواهد بود.
همچنین اگر p یک اشارهگر باشد، *p مقداری را که p به آن اشاره دارد، برمیگرداند. از طرف دیگر، با استفاده از عملگر & میتوان آدرس چیزی را که در *p قرار گرفته، به دست آورد. این رفتار متقابل از ویژگیهای کلیدی در مبحث ارجاع ها و اشاره گرها در ++C است که درک درست آن برای استفاده مؤثر از اشارهگرها اهمیت زیادی دارد.
پس 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
يک تابع، چپمقدار نيست اما اگر نوع بازگشتی آن يک ارجاع باشد، میتوان تابع را به يک چپمقدار تبديل کرد.
اخطار: وقتی يک تابع پايان میيابد، متغيرهای محلی آن نابود میشوند. پس هيچ وقت ارجاعی به يک متغير محلی بازگشت ندهيد زيرا وقتی کار تابع تمام شد، آدرس متغيرهای محلیاش غير معتبر میشود و ارجاع بازگشت داده شده ممکن است به يک مقدار غير معتبر اشاره داشته باشد. تابع ()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
گرچه اشارهگرها از انواع عددی صحیح نیستند، اما برخی از عملیات حسابی را میتوان روی آنها انجام داد. نتیجهی این اعمال آن است که اشارهگر به خانهی دیگری از حافظه اشاره میکند. اشارهگرها را میتوان مانند اعداد صحیح افزایش یا کاهش داد، و میتوان عددی صحیح را به آنها اضافه یا از آنها کم کرد. البته، میزان افزایش یا کاهش یک اشارهگر بستگی به نوع دادهای دارد که اشارهگر به آن اشاره میکند. این قابلیت، از ویژگیهای انعطافپذیر و در عین حال حساس ارجاع ها و اشاره گرها در ++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 بايت افزايش میدهد. با استفاده از خاصيت مذکور میتوان آرايه را پيمايش نمود: يک اشارهگر را با آدرس اولين عنصر آرايه مقداردهی کنيد، سپس اشارهگر را پیدرپی افزايش دهيد. هر افزايش سبب میشود که اشارهگر به عنصر بعدی آرايه اشاره کند. يعنی اشارهگری که به اين نحو به کار گرفته شود مثل ايندکس آرایه عمل میکند.
بازگشت از نوع ارجاع در ++C
در بحث توابع، با دو شیوهی ارسال داده یعنی ارسال از طریق مقدار و ارسال از طریق ارجاع آشنا شدیم. این دو شیوه تبادل، در مورد بازگشت از تابع نیز صدق میکنند: بازگشت از طریق مقدار و بازگشت از طریق ارجاع. توابعی که تاکنون دیدهایم، همگی بازگشت بهصورت مقداری داشتند؛ یعنی همیشه یک مقدار به فراخواننده بازمیگشت. اما میتوان تابعی را طوری تعریف کرد که بهجای بازگرداندن مقدار، یک ارجاع را بازگرداند. مثلاً بهجای اینکه مقدار m را بازگرداند، یک ارجاع به m بازگردانده شود. این شیوه از بازگشت تابع، یکی از کاربردهای پیشرفته در مبحث ارجاع ها و اشاره گرها در ++C است و میتواند در بهینهسازی عملکرد برنامه و جلوگیری از کپیهای غیرضروری مؤثر باشد.
وقتی بازگشت به طريق مقدار باشد، تابع يک راستمقدار خواهد بود زيرا مقدارها ليترال هستند و ليترالها راستمقدارند. به اين ترتيب تابع را فقط در سمت راست يک جايگزينی میتوان به کار برد مثل:
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;، مقدار ۵۵ در حقیقت درون متغیر m قرار میگیرد (در صورتی که m > n باشد). به بیانی ساده، فراخوانی max(m, n) در این حالت خود m را بازمیگرداند، نه صرفاً مقدار آن را. این رفتار دقیقاً در راستای مفهوم ارجاع ها و اشاره گرها در ++C است، جایی که میتوان بهجای دادهها، به خود متغیرها دسترسی داشت و آنها را مستقیماً تغییر داد.
همچنين با استفاده از اشارهگر میتوانيم مستقيما به عنصر مورد نظر در آرايه دستيابی کنيم:
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 به شکل زیر ارزیابی میشود:
این تفاوت رفتاری میان اشارهگرهای معمولی و اشارهگرهای ثابت، از جمله نکات مهم در مبحث ارجاع ها و اشاره گرها در ++C است. درک درست آن برای کار با آرایهها و ساختارهای مشابه ضروری است.
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 ایجاد شده است، اما به هیچ جایی اشاره نمیکند، زیرا هنوز آدرسی درون آن قرار نگرفته. به چنین اشارهگری، اشارهگر سرگردان گفته میشود. اگر سعی کنیم یک اشارهگر سرگردان را مقداریابی یا ارجاع کنیم، با خطا مواجه میشویم. این وضعیت یکی از مشکلات رایج در استفاده از ارجاع ها و اشاره گرها در ++C است و میتواند منجر به رفتارهای غیرمنتظره یا خرابی برنامه شود.
مثال:
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* قابل دستيابی است.
آرايههای پويا در ++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 بستهبندی ايستا میگويند زيرا اين کد باعث میشود که حافظه مورد نياز برای 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 هستند که میتواند کاربردهای خاصی در مدیریت حافظه و ساختارهای داده پیچیده داشته باشد.
آرايهای از اشارهگرها در ++C
میتوانيم آرايهای تعريف کنيم که اعضای آن از نوع اشارهگر باشند. مثلا دستور:
float* p[4];
آرايه p را با چهار عنصر از نوع *float (يعنی اشارهگری به float) اعلان میکند. عناصر اين آرايه را مثل اشارهگرهای معمولی میتوان مقداردهی کرد:
p[0] = new float(3.14159); p[1] = new float(1.19);
اين آرايه را میتوانيم شبيه شکل مقابل مجسم کنيم: مثال بعد نشان میدهد که آرايهای از اشارهگرها به چه دردی میخورد. از اين آرايه میتوان برای مرتبکردن يک فهرست نامرتب به روش حبابی استفاده کرد. به جای اين که خود عناصر جابجا شوند، اشارهگرهای آنها جابجا میشوند.
مثال: مرتبسازی حبابی غيرمستقيم
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
اين بخش ممکن است کمی عجيب به نظر برسد. حقيقت اين است که نام يک تابع مثل نام يک آرايه، يک اشارهگر ثابت است. نام تابع، آدرسی از حافظه را نشان میدهد که کدهای درون تابع در آن قسمت جای گرفتهاند. پس بنابر قسمت قبل اگر اشارهگری به تابع اعلان کنيم، در اصل اشارهگری به اشارهگر ديگر تعريف کردهايم. اما اين تعريف، نحو متفاوتی دارد:
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 را میتوانيم به شکل زير تصور کنيم:
فايده اشارهگر به توابع اين است که به اين طريق میتوانيم توابع مرکب بسازيم. يعنی میتوانيم يک تابع را به عنوان آرگومان به تابع ديگر ارسال کنيم. اين کار با استفاده از اشارهگر به تابع امکان پذير است.
مثال: تابع مرکب جمع
تابع ()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 معادل کاراکتر ‘0’ نیز هست. وقتی مقدار صفر برای اشارهگرها به کار رود، به معنای «هیچ چیز» یا NULL است. NULL یک کلمه کلیدی است و کامپایلر آن را میشناسد. هنگامی که مقدار NULL یا صفر در یک اشارهگر قرار میگیرد، آن اشارهگر به خانهی 0x0 در حافظه اشاره دارد.
این خانه حافظه، یک خانه استثنایی است که قابل پردازش نیست. نه میتوان آن خانه را مقداریابی کرد و نه میتوان مقداری را درون آن قرار داد. به همین دلیل به NULL «هیچ چیز» میگویند. این ویژگی یکی از مفاهیم حیاتی در ارجاع ها و اشاره گرها در ++C است که به ما کمک میکند تا با مدیریت حافظه بهطور ایمنتر و مؤثرتر کار کنیم.
وقتی اشارهگری را بدون استفاده از 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
در این جلسه به توضیح مفاهیم ارجاع ها و اشاره گرها در ++C پرداختیم. هدف این مقاله آموزشی آشنایی شما با اشارهگرها و نحوه کار با آدرسهای حافظه بود. عملگرهای New و Delete را همراه با مثال بیان کردیم. در درس بعدی با مقاله فایل ها و رشته های کاراکتری در ++C در خدمت شما خواهیم بود.















