सामग्री पर जाएं

वेरिएबल, स्कोप और लाइफ़टाइम

चार स्टोरेज ड्यूरेशन, स्कोप और लाइफ़टाइम में अंतर, इनिशियलाइज़ेशन की गड़बड़ियों, और स्टैटिक इनिशियलाइज़ेशन ऑर्डर फ़ियास्को के बारे में जानें।

स्कोप और लाइफ़टाइम एक ही चीज़ नहीं हैं

C++ की सबसे आम गलतफ़हमियों में से एक है स्कोप (scope) और लाइफ़टाइम (lifetime) को एक मान लेना। ये आपस में जुड़ी हुई लेकिन अलग अवधारणाएँ हैं:

स्कोप कंपाइल-टाइम गुण है — यह तय करता है कि सोर्स कोड में कोई नाम कहाँ दिखाई देता है। किसी ब्लॉक {} के अंदर घोषित वेरिएबल केवल उसी ब्लॉक और उसके अंदर के नेस्टेड ब्लॉकों में दिखाई देता है।

लाइफ़टाइम रनटाइम गुण है — यह तय करता है कि कोई ऑब्जेक्ट मेमोरी में कब मौजूद रहता है। किसी ऑब्जेक्ट का लाइफ़टाइम तब शुरू होता है जब उसका कंस्ट्रक्टर पूरा होता है और तब खत्म होता है जब उसका डिस्ट्रक्टर शुरू होता है।

कई मामलों में स्कोप और लाइफ़टाइम एक साथ चलते हैं: लोकल वेरिएबल अपने ब्लॉक में दिखाई देता है और ब्लॉक खत्म होने तक जीवित रहता है। लेकिन वे अलग भी हो सकते हैं। static लोकल वेरिएबल केवल अपने फ़ंक्शन के अंदर दिखाई देता है (सीमित स्कोप) लेकिन पूरे प्रोग्राम तक जीवित रहता है (असीमित लाइफ़टाइम)। डायनैमिक रूप से आवंटित ऑब्जेक्ट का लाइफ़टाइम उस पॉइंटर के स्कोप से आगे तक चलता है जिसने उसे बनाया था।

चार स्टोरेज ड्यूरेशन

C++ चार स्टोरेज ड्यूरेशन परिभाषित करती है जो तय करते हैं कि ऑब्जेक्ट कब बनाए और कब नष्ट किए जाएँ:

storage.cppc++23
#include <iostream>
#include <string>

// Static storage duration — lives for the entire program
int global_count = 0;                    // Initialized before main()
static int file_local = 42;              // Same, but only visible in this file

void demonstrate_storage() {
    // Automatic storage duration — created on entry, destroyed on exit
    int local = 10;                      // Born here, dies at closing brace

    // Static storage duration (local scope)
    static int call_count = 0;           // Initialized ONCE, persists across calls
    ++call_count;
    std::cout << "Call #" << call_count << '\n';

    // Thread-local storage duration (C++11)
    // thread_local int per_thread = 0;  // One copy per thread, lives until thread ends
}

int main() {
    demonstrate_storage();  // Call #1
    demonstrate_storage();  // Call #2 — call_count is still alive!
    demonstrate_storage();  // Call #3

    // Dynamic storage duration — you control the lifetime
    int* heap_int = new int{99};         // Born here
    std::cout << *heap_int << '\n';
    delete heap_int;                     // Dies here — you must not forget!
    heap_int = nullptr;                  // Good practice: nullify after delete

    // In modern C++, prefer smart pointers over raw new/delete
    // auto ptr = std::make_unique<int>(99);  // Covered in later modules
}
आउटपुट

इस उदाहरण को कंपाइल करके चलाने के लिए चलाएं दबाएं।

इनिशियलाइज़ेशन के रूप और ज़ीरो-इनिशियलाइज़ेशन

C++ में इनिशियलाइज़ेशन की तीन बुनियादी अवधारणाएँ हैं:

डिफ़ॉल्ट इनिशियलाइज़ेशन (Default initialization) — वेरिएबल बिना किसी इनिशियलाइज़र के बनाया जाता है। बिल्ट-इन टाइप (int, double आदि) के लिए इसका मतलब है कि वैल्यू अनिर्धारित (indeterminate) है (उसे पढ़ना अपरिभाषित व्यवहार है!)। क्लास टाइप के लिए डिफ़ॉल्ट कंस्ट्रक्टर कॉल होता है।

