When managing a Calendar object, manually invoking an initializer for every new instance is cumbersome. Instead, C++ provides special member functions that automtae object setup and teardown.
Constructors
Constructors are special methods invoked automatically upon object creation. Their primary role is to initialize the object's state rather than allocate the object itself. Key characteristics include:
- The function name matches the class name.
- They return no value (not even
void). - They are called automatically when an instance is created.
- They support overloading.
class Calendar
{
public:
// Default constructor
Calendar()
{
_y = 2000;
_m = 1;
_d = 1;
}
// Parameterized constructor
Calendar(int y, int m, int d)
{
_y = y;
_m = m;
_d = d;
}
void display()
{
std::cout << _y << '/' << _m << '/' << _d << std::endl;
}
private:
int _y;
int _m;
int _d;
};
int main()
{
Calendar today;
Calendar specificDay(2023, 10, 15);
today.display();
specificDay.display();
return 0;
}
If no constructor is explicitly defined, the compiler generates a default one. However, this implicit constructor only initializes member variables under specific conditions:
- Custom Types: The compiler calls the default constructor for members that are classes or structs.
- Built-in Types: Variables like
intorcharremain uninitialized (containing garbage values) unless the compiler specifically zeroes them out.
C++11 introduced in-class initializers to address this:
class Calendar
{
public:
void display()
{
std::cout << _y << '/' << _m << '/' << _d << std::endl;
}
private:
int _y = 2020;
int _m = 1;
int _d = 1;
};
A default constructor is any constructor that can be called with zero arguments. This includes:
- The compiler-generated default constructor.
- A user-defined constructor with no parameters.
- A user-defined constructor where all parameters have default values.
Destructors
While constructors handle setup, destructors handle cleanup when an object goes out of scope. They do not destroy the object itself (the compiler handles memory deallocation), but they release resources (like heap memory or file handles) owned by the object.
- The name is the class name prefixed with a tilde (
~). - They take no arguments and return no value.
- A class can only have one destructor, and it cannot be overloaded.
- The compiler calls it automatically at the end of the object's lifetime.
class Buffer
{
public:
Buffer(size_t sz = 10)
{
std::cout << "Buffer allocated" << std::endl;
_data = (int*)malloc(sz * sizeof(int));
_size = sz;
}
~Buffer()
{
std::cout << "Buffer destroyed" << std::endl;
if (_data)
{
free(_data);
_data = nullptr;
}
}
private:
int* _data;
size_t _size;
};
int main()
{
Buffer buf;
return 0;
}
Similar to constructors, if a class contains members of custom types, the compiler-generated destructor will invoke those members' destructors. For classes like Calendar that hold no dynamic resources, writing an explicit destructor is unnecessary. However, for resource-managing classes like Buffer, omitting a destructor leads to memory leaks.
Copy Constructors
A copy constructor initializes a new object as a copy of an existing object. It is a specific overload of the constructor.
- It must take a single argument: a reference to an object of the same class (usually
const). - Passing the object by value (instead of by reference) causes infinite recursion, as the compiler tries to copy the argument to pass it, calling the copy constructor again.
class Calendar
{
public:
Calendar(int y = 2000, int m = 1, int d = 1) : _y(y), _m(m), _d(d) {}
// Copy constructor
Calendar(const Calendar& src)
{
_y = src._y;
_m = src._m;
_d = src._d;
std::cout << "Copying Calendar..." << std::endl;
}
void display()
{
std::cout << _y << '/' << _m << '/' << _d << std::endl;
}
private:
int _y;
int _m;
int _d;
};
void showDate(Calendar c)
{
c.display();
}
int main()
{
Calendar date1(2023, 12, 25);
Calendar date2 = date1; // Calls copy constructor
showDate(date1); // Calls copy constructor for parameter 'c'
return 0;
}
Shallow vs. Deep Copy
The compiler provides a default copy constructor that performs a shallow copy (member-wise copy). For classes containing raw pointers to dynamic memory, this is dangerous. Both the original and the copy will point to the same memory address; when one destructs and frees the memory, the other becomes a dangling pointer.
To fix this, implement a deep copy:
class DynamicArray
{
public:
DynamicArray(size_t capacity = 5)
{
_arr = new int[capacity];
_cap = capacity;
_len = 0;
}
// Deep Copy Constructor
DynamicArray(const DynamicArray& other)
{
_cap = other._cap;
_len = other._len;
_arr = new int[_cap];
for (size_t i = 0; i < _len; ++i)
{
_arr[i] = other._arr[i];
}
}
~DynamicArray()
{
delete[] _arr;
}
private:
int* _arr;
size_t _cap;
size_t _len;
};
If a class manages resources (like DynamicArray), a custom copy constructor is mandatory. If it only holds values (like Calendar), the default generated one suffices.
Copy constructors are heavily used during:
- Initialization of a new object from an existing one.
- Passing objects by value to functions.
- Returning objects by value from functions (though RVO may optimize this).
For performance, prefer passing by const reference whenever possible to avoid unnecessary copies.