Understanding the Destruction Order of Static Objects in C++

Understanding the Destruction Order of Static Objects in C++

In the vast world of C++ programming, the static keyword acts as a low-key yet crucial behind-the-scenes hero, widely used in variables, functions, and class members, playing an indispensable role. From modifying local variables to retain their state, to limiting the scope of functions, and to achieving shared class members, the presence of static is ubiquitous.

However, while focusing on the various conveniences brought by static, we often overlook an important aspect—the destruction order of static objects. The destructor, as a cleanup mechanism at the end of an object’s lifecycle, has unique rules and can easily be misunderstood for static objects. These details, although not often mentioned, can lead to hard-to-trace errors in complex projects, resulting in abnormal program execution. Next, let us delve into the mysteries of static destruction and unveil its secrets.

A. Multifaceted Analysis of the Static Keyword

Before we explore the destruction order of static, let us first comprehensively understand the powerful functions of the static keyword in C++. It is like a jack-of-all-trades, exhibiting different magical effects in various scenarios.

1.1 Modifying Variables

(1) Modifying Local Variables: When static modifies a local variable, this local variable gains “superpowers”—persistence. Ordinary local variables are destroyed at the end of the function, like a fleeting moment. But static local variables are different; they exist throughout the program’s execution, like a loyal guardian, steadfast in their post. Their lifecycle begins from the first initialization and continues until the program ends. For example, we can define a static local variable in a function to count the number of times the function is called:

void countFunctionCalls() {
    static int callCount = 0;
    callCount++;
    std::cout << "Function has been called " << callCount << " times" << std::endl;
}

Each time countFunctionCalls is called, the value of callCount retains the value from the previous call and increments based on that, rather than being reinitialized to 0 like an ordinary local variable.

(2) Modifying Global Variables: When static modifies a global variable, it is like putting a “mysterious veil” over this global variable, limiting its scope to the current file. Global variables are by default externally linkable, meaning they can be accessed from other files using the extern keyword. However, once modified by static, they can only be accessed within the current file, and other files cannot peek into their existence.

This approach helps avoid conflicts between global variables with the same name in different files, enhancing code modularity and safety. For instance, in a large project, multiple files may need to use a configuration variable named config. Without using static, careful management of this variable is required to prevent accidental modifications across different files. By using static, each file can have its own independent config variable, without interference.

(3) Modifying Class Member Variables: In a class, a member variable modified by static acts like a shared treasure, accessible by all objects of the class. Each object can access and modify this static member variable, and changes to it will affect all objects. Static member variables do not belong to any specific object but to the entire class. Their initialization must occur outside the class, for example:

class MyClass {
public:
    static int sharedValue;
};
int MyClass::sharedValue = 0;

In the code above, sharedValue is a static member variable of the MyClass class, initialized outside the class. All objects of the MyClass class can access and modify sharedValue, for instance:

MyClass obj1, obj2;
obj1.sharedValue = 10;
std::cout << "obj2's sharedValue: " << obj2.sharedValue << std::endl; // Outputs 10

1.2 Modifying Functions

When static modifies a function, this function becomes a “hermit,” with its scope limited to the current file. Other files cannot call this static function; only other functions within the current file can access it. This is very useful in modular programming, allowing us to declare some auxiliary functions that are only used within the current file as static functions, hiding implementation details and enhancing code encapsulation. For example, we can implement a mathematical computation module in one file, with some internally used auxiliary functions declared as static functions:

// mathUtils.cpp
static int add(int a, int b) {
    return a + b;
}
int calculateSum() {
    int result = add(3, 5);
    return result;
}

In the code above, the add function is declared as a static function, callable only within the mathUtils.cpp file, preventing access from other files. This avoids naming conflicts and protects the implementation details of the function from being modified arbitrarily by external code.

1.3 Static Members

(1) Static Member Variables

Static member variables belong to the class, not to any specific object of the class. This means that regardless of how many objects of the class are created, there is only one copy of the static member variable in memory, shared by all objects. For example, we have a Student class that includes a static member variable totalStudentCount to count the total number of students, as shown in the code below:

class Student {
public:
    Student() {
        totalStudentCount++;
    }
    ~Student() {
        totalStudentCount--;
    }
    static int getTotalStudentCount() {
        return totalStudentCount;
    }
private:
    static int totalStudentCount;
};
int Student::totalStudentCount = 0;

int main() {
    Student s1, s2;
    std::cout << "Total students: " << Student::getTotalStudentCount() << std::endl;
    return 0;
}

In the code above, totalStudentCount is a static member variable initialized outside the class. Each time a Student object is created, totalStudentCount increments; when an object is destroyed, it decrements. The current total number of students can be obtained through the Student::getTotalStudentCount() function.

The memory allocation for static member variables occurs in the static storage area, with a lifecycle from the start of the program to its end. Unlike ordinary member variables, which are allocated memory when an object is created and stored in the object’s memory space (each object has its own independent copy), static member variables do not depend on the creation of objects; they can be accessed even if no objects are created.

(2) Static Member Functions

Static member functions also belong to the class, not to the objects of the class. An important characteristic is that they do not have a this pointer, as they are not associated with any specific object. This means that static member functions can only access static member variables and static member functions, and cannot access non-static members. For example:

class MathUtils {
public:
    static int add(int a, int b) {
        return a + b;
    }
private:
    static int result;
};

int MathUtils::result = 0;

int main() {
    int sum = MathUtils::add(3, 5);
    std::cout << "Sum: " << sum << std::endl;
    return 0;
}

In this MathUtils class, the add function is a static member function that can be called directly by the class name without creating a MathUtils object. Since there is no this pointer, it cannot access non-static members. In contrast, ordinary member functions have a this pointer that points to the object calling the function, allowing access to all members of the object, including both static and non-static members. Static member functions are often used to implement operations related to the class that do not depend on the state of specific objects, such as methods in utility classes or object creation methods in factory patterns.

B. Principles and Rules of Static Destruction

The destructor, simply put, serves the opposite purpose of the constructor. The constructor is called when an object is created to initialize the object’s member variables and resources; the destructor is called when an object is destroyed, responsible for cleaning up the resources occupied by the object, such as releasing dynamically allocated memory, closing open files, and disconnecting network connections.

Destructors have unique characteristics. They have no return value, not even void, because their mission is to quietly complete the cleanup work without needing to return any results to the outside world. Their function name is prefixed with a tilde (~) to indicate that it is a destructor, distinguishing it from the constructor. For example, for a class named Student, its destructor would be ~Student(). Moreover, destructors cannot have parameters, meaning they cannot be overloaded; a class can only have one destructor. This is a design rule of C++, ensuring the uniqueness and determinism of destructors, allowing the compiler to accurately call them at the appropriate time.

2.1 Timing of Invocation

(1) Local Objects: When a local object leaves its scope, its destructor is automatically called. For example, an object defined within a function will have its destructor called when the function execution ends and control leaves that function, as shown in the following code:

void testFunction() {
    class LocalClass {
    public:
        ~LocalClass() {
            std::cout << "LocalClass's destructor is called" << std::endl;
        }
    };
    LocalClass obj;
}// When the program executes here, obj leaves its scope, and its destructor is called

In the testFunction, obj is a local object, and when the function ends, obj‘s destructor will automatically execute, outputting “LocalClass’s destructor is called”.

(2) Static Objects: Static objects include static local objects and static global objects, whose destructors are called when the program ends. Static local objects are initialized when the function first executes to their definition, and thereafter, during multiple calls to the function, they are not reinitialized. Only when the entire program ends are the destructors of static local and static global objects called. For example:

class StaticClass {
public:
    ~StaticClass() {
        std::cout << "StaticClass's destructor is called" << std::endl;
    }
};
static StaticClass globalStaticObj;
void anotherFunction() {
    static StaticClass localStaticObj;
}
int main() {
    anotherFunction();
    return 0;
}// At the end of the program, the destructors of localStaticObj and globalStaticObj are called

In this example, globalStaticObj is a static global object, and localStaticObj is a static local object; their destructors will be called when the main function ends and the program is about to exit.

