C++ Classes and Objects: Default Member Functions and Operator Overloading

  1. Default Member Functions in C++ Classes

Default member functions are special member functions that the compiler automatically generates when the user does not explicitly define them.

When you declare a class without providing implementations, the compiler automatically generates six default member functions. While all six are part of the language specificasion, the first four are the most critical, with the last two address-of operators being less important and primarily for informational purposes.

The six default member functions include:

  • Constructor for object initialization
  • Destructor for cleanup operations
  • Copy constructor for creating objects from existing instances
  • Assignment operator for assigning values between objects
  • Address-of operator for regular objects
  • Address-of operator for const objects

Understanding default member functions requires examining two key aspects:

• First, what behavior does the compiler's default implementation provide when we don't write these functions, and does it meet our requirements?
• Second, when the default implementation doesn't suffice, how do we implement these functions ourselves?


  1. Constructors

Constructors are special member functions responsible for initializing objects when they are instantiated. Despite their name suggesting object creation, constructors primarily handle initialization rather than memory allocation (since local objects are created when their stack frame is allocated).

Key characteristics of constructors:

  1. The constructor name must match the class name exactly.
  2. Constructors have no return type (neither void nor any other type is permitted).
  3. The compiler automatically calls the appropriate constructor during object instantiation.
  4. Constructors support functon overloading.
  5. If no constructor is explicitly defined, the C++ compiler generates a default parameterless constructor. Once a user defines any constructor, the compiler stops generating this default.
  6. Parameterless constructors, fully defaulted constructors, and the compiler-generated default constructor are all considered "default constructors." However, only one can exist in a class at a time, as having both a parameterless constructor and a fully defaulted constructor creates ambiguity during calls. The key distinction is that any constructor callable without arguments qualifies as a default constructor.
  7. The compiler-generated default constructor performs no specific initialization for built-in type members (their values are indeterminate), while for user-defined type members, it calls their default constructor. If a member lacks a default constructor, initialization must be handled through an initialization list, discussed later.

C++ distinguishes between built-in types (primitive types provided by the language) and user-defined types (created using class or struct keywords).

Example with built-in types:

#include <iostream>

class Date
{
public:
    // Default constructor
    Date()
    {
        year_ = 2000;
        month_ = 6;
        day_ = 1;
    }
    
    // Parameterized constructor
    Date(int year, int month, int day)
    {
        year_ = year;
        month_ = month;
        day_ = day;
    }

    // Fully defaulted constructor
    // Cannot coexist with parameterless constructor
    /*Date(int year = 2024, int month = 7, int day = 1)
    {
        year_ = year;
        month_ = month;
        day_ = day;
    }*/

    void display()
    {
        std::cout << year_ << "年" << month_ << "月" << day_ << "日" << std::endl;
    }

private:
    int year_;
    int month_;
    int day_;
};

int main()
{
    Date obj1;  // Calls default constructor
    obj1.display();
    Date obj2(2024, 7, 14);  // Calls parameterized constructor
    obj2.display();
    return 0;
}

Example with user-defined types:

#include <iostream>

using ValueType = int;

class Stack
{
public:
    Stack(size_t capacity = 4)
    {
        data_ = static_cast<ValueType*>(malloc(sizeof(ValueType) * capacity));
        if (data_ == nullptr)
        {
            perror("malloc failed");
            return;
        }
        capacity_ = capacity;
        top_ = 0;
    }

private:
    ValueType* data_;
    size_t capacity_;
    size_t top_;
};

// Queue implemented using two Stack instances
class Queue
{
private:
    Stack enqueueStack;
    Stack dequeueStack;
};

int main()
{
    Queue q;
    return 0;
}

In this example, the compiler-generated constructor for Queue automatically calls Stack's constructor for both member objects, properly initializing the queue's internal components.


  1. Destructors

Destructors perform the inverse operation of constructors. They do not destroy the object itself (local objects are automatically freed when their stack frame is destroyed), but rather handle cleanup of resources held by the object when it is destroyed. The C++ standard mandates that destructors are automatically called when an object reaches the end of its lifetime.

