Skip to content

Constructors

In the previous chapter, we learned how to define a class—writing member variables, member functions, and using public and private to control access permissions. But we have been circling around one question: when an object is created, what is inside its member variables? The answer is—if you do nothing, the member variables of a local object contain garbage values! They are random data left over from the last time that memory was used.

Once an object is created, it should be in a valid, usable, and predictable state. The constructor is C++'s solution: it executes automatically when the object is created and is responsible for bringing member variables to the correct initial state. As long as the constructor is written correctly, elementary errors like "forgetting to initialize" become impossible.

In this chapter, we will break down all the forms of constructors—default constructors, parameterized constructors, copy constructors, member initializer lists, and delegating constructors introduced in C++11. Each has its use cases and hidden pitfalls.

Default Constructor — Creating Objects Without Arguments

A default constructor requires no arguments. When you write Point p;, this is what gets called.

cpp
class Point {
public:
    Point() : x(0), y(0) {} // Member initializer list
    int x, y;
};

The : x(0), y(0) following the constructor signature is the member initializer list; we will get familiar with it first and explain it in detail later. The key point is the responsibility of the default constructor: as soon as the object comes into existence, it is already a valid origin coordinate.

If you don't write any constructor, the compiler generates a default constructor for you. However, it does not initialize basic types like int or double at all; their values remain garbage. Therefore, when a class has basic type members, you almost always need to write a default constructor yourself.

Pitfall Warning: The rule for compiler-generated default constructors is simple—once you write any constructor (even one with parameters), the compiler stops generating a default constructor for you. Many people write a Point(int x, int y) and then find that Point p; fails to compile, leaving them confused. The reason is here: you wrote a parameterized constructor, so the compiler assumes "since you are managing initialization yourself, you must write the default constructor too."

The solution is simple—either write a Point() {} yourself, or use the C++11 = default syntax to ask the compiler to generate it for you:

cpp
class Point {
public:
    Point() = default; // Force compiler generation
    Point(int x, int y) : x(x), y(y) {}
    int x, y;
};

Note that a default constructor generated by = default still does not initialize basic types to zero. If you need zero-initialization, you still need to write Point() : x(0), y(0) {} or use in-class member initializers (covered in the next chapter).

Parameterized Constructor — Giving Initialization Control to the Caller

Often, we want an object to be created with specific data rather than a "zero-value" default state. A parameterized constructor accepts arguments to initialize member variables.

cpp
class Point {
public:
    Point(int x, int y) : x(x), y(y) {}
    int x, y;
};

Constructors support overloading, so you can provide both default and parameterized constructors, allowing the caller to choose as needed. However, we need to discuss an easily overlooked keyword—explicit. When a constructor accepts only one argument (or if the remaining arguments have default values), it acts as an implicit type conversion function. Look at the code:

cpp
void printPoint(const Point& p) {
    // ...
}

int main() {
    printPoint(10); // Implicit conversion: int -> Point
}

In the printPoint(10) call, the function signature asks for a const Point&, but you passed an int. The compiler helpfully called the constructor to perform an implicit conversion. In a short example, this looks fine, but in large projects, such implicit conversions can create hard-to-locate bugs—you might have just written the wrong parameter type, and instead of complaining, the compiler "helps out" and causes trouble.

The explicit keyword is used to prohibit this kind of implicit conversion:

cpp
class Point {
public:
    explicit Point(int x) : x(x), y(0) {} // No implicit conversion
    // ...
};

My suggestion is: all single-argument constructors should be explicit, unless you have a very clear reason to need implicit conversion. It is a nearly zero-cost defensive measure.

Member Initializer List — The Proper Battlefield for Initialization

We have been using the member initializer list all along; now let's formally break it down.

A constructor's initializer list is written after the parameter list, following a colon, separated by commas, with each member followed by an initial value in parentheses (or braces):

cpp
class Point {
public:
    Point(int x, int y) : x(x), y(y) {}
    int x, y;
};

You might ask: can't I just assign values in the constructor body? Why do I need a dedicated initializer list?

cpp
// Bad practice for class members
Point(int x, int y) {
    this->x = x;
    this->y = y;
}

For basic types like int and double, both approaches yield the same result. The problem arises with const members and reference members—these things can only be initialized, not assigned. By the time the constructor body starts executing, all members have already been default-constructed. Trying to assign values then is too late for const members and references—the compiler will error out directly.

cpp
class Widget {
    const int id;
    int& ref;
public:
    // Error: 'id' and 'ref' must be initialized in the list
    Widget(int i, int& r) {
        id = i;   // Illegal
        ref = r;  // Illegal
    }
};

Even without const and reference members, the initializer list is still superior. For class-type members (like std::string), assigning inside the function body means default-constructing first and then assigning over it—two steps. The initializer list constructs directly with the target value—one step.

Pitfall Warning: The initialization order of members is determined by their declaration order in the class definition, not the order in the initializer list. This is crucial—if your initializer list writes y(x) and x(10), but y is declared before x in the class, the actual execution order is to initialize y with the value of x (which is still garbage at this point), and then initialize x to 10. Most compilers will warn you if the orders differ, but it's best to cultivate the habit of keeping declaration order and initializer list order consistent, so you don't bury landmines for yourself.

Copy Constructor — Creating New Objects from Existing Ones

A copy constructor creates a new object from an existing object of the same type, with a fixed signature of ClassName(const ClassName& other):

cpp
class Point {
public:
    Point(const Point& other) : x(other.x), y(other.y) {}
    int x, y;
};

The copy constructor is called in three scenarios: copy initialization (Point p = p2), passing arguments by value (the formal parameter is created via copy constructor), and returning by value (the return value is copied via copy constructor, though modern compilers usually optimize this away with RVO).

If you don't write a copy constructor yourself, the compiler generates a default version—its behavior is memberwise copy, calling the copy constructor for each member (or directly copying the value for basic types). For a class like Point containing only basic types, the default version is perfectly sufficient.

Pitfall Warning: Memberwise copy is disastrous for classes containing raw pointers. Suppose your class has a char* pointing to dynamically allocated memory. The default copy constructor will only copy the pointer's value (the address), not the content the pointer points to. The result is two objects' pointers pointing to the same block of memory—one destructor releases the memory, and the other is still using it, becoming a dangling pointer. This is the classic "shallow copy" problem. We will discuss how to solve this deeply when we cover RAII and smart pointers later.

cpp
class Buffer {
    int* data;
public:
    // Default copy constructor leads to double-free!
};

For now, just remember one thing: if your class manages resources (dynamic memory, file handles, network connections, etc.), you must write the copy constructor yourself (or disable it entirely, covered later).

Delegating Constructor — Letting Constructors Help Each Other

C++11 introduced delegating constructors, allowing one constructor to call another constructor of the same class in its initializer list, reducing code duplication.

cpp
class Point {
public:
    Point() : Point(0, 0) {} // Delegate to parameterized constructor
    Point(int x, int y) : x(x), y(y) {
        // Core initialization logic
    }
    int x, y;
};

The initializer list of Point() doesn't contain member names, but Point(0, 0)—calling another constructor. The execution order is: the target constructor's initializer list and body are executed first, then control returns to the delegating constructor's body.

This feature is particularly useful when there are many constructors and overlapping initialization logic—put the core logic in a "main" constructor, and other constructors delegate to it.

However, delegating constructors have one hard rule: once a delegation appears in the initializer list, you cannot initialize any members yourself. Writing Point() : Point(0, 0), x(1) {} is illegal—either delegate entirely, or initialize everything yourself; you cannot mix them.

Practical Exercise — constructors.cpp

Integrate all the constructor types covered in this chapter into a Point class, and mark every constructor call with an output:

Expand (37 lines)Collapse
cpp
#include <iostream>

class Point {
public:
    // Default constructor
    Point() : Point(0, 0) {
        std::cout << "Delegating default constructor" << std::endl;
    }

    // Parameterized constructor
    Point(int x, int y) : x(x), y(y) {
        std::cout << "Parameterized constructor" << std::endl;
    }

    // Copy constructor
    Point(const Point& other) : x(other.x), y(other.y) {
        std::cout << "Copy constructor" << std::endl;
    }

    void print() const {
        std::cout << "Point(" << x << ", " << y << ")" << std::endl;
    }

private:
    int x, y;
};

int main() {
    Point p1;           // Default constructor
    Point p2(10, 20);   // Parameterized constructor
    Point p3 = p2;      // Copy constructor
    p1.print();
    p2.print();
    p3.print();

    return 0;
}

Compile and run: g++ -std=c++11 constructors.cpp -o main && ./main

Expected output:

text
Parameterized constructor
Delegating default constructor
Parameterized constructor
Copy constructor
Point(0, 0)
Point(10, 20)
Point(10, 20)

Verify this: the delegating constructor Point() calls Point(int, int) first (outputs "Parameterized constructor"), then executes its own body (outputs "Delegating default constructor"). The copy constructor is triggered correctly in both scenarios.

Try It Yourself

Exercise 1: Implement a Date Class

Write a Date class containing year, month, and day members. Provide a default constructor (initializing to 2000/1/1), a parameterized constructor (accepting year, month, day, performing basic validity checks—month 1-12, day 1-31), and a print() method. Verification: create several date objects, including an invalid date (e.g., month 13), and observe if the validation logic works.

Exercise 2: Implement a Vector3D Class

Write a Vector3D class containing x, y, z as double members. Use delegating constructor to make the default constructor delegate to the parameterized constructor Vector3D(double x, double y, double z). Also implement a copy constructor and a magnitude() method that returns the vector's magnitude. Verification: create a default vector, a custom vector, and a copied vector, and print their values and magnitudes.

Summary

The constructor is the starting point of an object's lifecycle, ensuring the object is born in a valid state. The default constructor creates objects without arguments, but remember—once you write any constructor, the default constructor is no longer auto-generated. Parameterized constructors initialize objects with specific data, and explicit prevents implicit conversion for single-argument constructors. The member initializer list is the proper way to initialize; it is the only choice for const and reference members, and the initialization order follows declaration order, not writing order. Copy constructors create new objects from existing ones; the default behavior is memberwise copy—a hidden bomb for classes with pointers. C++11's delegating constructors allow constructors to reuse each other, reducing code duplication.

In the next chapter, we will discuss destructors—the constructor brings the object in, and the destructor is responsible for safely sending it out. Together, they form the core philosophy of C++ resource management: RAII.

v0.7.0-9-g940ec1b · 940ec1b · 2026-07-05