(3) Global Objects: The timing of the destructor call for global objects is similar to that of static objects, also being called when the program ends. Global objects are created and initialized at program startup, existing throughout the program’s execution, and are not destroyed until the program ends, at which point their destructors are called. For example:

class GlobalClass {
public:
    ~GlobalClass() {
        std::cout << "GlobalClass's destructor is called" << std::endl;
    }
};
GlobalClass globalObj;
int main() {
    return 0;
}// At the end of the program, globalObj's destructor is called

In this code, globalObj is a global object, and when the main function returns, the program is about to end, globalObj‘s destructor will be called, outputting “GlobalClass’s destructor is called”.

(4) Dynamically Created Objects: Objects created dynamically using the new keyword need to be manually released using the delete keyword, and the destructor will be called when the memory is released. Failing to use delete to release dynamically allocated objects will lead to memory leaks, which is a critical issue in C++ programming. For example:

class DynamicClass {
public:
    ~DynamicClass() {
        std::cout << "DynamicClass's destructor is called" << std::endl;
    }
};
int main() {
    DynamicClass* ptr = new DynamicClass();
    delete ptr; // Calls DynamicClass's destructor
    return 0;
}

In the main function, we create a DynamicClass object using new and assign its pointer to ptr. When we use delete ptr, the destructor of the DynamicClass object will be called, outputting “DynamicClass’s destructor is called” and then releasing the memory occupied by that object. If we omit delete ptr, the memory occupied by this DynamicClass object will never be released, causing a memory leak.

2.2 Characteristics of Destructors

Destructors are a special type of member function in C++, primarily responsible for performing necessary cleanup work when an object is destroyed, such as releasing resources dynamically allocated during the object’s lifecycle, like memory, file handles, network connections, etc. Their definition is unique, with the function name prefixed by a tilde (~); for example, for the class MyClass, its destructor is ~MyClass().

Destructors have several notable characteristics: they have no parameters and no return type, as their purpose is purely to clean up resources related to the object, requiring no additional information and returning no data to the caller. Moreover, a class can only have one destructor; if the user does not explicitly define a destructor, the system will automatically generate a default destructor.

However, the default destructor typically does not perform any actual cleanup operations; it is merely an empty function body. Only when a class contains resources that need to be manually released does the user need to define a destructor. When an object’s lifecycle ends, such as when a local object leaves its scope, a dynamically allocated object is deleted, or a global or static object is terminated at program exit, the C++ compiler will automatically call the destructor. For example:

class Resource {
public:
    Resource() {
        data = new int[10];
        std::cout << "Resource constructed" << std::endl;
    }
    ~Resource() {
        delete[] data;
        std::cout << "Resource destructed" << std::endl;
    }
private:
    int* data;
};

int main() {
    {
        Resource res;
    }
    std::cout << "End of main" << std::endl;
    return 0;
}

In the code above, the destructor of the Resource class is responsible for releasing the integer array dynamically allocated in the constructor. In the main function, when the res object leaves its scope, the destructor will be automatically called, outputting “Resource destructed” before outputting “End of main.” This clearly demonstrates the characteristic of destructors executing cleanup work automatically when an object is destroyed.

2.3 Destruction Order of Static Objects

Static objects include global static objects and local static objects, and their destruction order follows specific rules. According to the C++ standard, the destruction order of global static objects is the reverse of their construction order. For example:

class GlobalStaticA {
public:
    GlobalStaticA() {
        std::cout << "GlobalStaticA constructed" << std::endl;
    }
    ~GlobalStaticA() {
        std::cout << "GlobalStaticA destructed" << std::endl;
    }
};

class GlobalStaticB {
public:
    GlobalStaticB() {
        std::cout << "GlobalStaticB constructed" << std::endl;
    }
    ~GlobalStaticB() {
        std::cout << "GlobalStaticB destructed" << std::endl;
    }
};

GlobalStaticA a;
GlobalStaticB b;

int main() {
    std::cout << "Inside main" << std::endl;
    return 0;
}

In this example, a and b are global static objects. When the program runs, a is constructed first, outputting “GlobalStaticA constructed,” followed by b, outputting “GlobalStaticB constructed.” When the program ends, b is destructed first, outputting “GlobalStaticB destructed,” followed by a, outputting “GlobalStaticA destructed.”

