Constructors and Destructors
A constructor is called automatically when an object is created. A destructor is called automatically when the object goes out of scope or is deleted. Together, they enforce the RAII (Resource Acquisition Is Initialisation) pattern — the single most important C++ idiom for safe resource management.
Default Constructor
#include <iostream>
#include <string>
using namespace std;
class Point {
public:
double x, y;
// Default constructor — called when no arguments are given
Point() : x(0.0), y(0.0) {
cout << "Point default-constructed at (0, 0)\n";
}
};
int main() {
Point p; // Calls default constructor
cout << p.x << ", " << p.y << "\n"; // 0, 0
Point arr[3]; // Each element default-constructed
return 0;
}If you define no constructor at all, the compiler generates a default one for you. As soon as you define any constructor, the compiler's default is suppressed unless you explicitly request it with = default.
Parameterised Constructor
#include <iostream>
#include <string>
using namespace std;
class Person {
string name;
int age;
public:
// Parameterised constructor
Person(const string& name, int age) : name(name), age(age) {
cout << "Person created: " << this->name << "\n";
}
void greet() const {
cout << "Hi, I'm " << name << " (" << age << ")\n";
}
};
int main() {
Person alice("Alice", 30); // Direct initialisation
Person bob = Person("Bob", 25); // Also OK
alice.greet(); // Hi, I'm Alice (30)
bob.greet(); // Hi, I'm Bob (25)
return 0;
}Member Initialiser List
Always prefer the member initialiser list (after the : before {) over assignments in the constructor body:
#include <iostream>
#include <string>
using namespace std;
class Circle {
double radius;
string colour;
double area; // Derived — computed in init list
public:
// Initialiser list — members are constructed IN DECLARATION ORDER
Circle(double r, const string& c)
: radius(r),
colour(c),
area(3.14159265358979 * r * r) // Can use previously listed members
{
// Constructor body — for complex logic, input validation
if (radius < 0) throw invalid_argument("Radius cannot be negative");
}
void print() const {
cout << colour << " circle r=" << radius << " area=" << area << "\n";
}
};
int main() {
Circle c(5.0, "red");
c.print(); // red circle r=5 area=78.5398
return 0;
}Note: Members are initialised in the order they are declared in the class, not the order listed in the initialiser list. Always list them in declaration order to avoid subtle bugs.
Destructor
#include <iostream>
#include <string>
using namespace std;
class FileHandle {
string filename;
bool isOpen;
public:
FileHandle(const string& name) : filename(name), isOpen(true) {
cout << "Opening " << filename << "\n";
// In real code: open the file here
}
~FileHandle() { // Destructor — same name with ~
if (isOpen) {
cout << "Closing " << filename << "\n";
// In real code: close the file here
isOpen = false;
}
}
void write(const string& data) {
if (isOpen) cout << "Writing to " << filename << ": " << data << "\n";
}
};
int main() {
{
FileHandle f("data.txt"); // Constructor called
f.write("Hello");
} // Destructor called automatically here (scope ends)
cout << "Back in main — file was closed\n";
return 0;
}
// Output:
// Opening data.txt
// Writing to data.txt: Hello
// Closing data.txt
// Back in main — file was closedThe destructor fires automatically when the object leaves scope — this is RAII in action. You never need to manually call ~FileHandle().
Copy Constructor
#include <iostream>
#include <string>
using namespace std;
class Matrix {
int rows, cols;
int* data; // Raw pointer — requires custom copy
public:
Matrix(int r, int c) : rows(r), cols(c), data(new int[r * c]()) {
cout << "Matrix(" << r << "x" << c << ") constructed\n";
}
// Copy constructor — DEEP COPY (copy the data, not just the pointer)
Matrix(const Matrix& other) : rows(other.rows), cols(other.cols),
data(new int[other.rows * other.cols]) {
cout << "Matrix copied\n";
for (int i = 0; i < rows * cols; i++) {
data[i] = other.data[i];
}
}
~Matrix() {
delete[] data;
cout << "Matrix destroyed\n";
}
void set(int r, int c, int val) { data[r * cols + c] = val; }
int get(int r, int c) const { return data[r * cols + c]; }
};
int main() {
Matrix m1(2, 2);
m1.set(0, 0, 42);
Matrix m2 = m1; // Copy constructor called
cout << m2.get(0, 0) << "\n"; // 42 — deep copy
m1.set(0, 0, 99);
cout << m2.get(0, 0) << "\n"; // 42 — m2 is independent!
return 0;
}
// Matrix(2x2) constructed
// Matrix copied
// 42
// 42
// Matrix destroyed
// Matrix destroyedWithout a custom copy constructor, the compiler generates a shallow copy — both objects share the same pointer, leading to a double-free crash.
The Rule of Three / Five / Zero
Whenever your class manually manages a resource (memory, file, socket):
| Rule | What to define |
|---|---|
| Rule of Three (C++03) | Destructor + Copy Constructor + Copy Assignment Operator |
| Rule of Five (C++11) | + Move Constructor + Move Assignment Operator |
| Rule of Zero (best) | Let smart pointers manage resources — define no special functions |
class Resource {
int* data;
public:
// 1. Constructor
Resource(int n) : data(new int[n]) {}
// 2. Destructor
~Resource() { delete[] data; }
// 3. Copy constructor
Resource(const Resource& other) : data(new int[1]) {
data[0] = other.data[0];
}
// 4. Copy assignment
Resource& operator=(const Resource& other) {
if (this != &other) { // Protect against self-assignment
delete[] data;
data = new int[1];
data[0] = other.data[0];
}
return *this;
}
// 5. Move constructor (C++11)
Resource(Resource&& other) noexcept : data(other.data) {
other.data = nullptr; // Leave other in valid empty state
}
// 6. Move assignment (C++11)
Resource& operator=(Resource&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
other.data = nullptr;
}
return *this;
}
};Delegating Constructors (C++11)
One constructor can call another to avoid code duplication:
class Config {
string host;
int port;
bool useSSL;
public:
// Main constructor
Config(const string& host, int port, bool ssl)
: host(host), port(port), useSSL(ssl) {}
// Delegates to main — adds defaults
Config(const string& host) : Config(host, 443, true) {}
// Delegates to main — default everything
Config() : Config("localhost", 8080, false) {}
void print() const {
cout << (useSSL ? "https" : "http") << "://"
<< host << ":" << port << "\n";
}
};
int main() {
Config c1;
Config c2("api.example.com");
Config c3("db.local", 5432, false);
c1.print(); // http://localhost:8080
c2.print(); // https://api.example.com:443
c3.print(); // http://db.local:5432
return 0;
}Practical Example: RAII File Wrapper
#include <iostream>
#include <fstream>
#include <string>
#include <stdexcept>
using namespace std;
class SafeFile {
fstream stream;
string path;
bool writable;
public:
// Constructor — opens the file
SafeFile(const string& filePath, bool write = false)
: path(filePath), writable(write) {
ios::openmode mode = write
? (ios::out | ios::trunc)
: ios::in;
stream.open(filePath, mode);
if (!stream.is_open()) {
throw runtime_error("Cannot open: " + filePath);
}
cout << "Opened: " << path << "\n";
}
// Destructor — closes the file (RAII)
~SafeFile() {
if (stream.is_open()) {
stream.close();
cout << "Closed: " << path << "\n";
}
}
// Prevent copying (file handles should not be copied)
SafeFile(const SafeFile&) = delete;
SafeFile& operator=(const SafeFile&) = delete;
void writeLine(const string& line) {
if (!writable) throw logic_error("File not opened for writing");
stream << line << "\n";
}
bool readLine(string& line) {
return static_cast<bool>(getline(stream, line));
}
};
int main() {
try {
// Write phase
{
SafeFile writer("output.txt", true);
writer.writeLine("Hello from RAII!");
writer.writeLine("This file closes automatically.");
} // SafeFile destructor runs here
// Read phase
SafeFile reader("output.txt");
string line;
while (reader.readLine(line)) {
cout << "> " << line << "\n";
}
} catch (const exception& e) {
cerr << "Error: " << e.what() << "\n";
}
return 0;
}Summary
- Constructor runs automatically when an object is created — use the member initialiser list to initialise members before the body runs
- Destructor runs automatically when an object leaves scope — the RAII pattern guarantees cleanup without try/finally
- Provide a copy constructor and copy assignment whenever your class owns a raw pointer — otherwise the default shallow copy causes double-free bugs
- The Rule of Five: if you define any of destructor, copy constructor, copy assignment, move constructor, or move assignment — define all five
- The Rule of Zero: design classes to use smart pointers or standard containers, so you need no custom special functions
- Delegating constructors let one constructor call another — eliminates duplication in constructor overloads
- Use
= deleteto explicitly forbid copying (sockets, file handles, mutexes)
Next, we'll cover inheritance — building new classes from existing ones using C++ class hierarchies.