पॉइंटर: एड्रेस, डिरेफ़रेंस और अरिथमेटिक
पॉइंटर को मेमोरी एड्रेस के रूप में समझें; एड्रेस-ऑफ़ और डिरेफ़रेंस ऑपरेटर, ऐरे के भीतर पॉइंटर अरिथमेटिक, पॉइंटर के साथ const correctness, और आधुनिक C++ में रॉ पॉइंटर अब भी कब उचित हैं, यह सीखें।
पॉइंटर क्या है?
पॉइंटर (pointer) एक ऐसा वेरिएबल है जो मेमोरी एड्रेस को स्टोर करता है। C++ का हर ऑब्जेक्ट मेमोरी में किसी न किसी एड्रेस पर रहता है, और पॉइंटर की मदद से आप उस ऑब्जेक्ट को अप्रत्यक्ष रूप से रेफ़र कर सकते हैं। पॉइंटर C++ की बुनियाद हैं — इन्हीं से डायनैमिक डेटा स्ट्रक्चर, पॉलीमॉर्फ़िज़्म और कुशल पैरामीटर पासिंग संभव होती है।
पॉइंटर टाइप * से डिक्लेयर किया जाता है: int* p; एक int के पॉइंटर को डिक्लेयर करता है। पॉइंटर खुद भी मेमोरी में जगह घेरता है (64-बिट सिस्टम पर आमतौर पर 8 बाइट) और उसकी वैल्यू एक एड्रेस होती है, वह डेटा नहीं जिसकी ओर वह इंगित करता है।
एड्रेस-ऑफ़ और डिरेफ़रेंस
दो बुनियादी पॉइंटर ऑपरेटर हैं & (एड्रेस-ऑफ़) और * (डिरेफ़रेंस)। & ऑपरेटर किसी वेरिएबल का एड्रेस देता है। * ऑपरेटर उस एड्रेस तक पहुँचकर इंगित किए गए ऑब्जेक्ट को एक्सेस करता है।
#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
}पॉइंटर अरिथमेटिक और ऐरे डिके (Array Decay)
पॉइंटर अरिथमेटिक केवल ऐरे के भीतर वैध है (या ऐरे के अंत से एक स्थान आगे तक)। कंपाइलर इस अरिथमेटिक को sizeof(T) से स्केल करता है। ज़्यादातर संदर्भों में ऐरे का नाम अपने पहले एलिमेंट के पॉइंटर में डिके हो जाता है — इसी कारण 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 की स्थिति * के सापेक्ष कहाँ है, इससे तय होता है कि क्या अपरिवर्तनीय (immutable) है:
- 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*) कोई भी एड्रेस रख सकते हैं, लेकिन कास्टिंग के बिना उन्हें डिरेफ़रेंस नहीं किया जा सकता। इनका उपयोग मुख्य रूप से C-स्टाइल API और एलोकेटर इम्प्लीमेंटेशन में होता है।
आधुनिक C++ में रॉ पॉइंटर का उपयोग केवल नॉन-ओनिंग, ऑब्ज़र्वर पॉइंटर के रूप में होना चाहिए। वे यह कहते हैं: "मैं इस ऑब्जेक्ट को देख सकता हूँ, लेकिन इसकी लाइफ़टाइम की ज़िम्मेदारी मेरी नहीं है।" कुछ सामान्य वैध उपयोग:
- डेटा स्ट्रक्चर लागू करना (जैसे ट्री नोड का अपने पैरेंट की ओर इंगित करना)
- ऐसे फ़ंक्शन पैरामीटर जो ऑब्जेक्ट को सिर्फ़ देखते हैं, उसके मालिक नहीं होते (हालाँकि अक्सर रेफ़रेंस बेहतर रहते हैं)
- C API के साथ इंटरफ़ेस करना
- कस्टम कंटेनर में इटरेटर
अगर कोई पॉइंटर किसी रिसोर्स का मालिक (owner) है, तो उसे std::unique_ptr या std::shared_ptr में रैप करें। अगर आप delete लिख रहे हैं, तो आधुनिक C++ में लगभग निश्चित है कि आप कुछ गलत कर रहे हैं।
डैंगलिंग पॉइंटर (dangling pointer) पॉइंटर से जुड़ा नंबर 1 बग है। जब पॉइंटर जिस ऑब्जेक्ट की ओर इंगित करता है वह नष्ट हो जाता है, तो पॉइंटर डैंगलिंग हो जाता है:
- किसी लोकल वेरिएबल का पॉइंटर रिटर्न करना
- किसी ऑब्जेक्ट को delete करना जबकि कोई दूसरा पॉइंटर अब भी उसे रेफ़र कर रहा हो
- ऐसे std::vector के भीतर का पॉइंटर, जो push_back के बाद रीएलोकेट हो गया हो
अनइनिशियलाइज़्ड पॉइंटर में कचरा (garbage) एड्रेस होते हैं। अगर अभी कोई टारगेट नहीं है, तो हमेशा nullptr से इनिशियलाइज़ करें। कंपाइलर अनइनिशियलाइज़्ड पॉइंटर के सभी उपयोगों पर चेतावनी नहीं देगा।
पॉइंटर बनाम रेफ़रेंस: कब क्या इस्तेमाल करें
रेफ़रेंस और पॉइंटर, दोनों इनडायरेक्शन (indirection) प्रदान करते हैं, लेकिन उनमें कुछ महत्वपूर्ण अंतर हैं। रेफ़रेंस 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
}&एड्रेस लेता है,*उसका अनुसरण करता है — ये एक-दूसरे के उलटे (inverse) ऑपरेशन हैं- पॉइंटर अरिथमेटिक केवल ऐरे के भीतर वैध है और
sizeof(T)से स्केल होता है - ऐरे के नाम पॉइंटर में डिके हो जाते हैं, जिससे साइज़ की जानकारी खो जाती है
constकी स्थिति को दाएँ से बाएँ पढ़ें:const int*बनामint* const- आधुनिक C++ में रॉ पॉइंटर केवल नॉन-ओनिंग ऑब्ज़र्वर होने चाहिए
- जब तक null होने की क्षमता या री-सीट करने की ज़रूरत न हो, पॉइंटर के बजाय रेफ़रेंस को प्राथमिकता दें
क्विज़ — अपना ज्ञान परखें
(15 XP)1. `const int* p` का क्या अर्थ है?
2. जब किसी ऐरे को ऐसे फ़ंक्शन में पास किया जाता है जो पॉइंटर पैरामीटर की अपेक्षा करता है, तो क्या होता है?
3. C++ में पॉइंटर अरिथमेटिक कब वैध है?