For local static objects, they are constructed when the program execution flow first reaches their definition, and destruction occurs at program exit, with the destruction order also being the reverse of the construction order. Let’s look at the following example:

class LocalStatic {
public:
    LocalStatic() {
        std::cout << "LocalStatic constructed" << std::endl;
    }
    ~LocalStatic() {
        std::cout << "LocalStatic destructed" << std::endl;
    }
};

void func() {
    static LocalStatic s;
}

int main() {
    func();
    std::cout << "Inside main" << std::endl;
    return 0;
}

In the func function, s is a local static object. When func is called for the first time, s is constructed, outputting “LocalStatic constructed.” When the program ends, s is destructed, outputting “LocalStatic destructed.” If there are multiple local static objects in the main function, their construction and destruction order will also follow the aforementioned rules. Understanding the destruction order of static objects is crucial for correctly managing resources, avoiding memory leaks, and ensuring program stability, especially in complex programs where dependencies between different static objects may affect the destruction order.

C. In-Depth Analysis of Static Object Destruction Order

3.1 Rules Within a Single File

In a simple scenario within a single file, the destruction order of static objects follows a clear and fixed rule: they are destructed in the reverse order of their construction. This is akin to building a tower of blocks, where the construction process is from the bottom up (construction order), while the dismantling occurs from the top down (destruction order).

Let’s visualize this with a specific code example:

#include <iostream>
class StaticObject {
public:
    StaticObject(const std::string&amp; name) : m_name(name) {
        std::cout << m_name << "'s constructor is called" << std::endl;
    }
    ~StaticObject() {
        std::cout << m_name << "'s destructor is called" << std::endl;
    }
private:
    std::string m_name;
};
StaticObject globalObj("Global Static Object");
void testFunction() {
    static StaticObject localStaticObj("Local Static Object");
}
int main() {
    testFunction();
    return 0;
}

In the code above, we first define a StaticObject class with a constructor and destructor that output the construction and destruction information of the object. Then, we define a global static object globalObj and a local static object localStaticObj within the testFunction.

When the program runs, the constructor of the global static object globalObj is called first, outputting “Global Static Object’s constructor is called.” Next, when testFunction is called, the constructor of the local static object localStaticObj is called, outputting “Local Static Object’s constructor is called.”

At program termination, the order of destruction is the reverse of the construction order. First, the destructor of the local static object localStaticObj is called, outputting “Local Static Object’s destructor is called,” followed by the destructor of the global static object globalObj, outputting “Global Static Object’s destructor is called.”

This simple example clearly illustrates that within a single file, static objects are strictly destructed in the reverse order of their construction, a rule established by C++ to ensure proper resource release and reasonable management of object lifecycles. This rule allows us to better predict and control program behavior, reducing errors caused by improper resource management.

3.2 Complexity in Multi-File Scenarios

When our project involves multiple files, the issue of static object destruction order becomes more complex. In multi-file scenarios, the destruction order of static objects in different files is undefined. This means we cannot precisely know which static object in which file will be destructed first or last. This uncertainty arises because the C++ standard does not explicitly specify the destruction order of static objects across multiple files; it mainly depends on the compiler’s implementation and the linker’s handling.

This undefined destruction order can lead to potential issues, the most common being resource dependency problems. For instance, suppose a static object obj1 is defined in file1.cpp, and its destructor needs to access a static object obj2 defined in file2.cpp. If obj2 is destructed before obj1, then when obj1 is destructed, it will attempt to access obj2, which has already been destroyed, leading to undefined behavior, potentially causing the program to crash or exhibit other hard-to-debug errors.

Here’s a simple multi-file example to illustrate this problem:

// file1.cpp
#include <iostream>
class Object1 {
public:
    ~Object1() {
        std::cout << "Object1 destructed, trying to access Object2" << std::endl;
        // Here, assume we need to access a member or method of Object2
        // Due to the undefined destruction order, Object2 may have already been destructed
    }
};
static Object1 obj1;
// file2.cpp
#include <iostream>
class Object2 {
public:
    ~Object2() {
        std::cout << "Object2 destructed" << std::endl;
    }
};
static Object2 obj2;