The destructor concept parallels the Destroy functionality in stack implementations. Classes like Date, which don't require cleanup, technically don't need explicit destructors.

Key characteristics of destructors:

  1. The destructor name is formed by prefixing the class name with a tilde (~).
  2. Destructors take no parameters and have no return type.
  3. A class can have only one destructor. If not explicitly defined, the compiler generates a default destructor.
  4. The compiler automatically calls the destructor when an object's lifetime ends.
  5. Similar to constructors, the compiler-generated destructor performs no action on built-in type members, but calls the destructor for user-defined type members.
  6. When an explicit destructor is provided, the compiler still automatically calls destructors for user-defined type members—in other words, user-defined members always have their destructors invoked.
  7. Destructors are optional for classes without resource allocation (like Date). The compiler-generated destructor suffices for such cases. For classes managing dynamic memory (like Stack), explicit destructor implementation is essential to prevent resource leaks.
  8. C++ guarantees that destructors are called in reverse order of object construction within the same scope.
#include <iostream>

using ValueType = int;

class Stack
{
public:
    Stack(size_t capacity = 4)
    {
        data_ = static_cast<ValueType*>(malloc(sizeof(ValueType) * capacity));
        if (data_ == nullptr)
        {
            perror("malloc failed");
            return;
        }
        capacity_ = capacity;
        top_ = 0;
    }

    ~Stack()
    {
        std::cout << "~Stack()" << std::endl;
        free(data_);
        data_ = nullptr;
        top_ = capacity_ = 0;
    }

private:
    ValueType* data_;
    size_t capacity_;
    size_t top_;
};

// Queue implemented using two Stack instances
class Queue
{
public:
    // Compiler-generated destructor would call Stack's destructor
    // Explicit destructor also triggers Stack's destructor
    ~Queue()
    {
        std::cout << "~Queue()" << std::endl;
    }

private:
    Stack pushStack;
    Stack popStack;
};

int main()
{
    Queue q;
    return 0;
}


  1. Copy Constructors

A copy constructor is a special constructor where the first parameter is a reference to the class type, with any additional parameters having default values.

Key characteristics of copy constructors:

  1. A copy constructor is essentially an overloaded constructor.
  2. The first parameter must be a reference to the class type. Using pass-by-value causes a compilation error due to infinite recursive calling.
  3. C++ mandates that user-defined types must use copy constructors for copy operations. Consequently, pass-by-value parameters and return values for user-defined types invoke the copy constructor.
  4. When no copy constructor is explicitly defined, the compiler generates one. The generated copy constructor performs member-wise copying (shallow copy) for built-in types and calls the member's copy constructor for user-defined types.
  5. For classes like Date containing only built-in types without dynamic resources, the compiler-generated copy constructor suffices. For classes like Stack containing pointers to dynamic memory, the compiler-generated shallow copy creates problems, requiring explicit implementation of deep copy. Classes like Queue, composed primarily of Stack members, can rely on the compiler-generated copy constructor because it delegates to Stack's copy constructor.
  6. Pass-by-value return creates a temporary object invoking the copy constructor. Pass-by-reference return returns an alias to the returned object without copying. However, returning a reference to a local object is dangerous as the object is destroyed when the function returns, creating a dangling reference.
#include <iostream>

class Date
{
public:
    // Default constructor
    Date()
    {
        year_ = 2000;
        month_ = 6;
        day_ = 1;
    }

    // Parameterized constructor
    Date(int year, int month, int day)
    {
        year_ = year;
        month_ = month;
        day_ = day;
    }

    // Copy constructor - pass by value
    // Error: invalid copy constructor, first parameter cannot be by value
    /*Date(Date d)
    {
    }*/

    // Copy constructor - pass by reference
    Date(const Date& source)
    {
        year_ = source.year_;
        month_ = source.month_;
        day_ = source.day_;
    }

