Chuyển đến nội dung

Biến, phạm vi và thời gian tồn tại

Tìm hiểu bốn khoảng thời gian lưu trữ, phạm vi và thời gian tồn tại là các khái niệm riêng biệt như thế nào, các cạm bẫy khi khởi tạo và lỗi thứ tự khởi tạo tĩnh.

Phạm vi và thời gian tồn tại không giống nhau

Một trong những quan niệm sai lầm phổ biến nhất trong C++ là nhầm lẫn phạm vi với thời gian sống. Chúng là những khái niệm liên quan nhưng khác biệt:

Phạm vi là thuộc tính thời gian biên dịch — nó xác định vị trí hiển thị tên trong mã nguồn. Một biến được khai báo bên trong khối{}chỉ hiển thị trong khối đó và bất kỳ khối lồng nhau nào.

Tuổi thọ là thuộc tính thời gian chạy — nó xác định thời điểm một đối tượng tồn tại trong bộ nhớ. Vòng đời của một đối tượng bắt đầu khi hàm tạo của nó hoàn thành và kết thúc khi hàm hủy của nó bắt đầu.

Trong nhiều trường hợp, phạm vi và thời gian tồn tại trùng khớp: một biến cục bộ hiển thị trong khối của nó và tồn tại cho đến khi khối kết thúc. Nhưng họ có thể phân kỳ. Biến cục bộstaticchỉ hiển thị bên trong chức năng của nó (phạm vi giới hạn) nhưng tồn tại trong toàn bộ chương trình (thời gian tồn tại không giới hạn). Một đối tượng được cấp phát động có thời gian tồn tại vượt quá phạm vi của con trỏ đã tạo ra nó.

Bốn thời lượng lưu trữ

C++ xác định bốn khoảng thời gian lưu trữ kiểm soát thời điểm các đối tượng được tạo và hủy:

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
}
Đầu ra

Nhấn Chạy để biên dịch và chạy ví dụ này.

Biểu mẫu khởi tạo và không khởi tạo

C++ có ba khái niệm khởi tạo cơ bản:

Khởi tạo mặc định — biến được tạo mà không cần trình khởi tạo. Đối với các kiểu dựng sẵn (int, double, v.v.), điều này có nghĩa là giá trị là không xác định (đọc nó là hành vi không xác định!). Đối với các loại lớp, hàm tạo mặc định được gọi.

Khởi tạo giá trị — được kích hoạt bởi dấu ngoặc đơn trốngT()hoặc dấu ngoặc nhọn trốngT{}. Đối với các kiểu có sẵn, khởi tạo bằng 0 này: số nguyên trở thành 0, con trỏ trở thành nullptr, boolean trở thành sai. Đối với các loại lớp, hàm tạo mặc định được gọi (và các thành viên được khởi tạo giá trị nếu không có hàm tạo nào khởi tạo chúng).

Khởi tạo bằng 0 — một giai đoạn cụ thể xảy ra đối với các biến tĩnh/luồng cục bộ trước bất kỳ lần khởi tạo nào khác. Tất cả các biến tĩnh đều được khởi tạo bằng 0 trước khi chương trình bắt đầu.

Đây là lý do tại saostatic int x;được đảm bảo là 0, nhưngint x;cục bộ thì không — biến cục bộ được khởi tạo mặc định (không xác định), trong khi biến tĩnh trước tiên trải qua quá trình khởi tạo bằng 0.

Khởi tạo trong thực tế

Hiểu khi nào một biến bằng 0 so với không xác định là rất quan trọng để tránh lỗi:

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)
}
Đầu ra

Nhấn Chạy để biên dịch và chạy ví dụ này.

Lệnh tiêu hủy: LIFO

Các đối tượng có thời lượng lưu trữ tự động (biến cục bộ) bị hủy theo thứ tự ngược lại trong quá trình xây dựng của chúng — Vào sau, ra trước (LIFO). Đây không chỉ là một quy ước; tiêu chuẩn đảm bảo điều đó.