In this example, the destructor of Object1 hypothetically needs to access Object2. Due to the undefined destruction order of static objects across multiple files, Object2 may be destructed before Object1, leading to issues when Object1 attempts to access Object2. Such problems are often difficult to debug in real projects, as they may not reproduce every time, and the location of the error may be far from the actual root cause.

The undefined destruction order of static objects in multi-file scenarios presents certain challenges in programming, requiring us to be particularly careful in designing and implementing code to avoid resource dependency issues caused by improper destruction order.

D. Common Issues with Static Destruction

4.1 Issues Caused by Destruction Order

In C++ programming, the destruction order of static objects is often overlooked but can lead to serious program errors. For instance, in the open-source code of OceanBase, there is a segment of code:

oceanbase::sql::ObSQLSessionInfo &amp;session() {
    static oceanbase::sql::ObSQLSessionInfo SESSION;
    return SESSION;
}

ObArenaAllocator &amp;session_alloc() {
    static ObArenaAllocator SESSION_ALLOC;
    return SESSION_ALLOC;
}

int ObTableApiProcessorBase::init_session() {
    int ret = OB_SUCCESS;
    static const uint32_t sess_version = 0;
    static const uint32_t sess_id = 1;
    static const uint64_t proxy_sess_id = 1;
    if (OB_FAIL(session().test_init(sess_version, sess_id, proxy_sess_id, &amp;session_alloc()))) {
        LOG_WARN("init session failed", K(ret));
    }
    // more...
    return ret;
}

At system exit, this code may lead to a core dump issue. Using ASAN diagnostics, it was found that the static object SESSION accesses SESSION_ALLOC during its destruction, while SESSION_ALLOC is also a static object. Since the destruction rule for static variables in C++ is that the constructor is called first and then the destructor, if SESSION_ALLOC is destructed before SESSION, accessing SESSION during its destruction will lead to illegal memory access, as SESSION_ALLOC has already been destructed, and accessing related resources will cause errors, resulting in a core dump.

To better understand this, we can use a simple example program to verify this destruction order:

#include <iostream>
using namespace std;

class A {
public:
    A() { cout << "construct A" << endl; }
    ~A() { cout << "deconstruct A" << endl; }
    void init() {}
};

class B {
public:
    B() { cout << "construct B" << endl; }
    ~B() { cout << "deconstruct B" << endl; }
    void init(A &amp;a) { a.init(); }
};

A &amp;getA() {
    static A a;
    return a;
}

B &amp;getB() {
    static B b;
    return b;
}

void func() {
    getB().init(getA());
}

int main(int argc, const char *argv[]) {
    func();
    return 0;
}

Running the above program produces the following output:

construct A
construct B
deconstruct B
deconstruct A

From the output, it is clear that A is constructed first, followed by B, while during destruction, B is destructed first, followed by A, which fully complies with the rule of first constructed, last destructed. In actual projects, issues like those encountered by OceanBase due to improper destruction order of static objects often require a clear understanding of the dependency relationships between various static objects in the code and reasonable design and handling.

4.2 Memory Leak Issues

In C++, memory leaks are a common and tricky problem, and improper use of static member pointers can easily lead to memory leaks. When a static member pointer points to dynamically allocated memory but does not correctly release that memory during destruction, it results in that memory being unrecoverable, causing a memory leak. For example:

class Resource {
public:
    Resource() {
        data = new int[100];
    }
    ~Resource() {
        // Memory for data is not released here, leading to a memory leak
    }
private:
    static int* data;
};

int* Resource::data = nullptr;

int main() {
    Resource res;
    return 0;
}

In the code above, the data member of the Resource class is a static pointer, which allocates memory in the constructor but does not release it in the destructor. When the program ends, the memory pointed to by data will leak. The danger of memory leaks is that as the program runs longer, available memory gradually decreases, potentially leading to performance degradation and slower program response times. Ultimately, when available memory is exhausted, the program may crash, or even cause the entire system to fail.