वैल्यू इनिशियलाइज़ेशन (Value initialization) — खाली कोष्ठक T() या खाली ब्रेसेस T{} से ट्रिगर होता है। बिल्ट-इन टाइप के लिए यह ज़ीरो-इनिशियलाइज़ करता है: इंटीजर 0 बन जाते हैं, पॉइंटर nullptr बन जाते हैं, बूलियन false बन जाते हैं। क्लास टाइप के लिए डिफ़ॉल्ट कंस्ट्रक्टर कॉल होता है (और अगर कोई कंस्ट्रक्टर मेंबर को इनिशियलाइज़ नहीं करता, तो मेंबर वैल्यू-इनिशियलाइज़ होते हैं)।

ज़ीरो इनिशियलाइज़ेशन (Zero initialization) — एक विशिष्ट चरण जो static/thread-local वेरिएबल के लिए किसी भी दूसरे इनिशियलाइज़ेशन से पहले होता है। प्रोग्राम शुरू होने से पहले सभी स्टैटिक वेरिएबल ज़ीरो-इनिशियलाइज़ हो जाते हैं।

इसीलिए static int x; का 0 होना गारंटीशुदा है, लेकिन लोकल int x; का नहीं — लोकल वेरिएबल डिफ़ॉल्ट-इनिशियलाइज़ होता है (अनिर्धारित), जबकि स्टैटिक वेरिएबल पहले ज़ीरो-इनिशियलाइज़ेशन से गुज़रता है।

व्यवहार में इनिशियलाइज़ेशन

यह समझना कि कोई वेरिएबल कब शून्य होता है और कब अनिर्धारित, बग से बचने के लिए बेहद ज़रूरी है:

init_forms.cppc++23
#include <iostream>

struct Point {
    int x;
    int y;
};

static int global_int;          // Zero-initialized → 0
static Point global_point;      // Zero-initialized → {0, 0}

int main() {
    int a;                      // Default-initialized → INDETERMINATE (UB to read!)
    int b{};                    // Value-initialized → 0
    int c = int();              // Value-initialized → 0
    int d = {};                 // Value-initialized → 0

    Point p1;                   // Default-initialized → members are INDETERMINATE
    Point p2{};                 // Value-initialized → {0, 0}
    Point p3{1, 2};             // Aggregate-initialized → {1, 2}

    // Array initialization
    int arr1[5];                // Default-initialized → INDETERMINATE values!
    int arr2[5]{};              // Value-initialized → {0, 0, 0, 0, 0}
    int arr3[5]{1, 2};          // Partially initialized → {1, 2, 0, 0, 0}

    std::cout << "global_int: " << global_int << '\n';  // Safe: 0
    std::cout << "b: " << b << '\n';                    // Safe: 0
    // std::cout << a;  // DANGER: UB — reading uninitialized variable!

    std::cout << "p2: (" << p2.x << ", " << p2.y << ")\n";  // Safe: (0, 0)
    std::cout << "p3: (" << p3.x << ", " << p3.y << ")\n";  // Safe: (1, 2)
}
आउटपुट

इस उदाहरण को कंपाइल करके चलाने के लिए चलाएं दबाएं।

डिस्ट्रक्शन ऑर्डर: LIFO

ऑटोमैटिक स्टोरेज ड्यूरेशन वाले ऑब्जेक्ट (लोकल वेरिएबल) अपने निर्माण के उलटे क्रम में नष्ट होते हैं — Last In, First Out (LIFO)। यह महज़ एक परंपरा नहीं है; स्टैंडर्ड इसकी गारंटी देता है।

जब ऑब्जेक्ट एक-दूसरे पर निर्भर हों, तो यह बहुत मायने रखता है। अगर ऑब्जेक्ट b, ऑब्जेक्ट a को रेफ़र करता है, तो आपको a को b से पहले घोषित करना होगा। तब b पहले नष्ट होता है (जब a अभी जीवित है), और a दूसरे नंबर पर नष्ट होता है। अगर क्रम उलटा होता, तो b का डिस्ट्रक्टर किसी मृत a का उपयोग करने की कोशिश कर सकता था — अपरिभाषित व्यवहार।

स्टैटिक वेरिएबल के लिए, main() के लौटने के बाद डिस्ट्रक्शन निर्माण के उलटे क्रम में होता है। एक ही ट्रांसलेशन यूनिट में परिभाषित ग्लोबल वेरिएबल ऊपर से नीचे की ओर बनते हैं और नीचे से ऊपर की ओर नष्ट होते हैं। लेकिन अलग-अलग ट्रांसलेशन यूनिट के बीच, ग्लोबल के निर्माण का क्रम अनिर्दिष्ट (unspecified) है — यही स्टैटिक इनिशियलाइज़ेशन ऑर्डर फ़ियास्को है।

सामान्य गलती

