Introduction
In C++, every class contains six special member functions that the compiler generates automatically when they are not explicitly defined. These are called default member functions and form the foundation of object initialization and cleanup in C++.
- The Six Default Member Functions
A class that contains no explicit members is called an empty class. However, even an empty class is not truly empty—the compiler automatically generates the following six default member functions:
Constructor Destructor Copy Constructor Copy Assignment Operator Address-of Operators (const and non-const versions)
- Constructors
What is a Constructor?
A constructor is a special member function with the same name as the class. It is called automatically when an object is created to initialize the object's data members to appropriate values. The constructor executes exactly once during the object's lifetime.
Key characteristics:
Function name matches the class name No return type (not even void) Called automatically during object instantiation Can be overloaded with different parameter lists
class Person
{
public:
// Default constructor (no parameters)
Person()
{
_age = 0;
_name = "Unknown";
}
// Parameterized constructor
Person(const std::string& name, int age)
{
_name = name;
_age = age;
}
private:
std::string _name;
int _age;
};
int main()
{
Person p1;
Person p2("Alice", 25);
return 0;
}
Important: When creating an object using the default constructor, do not include parentheses after the object name, otherwise the compiler will interpret it as a function declaration.
Default Values in Constructors
Using default parameters allows a single constructor to handle multiple initialization scenarios:
class Person
{
public:
Person(const std::string& name = "Guest", int age = 18)
{
_name = name;
_age = age;
}
private:
std::string _name;
int _age;
};
This enables flexible object creation:
Person p1; // Uses all defaults: "Guest", 18
Person p2("Bob"); // Uses one default: "Bob", 18
Person p3("Carol", 30); // Uses no defaults
Compiler-Generated Constructors
When no constructor is explicitly defined, the compiler generates a default constructor. However, this automatically generated version has limitations:
It does not initialize built-in types (int, double, etc.)—values remain uninitialized It does initialize pointers to nullptr For user-defined types, it calls their respective default constructors
Best Practice: Always provide a constructor, especially when the class contains built-in type members.
- Destructors
What is a Destructor?
A destructor performs the opposite function of a constructor—it is called automatically when an object is destroyed to clean up resources that the object acquired during its lifetime.
Key characteristics:
Destructor name is the class name prefixed with tilde (~) No parameters, no return type Only one destructor per class (cannot be overloaded) Called automatically when an object goes out of scope Like constructors, destructors do not handle built-in type members
class ResourceHandler
{
public:
ResourceHandler()
{
_data = new int[100];
std::cout << "Memory allocated" << std::endl;
}
~ResourceHandler()
{
delete[] _data;
std::cout << "Memory freed" << std::endl;
}
private:
int* _data;
};
For classes that do not dynamically allocate resources (like simple data classes), the compiler-generated destructor is sufficient.
Note: Objects are destroyed in reverse order of their construction—members constructed last are destroyed first.
- Copy Constructors
Why Copy Constructors Matter
In C, passing a struct by value creates a shallow copy—all member values are copied exactly. However, this approach causes problems when the struct contains pointers to dynamically allocated memory:
Consider a Stack implementation where _array points to heap-allocated memory. When passing such an object by value, only the pointer address is copied, not the actual data. After the function call completes and the destructor runs, the same memory is freed twice, causing undefined behavior.
C++ solves this by requiring copy constructors for pass-by-value scenarios.
Copy Constructor Definition
A copy constructor takes a reference to another object of the same type and creates a new object as a copy:
class Person
{
public:
Person(const Person& other)
{
_name = other._name;
_age = other._age;
}
private:
std::string _name;
int _age;
};
Key Characteristics
Copy constructor is a special form of constructor Parameter must be a reference to the same type (const recommended) Pass-by-value parameter will cause compilation error—it creates infinite recursion If not explicitly defined, compiler generates a default copy constructor that performs shallow copying
Common Mistake: Using pass-by-value in copy constructor declaration
// INCORRECT - causes infinite recursion
Person(Person d) { }
// CORRECT - uses reference
Person(const Person& d) { }
Deep Copy vs Shallow Copy
When classes contain pointers to dynamically allocated memory, shallow copy creates duplicate pointers pointing to the same memory. A deep copy allocates new memory and copies the actual data:
class Buffer
{
public:
Buffer(const Buffer& other)
{
_size = other._size;
_data = new char[_size];
memcpy(_data, other._data, _size); // Deep copy
}
private:
char* _data;
size_t _size;
};
Additional Notes
Copy initialization (Person p3 = p1;) is equivalent to copy construction (Person p3(p1);) Pass-by-reference does not invoke the copy constructor Return-by-value creates a temporary object, which may invoke the copy constructor
- Assignment Operator Overloading
Operator Overloading Basics
Operator overloading allows using standard operators with custom types, imprvoing code readability:
class Percentage
{
public:
bool operator==(const Percentage& other) const
{
return _value == other._value;
}
private:
int _value;
};
Important rules for operator overloading:
Cannot create new operators—only existing operators can be overloaded At least one parameter must be a user-defined type Operators . :: sizeof ?: and .* cannot be overloaded When defined as class member functions, the implicit this parameter reduces the apparent number of operands by one
Assignment Operator Overloading
The assignment operator copies values from one existing object to another:
class Person
{
public:
Person& operator=(const Person& other)
{
if (this != &other)
{
_name = other._name;
_age = other._age;
}
return *this;
}
private:
std::string _name;
int _age;
};
Key points:
Differ from copy constructor: assignment works on existing objects, copy constructor creates new objects Returning *this enables chained assignment: p1 = p2 = p3; Self-assignment check prevents unnecessary work Compiler generates a default assignment operator that performs shallow copying
- Complete Example: A Working Date Class
The header file:
#pragma once
#include <iostream>
#include <cassert>
class Date
{
public:
Date(int year = 2024, int month = 1, int day = 1);
Date(const Date& other);
Date& operator=(const Date& other);
~Date();
Date& operator+=(int days);
Date operator+(int days) const;
Date& operator-=(int days);
Date operator-(int days) const;
Date& operator++();
Date operator++(int);
Date& operator--();
Date operator--(int);
bool operator>(const Date& other) const;
bool operator==(const Date& other) const;
bool operator>=(const Date& other) const;
bool operator<(const Date& other) const;
bool operator<=(const Date& other) const;
bool operator!=(const Date& other) const;
int operator-(const Date& other) const;
void Print() const;
private:
int _GetMonthDay(int year, int month) const;
int _year;
int _month;
int _day;
};
The implementation file:
#include "Date.h"
int Date::_GetMonthDay(int year, int month) const
{
static const int days[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0))
{
return 29;
}
return days[month];
}
Date::Date(int year, int month, int day)
: _year(year), _month(month), _day(day)
{
}
Date::Date(const Date& other)
: _year(other._year), _month(other._month), _day(other._day)
{
}
Date& Date::operator=(const Date& other)
{
if (this != &other)
{
_year = other._year;
_month = other._month;
_day = other._day;
}
return *this;
}
Date::~Date()
{
}
Date& Date::operator+=(int days)
{
if (days < 0)
{
return *this -= (-days);
}
_day += days;
while (_day > _GetMonthDay(_year, _month))
{
_day -= _GetMonthDay(_year, _month);
_month++;
if (_month > 12)
{
_month = 1;
_year++;
}
}
return *this;
}
Date Date::operator+(int days) const
{
Date temp = *this;
temp += days;
return temp;
}
Date& Date::operator-=(int days)
{
if (days < 0)
{
return *this += (-days);
}
_day -= days;
while (_day <= 0)
{
_month--;
if (_month < 1)
{
_month = 12;
_year--;
}
_day += _GetMonthDay(_year, _month);
}
return *this;
}
Date Date::operator-(int days) const
{
Date temp = *this;
temp -= days;
return temp;
}
Date& Date::operator++()
{
*this += 1;
return *this;
}
Date Date::operator++(int)
{
Date temp = *this;
*this += 1;
return temp;
}
Date& Date::operator--()
{
*this -= 1;
return *this;
}
Date Date::operator--(int)
{
Date temp = *this;
*this -= 1;
return temp;
}
bool Date::operator>(const Date& other) const
{
if (_year != other._year) return _year > other._year;
if (_month != other._month) return _month > other._month;
return _day > other._day;
}
bool Date::operator==(const Date& other) const
{
return _year == other._year &&
_month == other._month &&
_day == other._day;
}
bool Date::operator>=(const Date& other) const
{
return *this > other || *this == other;
}
bool Date::operator<(const Date& other) const
{
return !(*this >= other);
}
bool Date::operator<=(const Date& other) const
{
return !(*this > other);
}
bool Date::operator!=(const Date& other) const
{
return !(*this == other);
}
int Date::operator-(const Date& other) const
{
Date maxDate = *this;
Date minDate = other;
int direction = 1;
if (*this < other)
{
maxDate = other;
minDate = *this;
direction = -1;
}
int count = 0;
while (minDate != maxDate)
{
++minDate;
++count;
}
return count * direction;
}
void Date::Print() const
{
std::cout << _year << "-" << _month << "-" << _day << std::endl;
}
- const Member Functions
The const qualifier after a member functon declaration indicates that the functon will not modify any member variables:
class Date
{
public:
void Print() const
{
std::cout << _year << "-" << _month << "-" << _day << std::endl;
}
private:
int _year;
int _month;
int _day;
};
Effectively, the compiler treats this as:
void Print(const Date* this)
Why const matters:
const objects can only call const member functions Non-const objects can call both const and non-const member functions Functions that do not modify state should be marked const for flexibility
If a const object attempts to call a non-const member function, compilation fails due to const-correctness violations.
- Address-of Operators
C++ provides two versions of the address-of operator:
class Date
{
public:
Date* operator&()
{
return this;
}
const Date* operator&() const
{
return this;
}
private:
int _year;
int _month;
int _day;
};
These operators are rarely overridden in practice. The compiler-generated versions work correctly for most scenarios. Override only when specific behavior is required.
Additional Observations
Temporary objects are const—they cannot be modified const and non-const versions of the same function can coexist through function overloading When both versions exist, the compiler selects the most appropriate one based on the context All member functions in a logically related group should have consistent const qualifiers