برای روشن کردن ترمینال، کلیک کن یا هر کلیدی را بزن

پرش به محتوای اصلی
>_
درس ۲۸پیشرفته

اشاره‌گر‌ها و حافظه پویا

از مفهوم اشاره‌گر تا تخصیص پویا و اشاره‌گر به اشاره‌گر

معمولی
توابع و اشاره‌گر‌ها•صفحات ۴۶-۴۷•۶۰ دقیقه•۱٬۹۴۵ کلمهزمان مطالعه: ۰ دقیقه
فهرست این درس
// اهداف۵
  • درک مفهوم اشاره‌گر و تفاوت آدرس و مقدار متغیر
  • استفاده از عملگر‌های & (آدرس) و * (دسترسی به مقدار)
  • یادگیری تخصیص پویای حافظه با 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 برای مقادیر ۱۰ تا ۱۵ به کار می‌روند:

اعشاری۱۰۱۱۱۲۱۳۱۴۱۵
هگزABCDEF

مثلاً عدد ۱۴ در مبنای ۱۶ برابر با 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 به‌جای اشاره‌گر‌های خام استفاده کنید.

مرور سریع مطالب

۲۱ بخش در این درس

تمرین‌ها

01
تمرین•حل کامل دارد

برنامه‌ای بنویسید که یک متغیر `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` استفاده می‌شود.
گام بعدی پیشنهادی

struct و ساختار‌ها

گروه‌بندی داده‌ها، آرایه ساختار‌ها و توابع عضو

۵۰ دقیقهپیشرفته۱۷ بخش

شروع درس

جستجوی سریع در درس‌ها

نام درس، موضوع یا کلمه کلیدی را بنویسید و با Enter به آن بروید