Skip to content

Constructors_destructors

Constructors and destructors are special member functions that manage object creation and destruction. Understanding them is crucial for proper resource management in C++.

A constructor is called when an object is created. It initializes the object’s state.

class Person {
private:
std::string name;
int age;
public:
// Constructor - same name as class, no return type
Person(const std::string& n, int a) {
name = n;
age = a;
}
};
int main() {
Person p("Alice", 30); // Constructor is called
}

Constructor with no parameters:

class Rectangle {
double width, height;
public:
// Default constructor
Rectangle() {
width = 1;
height = 1;
}
};
Rectangle r1; // Uses default constructor
Rectangle r2(); // Function declaration!
Rectangle r3{}; // Preferred C++11 syntax

Constructor with parameters:

class Rectangle {
double width, height;
public:
Rectangle(double w, double h) {
width = w;
height = h;
}
};
Rectangle r(5, 3);

Creates a new object as a copy of an existing one:

class Rectangle {
double width, height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
// Copy constructor
Rectangle(const Rectangle& other) {
width = other.width;
height = other.height;
}
};
Rectangle r1(5, 3);
Rectangle r2(r1); // Copy constructor
Rectangle r3 = r1; // Also calls copy constructor

Transfers ownership of resources:

class Buffer {
char* data;
size_t size;
public:
Buffer(size_t s) : size(s) {
data = new char[size];
}
// Move constructor
Buffer(Buffer&& other) noexcept {
data = other.data;
size = other.size;
other.data = nullptr;
other.size = 0;
}
~Buffer() { delete[] data; }
};

Multiple constructors with different parameters:

class Rectangle {
double width, height;
public:
// Default constructor
Rectangle() : width(1), height(1) {}
// Parameterized constructor
Rectangle(double w, double h) : width(w), height(h) {}
// Square constructor
Rectangle(double side) : width(side), height(side) {}
};
Rectangle r1; // Default: 1x1
Rectangle r2(5, 3); // 5x3
Rectangle r3(4); // 4x4 (square)

The preferred way to initialize members:

class Person {
private:
const std::string name; // Must be initialized
int age;
double salary;
public:
// Using initializer list
Person(const std::string& n, int a, double s)
: name(n), age(a), salary(s) {
// Constructor body can be empty
}
};
// Bad way (assignment, not initialization)
Person(const std::string& n, int a, double s) {
name = n; // Error! const member
age = a;
salary = s;
}

Why use initializer lists?

  1. Required for const and reference members
  2. More efficient (avoids extra assignment)
  3. Required for base classes and member objects without default constructors

One constructor calls another:

class Rectangle {
double width, height;
public:
Rectangle() : Rectangle(1, 1) {} // Delegate to parameterized
Rectangle(double w, double h) : width(w), height(h) {}
Rectangle(double side) : Rectangle(side, side) {} // Square
};

A destructor is called when an object is destroyed. It cleans up resources:

class Resource {
public:
Resource() { std::cout << "Acquired\n"; }
~Resource() { std::cout << "Released\n"; }
};
int main() {
Resource r; // Constructor called
std::cout << "Doing work\n";
return 0; // Destructor called when r goes out of scope
}

Output:

Acquired
Doing work
Released
  1. Local variable goes out of scope
  2. Object is deleted with delete
  3. Program ends (for global/static objects)

Always make base class destructors virtual when using polymorphism:

class Base {
public:
virtual ~Base() { std::cout << "Base destructor\n"; }
};
class Derived : public Base {
public:
~Derived() { std::cout << "Derived destructor\n"; }
};
int main() {
Base* ptr = new Derived();
delete ptr; // Both destructors called
}

Without virtual destructor:

Base destructor // Only base destructor called!

With virtual destructor:

Derived destructor
Base destructor

If you define any of these, define all three:

  1. Destructor
  2. Copy constructor
  3. Copy assignment operator
class MyClass {
int* data;
public:
MyClass() : data(new int[100]) {}
// Destructor
~MyClass() { delete[] data; }
// Copy constructor
MyClass(const MyClass& other) : data(new int[100]) {
std::copy(other.data, other.data + 100, data);
}
// Copy assignment
MyClass& operator=(const MyClass& other) {
if (this != &other) {
delete[] data;
data = new int[100];
std::copy(other.data, other.data + 100, data);
}
return *this;
}
};