To solve this problem, we need to correctly release the memory pointed to by static member pointers in the destructor. The modified code is as follows:

class Resource {
public:
    Resource() {
        data = new int[100];
    }
    ~Resource() {
        delete[] data;
        data = nullptr;
    }
private:
    static int* data;
};

int* Resource::data = nullptr;

int main() {
    Resource res;
    return 0;
}

In this way, when the Resource object is destructed, the memory pointed to by data will be correctly released, thus avoiding memory leak issues. Additionally, we can use smart pointers to manage static member pointers, further enhancing the safety and reliability of the code. For example:

#include <memory>

class Resource {
public:
    Resource() {
        data = std::make_unique<int[]>(100);
    }
    ~Resource() {
        // Smart pointers automatically release memory, no need for manual delete
    }
private:
    static std::unique_ptr<int[]> data;
};

std::unique_ptr<int[]> Resource::data = nullptr;

int main() {
    Resource res;
    return 0;
}

By using std::unique_ptr, memory management for data becomes safer and more convenient, eliminating the need for manual memory release, as the smart pointer will automatically release the memory it points to when its lifecycle ends.

In addition to std::unique_ptr, C++ also provides std::shared_ptr for scenarios where multiple pointers share the same resource, managing the resource’s lifecycle through reference counting, automatically releasing the resource when the reference count reaches zero. There is also std::weak_ptr, a weak reference smart pointer used to solve the circular reference problem of std::shared_ptr. Depending on different scenario needs, choosing and using smart pointers appropriately can significantly enhance the safety and reliability of the code.

4.3 Destruction Issues in Multithreaded Environments

In multithreaded environments, the destruction of static variables faces challenges of thread safety. For instance, in a crash case of the MNN inference engine on the iOS platform, there was a situation where a child thread accessed a static variable, and when the child thread crashed, the main thread called the _exit function. The exception information showed Exception Type: EXC_BAD_ACCESS, Exception Codes: KERN_INVALID_ADDRESS at 0x000095f59f17ce20, Exception Subtype: SIGSEGV, triggered by Thread: 28.

Theoretically, the C++11 standard has relevant specifications for the use of static variables in multithreaded environments. During the construction phase, if multiple threads attempt to initialize the same local static variable simultaneously, C++11 specifies that subsequent threads must wait for the thread currently initializing to complete. During the destruction phase, objects with thread lifecycles are destructed before static variables, and static variables are released in the reverse order of their construction.

Different compilers have varying implementations of these features. For example, GCC has supported thread-safe construction and destruction of static variables since version 4.3. During the construction phase, for local static variables, the thread constructing the static variable first locks, and other threads wait for the former to complete the lock operation. For global static variables, they are constructed in the main thread in declaration order, before the child threads start. During the destruction phase, the destructors of global and local static variables are called only after all threads have ended, ensuring thread safety during destruction. In contrast, the support for multithreaded construction and destruction in the Apple clang compiler is not very clear, and based on actual cases, it is partially supported.

In multithreaded environments, in addition to the thread safety issues during the construction and destruction phases, if multiple threads access static variables with write operations or certain asynchronous operation functions between these two phases, data races and inconsistencies may still occur. For example:

class SharedData {
public:
    static int value;
    static void increment() {
        ++value;
    }
};

int SharedData::value = 0;

void threadFunction() {
    for (int i = 0; i < 1000; ++i) {
        SharedData::increment();
    }
}

int main() {
    std::thread t1(threadFunction);
    std::thread t2(threadFunction);

    t1.join();
    t2.join();

    std::cout << "Final value: " << SharedData::value << std::endl;
    return 0;
}

In the code above, the value of the SharedData class is a static member variable, and the increment function modifies it. When multiple threads call the increment function simultaneously, without a synchronization mechanism, it leads to data races, and the final value of value may not be the expected 2000. To solve this problem, we can use mutexes and other synchronization mechanisms to ensure thread safety:

#include <mutex>

class SharedData {
public:
    static int value;
    static std::mutex mtx;
    static void increment() {
        std::lock_guard<std::mutex> lock(mtx);
        ++value;
    }
};

int SharedData::value = 0;
std::mutex SharedData::mtx;