    // This is a regular constructor, not a copy constructor
    Date(Date* ptr)
    {
        year_ = ptr->year_;
        month_ = ptr->month_;
        day_ = ptr->day_;
    }

    void display()
    {
        std::cout << year_ << "年" << month_ << "月" << day_ << "日" << std::endl;
    }

private:
    int year_;
    int month_;
    int day_;
};

Date& process()
{
    Date temp(2024, 8, 1);
    return temp;  // Danger: returning reference to destroyed object
}

int main()
{
    Date original(2024, 7, 14);

    // This is not a copy constructor, just a regular constructor
    Date pointerObj(&original);
    pointerObj.display();

    // This is a proper copy constructor call
    Date copy1(original);
    copy1.display();

    // Another copy constructor syntax
    Date copy2 = original;
    copy2.display();

    // Dangerous: returning reference to local object
    Date result = process();
    result.display();

    return 0;
}

#include <iostream>

using ValueType = int;

class Stack
{
public:
    Stack(size_t capacity = 4)
    {
        data_ = static_cast<ValueType*>(malloc(sizeof(ValueType) * capacity));
        if (data_ == nullptr)
        {
            perror("malloc failed");
            return;
        }
        capacity_ = capacity;
        top_ = 0;
    }

    void push(ValueType element)
    {
        if (top_ == capacity_)
        {
            size_t newCapacity = capacity_ * 2;
            ValueType* newData = static_cast<ValueType*>(
                realloc(data_, newCapacity * sizeof(ValueType)));
            if (newData == nullptr)
            {
                perror("realloc failed");
                return;
            }
            data_ = newData;
            capacity_ = newCapacity;
        }
        data_[top_++] = element;
    }

    ~Stack()
    {
        free(data_);
        data_ = nullptr;
        capacity_ = top_ = 0;
        std::cout << "~Stack()" << std::endl;
    }

    // Deep copy constructor
    Stack(const Stack& source)
    {
        std::cout << "Stack(const Stack& source)" << std::endl;

        // Allocate independent memory and copy contents
        data_ = static_cast<ValueType*>(malloc(sizeof(ValueType) * source.capacity_));
        if (data_ == nullptr)
        {
            perror("malloc failed");
            return;
        }
        memcpy(data_, source.data_, sizeof(ValueType) * source.top_);
        top_ = source.top_;
        capacity_ = source.capacity_;
    }

private:
    ValueType* data_;
    size_t capacity_;
    size_t top_;
};

// Queue implemented using two Stack instances
class Queue
{
private:
    Stack enqueueStack;
    Stack dequeueStack;
};

int main()
{
    Stack s1;
    s1.push(1);
    s1.push(2);

    // Without explicit copy constructor, compiler-generated shallow copy
    // causes both objects to share the same pointer, leading to double-free crash
    Stack s2 = s1;

    Queue q1;
    // Compiler-generated copy constructor calls Stack's copy constructor
    // Deep copy is handled properly if Stack implements deep copy
    Queue q2 = q1;

    return 0;
}


  1. Operator Overloading

5.1 Introduction to Operator Overloading

When operators are used with class-type objects, C++ allows defining new meanings through operator overloading. The language mandates that class-type objects using operators must call the corresponding operator overload function; without one, compilation fails.

Operator overloads are special functions named by combining "operator" with the operator symbol. Like regular functions, they have return types, parameter lists, and function bodies.

Overloaded operator parameters match the number of operands. Unary operators have one parameter, binary operators have two, with the left operand passed as the first parameter and the right operand as the second.

When an overloaded operator is a member function, the first operand is implicitly passed via the this pointer, meaning member operator functions have one fewer parameter than operands.

Operator precedence and associativity remain consistent with the corresponding built-in operators.

New operators cannot be created by combining symbols that don't already exist (e.g., operator@).

The following five operators cannot be overloaded:

Overloaded operators must have at least one class-type parameter; operator overloading cannot change the meaning of built-in type operations.

A class should overload operators where the operation makes semantic sense. For example, Date supporting operator- makes sense, but operator+ does not.

When overloading ++, both prefix and postfix forms use the same function name operator++, creating ambiguity. C++ resolves this by adding an unused int parameter to the postfix overload, enabling function overloading.

When overloading > for stream operations, these should be implemented as non-member functions. If implemented as member functions, the this pointer occupies the first parameter position, making the call syntax (object 1. The assignment operator must be implemented as a member function. Parameters should be const references to avoid unnecessary copying.

  1. The operator should return a referencce to the current class type. Reference returns improve efficiency, and the return value enables chained assignments.
  2. When not explicitly defined, the compiler generates a default assignment operator. Its behavior mirrors the default copy constructor—performing member-wise shallow copy for built-in types and calling member assignment operators for user-defined types.
  3. For classes like Date containing only built-in types without resources, the compiler-generated assignment operator suffices. For classes like Stack managing dynamic memory, the compiler-generated shallow copy is inadequate, requiring explicit deep copy implementation. Classes like Queue, composed of Stack members, can rely on the compiler-generated operator since it delegates to Stack's assignment operator.
#include <iostream>

class Date
{
public:
    Date()
    {
        year_ = 2000;
        month_ = 6;
        day_ = 1;
    }

    Date(int year, int month, int day)
    {
        year_ = year;
        month_ = month;
        day_ = day;
    }

    // Copy constructor
    Date(const Date& source)
    {
        year_ = source.year_;
        month_ = source.month_;
        day_ = source.day_;
    }

    // Assignment operator overload
    Date& operator=(const Date& source)
    {
        // Self-assignment check is optional but safe
        if (this != &source)
        {
            year_ = source.year_;
            month_ = source.month_;
            day_ = source.day_;
        }
        // Return the current object for chaining
        return *this;
    }

    void display()
    {
        std::cout << year_ << "年" << month_ << "月" << day_ << "日" << std::endl;
    }

private:
    int year_;
    int month_;
    int day_;
};

int main()
{
    // Assignment operates on existing objects
    // Copy construction initializes a new object from existing one
    Date d1(2024, 7, 14);
    Date d2(d1);       // Copy construction
    Date d3 = d2;      // Copy construction
    Date d4(2025, 5, 5);
    d3 = d4;           // Assignment operator
    d3.display();

    return 0;
}


  1. Address-of Operator Overloading

6.1 const Member Functions

Member functions modified with const are called const member functions. The const qualifier follows the parameter list.

The const qualifier effectively modifies the implicit this pointer, indicating that the function cannot modify any class members.

When const is applied to Date's display function, the implicit this pointer changes from Date* const this to const Date* const this.

#include <iostream>

class Date
{
public:
    Date(int year = 1, int month = 1, int day = 1)
    {
        year_ = year;
        month_ = month;
        day_ = day;
    }

    // Equivalent to: void display(const Date* const this) const
    void display() const
    {
        std::cout << year_ << "年" << month_ << "月" << day_ << "日" << std::endl;
    }

private:
    int year_;
    int month_;
    int day_;
};

int main()
{
    // Non-const objects can call const member functions (permission narrowing)
    Date d1(2024, 7, 14);
    d1.display();

    const Date d2(2024, 8, 1);
    d2.display();

    return 0;
}

6.2 Address-of Operator Overloading

Address-of operator overloading includes regular address-of operator and const address-of operator overloads. The compiler-generated versions typically suffice; explicit implementation is rarely necessary.

Explicit implementation might be used in special scenarios where you want to prevent others from obtaining the object's actual address.

#include <iostream>

class Date
{
public:
    Date()
    {
        year_ = 2024;
        month_ = 7;
        day_ = 14;
    }

    Date(int year, int month, int day)
    {
        year_ = year;
        month_ = month;
        day_ = day;
    }

    Date* operator&()
    {
        // Return actual address
        // return this;
        // Return null
        // return nullptr;
        // Return fake address for demonstration
        return reinterpret_cast<Date*>(0x1256EF7);
    }

    const Date* operator&() const
    {
        // return this;
        // return nullptr;
        return reinterpret_cast<const Date*>(0x1256EA9);
    }

private:
    int year_;
    int month_;
    int day_;
};

int main()
{
    Date d1;
    std::cout << &d1 << std::endl;

    const Date d2(2024, 8, 1);
    std::cout << &d2 << std::endl;

    return 0;
}


  1. Practical Implementation: Date Class

7.1 Header File

#pragma once

#include <iostream>

// Date class implementation
class Date
{
public:
    Date(int year = 2000, int month = 1, int day = 1);

    // Copy constructor
    Date(const Date& source);

    // Assignment operator
    Date& operator=(const Date& source);

    // Destructor
    ~Date()
    {
        year_ = 0;
        month_ = 0;
        day_ = 0;
    }

    void display() const;
    bool isValid() const;

    static int getDaysInMonth(int year, int month)
    {
        static const int daysArray[13] = {
            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 daysArray[month] + 1;
        }
        return daysArray[month];
    }

    Date operator+(int days) const;
    Date& operator+=(int days);

    Date operator-(int days) const;
    Date& operator-=(int days);

    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;

    // Prefix ++
    Date& operator++();

    // Postfix ++
    Date operator++(int);

    // Date subtraction: calculates days between two dates
    int operator-(const Date& other) const;

    // Stream insertion and extraction operators
    friend std::ostream& operator<<(std::ostream& output, const Date& d);
    friend std::istream& operator>>(std::istream& input, Date& d);

    // Address-of operator overloads
    Date* operator&()
    {
        return reinterpret_cast<Date*>(0x11451EF6);
    }

    const Date* operator&() const
    {
        return reinterpret_cast<const Date*>(0x1145EE6);
    }

private:
    int year_;
    int month_;
    int day_;
};

// Stream operator declarations
std::ostream& operator<<(std::ostream& output, const Date& d);
std::istream& operator>>(std::istream& input, Date& d);

7.2 Implementation File

#include "Date.h"

Date::Date(int year, int month, int day)
{
    year_ = year;
    month_ = month;
    day_ = day;

    if (!isValid())
    {
        std::cout << "Invalid date: ";
        display();
    }
}

// Copy constructor
Date::Date(const Date& source)
{
    year_ = source.year_;
    month_ = source.month_;
    day_ = source.day_;
}

// Assignment operator
Date& Date::operator=(const Date& source)
{
    if (this != &source)
    {
        year_ = source.year_;
        month_ = source.month_;
        day_ = source.day_;
    }
    return *this;
}

void Date::display() const
{
    std::cout << year_ << " " << month_ << " " << day_ << std::endl;
}

bool Date::isValid() const
{
    if (month_ < 1 || month_ > 12 || day_ <= 0 ||
        day_ > getDaysInMonth(year_, month_))
    {
        return false;
    }
    return true;
}

Date Date::operator+(int days) const
{
    Date result = *this;
    result += days;
    return result;
}

Date& Date::operator+=(int days)
{
    if (days < 0)
    {
        return *this -= (-days);
    }

    day_ += days;

    while (day_ > getDaysInMonth(year_, month_))
    {
        day_ -= getDaysInMonth(year_, month_);
        month_++;

        if (month_ == 13)
        {
            month_ = 1;
            year_++;
        }
    }

    return *this;
}

Date Date::operator-(int days) const
{
    Date result = *this;
    result -= days;
    return result;
}

Date& Date::operator-=(int days)
{
    if (days < 0)
    {
        return *this += (-days);
    }

    day_ -= days;

    while (day_ <= 0)
    {
        month_--;

        if (month_ == 0)
        {
            month_ = 12;
            year_--;
        }

        day_ += getDaysInMonth(year_, month_);
    }

    return *this;
}

bool Date::operator<(const Date& other) const
{
    if (year_ < other.year_)
    {
        return true;
    }
    else if (year_ == other.year_)
    {
        if (month_ < other.month_)
        {
            return true;
        }
        else if (month_ == other.month_)
        {
            return day_ < other.day_;
        }
    }
    return false;
}

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 year_ == other.year_ && month_ == other.month_ && day_ == other.day_;
}