स्टैटिक इनिशियलाइज़ेशन ऑर्डर फ़ियास्को (static initialization order fiasco) C++ के सबसे कुख्यात गड़बड़ियों में से एक है। जब आपके ग्लोबल या स्टैटिक वेरिएबल अलग-अलग ट्रांसलेशन यूनिट (अलग-अलग .cpp फ़ाइलों) में हों, तो C++ स्टैंडर्ड यह निर्दिष्ट नहीं करता कि वे किस क्रम में इनिशियलाइज़ होंगे। अगर कोई ग्लोबल किसी दूसरी ट्रांसलेशन यूनिट के ग्लोबल पर निर्भर है, तो हो सकता है कि उसके बनने से पहले ही उसका उपयोग हो जाए।

क्लासिक समाधान Construct on First Use इडियम है: ग्लोबल वेरिएबल की जगह एक फ़ंक्शन रखें जो किसी लोकल स्टैटिक वेरिएबल का रेफ़रेंस लौटाता हो। लोकल स्टैटिक की गारंटी है कि वह फ़ंक्शन के पहली बार कॉल होने पर इनिशियलाइज़ होगा (और C++11 से यह थ्रेड-सेफ़ है)।

  • ट्रांसलेशन यूनिट के पार ग्लोबल के इनिशियलाइज़ेशन का क्रम अनिर्दिष्ट है
  • अगर ग्लोबल A (file1.cpp में) ग्लोबल B (file2.cpp में) पर निर्भर है, तो A के बनते समय B शायद अभी मौजूद ही न हो
  • समाधान: Construct on First Use इडियम अपनाएँ — ग्लोबल को लोकल स्टैटिक वाले फ़ंक्शन में लपेट दें
  • लोकल स्टैटिक इनिशियलाइज़ेशन C++11 से थ्रेड-सेफ़ है ('magic statics' के नाम से जाना जाता है)

Construct on First Use इडियम

यह पैटर्न निर्माण को पहले उपयोग तक टालकर स्टैटिक इनिशियलाइज़ेशन ऑर्डर फ़ियास्को को हल करता है:

construct_on_first_use.cppc++23
#include <string>
#include <iostream>

// BAD: global depends on another global — order is unspecified across files
// std::string bad_prefix = "[LOG] ";  // What if this is used before construction?

// GOOD: Construct on First Use idiom
std::string& log_prefix() {
    static std::string prefix = "[LOG] ";  // Constructed on first call, thread-safe
    return prefix;
}

std::string& app_name() {
    static std::string name = "MyApp";
    return name;
}

void log(const std::string& message) {
    // Safe: log_prefix() and app_name() are guaranteed to be initialized
    std::cout << log_prefix() << app_name() << ": " << message << '\n';
}

int main() {
    log("Starting up");
    log("Ready");
}
आउटपुट

इस उदाहरण को कंपाइल करके चलाने के लिए चलाएं दबाएं।

मुख्य बातें
  • स्कोप (नाम कहाँ दिखता है) और लाइफ़टाइम (ऑब्जेक्ट कब मौजूद रहता है) अलग-अलग अवधारणाएँ हैं जो एक-दूसरे से भिन्न हो सकती हैं
  • C++ में चार स्टोरेज ड्यूरेशन हैं: ऑटोमैटिक (स्टैक), स्टैटिक (प्रोग्राम का पूरा जीवनकाल), थ्रेड-लोकल, और डायनैमिक (हीप)
  • डिफ़ॉल्ट-इनिशियलाइज़ हुए बिल्ट-इन टाइप के मान अनिर्धारित होते हैं — उन्हें पढ़ना अपरिभाषित व्यवहार है
  • {} के साथ वैल्यू इनिशियलाइज़ेशन बिल्ट-इन टाइप के लिए ज़ीरो-इनिशियलाइज़ेशन की गारंटी देता है
  • लोकल वेरिएबल LIFO क्रम में नष्ट होते हैं — निर्भरताओं को उन पर निर्भर चीज़ों से पहले घोषित करें
  • ट्रांसलेशन यूनिट के पार स्टैटिक इनिशियलाइज़ेशन ऑर्डर फ़ियास्को से बचने के लिए Construct on First Use इडियम का उपयोग करें

क्विज़ — अपना ज्ञान परखें

(15 XP)

1. C++ में लोकल `int x;` (बिना इनिशियलाइज़र के) की वैल्यू क्या होती है?

2. स्टैटिक इनिशियलाइज़ेशन ऑर्डर फ़ियास्को क्या है?

3. किसी फ़ंक्शन के अंदर `static int count = 0;`, `int count = 0;` से कैसे अलग है?