استفاده از عملگرهای & (آدرس) و * (دسترسی به مقدار)
یادگیری تخصیص پویای حافظه با new و آزادسازی با delete
درک رابطهی بین آرایه و اشارهگر و حسابگر اشارهگر (pointer arithmetic)
آشنایی با اشارهگر به اشارهگر و فراخوانی با اشارهگر (Call by Reference با pointer)
اشارهگر چیست؟
هر متغیر در حافظه یک آدرس دارد (مثلاً 0x22ffc). یک اشارهگر (Pointer) متغیری است که بهجای ذخیرهی مقدار، آدرس یک متغیر دیگر را در خود نگه میدارد.
با اشارهگرها میتوانیم:
بهصورت پویا حافظه تخصیص دهیم
آرایهها و رشتهها را بهصورت بهینه پیمایش کنیم
توابعی بنویسیم که متغیرهای فراخواننده را تغییر دهند (Call by Reference)
ساختارهای دادهی پیشرفته مثل لیست پیوندی و درخت بسازیم
سه عملگر کلیدی اشارهگر
int *p; — تعریف اشارهگر: یعنی p یک متغیر است که آدرس یک int در آن ذخیره میشود.
p = &x; — عملگر آدرس (&): آدرس متغیر x را در p قرار میدهد.
cout << *p; — عملگر دسترسی به مقدار (*): مقدار ذخیرهشده در آدرسی که p به آن اشاره میکند را میخواند (به این کار Dereference میگویند).
cout << p; — خود p یعنی آدرسی که در p ذخیره شده (یک عدد هگزادسیمال مثل 0x22ffc).
مثال پایهی اشارهگر
این مثال تفاوت میان چاپ آدرس، چاپ مقدار از طریق متغیر اصلی، و چاپ مقدار از طریق اشارهگر را نشان میدهد.
توجه: آدرس چاپشده در سیستم شما و در هر اجرا متفاوت خواهد بود — چیزی که اهمیت دارد تساوی &x و p است، نه خودِ عدد آدرس.
تعریف، مقداردهی و dereferencecpp
$خروجی برنامه
meghdare x = 10
addrese x (&x) = 0x7ffef8a4b44c
meghdare p = 0x7ffef8a4b44c
meghdare *p = 10
نوع اشارهگر باید با نوع متغیر یکی باشد
یک اشارهگر int *p فقط میتواند آدرس متغیرهای int را در خود نگه دارد. اگر سعی کنیم آدرس یک double را در آن بریزیم، با خطای کامپایل مواجه میشویم:
main.cppcpp
برای متغیرهای double باید از double *p; استفاده کنید. دلیل این محدودیت این است که کامپایلر باید بداند در آدرس مورد نظر چند بایت خوانده شود (مثلاً int معمولاً ۴ بایت، double معمولاً ۸ بایت).
اشارهگر مقداردهینشده، خطرناک است
اگر اشارهگری را تعریف کنید اما به آن مقدار ندهید، شامل یک آدرس آشغال (garbage) است:
main.cppcpp
راهحل: همیشه اشارهگرها را مقداردهی اولیه کنید. اگر هنوز آدرس مشخصی ندارید، از nullptr استفاده کنید:
main.cppcpp
نمایش هگزادسیمال آدرسها
آدرسهای حافظه معمولاً در مبنای ۱۶ (Hexadecimal) نمایش داده میشوند و با پیشوند 0x مشخص میگردند، مثلاً 0x22ffc. در این سیستم ارقام 0 تا 9 معمولی هستند و سپس A تا F برای مقادیر ۱۰ تا ۱۵ به کار میروند:
اعشاری
۱۰
۱۱
۱۲
۱۳
۱۴
۱۵
هگز
A
B
C
D
E
F
مثلاً عدد ۱۴ در مبنای ۱۶ برابر با E است. برای تبدیل باینری به هگز، ارقام باینری را از راست به چپ به گروههای ۴تایی تقسیم میکنیم و هر گروه را به یک رقم هگز تبدیل میکنیم.
تخصیص پویای حافظه (Dynamic Allocation)
تا الآن وقتی متغیری مثل int x; میساختیم، حافظه بهصورت خودکار روی پشته (Stack) در زمان فراخوانی تابع اختصاص مییافت. اما گاهی نیاز داریم در زمان اجرا (Runtime) تصمیم بگیریم چقدر حافظه لازم داریم.
با دستور new میتوانیم از هیپ (Heap) حافظه بگیریم:
main.cppcpp
این حافظه تا زمانی که صراحتاً با delete آزاد نشود، باقی میماند.
main.cppcpp
تخصیص و آزادسازی یک متغیر پویا
بعد از delete، اشارهگر p همچنان آدرس قدیمی را در خود دارد. به این حالت Dangling Pointer میگویند. برای جلوگیری از خطا، بهتر است بعد از delete، آن را برابر nullptr قرار دهیم.
new و delete برای یک intcpp
$خروجی برنامه
meghdar = 10
آرایه پویا (Dynamic Array)
برای ساخت آرایهای که اندازهاش در زمان اجرا تعیین شود، از new int[n] استفاده میکنیم:
main.cppcpp
نکتهی بسیار مهم: برای آزادسازی آرایهی پویا باید از delete[] (با براکت) استفاده کنید، نه delete ساده. استفادهی اشتباه از delete باعث نشت حافظه (Memory Leak) یا رفتار تعریفنشده میشود.
این ساختار در سؤالات امتحانی معمولاً بهجای آرایههای int a[n] (VLA غیراستاندارد) به کار میرود.
آرایه پویا با new[] و delete[]
در این مثال، ابتدا اندازه آرایه از کاربر گرفته میشود، سپس آرایهی پویا ساخته و در نهایت با delete[] آزاد میگردد.
ساخت آرایه پویا، خواندن و چاپcpp
$خروجی برنامه
tedad anasor: 3
5 12 8
khoroji: 5 12 8
delete ساده در برابر delete[] برای آرایه
یکی از خطاهای متداول در جزوه اصلی، استفاده از delete ساده برای آرایهی پویا (که با new int[100] ساخته شده) بود. این کار باعث رفتار تعریفنشده میشود.
نسخه اشتباه جزوه
کد دارای باگcpp
نسخه اصلاحشده و استاندارد
کد صحیحcpp
رابطهی آرایه و اشارهگر
در C++، نام آرایه بدون اندیس معادل آدرس اولین خانهی آن است. یعنی اگر int a[5]; داشته باشیم، a و &a[0] یکچیز هستند. بههمین دلیل میتوانیم از یک اشارهگر برای پیمایش آرایه استفاده کنیم:
main.cppcpp
هر p++ اشارهگر را به اندازهی sizeof(int) بایت (معمولاً ۴) جلو میبرد، یعنی به خانهی بعدی آرایه.
پیمایش آرایه با اشارهگر
این برنامه معادل ابتدای درس است و خروجی 22223344 تولید میکند. cout << *a مقدار اولین خانه (22) را چاپ میکند، سپس حلقهی for با اشارهگر p از ابتدا تا انتها آرایه را پیمایش میکند و هر بار *p را چاپ میکند (22، 33، 44، بدون فاصله). چون cout << *a هم 22 چاپ میکند و حلقه هم از 22 شروع میشود، خروجی نهایی پشت سر هم 22223344 است.
استفاده از pointer arithmetic در حلقهcpp
$خروجی برنامه
22223344
روش دوگانه دسترسی به عنصر آرایه
به دو شکل میتوان به عنصر iام آرایه a دسترسی پیدا کرد. این دو روش از نظر کامپایلر کاملاً معادل هستند.
flow.svg
اشارهگر به اشارهگر (Pointer to Pointer)
میتوانیم اشارهگری بسازیم که به یک اشارهگرِ دیگر اشاره میکند! این کار با دو ستاره ** انجام میشود:
main.cppcpp
cout << *pc; — مقدار c یعنی 't' چاپ میشود.
cout << **ppc; — با دو dereference، مقدار c چاپ میشود.
cout << *ppc; — آدرسی که در pc است (یعنی آدرس c) چاپ میشود.
از این ساختار در آرگومانهای main (int argc, char **argv) و ساختارهای دادهی پویای پیچیده استفاده میشود.
مثال اشارهگر به اشارهگر
این مثال هم با char و هم با int نشان میدهد که چگونه یک اشارهگر سطح ۲، آدرس یک اشارهگر سطح ۱ را در خود نگه میدارد.
char **ppc و int **P2cpp
$خروجی برنامه
*pc = t
**ppc = t
**P2 = 5
فراخوانی با اشارهگر (Call by Reference با Pointer)
راه دیگر برای اینکه تابع بتواند متغیر اصلی را تغییر دهد، فرستادن آدرس متغیر به تابع و گرفتن آن با یک اشارهگر است:
main.cppcpp
این روش قبل از اختراع ارجاع & در C++، تنها راه Call by Reference در زبان C بود.
تابع با پارامتر اشارهگر
تابع f با دریافت آدرس یک int، مقدار آن را صفر میکند. در main هم &a (آدرس یک متغیر معمولی) فرستاده میشود و هم b (خود اشارهگر که از قبل آدرس دارد).
تغییر متغیر اصلی از طریق آدرسcpp
$خروجی برنامه
a = 0
*b = 0
مثال: تابع الحاق رشته با اشارهگر
این تابع کلاسیک، دو رشتهی C-like (آرایه کاراکتری) را به هم میچسباند. پارامترها در واقع اشارهگر به اولین کاراکتر هر رشتهاند. این همان روشی است که در strcat کتابخانهی استاندارد استفاده میشود.
نکته: برای طول رشته از strlen استفاده میکنیم و بهتر است آن را یک بار محاسبه کنیم، نه در هر دور حلقه (برای کارایی).
تابع cat که دو رشته C-like را به هم میچسباندcpp
$خروجی برنامه
Salam, Donya!
نشت حافظه (Memory Leak) — خطر جدی
اگر حافظهای را با new بگیرید و فراموش کنید با delete آن را پس بدهید، آن حافظه تا پایان برنامه آزاد نمیشود. به این حالت Memory Leak میگویند. در برنامههای طولانی یا سرورهایی که ساعتها کار میکنند، این نشتها انباشته شده و در نهایت باعث کمبود حافظه و کرش میشوند.
نشانههای نشت حافظه:
برنامه بهمرور کندتر میشود
مصرف حافظهی برنامه دائماً بالا میرود
در نهایت Out of Memory رخ میدهد
راهحلهای مدرن:
از Smart Pointers در هدر <memory> استفاده کنید: unique_ptr, shared_ptr
از std::vector بهجای آرایهی پویا استفاده کنید
قانون طلایی: هر new باید با یک delete متناظر باشد.
بهترین روشها در استفاده از اشارهگر
همیشه اشارهگر را مقداردهی اولیه کنید: یا با آدرس یک متغیر معتبر یا با nullptr.
بعد از delete، اشارهگر را nullptr کنید تا اشتباهاً دوباره از آن استفاده نکنید.
برای آرایه از delete[] استفاده کنید، نه delete.
بررسی nullptr قبل از dereference: if (p != nullptr) cout << *p;
در C++ مدرن ترجیحاً از std::vector, std::string, و Smart Pointers بهجای اشارهگرهای خام استفاده کنید.
برنامهای بنویسید که یک متغیر `int x = 7;` تعریف کند، یک اشارهگر به آن بسازد و **از طریق اشارهگر** مقدار آن را دو برابر کند. سپس `x` را چاپ کنید (باید ۱۴ باشد).
نمایش راهنما
`*p = *p * 2;` یا `*p *= 2;`
02
تمرین•حل کامل دارد
برنامهای بنویسید که `n` را از کاربر بگیرد، یک آرایهی پویای `n` عنصری بسازد، اعداد را بخواند، مجموع آنها را چاپ کند و در پایان حافظه را آزاد کند.
نمایش راهنما
از `new int[n]` برای تخصیص و `delete[] p` برای آزادسازی استفاده کنید.
03
تمرین•حل کامل دارد
تابع `void swapPtr(int *a, int *b)` بنویسید که با استفاده از **اشارهگر** (نه ارجاع `&`) مقادیر دو متغیر را جابجا کند. در `main` آن را تست کنید.
نمایش راهنما
داخل تابع از `*a` و `*b` برای دسترسی به مقادیر استفاده کنید. در فراخوانی از `&x` و `&y` بفرستید.
04
تمرین•حل کامل دارد
تابع `void findMinMax(int a[], int n, int *pmin, int *pmax)` بنویسید که کوچکترین و بزرگترین عنصر آرایه را پیدا کرده و از طریق اشارهگر در متغیرهای `main` قرار دهد.
نمایش راهنما
داخل تابع `*pmin` و `*pmax` را در حلقه بهروز کنید. در `main` آن را بهصورت `findMinMax(a, n, &mn, &mx)` صدا بزنید.
// نکات-کلیدی۶
01اشارهگر متغیری است که آدرس یک متغیر دیگر را در خود نگه میدارد؛ `int *p` یک اشارهگر به `int` تعریف میکند.
02عملگر `&` آدرس متغیر را میدهد و `*` به مقدار در آن آدرس دسترسی پیدا میکند (Dereference).
03دستور `new` حافظهای از هیپ میگیرد و `delete` آن را آزاد میکند. برای آرایه باید از `delete[]` استفاده کرد.
04اشارهگرهای مقداردهینشده خطرناکاند؛ همیشه آنها را با `nullptr` مقداردهی اولیه کنید.
05نام آرایه بدون اندیس برابر آدرس اولین خانه است؛ یعنی `a` معادل `&a[0]` و `a[i]` معادل `*(a+i)` است.
06با `int **P2` میتوان اشارهگر به اشارهگر ساخت که برای ساختارهای دادهی پیشرفته و آرگومانهای `main` استفاده میشود.
نام درس، موضوع یا کلمه کلیدی را بنویسید و با Enter به آن بروید
1#include<iostream>2usingnamespace std;34intmain(){5int x =10;6int*p;// تعریف اشارهگر به int7 p =&x;// قرار دادن آدرس x در p89 cout <<"meghdare x = "<< x << endl;10 cout <<"addrese x (&x) = "<<&x << endl;11 cout <<"meghdare p = "<< p << endl;// همان آدرس x12 cout <<"meghdare *p = "<<*p << endl;// همان مقدار x13return0;14}
double x =3.14;int*p;p =&x;// خطای کامپایل: cannot convert double* to int*
int*p;cout <<*p;// رفتار تعریفنشده! ممکن است برنامه کرش کند
int*p =nullptr;if(p !=nullptr) cout <<*p;
int*p =newint;// اختصاص یک int از هیپ*p =10;// مقداردهی
delete p;// آزادسازی حافظه
1#include<iostream>2usingnamespace std;34intmain(){5int*p;6 p =newint;// تخصیص حافظه از هیپ7*p =10;8 cout <<"meghdar = "<<*p << endl;910delete p;// آزادسازی11 p =nullptr;// جلوگیری از Dangling Pointer12return0;13}
int*p;int n;cin >> n;p =newint[n];// آرایهی پویای n عنصری
1#include<iostream>2usingnamespace std;34intmain(){5int n;6 cout <<"tedad anasor: ";7 cin >> n;89int*p =newint[n];// تخصیص آرایه پویا10for(int i =0; i < n; i++)11 cin >> p[i];1213 cout <<"khoroji: ";14for(int i =0; i < n; i++)15 cout << p[i]<<" ";16 cout << endl;1718delete[] p;// آزادسازی آرایه19 p =nullptr;20return0;21}
1int*p =newint[100];2// ...3delete p;// اشتباه! برای آرایه باید delete[] باشد4// باعث نشت حافظه یا کرش میشود
voidf(int*x){*x =5;// مقدار خانهی حافظهای که x به آن اشاره میکند = 5}intmain(){int n =10;f(&n);// آدرس n فرستاده میشود// حالا n = 5 شده است}
1#include<iostream>2usingnamespace std;34voidf(int*x){5*x =0;// مقدار خانهای که x به آن اشاره میکند = 06}78intmain(){9int a =10;10f(&a);// فرستادن آدرس a11 cout <<"a = "<< a << endl;// 0 چاپ میشود1213int*b =newint;14*b =99;15f(b);// فرستادن خود اشارهگر (نیازی به & ندارد)16 cout <<"*b = "<<*b << endl;// 0 چاپ میشود17delete b;18 b =nullptr;19return0;20}
1#include<iostream>2#include<cstring>3usingnamespace std;45voidcat(char*ch1,constchar*ch2){6int L =strlen(ch1);7int len2 =strlen(ch2);8for(int i =0; i < len2; i++)9 ch1[L + i]= ch2[i];10 ch1[L + len2]='\0';11}1213intmain(){14char c1[100]="Salam, ";15char c2[]="Donya!";16cat(c1, c2);17 cout << c1 << endl;18return0;19}