اشارهگرها: آدرسها، ارجاعزدایی و محاسبات اشارهگر
اشارهگرها را به عنوان آدرسهای حافظه درک کنید، عملگرهای آدرسدهی و ارجاعزدایی، محاسبات اشارهگر درون آرایهها، صحت const با اشارهگرها و زمان مناسب استفاده از اشارهگرهای خام در C++ مدرن را بیاموزید.
اشارهگر چیست؟
اشارهگر (pointer) متغیری است که یک آدرس حافظه را ذخیره میکند. هر شیء در C++ در آدرسی از حافظه قرار دارد و اشارهگر به شما امکان میدهد بهصورت غیرمستقیم به آن شیء دسترسی داشته باشید. اشارهگرها در C++ بنیادی هستند — آنها ساختارهای داده پویا، چندریختی (polymorphism) و ارسال کارآمد پارامترها را ممکن میسازند.
نوع اشارهگر با * اعلان میشود: int* p; یک اشارهگر به int اعلان میکند. خود اشارهگر حافظه اشغال میکند (معمولاً ۸ بایت در سیستمهای ۶۴ بیتی) و مقدار آن یک آدرس است، نه دادهای که به آن اشاره میکند.
عملگر آدرسدهی و ارجاعزدایی
دو عملگر اساسی اشارهگر عبارتند از & (آدرسدهی) و * (ارجاعزدایی). عملگر & آدرس یک متغیر را به دست میآورد. عملگر * آدرس را دنبال کرده و به شیء مورد اشاره دسترسی پیدا میکند.
#include <iostream>
int main() {
int value = 42;
int* ptr = &value; // ptr holds the address of value
std::cout << "value: " << value << '\n'; // 42
std::cout << "&value: " << &value << '\n'; // e.g. 0x7ffd5a3c
std::cout << "ptr: " << ptr << '\n'; // same address
std::cout << "*ptr: " << *ptr << '\n'; // 42 (dereference)
*ptr = 100; // modify value through the pointer
std::cout << "value after *ptr = 100: " << value << '\n'; // 100
int* null_ptr = nullptr; // points to nothing — safe sentinel
// *null_ptr = 5; // UNDEFINED BEHAVIOR — crash on most systems
}محاسبات اشارهگر و تبدیل آرایه
محاسبات اشارهگر فقط درون یک آرایه (یا یک عنصر پس از انتها) معتبر است. کامپایلر محاسبات را بر اساس sizeof(T) مقیاسبندی میکند. نام آرایه در بیشتر زمینهها به اشارهگری به اولین عنصر خود تبدیل (decay) میشود — به همین دلیل است که آرایههای سبک C هنگام ارسال به توابع، اطلاعات اندازه خود را از دست میدهند.
#include <iostream>
#include <cstddef> // std::ptrdiff_t
int main() {
int arr[5] = {10, 20, 30, 40, 50};
int* p = arr; // array decays to pointer to first element
std::cout << *p << '\n'; // 10
std::cout << *(p + 2) << '\n'; // 30 (moves 2*sizeof(int) bytes)
std::cout << p[3] << '\n'; // 40 (p[i] is *(p + i))
// Iterating via pointer
for (int* it = arr; it != arr + 5; ++it) {
std::cout << *it << ' ';
}
std::cout << '\n'; // 10 20 30 40 50
// Pointer difference
std::ptrdiff_t diff = (arr + 4) - arr; // 4 (elements, not bytes)
std::cout << "diff: " << diff << '\n';
// WARNING: arithmetic outside the array is UNDEFINED BEHAVIOR
// int* bad = arr + 10; // UB — don't do this
}اشارهگرهای const در مقابل اشارهگر به const
جایگاه const نسبت به * تعیین میکند که چه چیزی تغییرناپذیر است:
- const int* p (یا int const* p) — اشارهگر به const: نمیتوانید مقدار مورد اشاره را از طریق p تغییر دهید، اما میتوانید p را به جای دیگری اشاره دهید.
- int* const p — اشارهگر const: خود اشارهگر تغییرناپذیر است (نمیتوان آن را تغییر داد)، اما میتوانید مقداری که به آن اشاره میکند را تغییر دهید.
- const int* const p — هم اشارهگر و هم مقدار مورد اشاره تغییرناپذیر هستند.
اعلانها را از راست به چپ بخوانید: int* const p خوانده میشود «p یک اشارهگر const به int است.»
اشارهگرهای void (void*) میتوانند هر آدرسی را نگه دارند اما بدون تبدیل نوع (casting) قابل ارجاعزدایی نیستند. آنها عمدتاً در APIهای سبک C و پیادهسازی تخصیصدهندهها (allocator) استفاده میشوند.
در C++ مدرن، اشارهگرهای خام باید منحصراً به عنوان اشارهگرهای ناظر بدون مالکیت استفاده شوند. آنها میگویند: «من میتوانم این شیء را ببینم، اما مسئول طول عمر آن نیستم.» کاربردهای مشروع رایج:
- پیادهسازی ساختارهای داده (گرههای درخت که به والد اشاره میکنند)
- پارامترهای تابع که مشاهده میکنند اما مالک نیستند (هرچند ارجاعها اغلب بهترند)
- ارتباط با APIهای C
- تکرارکنندهها (iterator) در ظروف سفارشی
اگر یک اشارهگر مالک یک منبع است، آن را در std::unique_ptr یا std::shared_ptr قرار دهید. اگر متوجه شدید که delete مینویسید، تقریباً مطمئناً در C++ مدرن اشتباه میکنید.
اشارهگرهای معلق (dangling pointers) باگ شماره ۱ اشارهگرها هستند. یک اشارهگر زمانی معلق میشود که شیء مورد اشاره آن نابود شود:
- بازگرداندن اشارهگر به یک متغیر محلی
- حذف یک شیء در حالی که اشارهگر دیگری هنوز به آن ارجاع دارد
- اشارهگر به درون std::vector که پس از push_back مجدداً تخصیص حافظه یافته است
اشارهگرهای مقداردهینشده حاوی آدرسهای زباله هستند. همیشه با nullptr مقداردهی اولیه کنید اگر هنوز هدفی ندارید. کامپایلر درباره همه استفادههای اشارهگر مقداردهینشده هشدار نمیدهد.
اشارهگر در مقابل ارجاع: کدام را کی استفاده کنیم
ارجاعها و اشارهگرها هر دو ارجاع غیرمستقیم فراهم میکنند، اما تفاوتهای مهمی دارند. ارجاعها نمیتوانند null باشند، قابل تغییر مقصد نیستند و نیازی به ارجاعزدایی صریح ندارند. ارجاعها را برای پارامترهای تابع ترجیح دهید؛ از اشارهگرها زمانی استفاده کنید که قابلیت null بودن یا تغییر مقصد مورد نیاز است.
#include <iostream>
void increment_ref(int& val) { ++val; } // cannot be null
void increment_ptr(int* val) { if (val) ++(*val); } // must null-check
int main() {
int x = 10;
increment_ref(x); // clean call syntax
increment_ptr(&x); // caller must take address
std::cout << x << '\n'; // 12
int* p = &x;
p = nullptr; // OK — pointers can be re-seated and nulled
int& r = x;
// r = ???; // references cannot be re-seated
// int& bad; // ERROR — references must be initialized
}&آدرس را میگیرد،*آن را دنبال میکند — اینها عملیات معکوس هستند- محاسبات اشارهگر فقط درون آرایهها معتبر است و بر اساس
sizeof(T)مقیاسبندی میشود - نامهای آرایه به اشارهگر تبدیل (decay) میشوند و اطلاعات اندازه را از دست میدهند
- جایگاه
constرا از راست به چپ بخوانید:const int*در مقابلint* const - در C++ مدرن، اشارهگرهای خام فقط باید ناظران بدون مالکیت باشند
- ارجاعها را بر اشارهگرها ترجیح دهید مگر اینکه به قابلیت null بودن یا تغییر مقصد نیاز داشته باشید
آزمون — دانش خود را بسنجید
(15 امتیاز)1. معنی `const int* p` چیست؟
2. وقتی یک آرایه به تابعی که پارامتر اشارهگر دارد ارسال میشود چه اتفاقی میافتد؟
3. محاسبات اشارهگر در C++ چه زمانی معتبر است؟