Điều này rất quan trọng khi các đối tượng phụ thuộc vào nhau. Nếu đối tượngbtham chiếu đối tượnga, bạn phải khai báoatrướcb. Sau đó,bbị hủy trước tiên (trong khiavẫn còn tồn tại) vàabị hủy thứ hai. Nếu thứ tự bị đảo ngược, hàm hủy củabcó thể cố gắng sử dụngađã chết - hành vi không xác định.

Đối với các biến tĩnh, quá trình hủy xảy ra theo thứ tự xây dựng ngược sau khimain()trả về. Các biến toàn cục được xác định trong cùng một đơn vị dịch thuật được xây dựng từ trên xuống dưới và bị hủy từ dưới lên trên. Nhưng trên các đơn vị dịch thuật, thứ tự xây dựng của toàn cục là không xác định - đây là lỗi thứ tự khởi tạo tĩnh.

Lỗi thường gặp

Lỗi thứ tự khởi tạo tĩnh là một trong những cạm bẫy khét tiếng nhất trong C++. Khi bạn có các biến toàn cục hoặc biến tĩnh trong các đơn vị dịch khác nhau (các tệp .cpp khác nhau), tiêu chuẩn C++ không chỉ định thứ tự khởi tạo chúng. Nếu một toàn cầu phụ thuộc vào một toàn cầu khác từ một đơn vị dịch thuật khác, thì nó có thể được sử dụng trước khi được xây dựng.

Cách khắc phục cổ điển là thành ngữ Xây dựng khi sử dụng lần đầu: thay thế biến toàn cục bằng một hàm trả về tham chiếu đến biến tĩnh cục bộ. Các số liệu thống kê cục bộ được đảm bảo được khởi tạo trong lần đầu tiên hàm được gọi (và điều này an toàn theo luồng kể từ C++11).

  • The order of initialization of globals across translation units is unspecified
  • If global A (in file1.cpp) depends on global B (in file2.cpp), B might not exist yet when A is constructed
  • Khắc phục: sử dụng thành ngữ Xây dựng khi sử dụng lần đầu - bao bọc toàn cục trong các hàm bằng thống kê cục bộ
  • Local static initialization is thread-safe since C++11 (known as 'magic statics')

The Construct on First Use Idiom

Mẫu này giải quyết lỗi thứ tự khởi tạo tĩnh bằng cách trì hoãn việc xây dựng cho đến lần sử dụng đầu tiên:

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");
}
Đầu ra

Nhấn Chạy để biên dịch và chạy ví dụ này.

Điểm chính
  • Phạm vi (nơi hiển thị tên) và thời gian tồn tại (khi một đối tượng tồn tại) là những khái niệm riêng biệt có thể khác nhau
  • C++ có bốn thời lượng lưu trữ: tự động (ngăn xếp), tĩnh (tuổi thọ chương trình), luồng cục bộ và động (đống)
  • Các kiểu dựng sẵn được khởi tạo mặc định có các giá trị không xác định - đọc chúng là hành vi không xác định
  • Khởi tạo giá trị với {} đảm bảo không khởi tạo cho các loại tích hợp
  • Các biến cục bộ bị hủy theo thứ tự LIFO - khai báo các phần phụ thuộc trước các biến phụ thuộc của chúng
  • Sử dụng thành ngữ Xây dựng khi sử dụng lần đầu để tránh lỗi thứ tự khởi tạo tĩnh trên các đơn vị dịch thuật

Câu đố — Kiểm tra kiến thức

(15 XP)

1. Giá trị của`int x;`cục bộ (không có bộ khởi tạo) trong C++ là bao nhiêu?

2. Lỗi thứ tự khởi tạo tĩnh là gì?

3. How does`static int count = 0;`inside a function differ from`int count = 0;`?