void threadFunction() {
    for (int i = 0; i < 1000; ++i) {
        SharedData::increment();
    }
}

int main() {
    std::thread t1(threadFunction);
    std::thread t2(threadFunction);

    t1.join();
    t2.join();

    std::cout << "Final value: " << SharedData::value << std::endl;
    return 0;
}

By using std::lock_guard and std::mutex, we ensure that operations on value are thread-safe during write operations.

Additionally, thread-local storage (TLS) is also an effective way to handle data in multithreaded environments. By using the thread_local keyword to declare variables, each thread can have its own independent copy of the variable, avoiding data races between threads. For example:

#include <iostream>
#include <thread>

thread_local int threadLocalValue = 0;

void threadFunction() {
    ++threadLocalValue;
    std::cout << "Thread " << std::this_thread::get_id() << ": threadLocalValue = " << threadLocalValue << std::endl;
}

int main() {
    std::thread t1(threadFunction);
    std::thread t2(threadFunction);

    t1.join();
    t2.join();

    return 0;
}

In this example, threadLocalValue is a thread-local storage variable, and each thread has its own independent copy. Modifications to threadLocalValue in threadFunction only affect the current thread’s copy, avoiding competition issues in the multithreaded environment. When addressing static destruction issues in multithreaded environments, we must fully understand the relevant specifications of the C++11 standard and reasonably apply locking mechanisms and thread-local storage techniques to ensure the thread safety and correctness of the program.

E. Frequently Asked Interview Questions

Question 1: Which is destructed first, global static variables or local static variables?

Answer:Typically, local static variables are destructed first, followed by global static variables. Both are destructed when main ends or exit is called, with the destruction order being the reverse of the construction order; global static variables are constructed before main, while local static variables are constructed when the function first executes to their declaration, so generally, global static construction occurs earlier.

Question 2: How is the destruction order determined for multiple local static objects within the same function?

Answer:It is the reverse of the construction order. That is, the last constructed is the first destructed, following the “last in, first out” principle.

Question 3: In a program with multiple files, how is the destruction order of global static variables determined?

Answer:The destruction order is undefined. The construction order of global static variables in different files is compiler-dependent, and following the “first constructed, last destructed” principle, their destruction order is also undefined.

Question 4: When are the static member variables of a class destructed?

Answer:They are destructed in accordance with the program’s lifecycle, after the main function ends or exit is called. They need to be defined outside the class, and within the same source file, they are destructed in the reverse order of their definition; across different source files, the destruction order is not specified by the compiler.

Question 5: In the following code, what is the order of destructor calls?

class A {};  
class B {};  
A a;  
int main() {  
    B b;  
    static A a2;  
    return 0;  
}  

Answer:First, b is destructed (local objects are destructed at the end of the function), then a2 is destructed (local static is destructed after main ends), and finally a is destructed (global objects are destructed at the end of the program).

Question 6: If the function containing a local static object is not called, will its destructor execute?

Answer:No. Local static objects are constructed when the function is first called to their declaration, and if they are not constructed, their destructors will not be triggered.

Question 7: If a derived class object contains static member variables, will the destruction of the object affect the destruction of the static member variables?

Answer:No. The destruction of class static member variables only depends on whether the program ends, and is not directly related to the creation or destruction of class objects.

Question 8: What is the destruction order in the following program?

class A {};  
class B {};  
class C {};  
class D {};  
C c;  
int main() {  
    A a;  
    B b;  
    static D d;  
    return 0;  
}  

Answer:First, b is destructed, then a (local objects are destructed in the reverse order of their definition), followed by d (static objects are destructed at program end), and finally c (global objects are destructed at program end, c is constructed earlier than d).

Question 9: When using atexit to register cleanup functions, how does their execution order compare to static variable destruction?

Answer:Typically, static variable destruction occurs first.atexit registered functions are triggered after all static storage duration objects are destructed, by the exit function.

Question 10: What happens if an uncaught exception is thrown during static object destruction?

Answer:In C++, this will call std::terminate to end the program. The destruction of static objects is a critical phase of program termination, and uncaught exceptions should be avoided.

Leave a Comment