C++ for C Developers | Lesson 6: Constructors & Destructors: Object Lifecycle Management
Goal: Learn about constructors (special functions for initializing objects) and destructors (special functions for cleaning up when objects are destroyed). Understand their role in ensuring objects are created in a valid state and resources are released correctly (linking back to RAII).
1. The Problem: Initialization in C vs. C++
In C, after declaring a struct
variable, you often initialize its members manually or rely on aggregate initialization {}
. There’s no built-in mechanism to guarantee initialization or perform setup actions automatically when a variable is created. Similarly, cleanup requires explicit function calls.
// C Example (from Lesson 5)
PointC p1; // Members p1.x, p1.y have indeterminate values here!
p1.x = 3.0; // Manual initialization
p1.y = 4.0;
// ... use p1 ...
// No automatic cleanup
C++ aims to ensure objects are always created in a well-defined, usable state and are properly cleaned up afterward. This is achieved through constructors and destructors.
2. Constructors: Initializing Objects
A constructor is a special member function that is automatically called whenever an object of a class is created. Its primary job is to initialize the object’s data members and perform any necessary setup.
flowchart TD subgraph C [C Style] C1["Declare struct (e.g., PointC p1)"] C2["Members uninitialized"] C3["Manual init: p1.x = 3.0;"] C4["Manual cleanup if needed"] end subgraph CPP [C++ Style] CPP1["Declare object (e.g., Message msg)"] CPP2["Constructor called automatically"] CPP3["Members initialized"] CPP4["Destructor called automatically"] end C1 --> C2 --> C3 --> C4 CPP1 --> CPP2 --> CPP3 --> CPP4
Rules for Constructors:
- They have the exact same name as the class.
- They have no return type (not even
void
). - They can be overloaded (like regular functions – Lesson 3), allowing for different ways to create objects.
flowchart TD subgraph Constructor Flow Start["Object Created"] Start --> InitCheck["Is constructor defined?"] InitCheck -->|Yes| CallCtor["Call Constructor"] InitCheck -->|No| DefaultInit["Compiler Default Initialization"] CallCtor --> Setup["Member init / setup logic"] Setup --> Done["Object Ready"] DefaultInit --> Done end
Types of Constructors:
- Default Constructor: A constructor that takes no arguments. If you don’t define any constructors yourself, the compiler will try to generate a public default constructor for you (it performs default initialization for members, which might be zero-initialization for built-in types or calling default constructors for member objects). Important: If you define any constructor (e.g., a parameterized one), the compiler will not generate the default constructor automatically. You’ll need to define it yourself if you want one.
- Parameterized Constructor: A constructor that accepts arguments to initialize data members.
Example:
#include <iostream>
#include <string>
class Message {
private:
std::string m_text;
int m_priority;
bool m_sent;
public:
// 1. Default Constructor (User-Defined)
// If we didn't define ANY constructors, the compiler would make one.
// But since we define others below, we MUST define this if we want
// to be able to create a Message object like: Message msg1;
Message() {
std::cout << "Default Constructor called!" << std::endl;
m_text = "[No Text]";
m_priority = 0; // Default priority
m_sent = false;
}
// 2. Parameterized Constructor
Message(const std::string& text, int priority) {
std::cout << "Parameterized Constructor called!" << std::endl;
m_text = text; // Assigning value inside constructor body
m_priority = priority;
m_sent = false; // Still initialize all members
}
// Member function to display message info
void display() const {
std::cout << "Message: '" << m_text << "', Priority: " << m_priority
<< ", Sent: " << (m_sent ? "Yes" : "No") << std::endl;
}
void send() {
m_sent = true;
std::cout << "Sending message: '" << m_text << "'" << std::endl;
// ... actual sending logic ...
}
}; // End of class Message
int main() {
std::cout << "Creating msg1 (uses Default Constructor):" << std::endl;
Message msg1; // Calls Message()
msg1.display();
std::cout << "\nCreating msg2 (uses Parameterized Constructor):" << std::endl;
Message msg2("Meeting reminder", 5); // Calls Message(const std::string&, int)
msg2.display();
msg2.send();
msg2.display();
// Message msg3(); // Pitfall: This DECLARES A FUNCTION named msg3, it doesn't create an object!
// To default construct, use: Message msg3;
return 0; // msg1 and msg2 go out of scope here - destructors will be called
}
flowchart LR A["Constructor"] A --> B1["Default Constructor"] A --> B2["Parameterized Constructor"] A --> B3["Copy Constructor (Lesson 7)"] B1 --> C1["No arguments"] B2 --> C2["Takes arguments"] C1 --> D1["Created automatically if no other constructors"] C2 --> D2["Custom initialization"]
3. Constructor Initialization Lists
While assigning values inside the constructor body works (like m_text = text;
), the preferred C++ way to initialize members is using member initialization lists.
Syntax: Add a colon :
after the constructor’s parameter list, followed by a comma-separated list of member_name(initial_value)
.
flowchart TB A["Initialization Style"] --> B1["Member Init List"] A --> B2["Assignment in Constructor Body"] B1 --> C1["Direct Initialization"] C1 --> D1["Preferred for const & refs"] C1 --> D2["Efficient"] B2 --> C2["Default Init First"] C2 --> D3["Then Assignment"] D3 --> D4["Less efficient"]
Why use initialization lists?
- True Initialization: It initializes members directly; assignment in the body first default-initializes then assigns. For complex objects, initialization is often more efficient.
- Required for
const
and Reference Members:const
members and reference members cannot be assigned to; they must be initialized in the initialization list. - Required for Base Class Constructors: When using inheritance (Lesson 8), you use the initialization list to call the base class constructor.
- Initializes Members in Declaration Order: Members are initialized in the order they are declared in the class, regardless of their order in the initialization list (good practice to match declaration order in the list for clarity).
Example with Initialization List:
#include <iostream>
#include <string>
class Rectangle {
private:
const double m_length; // const member - MUST use init list
const double m_width; // const member - MUST use init list
std::string m_label;
public:
// Parameterized Constructor using Initialization List
Rectangle(double length, double width, const std::string& label)
: m_length(length), // Initialize m_length with length
m_width(width), // Initialize m_width with width
m_label(label) // Initialize m_label with label
{ // Constructor body can be empty or contain other setup code
std::cout << "Rectangle Constructor called for '" << m_label << "'" << std::endl;
// No assignments needed here, members are already initialized!
// Could add validation logic here if needed.
if (m_length <= 0 || m_width <= 0) {
std::cerr << "Warning: Rectangle '" << m_label << "' has non-positive dimensions!" << std::endl;
}
}
// Default Constructor
// Rectangle() : m_length(1.0), m_width(1.0), m_label("Default") {} // Possible default
double area() const {
return m_length * m_width;
}
void display() const {
std::cout << "Rectangle '" << m_label << "': "
<< m_length << " x " << m_width << ", Area: " << area() << std::endl;
}
};
int main() {
Rectangle r1(10.0, 5.0, "Box");
r1.display();
Rectangle r2(2.5, 4.0, "Panel");
r2.display();
return 0; // r1 and r2 go out of scope here
}
4. Destructors: Cleaning Up Objects
A destructor is a special member function that is automatically called just before an object is destroyed. This happens when:
- A stack-allocated object goes out of scope.
- A heap-allocated object is explicitly
delete
d.
flowchart TD subgraph Object Destruction Create["Object Created"] Create --> Use["Used in Scope"] Use --> EndScope["Scope Ends"] EndScope --> CallDtor["Destructor Called Automatically"] CallDtor --> Cleanup["Resources Released"] end
Its primary job is to release any resources the object acquired during its lifetime (e.g., dynamic memory, file handles, network connections). This is the heart of RAII (Lesson 4).
Rules for Destructors:
- They have the exact same name as the class, preceded by a tilde (
~
). - They have no return type (not even
void
). - They take no parameters.
- A class can only have one destructor.
- If you don’t provide one, the compiler generates a default one (which does nothing unless members have their own destructors).
Example Demonstrating RAII with Constructor/Destructor:
#include <iostream>
class DynamicArray {
private:
int* m_data;
size_t m_size;
public:
// Constructor: Acquires resource (dynamic memory)
DynamicArray(size_t size) : m_size(size) {
m_data = new int[m_size]; // Allocate memory
std::cout << "DynamicArray Constructor: Allocated memory for "
<< m_size << " integers at " << m_data << std::endl;
// Initialize array (optional)
for (size_t i = 0; i < m_size; ++i) {
m_data[i] = 0;
}
}
// Destructor: Releases resource (dynamic memory)
~DynamicArray() {
std::cout << "DynamicArray Destructor: Deallocating memory for "
<< m_size << " integers at " << m_data << std::endl;
delete[] m_data; // CRITICAL: Use delete[] because we used new[]
m_data = nullptr; // Good practice
m_size = 0;
}
// Prevent copying (rule of three/five/zero - simplified for now)
DynamicArray(const DynamicArray&) = delete;
DynamicArray& operator=(const DynamicArray&) = delete;
// Example method
void print() const {
std::cout << "Array contents (" << m_size << " elements): ";
for (size_t i = 0; i < m_size; ++i) {
std::cout << m_data[i] << " ";
}
std::cout << std::endl;
}
};
void testScope() {
std::cout << "--- Entering testScope ---" << std::endl;
DynamicArray arr(5); // Constructor called
arr.print();
// arr manages its own memory via RAII
std::cout << "--- Exiting testScope ---" << std::endl;
// arr goes out of scope HERE - Destructor is automatically called!
}
int main() {
std::cout << "*** Before testScope ***" << std::endl;
testScope();
std::cout << "*** After testScope ***" << std::endl;
std::cout << "\n*** Using new/delete ***" << std::endl;
DynamicArray* heap_arr = new DynamicArray(3); // Constructor called
heap_arr->print();
// Manual deletion required for heap objects
delete heap_arr; // Destructor called HERE
heap_arr = nullptr;
std::cout << "*** After delete ***" << std::endl;
// Note: This manual new/delete is exactly what smart pointers (Lesson 4) help avoid!
return 0;
}
Summary of Lesson 6:
- Constructors initialize objects automatically when they are created. They share the class name, have no return type, and can be overloaded. Use them to ensure objects start in a valid state.
- Initialization Lists (
: member(value)
) are the preferred way to initialize members in a constructor, especially forconst
members, reference members, and efficiency. - Destructors clean up objects automatically just before they are destroyed (
~ClassName
). They have no return type and take no parameters. - Destructors are essential for RAII, ensuring resources acquired by an object (like memory allocated with
new
) are released correctly (e.g., by callingdelete
ordelete[]
). - Constructors/Destructors manage the object lifecycle, making C++ code safer and more robust compared to manual C-style initialization and cleanup.
Next Lesson: We’ll explore one of the most powerful features of C++: the Standard Template Library (STL), starting with std::vector
, a dynamic array that manages its own memory, making code like our DynamicArray
example much simpler.
More Sources:
cppreference – Constructors: https://en.cppreference.com/w/cpp/language/constructor
cppreference – Destructors: https://en.cppreference.com/w/cpp/language/destructor