تخفیف ویژه زمستانه پی استور

تا 60 درصد تخفیف

شامل پروژه‌ها و دوره‌های آموزشی
روز
ساعت
دقیقه
ثانیه
آخرین فرصت‌ها

با خرید اشتراک ویژه ، با هزینه بسیار کمتر به فایل های پاورپوینت دسترسی داشته باشید!

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

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

فهرست مطالب

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

حافظه کامپیوتر را می‌توان به صورت یک آرایه بزرگ در نظر گرفت. برای مثال کامپیوتر‌ی با ۲۵۶ مگابایت RAM در حقیقت حاوی آرایه‌ای به اندازه ۲۶۸،۴۳۵،۴۵۶ (=۲۲۸) خانه است که اندازه هر خانه یک بایت است. این خانه‌ها دارای ایندکس صفر تا ۲۶۸،۴۳۵،۴۵۵ هستند. به ایندکس هر بایت، آدرس حافظه آن می‌گویند.

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

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

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

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

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

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

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

 

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

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

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

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

n = 44
&n = 0x00c9fdc3

خروجی نشان‌ می‌دهد که‌ آدرس‌ n در این اجرا برابر با 0x00c9fdc3 است. می‌توان فهمید که این مقدار باید یک آدرس باشد زیرا به شکل شانزده‌دهی نمایش داده شده. اعداد شانزده‌دهی را از روی علامت 0x می‌توان تشخیص داد. معادل دهدهی عدد بالا مقدار ۱۳,۲۳۷,۶۹۹ می‌باشد.

ارجاع‌ ها در ++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 با مقدار ۴۴ مقداردهی شده و آدرس آن 0x0064fddc می‌باشد. اشاره‌گر pn با مقدار n& یعنی آدرس n مقداردهی شده. پس مقدار درون pn برابر با 0x0064fddc است‌ (خط دوم خروجی این موضوع را تایید می‌کند) .

اشارگر‌ها و ارجاع‌ها

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

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

مقداریابی در ++C

فرض کنید n دارای مقدار ۲۲ باشد و pn اشاره‌گری به n باشد. با این حساب باید بتوان از طریق 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;
   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 در سمت چپ قرار گرفته و مقدار ۵۵ در سمت راست. این دستور را نمی‌توان به شکل زیر نوشت:

۵۵=n;

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

چیزهایی که می‌توانند در سمت چپ جایگزینی قرار بگیرند «چپ‌مقدار» خوانده می‌شوند و چیزهایی که می‌توانند در سمت راست جایگزینی قرار بگیرند «راست‌مقدار» نامیده می‌شوند. متغیرها (و به طور کلی اشیا) چپ‌مقدار هستند و لیترال‌ها (مثل ۱۵ و “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	۲- 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] است. این کار از طریق بازگشت از طریق ارجاع ممکن شده است.[/vc_column_text][vc_column_text]

آرایه‌ها و اشاره‌گر ها در ++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ها ۴ بایت را اشغال‌ کنند آنگاه p++ مقدار درون p  را ۴ بایت افزایش می‌دهد و  ;p += 5 مقدار درون p را ۲۰ بایت افزایش می‌دهد. با استفاده از خاصیت مذکور می‌توان آرایه را پیمایش نمود: یک اشاره‌گر را با آدرس اولین عنصر آرایه مقداردهی کنید، سپس اشاره‌گر را پی‌در‌پی افزایش دهید. هر افزایش سبب می‌شود که اشاره‌گر به عنصر بعدی آرایه اشاره کند. یعنی اشاره‌گری که به این نحو به کار گرفته شود مثل ایندکس آرایه عمل می‌کند.

بازگشت از نوع ارجاع در ++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;
}
۴۴۵۵, ۲۲, ۵۵
, ۲۲, ۴۴

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

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

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 به هیچ آدرسی اشاره نمی‌کند و سیستم عامل نمی‌داند که مقدار ۳.۱۴۱۵۹ را کجا ذخیره کند. برای رفع این مشکل می‌توان اشاره‌گرها را هنگام اعلان، مقداردهی کرد:

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 تخصیص می‌یابد و همچنین مقدار ۳.۱۴۱۵۹ در آن آدرس قرار می‌گیرد. اگر عملگر 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 اشاره‌گرهای ثابتی هستند که به بلوکی حاوی ۲۰ متغیر float اشاره دارند. به اعلان a بسته‌بندی ایستا۱ می‌گویند زیرا این کد باعث می‌شود که حافظه مورد نیاز برای a در زمان کامپایل تخصیص داده شود. وقی برنامه اجرا شود، به هر حال حافظه مربوطه تخصیص خواهد یافت حتی اگر از آن هیچ استفاده‌ای نشود.

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

float* p = new float[20];

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

آرایه ایستای 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:
       ۱: ۴۴.۴
       ۲: ۷۷.۷
       ۳: ۲۲.۲
       ۴: ۸۸.۸
۴۴.۴ ۷۷.۷ ۲۲.۲ ۸۸.۸
Enter number of items: 2
Enter 2 items, one per line:
       ۱: ۳.۳۳
       ۲: ۹.۹۹
۳.۳۳ ۹.۹۹

اشاره‌گر ثابت در ++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) مقدار ۳۰=۱۶+۹+۴+۱ را محاسبه نموده و بازمی‌گرداند. تعریف توابع و خروجی آزمایشی به شکل زیر است:

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;
}
۳۰
۱۰۰

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

ثابت‌ صفر (۰) از نوع‌ 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

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

یک پاسخ

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *