Understanding C++ Constructor Types and Memory Semantics

Constructors in C++ are special member functions invoked automatically when an object of a class is created. They initialize the object’s state, have no return type (not even void), and share the same name as their enclosing class. Because constructors support overloading, multiple variants can coexist—each distinguished by its parameter signature.

Common constructor categories include:

  • Default (parameterless) constructor: Takes no arguments; may be implicitly provided by the compiler if no user-defined constructor exists.
  • Parameterized constructor: Accepts one or more arguments to initialize member variables.
  • Copy constructor: Initializes a new object using an existing lvalue object. Its parameter must be a const lvalue reference to prevent infinite recursion during argument binding.
  • Move constructor: Transfers ownership of resources from a temporary (rvalue) object, typically avoiding expensive deep copies.

When any user-defined constructor is declared, the compiler suppresses implicit generation of the default constructor. However, copy and move constructors follow different rules: declaring a copy constructor disables implicit move constructor generation only if no move constructor is explicitly defined—and vice versa—unless the class has user-declared destructors or copy/move operations that affect the "rule of five."

Default Constructor Usage

If no constructor is defined, the compiler supplies a trivial default constructor that performs no initialization. This often leaves built-in members (e.g., int, raw pointers) with indeterminate values. To ensure deterministic behavior, define an explicit default constructor that sets meaningful initial states:

class Clock {
public:
    int hours = 0;
    int minutes = 0;
    int seconds = 0;

    // Explicit default constructor
    Clock() = default; // Or: Clock() : hours{0}, minutes{0}, seconds{0} {}
};

Parameterized Constructor

A parameterized constructor allows clients to supply initial values at construction time. Unlike assignment inside the body, prefer member initializer lists for efficiency and correctness—especially for const members or references:

class Clock {
public:
    int hours, minutes, seconds;

    Clock(int h, int m, int s) 
        : hours{h % 24}, minutes{m % 60}, seconds{s % 60} {}
};

int main() {
    // Clock c; // Error: no default constructor available
    Clock c{9, 30, 45}; // OK
}

Copy Constructor: Shallow vs. Deep Copy

The copy constructor receives a const T& parameter. When a class manages dynamic memory (e.g., via new), shallow copying—where pointer members are copied bitwise—leads to aliasing and double-deletion risks. Deep copying allocates new memory and copies content, ensuring independence between objects.

class Buffer {
public:
    size_t size_;
    int* data_;

    Buffer(size_t n) : size_{n}, data_{new int[n]{}} {}

    // Deep copy constructor
    Buffer(const Buffer& other) 
        : size_{other.size_}, data_{new int[other.size_]} {
        std::copy(other.data_, other.data_ + size_, data_);
    }

    ~Buffer() { delete[] data_; }
};

// Usage
Buffer original{100};
Buffer copy{original}; // Independent heap allocation

Move Constructor

Move constructors accept an rvalue reference (T&&) and transfer ownership instead of duplicating resources. After moving, the source object must remain in a valid but unspecified state—typically by nulling out pointers or resetting counts.

class Buffer {
public:
    size_t size_;
    int* data_;

    Buffer(size_t n) : size_{n}, data_{new int[n]{}} {}

    Buffer(const Buffer& other) 
        : size_{other.size_}, data_{new int[other.size_]} {
        std::copy(other.data_, other.data_ + size_, data_);
    }

    // Move constructor
    Buffer(Buffer&& other) noexcept 
        : size_{other.size_}, data_{other.data_} {
        other.size_ = 0;
        other.data_ = nullptr;
    }

    ~Buffer() { delete[] data_; }
};

Buffer create_buffer() {
    return Buffer{500}; // Returns prvalue → triggers move, not copy
}

int main() {
    Buffer b = create_buffer(); // Move-construction preferred
}

Note the use of noexcept, which enables certain optimizations (e.g., std::vector reallocations) and signals that the operation won’t throw exceptions.

Tags: C++ constructor move-semantics copy-constructor deep-copy

Posted on Tue, 09 Jun 2026 17:22:52 +0000 by o2cathy