bool Date::operator!=(const Date& other) const
{
    return !(*this == other);
}

// Prefix ++
Date& Date::operator++()
{
    return *this += 1;
}

// Postfix ++
Date Date::operator++(int)
{
    Date temp = *this;
    *this += 1;
    return temp;
}

int Date::operator-(const Date& other) const
{
    int direction = 1;
    int days = 0;

    Date max = *this;
    Date min = other;

    if (*this < other)
    {
        max = other;
        min = *this;
    }

    while (min != max)
    {
        days++;
        ++min;
        direction = -1;
    }

    return days * direction;
}

std::ostream& operator<<(std::ostream& output, const Date& d)
{
    output << d.year_ << "年" << d.month_ << "月" << d.day_ << "日";
    return output;
}

std::istream& operator>>(std::istream& input, Date& d)
{
    while (true)
    {
        std::cout << "Enter year month day" << std::endl;
        input >> d.year_ >> d.month_ >> d.day_;

        if (!d.isValid())
        {
            std::cout << "Invalid date: ";
            d.display();
        }
        else
        {
            break;
        }
    }

    return input;
}

7.3 Test File

#include "Date.h"

void testAddition()
{
    Date d1;
    Date d2(2024, 7, 12);
    d1.display();
    d2.display();

    d2 += 30000;
    d2.display();

    Date d3 = d1 + 10000;
    d3.display();
}

