Can a local variable's memory be accessed outside its scope? — C++ के अंदरूनी हिस्से में गोता लगाना
\n\nएक स्थिति की कल्पना करें: आप C++ में लिख रहे हैं, एक फंक्शन बनाते हैं, उसके अंदर एक वेरिएबल घोषित करते हैं, और फिर उस फंक्शन के बाहर उस तक पहुँचने का प्रयास करते हैं। "यह असंभव है!" — आप कहेंगे। और आप... लगभग सही होंगे। लेकिन केवल "लगभग", क्योंकि C++ को आश्चर्य देना पसंद है।
\n\nप्रश्न "Can a local variable's memory be accessed outside its scope?" — Stack Overflow पर हज़ारों व्यूज़ के साथ सबसे लोकप्रिय प्रश्नों में से एक है। और यह व्यर्थ नहीं है: स्पष्ट सरलता के पीछे मेमोरी, स्टैक और कंपाइलर के काम करने की गहरी समझ छिपी है। आज हम उदाहरणों के साथ इस प्रश्न का विश्लेषण करेंगे, अंदरूनी हिस्से में झाँकेंगे और जानेंगे कि कंपाइलर आपको क्यों "बचा" सकता है, और क्यों "फँसा" भी सकता है।
\n\n1. C++ में दायरा (scope) क्या है?
\n\nशुरुआत में, आधार को याद करते हैं। दायरा कोड का वह भाग है जहाँ वेरिएबल का नाम उपलब्ध होता है। स्थानीय वेरिएबल ब्लॉक {} (फंक्शन, लूप, कंडीशनल स्टेटमेंट) के अंदर रहते हैं। जैसे ही प्रोग्राम क्लोज़िंग कर्ली ब्रेस से बाहर निकलता है, वेरिएबल "मर जाता है" — इसका नाम अब मौजूद नहीं रहता।
लेकिन यहाँ महत्वपूर्ण बात यह है: नाम मर जाता है, लेकिन मेमोरी जरूरी नहीं। वेरिएबल द्वारा घेरी गई भौतिक मेमोरी स्टैक पर तब तक बनी रहती है जब तक कि किसी अन्य फंक्शन कॉल द्वारा इसे ओवरराइट नहीं किया जाता। और यहीं से सबसे दिलचस्प शुरू होता है।
\n\n#include <iostream>\n\nvoid foo() {\n int x = 42;\n std::cout << "foo के अंदर: x = " << x << std::endl;\n // x केवल } तक जीवित है\n}\n\nint main() {\n foo();\n // यहाँ x अब मौजूद नहीं है\n // लेकिन क्या इसकी मेमोरी तक पहुँचा जा सकता है?\n return 0;\n}\n\n\nऔपचारिक रूप से — नहीं। लेकिन प्रोग्रामर जिज्ञासु लोग होते हैं। आइए कंपाइलर को धोखा देने का प्रयास करें।
\n\n2. क्लासिक ट्रिक: स्थानीय वेरिएबल पर पॉइंटर लौटाना
\n\nस्थानीय वेरिएबल को "निकालने" का सबसे स्पष्ट तरीका फंक्शन से उस पर पॉइंटर लौटाना है। कंपाइलर आपको चेतावनी भी देगा (यदि चेतावनियाँ चालू हैं), लेकिन कोड कंपाइल हो जाएगा।
\n\n#include <iostream>\n\nint* dangerous() {\n int local = 100;\n return &local; // ध्यान दें! स्थानीय वेरिएबल का पता लौटा रहे हैं\n}\n\nint main() {\n int* ptr = dangerous();\n // ptr अब मृत मेमोरी की ओर इशारा कर रहा है\n std::cout << "मान: " << *ptr << std::endl; // अपरिभाषित व्यवहार!\n return 0;\n}\n\n\nयहाँ क्या हो रहा है? फंक्शन dangerous वेरिएबल local का पता लौटाता है, जो स्टैक पर रखा गया है। फंक्शन से बाहर निकलने के बाद, स्टैक मेमोरी (तार्किक रूप से) मुक्त हो जाती है, लेकिन भौतिक रूप से पता बना रहता है। जब हम main में पॉइंटर को डीरेफरेंस करते हैं, तो हम "कचरा" पढ़ते हैं — जो मेमोरी में बचा है। यह 100 हो सकता है, या 0xDEADBEEF हो सकता है।
इसे "डैंगलिंग पॉइंटर" (dangling pointer) कहा जाता है, और इसका उपयोग करना अपरिभाषित व्यवहार (Undefined Behavior, UB) की ओर ले जाता है। UB का मतलब है कि कंपाइलर को कुछ भी करने का अधिकार है: सही आउटपुट से लेकर प्रोग्राम के क्रैश होने या आपकी फ़ाइलों को डिलीट करने तक।
\n\n3. कोड कभी-कभी "काम" क्यों करता है? सुरक्षा का भ्रम
\n\nकई शुरुआती, उपरोक्त उदाहरण चलाकर, आश्चर्यचकित होते हैं: "मुझे 100 मिला! सब कुछ काम कर रहा है!"। लेकिन यह एक जाल है। इसके कारण ये हैं:
\n\n- \n
- स्टैक तुरंत साफ़ नहीं होता। फंक्शन से बाहर निकलने के बाद, मेमोरी को केवल मुक्त के रूप में चिह्नित किया जाता है। डेटा तब तक अछूता रह सकता है जब तक कि अगली कॉल इसे ओवरराइट न करे। \n
- कंपाइलर ऑप्टिमाइज़ कर सकता है। यदि वेरिएबल का वापसी के बाद उपयोग नहीं किया जाता है, तो कंपाइलर इसे स्टैक से पूरी तरह हटा सकता है, और आप पड़ोसी फ्रेम से डेटा पढ़ेंगे। \n
- किसी अन्य फंक्शन को कॉल करने से स्टैक ओवरराइट हो जाता है।
dangerousके बाद कोई अन्य फंक्शन कॉल करने का प्रयास करें — और "जादुई" मान गायब हो जाएगा। \n
#include <iostream>\n\nint* da