Μετάβαση στο περιεχόμενο

Μεταβλητές, Πεδίο εφαρμογής & Διάρκεια ζωής

Μάθετε τις τέσσερις διάρκειες αποθήκευσης, πώς το εύρος και η διάρκεια ζωής είναι διαφορετικές έννοιες, οι παγίδες αρχικοποίησης και το φιάσκο της στατικής σειράς αρχικοποίησης.

Το πεδίο εφαρμογής και η διάρκεια ζωής δεν είναι το ίδιο πράγμα

Μία από τις πιο κοινές παρανοήσεις στο 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++ έχει τρεις βασικές έννοιες αρχικοποίησης:

Προεπιλεγμένη προετοιμασία — η μεταβλητή κατασκευάζεται χωρίς αρχικοποιητή. Για ενσωματωμένους τύπους (int, double, κ.λπ.), αυτό σημαίνει ότι η τιμή είναι απροσδιόριστη (η ανάγνωσή της είναι απροσδιόριστη συμπεριφορά!). Για τύπους κλάσεων, καλείται ο προεπιλεγμένος κατασκευαστής.

Αρχικοποίηση τιμής — ενεργοποιείται από κενές παρενθέσειςT()ή κενές αγκύλεςT{}. Για ενσωματωμένους τύπους, αυτό το μηδενική αρχικοποίηση: οι ακέραιοι αριθμοί γίνονται 0, οι δείκτες γίνονται nullptr, οι δυαδικές τιμές γίνονται ψευδείς. Για τύπους κλάσεων, καλείται ο προεπιλεγμένος κατασκευαστής (και τα μέλη αρχικοποιούνται με τιμή εάν κανένας κατασκευαστής δεν τα αρχικοποιεί).

Μηδενική προετοιμασία — μια συγκεκριμένη φάση που λαμβάνει χώρα για στατικές/τοπικές μεταβλητές νήματος πριν από οποιαδήποτε άλλη προετοιμασία. Όλες οι στατικές μεταβλητές αρχικοποιούνται πριν από την έναρξη του προγράμματος.

Αυτός είναι ο λόγος για τον οποίο το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(). Οι καθολικές μεταβλητές που ορίζονται στην ίδια μονάδα μετάφρασης κατασκευάζονται από πάνω προς τα κάτω και καταστρέφονται από κάτω προς τα πάνω. Αλλά σε όλες τις μεταφραστικές μονάδες, η σειρά κατασκευής των καθολικών είναι απροσδιόριστη — αυτό είναι το φιάσκο της στατικής σειράς αρχικοποίησης.

Παγίδα

Το στατικό φιάσκο εντολής αρχικοποίησης είναι μια από τις πιο διαβόητες παγίδες στο C++. Όταν έχετε καθολικές ή στατικές μεταβλητές σε διαφορετικές μεταφραστικές μονάδες (διαφορετικά αρχεία .cpp), το πρότυπο C++ δεν καθορίζει τη σειρά με την οποία αρχικοποιούνται. Εάν ένα καθολικό εξαρτάται από ένα άλλο από διαφορετική μονάδα μετάφρασης, μπορεί να χρησιμοποιηθεί πριν κατασκευαστεί.

Η κλασική επιδιόρθωση είναι το ιδίωμα Κατασκευή για πρώτη χρήση: αντικαταστήστε την καθολική μεταβλητή με μια συνάρτηση που επιστρέφει μια αναφορά σε μια τοπική στατική μεταβλητή. Τα τοπικά στατικά είναι εγγυημένα ότι αρχικοποιούνται την πρώτη φορά που καλείται η συνάρτηση (και αυτό είναι ασφαλές για νήματα από το C++11).

  • Η σειρά αρχικοποίησης των καθολικών στις μεταφραστικές μονάδες δεν είναι καθορισμένη
  • Εάν το καθολικό Α (στο αρχείο1.cpp) εξαρτάται από το καθολικό Β (στο αρχείο2.cpp), το Β ενδέχεται να μην υπάρχει ακόμα όταν κατασκευάζεται το Α
  • Διόρθωση: χρησιμοποιήστε το ιδίωμα Construct on First Use — αναδιπλώστε τα καθολικά σε συναρτήσεις με τοπικά στατικά
  • Η τοπική στατική προετοιμασία είναι ασφαλής ως προς το νήμα από το C++11 (γνωστό ως 'magic statics')

The Construct on First Use Idiom

Αυτό το μοτίβο επιλύει το φιάσκο της σειράς στατικής αρχικοποίησης αναβάλλοντας την κατασκευή μέχρι την πρώτη χρήση:

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++ έχει τέσσερις διάρκειες αποθήκευσης: αυτόματη (στοίβα), στατική (διάρκεια ζωής προγράμματος), thread-local και δυναμική (heap)
  • Οι ενσωματωμένοι τύποι με προεπιλογή αρχικοποίησης έχουν απροσδιόριστες τιμές — η ανάγνωσή τους είναι απροσδιόριστη συμπεριφορά
  • Η αρχικοποίηση τιμής με {} εγγυάται μηδενική εκκίνηση για ενσωματωμένους τύπους
  • Οι τοπικές μεταβλητές καταστρέφονται με σειρά LIFO — δηλώνουν τις εξαρτήσεις πριν από τα εξαρτώμενα τους
  • Χρησιμοποιήστε το ιδίωμα Construct on First Use για να αποφύγετε το φιάσκο της σειράς στατικής αρχικοποίησης στις μεταφραστικές μονάδες

Κουίζ — Δοκιμάστε τις γνώσεις σας

(15 XP)

1. Ποια είναι η τιμή ενός τοπικού`int x;`(χωρίς αρχικοποιητή) στο C++;

2. Τι είναι το φιάσκο εντολής στατικής αρχικοποίησης;

3. Σε τι διαφέρει το`static int count = 0;`μέσα σε μια συνάρτηση από το`int count = 0;`;