void testSubtraction()
{
    Date d1;
    Date d2(2024, 7, 13);
    d1.display();
    d2.display();

    d2 -= 30000;
    d2.display();
}

void testComparison()
{
    Date d1(2024, 7, 13);
    Date d2(2024, 7, 19);

    std::cout << (d1 < d2) << std::endl;
    std::cout << (d1 <= d2) << std::endl;
    std::cout << (d1 > d2) << std::endl;
    std::cout << (d1 >= d2) << std::endl;
    std::cout << (d1 == d2) << std::endl;
    std::cout << (d1 != d2) << std::endl;
}

void testIncrement()
{
    Date d1(2024, 7, 13);
    Date d2(2024, 7, 13);

    Date temp1 = d1++;
    temp1.display();
    d1.display();

    Date temp2 = ++d2;
    temp2.display();
    d2.display();
}

void testDateDifference()
{
    Date d1(2024, 7, 13);
    Date d2(2000, 7, 20);
    std::cout << d1 - d2 << std::endl;

    Date d3(2024, 6, 31);
}

void testStreamOperators()
{
    Date d1(2024, 7, 13);
    Date d2(2000, 7, 20);

    std::cout << d1 << d2 << std::endl;
    std::cin >> d1 >> d2;
    std::cout << d1 << d2 << std::endl;
}

void testCopyAndAssign()
{
    Date d1(2024, 7, 13);
    Date d2(d1);
    d2.display();

    Date d3 = d1;
    d3.display();

    Date d4(2024, 6, 5);
    Date d5(2012, 3, 14);
    d4 = d5;
    d4.display();
}

void testAddressOperator()
{
    Date d1(2024, 7, 13);
    const Date d2(2000, 8, 19);

    std::cout << &d1 << std::endl;
    std::cout << &d2 << std::endl;
}

void testCompoundAssignment()
{
    Date d1;
    Date d2(2024, 7, 14);

    std::cout << (d2 += -500) << std::endl;
    std::cout << (d1 -= -500) << std::endl;
}

int main()
{
    // Uncomment to run specific tests
    // testAddition();
    // testSubtraction();
    // testComparison();
    // testIncrement();
    // testDateDifference();
    // testStreamOperators();
    // testCopyAndAssign();
    // testAddressOperator();
    testCompoundAssignment();

    return 0;
}

Tags: cpp Classes Objects Constructors destructors

Posted on Sun, 19 Jul 2026 17:10:36 +0000 by map200uk