Default Member Functions
When designing a class in C++, if the programmer does not explicitly define certain essential member functions, the compiler will automatically generate them. These are known as default member functions. There are six primary default member functions that manage the lifecycle and operations of an object.
Constructors
A constructor is responsible for initializing an object upon its creation. It possesses the following characteristics:
- Its name is identical to the class name.
- It does not have a return type, not even
void. - It is automatically invoked when an object is instantiated.
- It supports function overloading.
Consider the following implementation demonstrating multiple constructors:
class CalendarDate
{
public:
// Parameterless constructor
CalendarDate()
{
year_ = 0;
month_ = 0;
day_ = 0;
}
// Parameterized constructor
CalendarDate(int y, int m, int d)
{
year_ = y;
month_ = m;
day_ = d;
}
// Fully defaulted constructor (commented out as it conflicts with the above)
// CalendarDate(int y = 1, int m = 1, int d = 1)
// {
// year_ = y;
// month_ = m;
// day_ = d;
// }
void Display()
{
std::cout << year_ << "/" << month_ << "/" << day_ << std::endl;
}
private:
int year_;
int month_;
int day_;
};
int main()
{
CalendarDate d1; // Invokes parameterless constructor
CalendarDate d2(2025, 1, 1); // Invokes parameterized constructor
// WARNING: Do not write CalendarDate d3();
// The compiler interprets this as a function declaration returning a CalendarDate, not an object instantiation.
d1.Display();
d2.Display();
return 0;
}
A key takeaway is that a fully defaulted constructor cannot coexist with a parameterless constructor because calling an object without arguments would be ambiguous. A "default constructor" refers to any constructor that can be called without providing arguments: this includes the parameterless constructor, the fully defaulted constructor, and the compiler-generated constructor. Only one default constructor can exist.
If no constructor is explicitly defined, the compiler generates a default one. This compiler-generated constructor applies specific rules: it leaves built-in types (like int, double, pointers) with uninitialized, indeterminate values, but for custom type members, it automatically invokes their respective default constructors.
class DynamicArray
{
public:
DynamicArray(int n = 4)
{
dataArr_ = (int*)malloc(sizeof(int) * n);
if (dataArr_ == nullptr)
{
return;
}
capacity_ = n;
size_ = 0;
}
private:
int* dataArr_;
size_t capacity_;
size_t size_;
};
class DualContainer
{
public:
// The compiler-generated constructor for DualContainer
// automatically calls the DynamicArray default constructor for both members.
private:
DynamicArray pushArr_;
DynamicArray popArr_;
};
int main()
{
DualContainer container1;
return 0;
}
In most scenarios, you must implement your own constructors. Relying on the compiler-generated version is only viable when the class exclusively contains custom types that already manage their own initialization.
Destructors
A destructor handles the cleanup and resource release of an object before its memory is reclaimed. While local objects are destroyed when they go out of scope, the destructor ensures any dynamically allocated resources (like heap memory) are freed properly. Its features include:
- The destructor name is the class name prefixed with a tilde (
~). - It takes no parameters and has no return type.
- There can be only one destructor per class. If not explicitly defined, the compiler generates a default one.
- It is automatically invoked when the object's lifetime ends.
~DynamicArray()
{
free(dataArr_);
dataArr_ = nullptr;
capacity_ = 0;
size_ = 0;
}
Similar to constructors, the compiler-generated destructor does nothing for built-in types. However, for custom type members, it will implicitly invoke that member's destructor. Even if you write a custom destructor, custom type members will still have their destructors called automatically.
class DualContainer
{
public:
// If we explicitly write an empty destructor:
~DualContainer() {}
// The destructors for pushArr_ and popArr_ are still automatically invoked.
private:
DynamicArray pushArr_;
DynamicArray popArr_;
};
If a class allocates no resources (like CalendarDate) or its custom members handle their own cleanup (like DualContainer), an explicit destructor is unnecessary. However, classes managing dynamic memory must define a destructor to prevent memory leaks. Furthermore, when multiple local objects are destroyed, C++ mandates they are destructed in the reverse order of their construction (Last-In, First-Out).
Copy Constructors
A copy constructor initializes a new object as a replica of an existing one. It is a special constructor whose first parameter is a reference to the class type itself, and any additional parameters must have default values.
- It is an overloaded variant of the constructor.
- The first parameter must be a reference. Passing by value will cause a compilation error because passing by value requires copying the object, which would recursively trigger the copy constructor, leading to infinite recursion.
class CalendarDate
{
public:
CalendarDate(int y = 1, int m = 1, int d = 1) : year_(y), month_(m), day_(d) {}
// Copy Constructor
CalendarDate(const CalendarDate& other)
{
year_ = other.year_;
month_ = other.month_;
day_ = other.day_;
}
void Display() const
{
std::cout << year_ << "/" << month_ << "/" << day_ << std::endl;
}
private:
int year_;
int month_;
int day_;
};
int main()
{
CalendarDate d1(2024, 7, 13);
CalendarDate d2(d1); // Copy constructor invoked
CalendarDate d3 = d1; // Also invokes copy constructor
return 0;
}
When a copy constructor is not defined, the compiler generates one that performs a shallow copy (byte-by-byte duplication). For built-in types and classes with out pointer resources (like CalendarDate), this is sufficient. However, for classes managing heap memory (like DynamicArray), shallow copying results in two objects pointing to the same memory, causing double-free errors when both destruct.
class DynamicArray
{
public:
DynamicArray(int n = 4)
{
dataArr_ = (int*)malloc(sizeof(int) * n);
capacity_ = n;
size_ = 0;
}
~DynamicArray()
{
free(dataArr_);
dataArr_ = nullptr;
size_ = capacity_ = 0;
}
void Insert(int val) { /* ... */ }
private:
int* dataArr_;
size_t capacity_;
size_t size_;
};
int main()
{
DynamicArray arr1;
arr1.Insert(10);
// Without a custom copy constructor, shallow copy occurs.
// arr1 and arr2 share the same dataArr_ pointer.
// Both destructors will attempt to free the same memory, crashing the program.
DynamicArray arr2 = arr1;
return 0;
}
For classes like DualContainer that solely contain custom types with properly defined copy constructors, the compiler-generated vertion works perfectly. A useful rule of thumb: if a class requires a custom destructor to release resources, it almost certainly requires a custom copy constructor to perform a deep copy.
Regarding return values, returning an object by value invokes the copy constructor to create a temporary object. Returning by reference avoids this copy but poses a severe risk: never return a reference to a local variable that will be destroyed when the function scope ends. Doing so creates a dangling reference.
DynamicArray& CreateArray()
{
DynamicArray localArr;
return localArr; // DANGER: localArr is destroyed here, returning a dangling reference.
}
Assignment Operator Overloading
Operator Overloading
C++ allows operators to be redefined for user-defined types via operator overloading. When an operator is applied to class objects, the compiler translates it into a call to the corresponding operator function.
- An operator overload function is named using the
operatorkeyword followed by the operator symbol (e.g.,operator==). - The number of parameters matches the number of operands. For binary operators, the left operand becomes the first parameter and the right operand the second.
- If overloaded as a member function, the left operand is implicitly passed via the
thispointer, reducing the explicit parameter count by one. - Five operators cannot be overloaded:
.,.*,::,?:, andsizeof. - You cannot create new operators (e.g.,
operator@) or change the meaning of operators for built-in types.
class CalendarDate
{
public:
CalendarDate(int y = 0, int m = 0, int d = 0) : year_(y), month_(m), day_(d) {}
// Member function operator overload
bool operator==(const CalendarDate& rhs) const
{
return year_ == rhs.year_ &&
month_ == rhs.month_ &&
day_ == rhs.day_;
}
int year_;
int month_;
int day_;
};
int main()
{
CalendarDate dt1(2024, 7, 13);
CalendarDate dt2(2024, 7, 13);
// Both calls are equivalent
dt1.operator==(dt2);
dt1 == dt2;
return 0;
}
Assignment Operator Overloading
The assignment operator handles copying values from one existing object to another existing object, distinguishing it from the copy constructor which initializes a new object.
- It must be overloaded as a member function.
- The parameter should be a
constreference to avoid unnecessary copying of the argument. - It should return a reference to the current object (
*this) to support chaining (e.g.,a = b = c).
class CalendarDate
{
public:
CalendarDate(int y = 1, int m = 1, int d = 1) : year_(y), month_(m), day_(d) {}
CalendarDate& operator=(const CalendarDate& rhs)
{
year_ = rhs.year_;
month_ = rhs.month_;
day_ = rhs.day_;
return *this; // Enables chaining
}
private:
int year_;
int month_;
int day_;
};
If not explicitly written, the compiler generates a default assignment operator that performs shallow copies, mirroring the behavior of the compiler-generated copy constructor. For classes managing dynamic memory, a deep copy implementation is mandatory to avoid double-free errors and memory leaks. Similar to the copy constructor, if a class requires a custom destructor, it requires a custom assignment operator.
Address-of Operator Overloading
Const Member Functions
Appending the const keyword after a member function's parameter list creates a const member function. This const modifies the implicit this pointer, changing it from ClassName* const to const ClassName* const. This guarantees that the function will not modify the object's state.
void Display() const
{
std::cout << year_ << "/" << month_ << "/" << day_ << std::endl;
}
This is crucial when working with const objects. A const object cannot call non-const member functions because passing a const ClassName* to a ClassName* parameter expands permissions, which the compiler prohibits. Any member function that does not alter the object should be marked const.
Address-of Operators
There are two versions of the address-of operator overload: one for standard objects and one for const objects. Typically, the compiler-generated versions are sufficient. They are only explicitly overridden in rare scenarios, such as when you want to intentionally hide the object's real memory address.
CalendarDate* operator&()
{
// return this; // Normal behavior
return nullptr; // Hide real address
}
const CalendarDate* operator&() const
{
// return this; // Normal behavior
return nullptr; // Hide real address
}