From WeChat Official Account: Program Cat Master
This article summarizes what I believe to be the five most dangerous “self-destructive” operations in C++ development, each of which can cause your program to crash or exhibit hard-to-debug strange behaviors.
These pitfalls are derived from painful lessons learned, and understanding and avoiding them will greatly enhance your code quality and development efficiency.
1. Improper Memory Management: The Traps of Manual new/delete
Self-destructive Behavior:
void memory_management_mistakes() {
// Case 1: Memory leak
int* leak = new int(100);
// Forget to delete leak
// Case 2: Double free
int* double_free = new int(200);
delete double_free;
delete double_free; // Crash!
// Case 3: Dangling pointer
int* dangling = new int(300);
delete dangling;
*dangling = 400; // Undefined behavior!
}
Why It’s Dangerous:
- Memory leaks can gradually exhaust system resources
- Double freeing can cause the program to crash immediately
- Dangling pointers can lead to data corruption or security vulnerabilities
- These issues are difficult to trace and debug in complex programs
Solutions:
- Prefer using smart pointers:
<span>std::unique_ptr</span>,<span>std::shared_ptr</span> - Use container classes instead of raw arrays:
<span>std::vector</span>,<span>std::array</span> - Follow the RAII (Resource Acquisition Is Initialization) principle
- Use memory detection tools: Valgrind, AddressSanitizer
2. Undefined Behavior (UB): Compiler’s Playground
Self-destructive Behavior:
void undefined_behavior_examples() {
// Case 1: Signed integer overflow
int i = INT_MAX;
i++; // Undefined behavior
// Case 2: Array out-of-bounds access
int arr[5] = {1, 2, 3, 4, 5};
int val = arr[5]; // Out-of-bounds access
// Case 3: Dereferencing a null pointer
int* ptr = nullptr;
*ptr = 42; // Crash!
}
Why It’s Dangerous:
- The compiler does not need to issue warnings for UB
- Can lead to crashes, incorrect results, or seemingly normal behavior
- Behavior may vary with compiler, optimization level, or platform
- Difficult to debug and reproduce
Solutions:
- Understand the list of undefined behaviors in the C++ standard
- Use static analysis tools: Clang-Tidy, Cppcheck
- Enable compiler warnings:
<span>-Wall -Wextra -pedantic</span> - Use safe containers instead of raw arrays and pointers
3. Object Slicing: The Invisible Killer of Polymorphism
Self-destructive Behavior:
class Animal {
public:
virtual void speak() { cout << "Animal sound" << endl; }
};
class Dog : public Animal {
public:
void speak() override { cout << "Woof!" << endl; }
void fetch() { cout << "Fetching..." << endl; }
};
void object_slicing() {
Dog dog;
Animal animal = dog; // Object slicing occurs!
animal.speak(); // Outputs "Animal sound" instead of "Woof!"
}
Why It’s Dangerous:
- Derived class-specific data and methods are “sliced off”
- Polymorphic behavior fails, calling the base class method instead of the derived class method
- No warnings at compile time, runtime behavior is incorrect
- Difficult to discover in large codebases
Solutions:
- Always pass polymorphic objects by pointer or reference
- Make the base class an abstract class (with pure virtual functions)
4. Exception Safety Issues: Time Bombs of Resource Leaks
Self-destructive Behavior:
void unsafe_resource_management() {
FILE* file = fopen("data.txt", "r");
if (!file) return;
int* buffer = new int[100];
process_file(file); // May throw an exception
delete[] buffer;
fclose(file); // If the above throws an exception, these two lines won't execute
}
Why It’s Dangerous:
- Resources cannot be properly released when exceptions are thrown
- Objects may be in an inconsistent state
- Leads to memory leaks, file handle leaks, and other resource issues
- Difficult to ensure all cases are handled in complex transactions
Solutions:
- Follow the RAII principle to manage all resources
- Use smart pointers to manage dynamic memory
- Implement “basic guarantee”: the program is in a valid state after an exception occurs
- Use the copy-and-swap idiom to achieve strong exception safety guarantees
- Mark functions that will not throw exceptions as
<span>noexcept</span>
5. Static Initialization Order Fiasco: Dependencies Across Compilation Units
Self-destructive Behavior:
// file1.cpp
int init_helper() { return 42; }
int global_var = init_helper(); // Dynamic initialization
// file2.cpp
extern int global_var;
struct Initializer {
Initializer() {
std::cout << global_var; // Could be 0 or 42!
}
} initializer; // Static initialization order is uncertain
Why It’s Dangerous:
- The initialization order of global variables across different compilation units is undefined
- Can lead to reading uninitialized values
- Issues are environment-dependent and difficult to reproduce
- Extremely difficult to debug in large projects
Solutions:
- Avoid using global variables with non-trivial initialization
- Use the “Construct On First Use” idiom:
int& get_global() {
static int instance = 42; // Thread-safe initialization (C++11)
return instance;
}
How to Avoid These Self-destructive Operations
- Embrace RAII: Let the object’s lifecycle automatically manage resources
- Understand Language Rules: Especially undefined behavior and object models
- Use Modern C++ Features: Smart pointers, range-based for, lambdas, etc.
- Avoid Raw Pointers and Manual Memory Management: Prefer standard library containers and smart pointers
- Write Exception-Safe Code: Ensure exceptions do not lead to resource leaks or inconsistent states
—END—