Learn C++ Constructors and Destructors from scratch. Covers types, syntax, memory management, virtual destructors, and real interview questions. Perfect for beginners.
If you have ever wondered how a C++ object gets its initial values the moment it is created, or how memory gets cleaned up the moment an object dies you are about to understand exactly that. Constructors and destructors are the two most fundamental concepts in C++ object-oriented programming, and honestly, once you really get them, a huge chunk of C++ starts making sense.
This guide covers everything: basic syntax, all types of constructors, destructor behavior, memory management, advanced topics like virtual destructors, move semantics, Rule of 3 and Rule of 5, RAII, smart pointers, and interview questions. Whether you are just starting out or preparing for a senior C++ interview, stick around this is the only article you will need.
Let’s get into it.
1. What is a Constructor in C++?
A constructor is a special member function of a class that gets called automatically whenever you create an object of that class. Think of it like a setup function that runs by itself you never have to call it explicitly.
Its main job is to initialize the data members of the object to valid starting values. Without a constructor, your object’s variables would hold garbage values (whatever junk was sitting in memory at that address), and that leads to unpredictable bugs.
Here is a simple mental model: imagine you buy a new notebook. The moment it comes out of the box, it already has your name written on the first page and the date stamped on the cover. Nobody asked you to do that manually the manufacturer set it up. That’s a constructor.
#include <iostream>
using namespace std;
class Student {
public:
string name;
int age;
// This is the constructor
Student() {
name = "Unknown";
age = 0;
cout <<"Constructor called!" <<endl;
}
};
int main() {
Student s1; // Constructor is called automatically here
cout << s1.name << ", " << s1.age << endl;
return 0;
}
Output:
Constructor called!
Unknown, 0
Notice : you did not write s1.Student() anywhere. It just ran on its own when the object was created.
2. What is a Destructor in C++?
A destructor is the opposite side of the coin. It is also a special member function, but it runs automatically when an object is destroyed — when it goes out of scope, or when you use delete on a dynamically allocated object.
The destructor’s job is cleanup: releasing dynamically allocated memory, closing file handles, releasing network connections, or anything else that needs to be undone when the object’s life ends.
Back to the notebook analogy: when you are done with the notebook and throw it away, someone shreds your personal information inside. You didn’t manually ask for that — it just happens as part of disposal. That’s a destructor.
#include <iostream>
using namespace std;
class Student {
public:
Student() {
cout << "Constructor called — object is being created." << endl;
}
~Student() { // The tilde ~ marks a destructor
cout << "Destructor called — object is being destroyed." << endl;
}
};
int main() {
Student s1; // Constructor called here
cout << "Inside main..." << endl;
// Destructor called automatically when s1 goes out of scope
return 0;
}
Output:
Constructor called — object is being created.
Inside main...
Destructor called — object is being destroyed.
This automatic call behavior is what makes C++ so powerful for resource management — and also what trips up beginners when they don’t understand the object lifecycle.
3. Why Do We Need Constructors and Destructors?
Need of Constructor
- Initialization guarantee: Without a constructor, data members start with garbage values. Constructors ensure objects are always in a valid state from the start.
- Automatic execution: You cannot forget to call a constructor. It always runs when an object is created.
- Encapsulation: You can enforce rules (like “age must be positive”) inside a constructor and reject bad input.
- Resource acquisition: Constructors are the right place to open files, allocate memory, or initialize hardware.
Need of Destructor
- Prevent memory leaks: If you allocate memory with
newinside a constructor, you need the destructor to calldelete. Otherwise that memory leaks. - Resource release: File handles, database connections, mutex locks — all need to be released when an object is done.
- Automatic and reliable: C++ guarantees the destructor is called when scope ends, even if an exception is thrown. This is the entire foundation of RAII, which we’ll cover later.
In languages like Python or Java, garbage collectors handle some of this automatically. C++ gives you direct control — more power, more responsibility. The constructor and destructor pair is how you exercise that responsibility correctly.
4. Basic Syntax of Constructor and Destructor
Constructor Syntax Rules
- Same name as the class
- No return type — not even
void - Can be overloaded (multiple constructors with different parameters)
- Can have default arguments
class ClassName {
public:
ClassName() { // Default constructor
// initialization
}
ClassName(int x) { // Parameterized constructor
// initialization using x
}
ClassName(const ClassName& obj) { // Copy constructor
// copy from obj
}
};
Destructor Syntax Rules
- Same name as the class, preceded by a tilde
~ - No return type
- No parameters — cannot be overloaded
- Only one destructor per class
class ClassName {
public:
~ClassName() {
// cleanup code
}
};
5. Characteristics of Constructor and Destructor
Characteristics of Constructor
- Automatically called when an object is created
- Can be overloaded — multiple constructors allowed
- Can use default arguments
- Can call other constructors (delegating constructors in C++11)
- Can be inline, explicit, or private
- Cannot be virtual (but derived class constructors can be called through base class pointers via virtual functions)
- Cannot have a return type
- Inherited classes get their own constructors — base class constructor is not inherited directly
Characteristics of Destructor
- Automatically called when an object goes out of scope or is deleted
- Cannot be overloaded — only one destructor per class
- Takes no arguments
- Can and should be virtual in base classes used with polymorphism
- Cannot be static
- Called in reverse order of construction in case of multiple objects
- Can throw exceptions only within the destructor body (but it is strongly discouraged)
6. Constructor vs Normal Function | Constructor vs Destructor
Constructor vs Normal Member Function
| Feature | Constructor | Normal Function |
|---|---|---|
| Name | Must match class name | Any valid identifier |
| Return type | None | Must have one (even void) |
| Called by | Compiler automatically | Programmer explicitly |
| Purpose | Initialize object | Any operation |
| Overloading | Yes | Yes |
| virtual keyword | Cannot be virtual | Can be virtual |
Constructor vs Destructor : Basic Difference
| Feature | Constructor | Destructor |
|---|---|---|
| Purpose | Initialize object | Clean up object |
| Called when | Object is created | Object is destroyed |
| Parameters | Can have parameters | Cannot have parameters |
| Overloading | Allowed | Not allowed |
| Count per class | Multiple allowed | Only one |
| Prefix | None | Tilde (~) |
| Virtual keyword | Cannot be virtual | Can (and should) be virtual |
| Execution order | Base → Derived | Derived → Base |
7. Types of Constructors in C++
7.1 Default Constructor
A default constructor takes no arguments. If you don’t write any constructor, the compiler generates one automatically but it won’t initialize primitive types like int or double to any specific value. Writing your own default constructor is almost always the right call.
class Box {
public:
int length, width, height;
Box() { // Default constructor
length = 1;
width = 1;
height = 1;
}
};
int main() {
Box b; // Calls default constructor
return 0;
}
7.2 Parameterized Constructor
This takes arguments so you can initialize an object with specific values at the time of creation. Much more flexible than a default constructor.
class Box {
public:
int length, width, height;
Box(int l, int w, int h) { // Parameterized constructor
length = l;
width = w;
height = h;
}
};
int main() {
Box b1(10, 5, 3);
Box b2(7, 7, 7);
return 0;
}
7.3 Copy Constructor
A copy constructor creates a new object as a copy of an existing one. It takes a reference to an object of the same class. If you don’t write one, the compiler gives you a default copy constructor that copies all members one-by-one — which is fine for simple types but causes serious problems with dynamically allocated memory (shallow copy issue).
class Box {
public:
int length;
Box(int l) { length = l; }
Box(const Box& obj) { // Copy constructor
length = obj.length;
cout << "Copy constructor called" << endl;
}
};
int main() {
Box b1(10);
Box b2 = b1; // Copy constructor called here
Box b3(b1); // Same -> copy constructor called
return 0;
}
Copy constructors are triggered in three situations: when initializing a new object from an existing one, when passing an object by value to a function, and when returning an object by value from a function.
7.4 Dynamic Constructor
A dynamic constructor allocates memory on the heap using new inside the constructor body. This is used when the size of data is not known at compile time.
class DynamicArray {
int* arr;
int size;
public:
DynamicArray(int n) {
size = n;
arr = new int[n]; // Dynamic allocation inside constructor
for (int i = 0; i < n; i++) arr[i] = 0;
}
~DynamicArray() {
delete[] arr; // Must release in destructor
}
};
7.5 Constructor with Default Arguments
You can give constructor parameters default values, making them optional. This lets one constructor serve the role of both a default and parameterized constructor.
class Box {
public:
int length, width;
Box(int l = 1, int w = 1) { // Default arguments
length = l;
width = w;
}
};
int main() {
Box b1; // Uses defaults: 1, 1
Box b2(5); // l=5, w=1
Box b3(5, 8); // l=5, w=8
return 0;
}
8. Intermediate Topics
8.1 Constructor Overloading
Just like regular function overloading, you can have multiple constructors in the same class as long as their parameter lists differ. The compiler picks the right one based on the arguments you pass.
class Rectangle {
int length, width;
public:
Rectangle() { length = width = 0; }
Rectangle(int l) { length = width = l; }
Rectangle(int l, int w) { length = l; width = w; }
};
8.2 Passing Objects to Constructor
You can pass an object of the same (or another) class as a parameter to a constructor. This is common when you want one object to initialize based on another.
class Point {
public:
int x, y;
Point(int a, int b) : x(a), y(b) {}
Point(const Point& p) : x(p.x), y(p.y) {}
};
class Line {
Point start, end;
public:
Line(Point s, Point e) : start(s), end(e) {}
};
8.3 Array of Objects with Constructor
When you create an array of objects, the default constructor is called for each element. If no default constructor exists, the compiler will give an error.
class Student {
public:
int roll;
Student() { roll = 0; } // Required for array creation
};
int main() {
Student arr[5]; // Default constructor called 5 times
return 0;
}
8.4 Order of Constructor and Destructor Execution
This is something that trips up a lot of programmers. The rule is simple: constructors are called in the order objects are created; destructors are called in reverse order.
class A {
public:
A() { cout << "A constructor" << endl; }
~A() { cout << "A destructor" << endl; }
};
class B {
public:
B() { cout << "B constructor" << endl; }
~B() { cout << "B destructor" << endl; }
};
int main() {
A a;
B b;
return 0;
}
Output:
A constructor
B constructor
B destructor
A destructor
Think of it like a stack — last in, first out. This LIFO order is guaranteed by the C++ standard.
8.5 Constructor in Structures
Unlike C, C++ structures (struct) are almost identical to classes — the only difference is that members are public by default. This means structures can also have constructors and destructors.
struct Point {
int x, y;
Point(int a, int b) : x(a), y(b) {} // Valid in C++
};
int main() {
Point p(3, 4);
cout << p.x << ", " << p.y << endl;
return 0;
}Constructor Declared Inside, Defined Outside
#include <iostream>
#include <string>
using namespace std;
class Laptop {
public:
string brand;
string processor;
int ram;
Laptop(string b, string p, int r); // Declaration only
};
// Definition outside using scope resolution operator ::
Laptop::Laptop(string b, string p, int r) {
brand = b;
processor = p;
ram = r;
}
int main() {
Laptop l1("Dell", "Intel i7", 16);
Laptop l2("Apple", "M2 Pro", 32);
Laptop l3("Lenovo", "Ryzen 9", 64);
cout << "Brand: " << l1.brand << " | CPU: " << l1.processor << " | RAM: " << l1.ram << "GB\n";
cout << "Brand: " << l2.brand << " | CPU: " << l2.processor << " | RAM: " << l2.ram << "GB\n";
cout << "Brand: " << l3.brand << " | CPU: " << l3.processor << " | RAM: " << l3.ram << "GB\n";
return 0;
}
Output:
Brand: Dell | CPU: Intel i7 | RAM: 16GB
Brand: Apple | CPU: M2 Pro | RAM: 32GB
Brand: Lenovo | CPU: Ryzen 9 | RAM: 64GB
With Initialization List
// Preferred way — use initialization list instead of assigning in body
Laptop::Laptop(string b, string p, int r)
: brand(b), processor(p), ram(r) {
// body is empty — members already initialized above
}
Same result, just more efficient and the professional standard in real codebases.
What Each Part Means
Laptop :: Laptop (string b, string p, int r)
| |
| └── Constructor name (same as class)
└── Class name (which class this belongs to)
:: = scope resolution operator ("belongs to")9. Memory and Resource Handling
9.1 Dynamic Memory Allocation — new and delete
new allocates memory on the heap at runtime and returns a pointer. delete frees it. This is different from stack memory, which is automatically managed.
int* p = new int(42); // Allocate single int
delete p; // Free it
int* arr = new int[10]; // Allocate array
delete[] arr; // Free array (note the brackets)
If you forget delete, that memory leaks — it’s gone, unavailable until your program exits.
9.2 Constructor with Dynamic Memory
class StringWrapper {
char* str;
public:
StringWrapper(const char* s) {
str = new char[strlen(s) + 1];
strcpy(str, s);
}
~StringWrapper() {
delete[] str; // Critical — must free what constructor allocated
}
};
9.3 Memory Leak Concept
A memory leak happens when your program allocates memory but never frees it. Over time, this eats up available RAM. In long-running applications — servers, embedded systems, games — even a small leak per request can eventually crash the system.
void leaky() {
int* p = new int(10);
// No delete p; -- this leaks every time leaky() is called
}
The constructor-destructor pair, used correctly, prevents leaks by design. If the constructor allocates, the destructor deallocates — always.
9.4 Dangling Pointer Issues
A dangling pointer points to memory that has already been freed. Accessing it is undefined behavior — your program might crash, output garbage, or seem to work fine (until it doesn’t).
int* p = new int(5);
delete p;
// p is now dangling — DO NOT dereference it
*p = 10; // Undefined behavior — crash or data corruption
// Fix: set pointer to nullptr after delete
delete p;
p = nullptr;
This issue is one of the biggest arguments for using smart pointers in modern C++, which we’ll cover in the Modern C++ section.
10. Advanced Constructor Concepts
10.1 Constructor Initialization List
The initialization list is a more efficient way to initialize member variables. It runs before the constructor body, directly constructing members rather than default-constructing then assigning.
class Box {
int length, width;
const int MAX; // const members MUST use initialization list
public:
Box(int l, int w) : length(l), width(w), MAX(100) {
// Constructor body
}
};
There are three situations where the initialization list is not just preferred — it’s required: const members, reference members, and base class constructors.
10.2 Delegating Constructors (C++11)
C++11 introduced the ability for one constructor to call another constructor of the same class. This reduces code duplication.
class Box {
int l, w, h;
public:
Box() : Box(1, 1, 1) {} // Delegates to parameterized constructor
Box(int l, int w, int h) : l(l), w(w), h(h) {
cout << "Box created: " << l << "x" << w << "x" << h << endl;
}
};
10.3 Explicit Constructor
By default, a single-argument constructor can be used for implicit conversion, which sometimes causes surprising bugs. The explicit keyword prevents this.
class Meters {
double value;
public:
explicit Meters(double v) : value(v) {}
};
void travel(Meters m) {}
int main() {
travel(5.0); // Error: implicit conversion blocked
travel(Meters(5.0)); // OK: explicit
return 0;
}
Use explicit on single-argument constructors unless you specifically want implicit conversion. This is a best practice that prevents hard-to-trace type errors.
10.4 Inline Constructor
When you define a constructor inside the class definition, it is implicitly inline — the compiler may expand the call at the call site rather than generating an actual function call, which improves performance for simple constructors.
class Point {
public:
int x, y;
Point(int a, int b) : x(a), y(b) {} // Inline by default
};
10.5 Static Members and Constructors
Static data members belong to the class, not to any object. They are initialized once, outside the class definition, and they are not initialized by the constructor.
class Counter {
static int count;
public:
Counter() { count++; }
~Counter() { count--; }
static int getCount() { return count; }
};
int Counter::count = 0; // Static member initialization (outside class)
int main() {
Counter a, b, c;
cout << Counter::getCount() << endl; // 3
return 0;
}
10.6 Private Constructor and the Singleton Design Pattern
Making a constructor private prevents external code from creating objects of that class. This is the foundation of the Singleton pattern — a design pattern where only one instance of a class can ever exist.
class Singleton {
static Singleton* instance;
Singleton() {} // Private constructor
public:
static Singleton* getInstance() {
if (!instance)
instance = new Singleton();
return instance;
}
};
Singleton* Singleton::instance = nullptr;
int main() {
Singleton* s1 = Singleton::getInstance();
Singleton* s2 = Singleton::getInstance();
cout << (s1 == s2) << endl; // 1 (true) — same object
return 0;
}
Singleton is used for things like configuration managers, logger objects, or database connection pools where having multiple instances would cause problems.
10.7 Copy Constructor : Deep Copy vs Shallow Copy
This is one of the most important concepts in C++ and a very common interview topic.
Shallow copy (compiler default): copies the pointer address. Both the original and the copy point to the same heap memory. When one is destroyed and frees the memory, the other has a dangling pointer.
Deep copy (user-defined): allocates new memory and copies the actual data. Each object owns its own independent copy.
class DeepCopy {
int* data;
public:
DeepCopy(int val) {
data = new int(val);
}
// Deep copy constructor
DeepCopy(const DeepCopy& obj) {
data = new int(*obj.data); // Allocate NEW memory, copy the value
}
~DeepCopy() {
delete data;
}
int getValue() { return *data; }
};
int main() {
DeepCopy d1(10);
DeepCopy d2 = d1; // Deep copy — d2 has its OWN memory
*d1.data = 99;
cout << d2.getValue() << endl; // Still 10 — not affected
return 0;
}
11. Advanced Destructor Concepts
11.1 Virtual Destructor
This is the most important destructor concept for anyone working with inheritance and polymorphism. If you have a base class pointer pointing to a derived class object, and you delete through that base pointer — without a virtual destructor, only the base class destructor runs. The derived class destructor is skipped, causing a memory leak.
class Base {
public:
Base() { cout << "Base constructor" << endl; }
virtual ~Base() { cout << "Base destructor" << endl; } // virtual!
};
class Derived : public Base {
int* data;
public:
Derived() {
data = new int(100);
cout << "Derived constructor" << endl;
}
~Derived() {
delete data;
cout << "Derived destructor" << endl;
}
};
int main() {
Base* ptr = new Derived();
delete ptr; // With virtual destructor: both destructors run correctly
return 0;
}
Rule of thumb: If a class has even one virtual function, give it a virtual destructor. This costs almost nothing in performance but prevents serious bugs.
11.2 Pure Virtual Destructor
You can declare a pure virtual destructor, making the class abstract. Unlike pure virtual functions, a pure virtual destructor must still be defined (because it will be called during object destruction).
class AbstractBase {
public:
virtual ~AbstractBase() = 0; // Pure virtual destructor
};
AbstractBase::~AbstractBase() {
// Must provide a definition
cout << "AbstractBase destructor" << endl;
}
11.3 Destructor in Inheritance
When a derived class object is destroyed, the derived class destructor runs first, followed by the base class destructor. This is always the case, regardless of virtual or not — the difference is whether both run when deleting through a base pointer.
class Animal {
public:
~Animal() { cout << "Animal destructor" << endl; }
};
class Dog : public Animal {
public:
~Dog() { cout << "Dog destructor" << endl; }
};
int main() {
Dog d; // Output on destruction: "Dog destructor" then "Animal destructor"
return 0;
}
11.4 Destructor in Polymorphism and Multiple Inheritance
In multiple inheritance, destructors are called in the reverse order of base class declaration in the derived class definition. Managing this correctly requires virtual destructors on every base class in the hierarchy.
class A { public: virtual ~A() { cout << "~A" << endl; } };
class B { public: virtual ~B() { cout << "~B" << endl; } };
class C : public A, public B {
public: ~C() { cout << "~C" << endl; } };
int main() {
C obj;
// Destruction order: ~C, ~B, ~A
return 0;
}
12. OOP Integration : Inheritance, Chaining, and Object Lifecycle
12.1 Constructor in Inheritance
When a derived class object is created, the base class constructor is always called first, then the derived class constructor. The derived class must explicitly call the base class constructor if it has parameters; otherwise the default base constructor is used.
class Vehicle {
int speed;
public:
Vehicle(int s) : speed(s) {
cout << "Vehicle constructor, speed: " << speed << endl;
}
};
class Car : public Vehicle {
string model;
public:
Car(int s, string m) : Vehicle(s), model(m) { // Calls base constructor
cout << "Car constructor, model: " << model << endl;
}
};
int main() {
Car c(120, "Tesla");
return 0;
}
Output:
Vehicle constructor, speed: 120
Car constructor, model: Tesla
12.2 Constructor Chaining
Constructor chaining is the process of one constructor calling another. In C++, this happens through delegating constructors (within the same class) or through the initialization list (calling the base class constructor).
12.3 Object Lifecycle in OOP
Every C++ object goes through a well-defined lifecycle:
- Memory allocation — stack or heap
- Constructor execution — base then derived
- Normal use — object is usable
- Destructor execution — derived then base
- Memory deallocation — stack auto, heap manual
Understanding this lifecycle is essential for writing correct C++ programs. Most resource management bugs happen when steps 4 or 5 are done incorrectly.
13. Special and Edge Cases
13.1 Can a Constructor be Virtual?
No. And the reason makes sense when you think about it: virtual functions need a vtable (virtual function table) to work, and the vtable is set up by the constructor. At the time the constructor is running, there is no vtable yet — so making a constructor virtual is a contradiction.
When creating a derived class object, you know the exact type at compile time, so dynamic dispatch is not needed anyway.
13.2 Can a Destructor be Static?
No. A destructor is always associated with a specific instance of the class. Static functions don’t operate on instances — they belong to the class as a whole. So a static destructor makes no conceptual sense, and the compiler will reject it.
13.3 Constructor Return Type Rules
Constructors have no return type — not void, not int, nothing. This is by design. A constructor’s job is to initialize an object that already exists (the memory was allocated before the constructor ran). Writing a return type is a syntax error.
13.4 Destructor with Exceptions
Throwing an exception from a destructor is almost always a terrible idea. If a destructor throws during stack unwinding (when another exception is already being handled), std::terminate() is called and your program crashes. Mark destructors noexcept and handle errors internally.
class Safe {
public:
~Safe() noexcept {
try {
// risky cleanup
} catch (...) {
// swallow — never let exception escape destructor
}
}
};
13.5 Default vs User-Defined Constructor
If you write no constructor at all, the compiler generates a default one. But the moment you write any constructor — including a parameterized one the compiler stops generating the default constructor. If you want both, you have to write both (or use = default).
class Box {
public:
Box(int l) {} // Parameterized constructor defined
// Box() {} // Now you NEED this explicitly if you want Box b;
Box() = default; // Or use this shorthand
};
14. Design and Best Practices
14.1 RAII : Resource Acquisition Is Initialization
RAII is arguably the most important programming idiom in C++. The idea is simple: tie the lifetime of a resource (memory, file handle, mutex lock, network socket) to the lifetime of an object. Acquire in the constructor, release in the destructor.
Because C++ guarantees destructors run when scope ends — even when exceptions are thrown — RAII gives you automatic, leak-proof resource management without garbage collection.
class FileHandler {
FILE* file;
public:
FileHandler(const char* name) {
file = fopen(name, "r");
if (!file) throw runtime_error("Cannot open file");
}
~FileHandler() {
if (file) fclose(file); // Always runs, even if exception thrown
}
// ... read methods
};
void processFile() {
FileHandler f("data.txt"); // File opened
// ... do work, throw exception, return early — doesn't matter
// Destructor ALWAYS closes file when f goes out of scope
}
RAII is the reason C++ programmers sleep well at night when managing resources. It’s the foundation behind smart pointers, mutex guards, and most of the standard library containers.
14.2 Rule of Three
If your class manages a resource (like heap memory) and you need to define one of these three: destructor, copy constructor, or copy assignment operator — then you almost certainly need to define all three. This is the Rule of Three.
- Destructor: Frees the resource
- Copy Constructor: Makes a deep copy when initializing from another object
- Copy Assignment Operator: Makes a deep copy when assigning from another object
class Buffer {
int* data;
int size;
public:
Buffer(int n) : size(n), data(new int[n]) {}
~Buffer() { delete[] data; } // 1. Destructor
Buffer(const Buffer& other) : size(other.size), data(new int[other.size]) {
copy(other.data, other.data + size, data); // 2. Copy constructor
}
Buffer& operator=(const Buffer& other) { // 3. Copy assignment
if (this != &other) {
delete[] data;
size = other.size;
data = new int[size];
copy(other.data, other.data + size, data);
}
return *this;
}
};
14.3 Rule of Five (Modern C++)
C++11 added move semantics. If your class defines any of the three (destructor, copy constructor, copy assignment), it likely also needs:
- Move Constructor: Transfers ownership of resources instead of copying
- Move Assignment Operator: Same for assignment
Together, these five form the Rule of Five.
Buffer(Buffer&& other) noexcept // Move constructor
: data(other.data), size(other.size) {
other.data = nullptr; // Leave moved-from object safe to destruct
other.size = 0;
}
Buffer& operator=(Buffer&& other) noexcept { // Move assignment
if (this != &other) {
delete[] data;
data = other.data;
size = other.size;
other.data = nullptr;
other.size = 0;
}
return *this;
}
14.4 Smart Pointers vs Manual Destructor Management
In modern C++, you should prefer smart pointers over raw new and delete. Smart pointers are RAII wrappers — their destructor automatically frees the memory.
unique_ptr— sole ownership, no copying, moved onlyshared_ptr— shared ownership, reference countedweak_ptr— non-owning reference to ashared_ptr
#include <memory>
void example() {
auto p = make_unique<int>(42); // Allocates
// No delete needed — destructs automatically when p goes out of scope
auto sp = make_shared<string>("hello");
auto sp2 = sp; // Both point to same string, ref count = 2
// Memory freed when LAST shared_ptr is destroyed
}
Smart pointers essentially eliminate entire classes of memory bugs. The Rule of Five matters less when you use smart pointers for all your resources.
What is a Getter?
A getter is a function that reads a private member variable and returns its value. You use it to safely access data that is hidden inside the class.
class Laptop {
private:
string brand; // private — cannot access directly outside
public:
string getBrand() { // Getter
return brand;
}
};
What is a Setter?
A setter is a function that writes or updates a private member variable. You can also add validation logic inside it.
class Laptop {
private:
int ram;
public:
void setRam(int r) { // Setter
if (r > 0)
ram = r; // Only set if valid value
else
cout << "Invalid RAM value!" << endl;
}
};
Why Not Just Make Members Public?
Great question. Compare these two:
// BAD — public member, no control
class Laptop {
public:
int ram;
};
int main() {
Laptop l;
l.ram = -999; // Nobody stops this — garbage value enters
}
// GOOD — private member with setter validation
class Laptop {
private:
int ram;
public:
void setRam(int r) {
if (r > 0) ram = r;
else cout << "RAM must be positive!" << endl;
}
};
int main() {
Laptop l;
l.setRam(-999); // Rejected — validation kicks in
}
Setters give you a gatekeeper between outside code and your data.
Full Example — Constructor + Getter + Setter Together
#include <iostream>
#include <string>
using namespace std;
class Laptop {
private:
string brand;
string processor;
int ram;
public:
// Constructor — sets initial values
Laptop(string b, string p, int r) {
brand = b;
processor = p;
setRam(r); // Using setter inside constructor for validation
}
// Getters — read private data
string getBrand() { return brand; }
string getProcessor() { return processor; }
int getRam() { return ram; }
// Setters — write private data with validation
void setBrand(string b) {
if (!b.empty())
brand = b;
else
cout << "Brand cannot be empty!" << endl;
}
void setProcessor(string p) {
processor = p;
}
void setRam(int r) {
if (r > 0)
ram = r;
else
cout << "Invalid RAM!" << endl;
}
};
int main() {
// Object created using constructor
Laptop l1("Dell", "Intel i7", 16);
// Reading values using getters
cout << "Brand : " << l1.getBrand() << endl;
cout << "Processor : " << l1.getProcessor() << endl;
cout << "RAM : " << l1.getRam() << "GB" << endl;
cout << "\n--- Updating values using setters ---\n" << endl;
// Updating values using setters
l1.setBrand("HP");
l1.setRam(32);
cout << "Brand : " << l1.getBrand() << endl;
cout << "RAM : " << l1.getRam() << "GB" << endl;
cout << "\n--- Testing validation ---\n" << endl;
l1.setRam(-8); // Invalid — rejected
l1.setBrand(""); // Invalid — rejected
return 0;
}
Output:
Brand : Dell
Processor : Intel i7
RAM : 16GB
--- Updating values using setters ---
Brand : HP
RAM : 32GB
--- Testing validation ---
Invalid RAM!
Brand cannot be empty!
Where Do They Each Belong in OOP?
| Concept | Topic | Purpose |
|---|---|---|
| Constructor | Constructors and Destructors | Initialize object when created |
| Destructor | Constructors and Destructors | Clean up when object is destroyed |
| Getter | Encapsulation | Read private data safely |
| Setter | Encapsulation | Write private data with control |
How They Work Together
Constructor → sets initial values when object is born
Getter → lets outside code READ private data
Setter → lets outside code UPDATE private data safely
Destructor → cleans up when object dies
Think of it like this — the constructor builds the house, getters let people look through the window, setters control who can enter and what they bring in, and the destructor demolishes the house when it’s no longer needed.
Move Constructor in C++
Before jumping into the move constructor itself, you need to understand why it exists because without that context, the syntax looks weird and pointless.
The Problem : Copying is Expensive
Imagine you have a class that holds a large chunk of heap memory. When you pass it around or return it from a function, C++ makes a full copy — allocates new memory, copies every single byte. That is slow and wasteful, especially when the original object is a temporary that is going to be thrown away immediately anyway.
#include <iostream>
using namespace std;
class BigData {
public:
int* data;
int size;
// Regular constructor
BigData(int n) {
size = n;
data = new int[n];
for (int i = 0; i < n; i++) data[i] = i;
cout << "Constructor called — memory allocated" << endl;
}
// Copy constructor — makes a full duplicate
BigData(const BigData& obj) {
size = obj.size;
data = new int[size]; // New allocation
for (int i = 0; i < size; i++)
data[i] = obj.data[i]; // Copy every element
cout << "Copy constructor — full copy made" << endl;
}
~BigData() {
delete[] data;
cout << "Destructor called" << endl;
}
};
Now when you do this:
BigData a(1000000); // 1 million integers allocated
BigData b = a; // Full copy — another 1 million integers copied
That copy of 1 million integers is expensive. And if a is a temporary object about to be destroyed anyway — why copy at all? Just steal its memory.
That is exactly what a move constructor does.
What is a Move Constructor?
A move constructor transfers ownership of resources from one object to another instead of copying them. The source object is left in a valid but empty state its pointer is set to nullptr so its destructor does not double-free the memory.
// Move constructor syntax
ClassName(ClassName&& obj) noexcept {
// steal the resources
// leave obj in safe empty state
}
The && is called an rvalue reference — it binds to temporary objects that are about to be destroyed.
lvalue vs rvalue Quick Explanation
This is the key concept behind move semantics.
int x = 10;
// x → lvalue — has a name, has an address, persists
// 10 → rvalue — temporary, no name, lives only in that expression
BigData a(100); // a is lvalue — has a name, persists
BigData b = a; // copy constructor — a still needed after this
BigData c = BigData(50);// BigData(50) is rvalue — temporary, dies immediately
// move constructor kicks in here
Full Example : Copy vs Move Side by Side
#include <iostream>
using namespace std;
class BigData {
public:
int* data;
int size;
// Regular constructor
BigData(int n) : size(n), data(new int[n]) {
for (int i = 0; i < n; i++) data[i] = i;
cout << "Constructor — allocated " << n << " integers" << endl;
}
// Copy constructor — deep copy
BigData(const BigData& obj) : size(obj.size), data(new int[obj.size]) {
for (int i = 0; i < size; i++)
data[i] = obj.data[i];
cout << "Copy Constructor — full copy made" << endl;
}
// Move constructor — steal resources
BigData(BigData&& obj) noexcept {
data = obj.data; // Steal the pointer
size = obj.size; // Steal the size
obj.data = nullptr; // Leave source empty — critical!
obj.size = 0;
cout << "Move Constructor — resources stolen, no copy" << endl;
}
// Destructor
~BigData() {
delete[] data; // Safe — nullptr delete does nothing
cout << "Destructor called" << endl;
}
void show() {
if (data)
cout << "First element: " << data[0] << " | Size: " << size << endl;
else
cout << "Object is empty (moved from)" << endl;
}
};
int main() {
cout << "--- Creating a ---" << endl;
BigData a(5);
cout << "\n--- Copy into b ---" << endl;
BigData b = a; // Copy constructor — a still valid
cout << "\n--- Move into c ---" << endl;
BigData c = move(a); // Move constructor — a is now empty
cout << "\n--- Checking state ---" << endl;
b.show(); // Fine — has its own copy
c.show(); // Fine — owns a's original data
a.show(); // Empty — resources were moved out
return 0;
}
Output:
--- Creating a ---
Constructor — allocated 5 integers
--- Copy into b ---
Copy Constructor — full copy made
--- Move into c ---
Move Constructor — resources stolen, no copy
--- Checking state ---
First element: 0 | Size: 5
First element: 0 | Size: 5
Object is empty (moved from)
Destructor called
Destructor called
Destructor called
What is std::move?
std::move does not actually move anything. It just casts an lvalue to an rvalue reference, telling the compiler:
“Treat this named object as a temporary — it’s okay to steal from it.”
BigData a(100);
BigData b = a; // Copy — a is still needed
BigData c = move(a); // Move — a is being given up intentionally
// After this: a.data == nullptr, a.size == 0
// Do NOT use a after moving from it
Copy vs Move : Performance Comparison
// Without move semantics
BigData createData() {
BigData temp(1000000);
return temp; // Copies 1 million ints — SLOW
}
// With move semantics
BigData createData() {
BigData temp(1000000);
return temp; // Compiler uses move — just pointer transfer — FAST
}
The compiler is smart enough to apply RVO (Return Value Optimization) and move semantics automatically in many cases. But writing the move constructor yourself ensures it works correctly when the compiler needs it.
Move Constructor Syntax Breakdown
BigData(BigData&& obj) noexcept
// | | |
// | | └── Promise: this won't throw an exception
// | └── rvalue reference — binds to temporaries
// └── Parameter type is the same class
{
data = obj.data; // Take ownership of the pointer
size = obj.size; // Take the size value
obj.data = nullptr; // MUST do this — prevent double delete
obj.size = 0; // Leave source in valid empty state
}
The noexcept is important — standard library containers like std::vector will only use your move constructor during reallocation if it is marked noexcept. Without it, they fall back to copying.
Where Move Constructor is Called Automatically
BigData a(10);
// 1. Explicit std::move
BigData b = move(a);
// 2. Returning a local object from a function
BigData createData() {
BigData temp(10);
return temp; // Move constructor (or RVO)
}
// 3. Passing a temporary
void process(BigData obj) {}
process(BigData(10)); // Temporary — move constructor used
// 4. Storing in containers
vector<BigData> v;
v.push_back(BigData(10)); // Temporary — move constructor used
Move Constructor vs Copy Constructor
| Feature | Copy Constructor | Move Constructor |
|---|---|---|
| Parameter | const ClassName& | ClassName&& |
| What it does | Allocates new memory, copies data | Steals pointer, sets source to null |
| Speed | Slow — O(n) data copy | Fast — O(1) pointer transfer |
| Source object after | Unchanged, fully valid | Empty but safe |
| When used | Copying a named object | Moving a temporary or std::move |
noexcept | Optional | Strongly recommended |
Rule of Five — Where Move Constructor Fits
If your class manages a resource, you need all five:
class BigData {
public:
BigData(int n); // 1. Regular constructor
~BigData(); // 2. Destructor
BigData(const BigData& obj); // 3. Copy constructor
BigData& operator=(const BigData& obj); // 4. Copy assignment
BigData(BigData&& obj) noexcept; // 5. Move constructor ← this one
BigData& operator=(BigData&& obj) noexcept; // 6. Move assignment
};
Move Assignment Operator — Bonus
Similar to move constructor but for assignment between existing objects:
BigData& operator=(BigData&& obj) noexcept {
if (this != &obj) {
delete[] data; // Free existing resource first
data = obj.data; // Steal
size = obj.size;
obj.data = nullptr; // Empty source
obj.size = 0;
}
return *this;
}
BigData a(10);
BigData b(20);
b = move(a); // Move assignment — not move constructor
// b already existed, so assignment operator runs
Real Life Analogy
Think of it like this:
Copy constructor — You have a USB drive with files. You plug in a new USB and copy all files to it. Both drives now have the same data. Takes time proportional to file size.
Move constructor — You just hand over the USB drive itself. No copying. Instant. The original drive is now empty — you gave it away.
Quick Rules to Remember
- Write a move constructor when your class owns heap memory
- Always set the source pointer to
nullptrafter stealing - Always mark it
noexcept— standard containers depend on it - After
std::move, treat the source object as empty — do not use it - Move constructor is called automatically on temporaries —
std::moveforces it on named objects - If you use smart pointers like
unique_ptr, the compiler generates a correct move constructor for free
15. Modern C++ Topics
15.1 Move Constructor and Move Semantics
Move semantics, introduced in C++11, allow you to transfer resources from a temporary (rvalue) object to a new object instead of copying them. This is dramatically more efficient when dealing with large data.
class BigData {
vector<int> data;
public:
BigData(vector<int>&& v) : data(move(v)) {} // Move constructor
};
int main() {
vector<int> v(1000000, 1);
BigData bd(move(v)); // v's data is MOVED, not copied — O(1) instead of O(n)
return 0;
}
Without move semantics, returning large objects from functions involved expensive copies. With move semantics, the compiler can transfer ownership of the internal buffer directly — no allocation, no copying.
15.2 = default and = delete
= default tells the compiler to generate the default implementation of a special member function. = delete prevents a function from being called — generating a compile error if someone tries to use it.
class NonCopyable {
public:
NonCopyable() = default; // Use compiler default
NonCopyable(const NonCopyable&) = delete; // Disable copying
NonCopyable& operator=(const NonCopyable&) = delete; // Disable copy assignment
NonCopyable(NonCopyable&&) = default; // Enable move
NonCopyable& operator=(NonCopyable&&) = default; // Enable move assign
};
This is cleaner and more expressive than the old trick of putting copy constructor in the private section. The = delete approach gives a clear compiler error message.
15.3 Smart Pointers in Depth
#include <memory>
#include <iostream>
using namespace std;
class Resource {
public:
Resource() { cout << "Resource acquired" << endl; }
~Resource() { cout << "Resource released" << endl; }
};
int main() {
// unique_ptr — single owner
{
unique_ptr<Resource> up = make_unique<Resource>();
// Destructor called automatically at end of block
}
// shared_ptr — multiple owners
{
shared_ptr<Resource> sp1 = make_shared<Resource>();
{
shared_ptr<Resource> sp2 = sp1; // ref count = 2
cout << "Count: " << sp1.use_count() << endl; // 2
} // sp2 destroyed, count = 1
cout << "Count: " << sp1.use_count() << endl; // 1
} // sp1 destroyed, count = 0 — Resource released
return 0;
}
15.4 Automatic Resource Management
The combination of RAII, smart pointers, and move semantics means modern C++ programs can manage resources as safely as garbage-collected languages — without the runtime overhead of a garbage collector. This is the “zero-cost abstraction” philosophy at work.
16. Interview Questions : Constructors and Destructors
Q1: What is the difference between a constructor and a destructor?
Constructor initializes an object when created. Destructor cleans up when the object is destroyed. Constructor can be overloaded; destructor cannot. Constructor has no prefix; destructor has ~. Constructor can take arguments; destructor cannot. Constructor cannot be virtual; destructor can and should be virtual in base classes.
Q2: What is the difference between a copy constructor and the assignment operator?
- Copy Constructor: Creates a new object as a copy. Called when a new object is initialized from an existing one. Signature:
ClassName(const ClassName& obj) - Assignment Operator: Copies data into an existing object. Called when
=is used between two already-existing objects. Signature:ClassName& operator=(const ClassName& obj)
Box b1(5);
Box b2 = b1; // Copy constructor (b2 is being created)
Box b3;
b3 = b1; // Assignment operator (b3 already exists)
Q3: Shallow Copy vs Deep Copy
- Shallow copy copies the pointer, not the data it points to. Both objects share the same memory. When one is destroyed, the other has a dangling pointer.
- Deep copy allocates new memory and copies the actual data. Objects are independent.
The compiler-generated copy constructor does a shallow copy. Write your own for deep copy when your class manages heap memory.
Q4: Why is a virtual destructor important?
When you have a base class pointer to a derived object and call delete, only the base destructor runs if it’s not virtual. This skips derived class cleanup and leaks memory. Making the base destructor virtual ensures both destructors run correctly via dynamic dispatch.
Q5: What is a copy constructor and when is it called?
A copy constructor creates a new object from an existing one. It is called in three situations:
- Object initialized from another:
Box b2 = b1; - Object passed by value to a function
- Object returned by value from a function
Q6: What is the Rule of Three? Rule of Five?
If your class defines a destructor, copy constructor, or copy assignment — define all three (Rule of Three). C++11 adds move constructor and move assignment — define all five (Rule of Five). Or, use smart pointers and follow the Rule of Zero: define none of them and let the compiler handle everything.
Q7: Output-Based Question — Constructor and Destructor Order
class A {
public:
A() { cout << "A()" << endl; }
~A() { cout << "~A()" << endl; }
};
class B : public A {
public:
B() { cout << "B()" << endl; }
~B() { cout << "~B()" << endl; }
};
int main() {
B obj;
return 0;
}
Output:
A()
B()
~B()
~A()
Constructor chain goes base → derived. Destructor chain goes derived → base.
Q8: What happens if you forget to write a virtual destructor?
class Base {
public:
~Base() { cout << "Base destructor" << endl; } // NOT virtual
};
class Derived : public Base {
int* data;
public:
Derived() { data = new int[100]; }
~Derived() {
delete[] data; // This NEVER RUNS if deleted via Base*
cout << "Derived destructor" << endl;
}
};
int main() {
Base* ptr = new Derived();
delete ptr; // Only Base destructor runs — memory leak!
return 0;
}
This is a classic memory leak. Fix: add virtual to Base’s destructor.
Q9: Can you call a constructor explicitly? What about a destructor?
You can call a constructor explicitly when using placement new (advanced topic — constructing an object at a specific memory address). You can technically call a destructor explicitly (like obj.~ClassName()), but it is almost never correct and usually leads to double-destruction bugs. With placement new it is necessary, but you should avoid this pattern unless you’re writing a memory allocator.
Q10: Real-Life Example — RAII in Action
Consider a mutex lock in a multithreaded program:
class LockGuard {
mutex& mtx;
public:
LockGuard(mutex& m) : mtx(m) { mtx.lock(); }
~LockGuard() { mtx.unlock(); } // Guaranteed to run — even if exception
};
void criticalSection(mutex& m) {
LockGuard guard(m); // Locked
// ... do work, throw exception, return early — doesn't matter
// Mutex is ALWAYS unlocked when guard goes out of scope
}
The standard library provides std::lock_guard which works exactly this way. This is RAII solving a real concurrency problem elegantly using constructors and destructors.
Q11: What is a Delegating Constructor and why use it?
Delegating constructors (C++11) let one constructor call another in the same class. Before C++11, shared initialization logic had to go into a private helper function. Now you can delegate directly, reducing duplication and keeping initialization in one place.
Q12: Debugging Constructor and Destructor Calls
Add print statements to constructors and destructors while learning. Count the calls: for every constructor call, there should be exactly one destructor call. If counts don’t match, you have a memory management bug. Tools like Valgrind, AddressSanitizer, or Microsoft’s CRT debug heap can catch these issues automatically.
// Quick debug trick
class Debug {
static int count;
public:
Debug() { count++; cout << "Created #" << count << endl; }
~Debug() { cout << "Destroyed, remaining: " << --count << endl; }
};
int Debug::count = 0;
Quick Reference Cheat Sheet
| Concept | Key Point |
|---|---|
| Default Constructor | No args; compiler generates if no constructor defined |
| Parameterized Constructor | Takes args; initialize with specific values |
| Copy Constructor | Takes const ClassName&; creates copy |
| Move Constructor | Takes ClassName&&; transfers ownership |
| Destructor | ~ClassName(); no args; no overloading |
| Virtual Destructor | Required when deleting derived via base pointer |
| Explicit Constructor | Prevents implicit type conversion |
| Delegating Constructor | One constructor calls another (C++11) |
| RAII | Acquire in constructor, release in destructor |
| Rule of Three | Destructor + copy ctor + copy assign |
| Rule of Five | Rule of Three + move ctor + move assign |
| = default | Compiler-generated special member function |
| = delete | Disable a function — compile error if called |
| unique_ptr | Sole ownership, no copy, auto-delete |
| shared_ptr | Shared ownership, reference counted |
| Deep vs Shallow Copy | Deep = new memory; Shallow = same pointer |
| Singleton Pattern | Private constructor + static instance |
| Constructor order | Base → Derived |
| Destructor order | Derived → Base (reverse of construction) |
Conclusion
If there is one thing to take away from this entire guide, it is this: constructors and destructors are the entry and exit points of every object’s life in C++. They are not just initialization helpers they are the mechanism through which C++ manages resources, enforces invariants, and gives you the power to write code that is both safe and efficient.
Start with understanding the basics default, parameterized, and copy constructors. Understand why destructors exist and how they pair with constructors for resource management. Then level up to virtual destructors, RAII, move semantics, and smart pointers. That progression mirrors how C++ itself evolved over the decades.
The real skill is not just knowing the syntax it is understanding when each concept is needed and why. Once that clicks, you will find yourself writing C++ code that is genuinely correct by construction, not just by luck.
If you are preparing for interviews, pay special attention to: virtual destructors, shallow vs deep copy, Rule of Three and Five, and RAII. Those topics come up constantly because they test whether a candidate truly understands C++ memory model not just the surface syntax.
Keep coding, keep breaking things in a sandbox, and the patterns will become second nature.
Found this guide helpful? Share it with someone learning C++. And if you have a specific constructor or destructor question that wasn’t covered here, drop it in the comments below.
For detailed understanding of Platform Devices and Drivers on Linux, refer to the Linux documentation on Platform Devices and Drivers .
Mr. Raj Kumar is a highly experienced Technical Content Engineer with 7 years of dedicated expertise in the intricate field of embedded systems. At Embedded Prep, Raj is at the forefront of creating and curating high-quality technical content designed to educate and empower aspiring and seasoned professionals in the embedded domain.
Throughout his career, Raj has honed a unique skill set that bridges the gap between deep technical understanding and effective communication. His work encompasses a wide range of educational materials, including in-depth tutorials, practical guides, course modules, and insightful articles focused on embedded hardware and software solutions. He possesses a strong grasp of embedded architectures, microcontrollers, real-time operating systems (RTOS), firmware development, and various communication protocols relevant to the embedded industry.
Raj is adept at collaborating closely with subject matter experts, engineers, and instructional designers to ensure the accuracy, completeness, and pedagogical effectiveness of the content. His meticulous attention to detail and commitment to clarity are instrumental in transforming complex embedded concepts into easily digestible and engaging learning experiences. At Embedded Prep, he plays a crucial role in building a robust knowledge base that helps learners master the complexities of embedded technologies.








