Hello! Welcome to your next lesson in C++ foundations.
In our last session, we took direct control of memory using new and delete. We saw how heap allocation gives us the flexibility to create objects whose lifetimes aren't tied to a specific scope, a necessity for a data-gathering telemetry engine. However, this power came with a great responsibility: for every new, we had to remember a matching delete. Forgetting to do so results in memory leaks, a critical failure for a long-running high-performance application.
Today, we'll learn the canonical C++ way to solve this problem. You will implement classes with constructors and destructors to manage object lifecycle. These special member functions are the foundation for automating resource management, making your code safer, more robust, and easier to reason about. This lesson introduces you to one of the most important design patterns in C++, RAII (Resource Acquisition Is Initialization), which is the key to writing leak-free, exception-safe code.
The Problem: Uninitialized Objects
Let's start with a simple question. When you declare an object of your class, what are the initial values of its member variables?
class TelemetryEvent {
public:
uint64_t timestamp;
uint32_t event_id;
};
int main() {
TelemetryEvent event;
std::cout << event.event_id << std::endl; // What does this print?
}
The answer is: whatever garbage was in that memory location. This is a source of countless bugs. A common but clumsy solution is to create a manual init() function that you must remember to call after creating every object. This is not ideal; it's just another thing to forget.
The following video introduces this problem and presents the C++ solution: the constructor.
The video 'Constructors in C++' from The Cherno's series clearly demonstrates the problem of uninitialized member variables and contrasts the manual init() method with the automatic, safer approach of using a constructor.
Please watch from the beginning to 2:55. Pay close attention to the example of uninitialized variables and how a constructor provides a robust way to ensure an object is always in a valid state upon creation.
Constructors: The Birth of an Object
As you just saw, a constructor is a special member function that is automatically called the moment an object of a class is created. Its primary job is to initialize the object, ensuring it starts its life in a well-defined state.
A constructor has two unique properties:
- Its name is exactly the same as the class name.
- It has no return type, not even
void.
Let's look at the syntax for a default constructor (one that takes no arguments) and a parameterized constructor.
Let's return to the same video to see how constructors are defined and used, and how they work with both stack and heap allocations.
Watch the following clips: 2:55 - 3:33: This shows the basic syntax of a constructor. 4:16 - 4:55: This explains how to create parameterized constructors to initialize an object with specific values. 4:55 - 5:16: This short segment confirms that constructors are called automatically, even for heap-allocated objects created with new, connecting this lesson to our previous one.
Here's an example applied to our TelemetryEvent:
class TelemetryEvent {
public:
uint64_t timestamp;
uint32_t event_id;
// Default constructor
TelemetryEvent() {
timestamp = 0;
event_id = 0;
std::cout << "Default TelemetryEvent constructed.\n";
}
// Parameterized constructor
TelemetryEvent(uint64_t ts, uint32_t id) {
timestamp = ts;
event_id = id;
std::cout << "Parameterized TelemetryEvent constructed.\n";
}
};
int main() {
TelemetryEvent event1; // Calls default constructor
TelemetryEvent event2(12345, 101); // Calls parameterized constructor
// Also works for heap allocation
TelemetryEvent* event_ptr = new TelemetryEvent(54321, 202); // Calls parameterized constructor
delete event_ptr; // We'll get to what happens here soon!
return 0;
}
The Best Practice: Member Initializer Lists
In the example above, we are assigning values to members inside the constructor's body. C++ provides a better, more efficient, and sometimes necessary way to do this: the member initializer list.
An initializer list is a sequence of member-to-value initializations that you write between the constructor's parameter list and its body.
// Using a member initializer list
TelemetryEvent(uint64_t ts, uint32_t id) : timestamp(ts), event_id(id) {
// Constructor body can be empty or used for other logic.
std::cout << "Parameterized TelemetryEvent constructed via initializer list.\n";
}
This is not just a stylistic choice. It is fundamentally different:
- Initialization vs. Assignment: The initializer list initializes the members directly. Assignment in the body first default-constructs the members and then assigns new values, which can be less efficient.
- Necessity: Some members, like
constvariables or reference variables, cannot be assigned to. They must be initialized in the member initializer list.
This next video explains the syntax, benefits, and rules of member initializer lists.
C++ object lifecycle 101: the essentials
The video 'C++ object lifecycle 101' explains member initializer lists with great clarity, covering the syntax, the order of initialization, and the efficiency gains.
Watch the segment from 2:07 to 4:21. Focus on the distinction between initialization and assignment, and the rule that initialization order follows the declaration order in the class, not the order in the list.
To reinforce why initializer lists are sometimes mandatory, the following text provides a clear example with const members.
Understanding C++ Constructors, Destructors and Some ...
The article 'Understanding C++ Constructors, Destructors and Some...' provides a concise explanation of why initializer lists are required for certain types of member variables.
Read the sections titled 'The Problem with Const Member Variables', 'Understanding Object Creation Flow', and 'The Solution: Member Initializer List'. This will solidify your understanding of why this feature is essential, not just optional.
Destructors: The End of an Object's Life
If a constructor is called at an object's birth, what is called at its death? The destructor.
A destructor is a special member function that is automatically called just before an object is destroyed. Its purpose is to release any resources the object acquired during its lifetime (e.g., heap memory, file handles, network sockets).
A destructor has these properties:
- Its name is the class name, prefixed with a tilde (
~). ForTelemetryEvent, it's~TelemetryEvent(). - It takes no arguments and has no return type.
- A class can only have one destructor.
When is a destructor called?
- For a stack object, when it goes out of scope.
- For a heap object, when
deleteis called on its pointer.
C++ object lifecycle 101: the essentials
Let's return to 'C++ object lifecycle 101' to learn about destructors.
Watch the segment from 6:16 to 8:07. This will define what a destructor is, its syntax, and its role in releasing resources.
RAII: Automating Resource Management
Now, we can connect everything. We have constructors to acquire resources and destructors to release them. The C++ idiom that combines these is called RAII (Resource Acquisition Is Initialization).
The idea is to bind the lifetime of a resource to the lifetime of an object.
- Acquire the resource in the constructor (e.g.,
newmemory). - Release the resource in the destructor (e.g.,
deletememory).
Because the compiler guarantees that the destructor is called whenever the object is destroyed, resource leaks become impossible! The resource is managed automatically.
Let's see this powerful principle in action. In the previous lesson, you saw how to manage an array on the heap with new[] and delete[]. The following article shows how to wrap that raw allocation inside a class to make it completely safe.
Understanding Memory Management, Part 2: C++ and RAII
The article 'Understanding Memory Management, Part 2: C++ and RAII' demonstrates RAII with a practical example, showing how a class with a constructor and destructor can eliminate memory leaks.
Read the section titled 'RAII'. It presents a Data class that encapsulates a dynamically allocated array. Notice how the error-prone manual cleanup is replaced by the automatic call to the destructor when the data object goes out of scope.
This is the core of modern C++ memory management. Instead of dealing with raw pointers and manual delete calls, we create small, focused classes that manage a single resource.
Test your understanding!
For your telemetry engine, you'll frequently need to measure how long a block of code takes to execute. This is a perfect use case for RAII.
Create a simple class called ScopedTimer that does the following:
- In its constructor, it should store the current time and print a message like "Timer started...".
- In its destructor, it should calculate and print the time elapsed since the constructor was called.
You can use <chrono> for time measurements. Here's a starting point:
#include <iostream>
#include <chrono>
class ScopedTimer {
private:
std::chrono::time_point<std::chrono::high_resolution_clock> start_time;
public:
// TODO: Implement the constructor
ScopedTimer() {
// Your code here
}
// TODO: Implement the destructor
~ScopedTimer() {
// Your code here
}
};
void some_long_operation() {
// Simulate work by sleeping for 500 milliseconds
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
int main() {
{
ScopedTimer timer; // Timer starts when this object is created
some_long_operation();
} // Timer automatically stops and reports time when 'timer' goes out of scope
return 0;
}
Show answer
#include <iostream>
#include <chrono>
#include <thread> // for std::this_thread::sleep_for
class ScopedTimer {
private:
std::chrono::time_point<std::chrono::high_resolution_clock> start_time;
public:
ScopedTimer() {
start_time = std::chrono::high_resolution_clock::now();
std::cout << "Timer started...\n";
}
~ScopedTimer() {
auto end_time = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);
std::cout << "Timer finished. Elapsed time: " << duration.count() << "ms\n";
}
};
void some_long_operation() {
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
int main() {
std::cout << "Entering scope...\n";
{
ScopedTimer timer;
some_long_operation();
}
std::cout << "Exited scope.\n";
return 0;
}
Notice how you don't need to manually start or stop the timer. The object's lifetime does all the work for you. This is the power of RAII.
Conclusion
Today you've learned about the fundamental mechanisms that govern an object's life in C++. By using constructors and destructors, you can ensure your objects are always properly initialized and cleaned up, laying the foundation for writing robust, high-performance C++ code.
Key Takeaways:
- Constructors are special functions that initialize an object at creation. They ensure every object starts in a valid state.
- Member Initializer Lists are the preferred way to initialize member variables in a constructor. They are more efficient and are required for
constand reference members. - Destructors are special functions that clean up an object's resources just before it's destroyed.
- RAII (Resource Acquisition Is Initialization) is a core C++ pattern where you bind the lifetime of a resource (like heap memory) to the lifetime of an object. This automates cleanup and prevents leaks.
We have seen how to properly create and destroy a single object that manages a resource. But what happens if you try to copy such an object? If our Data class from the article was copied, both the original and the copy would have pointers to the same memory, leading to a "double free" error when their destructors run.
In our next lesson, "Implement the Rule of Five," we will tackle this problem head-on. You will learn about special constructors and assignment operators that allow you to define what it means to copy and move your resource-managing objects, giving you complete control over the entire object lifecycle.