Add move constructor and move assignment:

class MyClass {
int* data;
public:
MyClass() : data(new int[100]) {}
// Destructor
~MyClass() { delete[] data; }
// Copy constructor
MyClass(const MyClass& other) : data(new int[100]) {
std::copy(other.data, other.data + 100, data);
}
// Copy assignment
MyClass& operator=(const MyClass& other) {
if (this != &other) {
delete[] data;
data = new int[100];
std::copy(other.data, other.data + 100, data);
}
return *this;
}
// Move constructor
MyClass(MyClass&& other) noexcept : data(other.data) {
other.data = nullptr;
}
// Move assignment
MyClass& operator=(MyClass&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
other.data = nullptr;
}
return *this;
}
};

Prefer not defining any - use RAII classes (like smart pointers):

class MyClass {
std::vector<int> data; // Manages its own memory
std::unique_ptr<Resource> resource;
public:
// Compiler-generated special member functions work!
};

Prevent implicit conversions with explicit:

class MyInt {
int value;
public:
explicit MyInt(int v) : value(v) {}
};
void printInt(MyInt m) {
std::cout << m.getValue() << std::endl;
}
int main() {
printInt(42); // Error: explicit prevents implicit
printInt(MyInt(42)); // OK: explicit call
}
#include <iostream>
#include <string>
#include <vector>
class Person {
private:
std::string name;
int age;
std::vector<std::string> hobbies;
public:
// Default constructor
Person() : name("Unknown"), age(0) {
std::cout << "Default constructor\n";
}
// Parameterized constructor
Person(const std::string& n, int a)
: name(n), age(a) {
std::cout << "Parameterized constructor: " << name << "\n";
}
// Copy constructor
Person(const Person& other)
: name(other.name), age(other.age), hobbies(other.hobbies) {
std::cout << "Copy constructor: " << name << "\n";
}
// Move constructor
Person(Person&& other) noexcept
: name(std::move(other.name)), age(other.age), hobbies(std::move(other.hobbies)) {
std::cout << "Move constructor: " << name << "\n";
}
// Destructor
~Person() {
std::cout << "Destructor: " << name << "\n";
}
// Copy assignment
Person& operator=(const Person& other) {
if (this != &other) {
name = other.name;
age = other.age;
hobbies = other.hobbies;
}
return *this;
}
void addHobby(const std::string& hobby) {
hobbies.push_back(hobby);
}
void display() const {
std::cout << "Name: " << name << ", Age: " << age;
std::cout << ", Hobbies: ";
for (const auto& h : hobbies) std::cout << h << " ";
std::cout << "\n";
}
};
int main() {
std::cout << "=== Creating p1 ===\n";
Person p1("Alice", 25);
p1.addHobby("Reading");
std::cout << "\n=== Copying p1 to p2 ===\n";
Person p2 = p1; // Copy constructor
std::cout << "\n=== Creating p3 ===\n";
Person p3("Bob", 30);
std::cout << "\n=== Moving p3 to p4 ===\n";
Person p4 = std::move(p3);
std::cout << "\n=== End of main ===\n";
return 0;
}

Output:

=== Creating p1 ===
Parameterized constructor: Alice
=== Copying p1 to p2 ===
Copy constructor: Alice
=== Creating p3 ===
Parameterized constructor: Bob
=== Moving p3 to p4 ===
Move constructor: Bob
=== End of main ===
Destructor: Bob
Destructor: Alice
Destructor: Alice
  • Constructors initialize objects when they’re created
  • Destructors clean up resources when objects are destroyed
  • Types: default, parameterized, copy, move constructors
  • Use initializer lists for efficient and proper initialization
  • Make base class destructors virtual for polymorphic deletion
  • Follow the Rule of Three/Five/Zero
  • Use explicit to prevent unintended implicit conversions

Now let’s learn about inheritance, one of the pillars of OOP.

Next Chapter: 10_inheritance.md - Inheritance and Composition