C++ Constructors

A constructor is a special member function that runs automatically every time an object of a class is created. Its job is to put the object into a valid, usable state the instant it comes into existence, whether that means setting default values, accepting arguments to customize the object, or copying data from another object. Constructors are the foundation of reliable object-oriented C++ code: without them you would need to manually initialize every object after creating it, which is error-prone and easy to forget. This lesson covers every kind of constructor C++ offers, how initialization actually works under the hood, and the mistakes that trip up even experienced programmers.

Overview: What Constructors Are and How They Work

A constructor is a member function that shares the exact name of its class, has no return type (not even void), and is invoked automatically whenever an object is created. You never call a constructor with dot or arrow syntax like a normal method; instead the compiler inserts the call for you at the point of object creation, whether that object lives on the stack, on the heap (via new), or as a member of another class.

If you don’t write any constructor at all, the compiler silently generates a default constructor (one that takes no arguments) that default-initializes each member. However, the moment you define any constructor of your own, the compiler stops generating the default one automatically — if you still want a no-argument constructor, you must provide it yourself.

Every class can have several constructors as long as their parameter lists differ; this is called constructor overloading. C++ picks the right one to call based on the arguments you supply when creating the object, using the same overload-resolution rules as ordinary functions.

Member Initializer Lists

The part of a constructor written after the colon — : member1(value1), member2(value2) — is the member initializer list. This is where each data member is actually constructed. Anything you do inside the curly-brace body happens after every member already exists; assigning a value there is a separate operation, not initialization. For simple types like int or double the difference is mostly a matter of style, but for const members, reference members, and members whose type has no default constructor, the initializer list is the only place you can set their value — assigning to them in the body will not compile.

Initialization Order

A crucial and frequently misunderstood rule: members are always initialized in the order they are declared in the class, never in the order they appear in the initializer list. Writing the initializer list out of order doesn’t change execution order — it just makes the code confusing (and most compilers will warn you with -Wreorder). We’ll see this in action later in the lesson.

Delegating Constructors and Default Member Initializers

Since C++11, one constructor can call another constructor of the same class from its initializer list — this is called delegation, and it lets you avoid duplicating initialization logic across overloads. C++11 also introduced default member initializers, where you write a default value directly at the point a member is declared (e.g. int age = 0;); that value is used automatically unless a constructor’s initializer list overrides it.

The explicit Keyword

A constructor that can be called with a single argument doubles as an implicit conversion from that argument’s type to your class type, which can silently allow surprising conversions. Marking a single-argument constructor explicit disables that implicit conversion, so the constructor can only be used for direct, intentional object creation. As a rule of thumb, mark single-argument constructors explicit unless you specifically want implicit conversions.

Copy Constructors and Rule of Three

C++ also generates a copy constructor automatically — one that takes a const ClassName& and copies each member. The compiler-generated version performs a shallow copy, which is fine for members like int or std::string, but dangerous for raw pointers to owned memory, because two objects would end up pointing at (and eventually both trying to delete) the same block of memory. If your class manages a resource like this, you typically need to write your own copy constructor (and, by the Rule of Three, a destructor and copy assignment operator too).

Constructor Syntax

class ClassName {
    // member variables
public:
    ClassName(parameter_list) : member1(value1), member2(value2) {
        // constructor body -- runs after all members are initialized
    }
};
Part Description
ClassName(parameter_list) The constructor’s name must match the class exactly; parameters are optional.
: member1(value1), member2(value2) The member initializer list; each member is constructed here, in declaration order.
{ ... } The constructor body; runs after every member already has a value. Can be empty.
(no return type) Constructors never specify a return type, not even void.

Examples

Example 1: Default and Parameterized Constructors

This example defines two constructors for a Person class: a default one that supplies fallback values, and a parameterized one that lets the caller supply real data.

#include <iostream>
#include <string>
using namespace std;

class Person {
private:
    string name;
    int age;
public:
    // Default constructor
    Person() : name("Unknown"), age(0) {}

    // Parameterized constructor
    Person(string n, int a) : name(n), age(a) {}

    void print() const {
        cout << name << " is " << age << " years old" << endl;
    }
};

int main() {
    Person p1;
    Person p2("Alice", 30);
    p1.print();
    p2.print();
    return 0;
}

Output:

Unknown is 0 years old
Alice is 30 years old

p1 is created with no arguments, so the default constructor runs and fills in the placeholder values. p2 supplies two arguments, so the compiler matches it to the parameterized constructor instead. Both members are set through the initializer list before the (empty) body runs.

Example 2: Constructor Overloading and Delegating Constructors

This Rectangle class has three constructors. The no-argument and one-argument versions both delegate to the two-argument version instead of repeating the initialization logic.

#include <iostream>
using namespace std;

class Rectangle {
private:
    double width;
    double height;
public:
    Rectangle() : Rectangle(1.0, 1.0) {}
    Rectangle(double side) : Rectangle(side, side) {}
    Rectangle(double w, double h) : width(w), height(h) {
        cout << "Creating rectangle " << width << " x " << height << endl;
    }

    double area() const { return width * height; }
};

int main() {
    Rectangle r1;
    Rectangle r2(4.0);
    Rectangle r3(3.0, 5.0);

    cout << "r1 area: " << r1.area() << endl;
    cout << "r2 area: " << r2.area() << endl;
    cout << "r3 area: " << r3.area() << endl;
    return 0;
}

Output:

Creating rectangle 1 x 1
Creating rectangle 4 x 4
Creating rectangle 3 x 5
r1 area: 1
r2 area: 16
r3 area: 15

Only the two-argument constructor’s body ever actually runs — that’s why "Creating rectangle..." prints exactly once per object, no matter which constructor the caller used. Delegation keeps the width/height assignment logic in a single place instead of duplicating it in three constructors.

Example 3: The Copy Constructor and Deep Copies

This Box class owns a dynamically allocated array. Its custom copy constructor performs a deep copy so that copies don’t share the same underlying memory.

#include <iostream>
using namespace std;

class Box {
private:
    int* data;
    int size;
public:
    Box(int n) : size(n) {
        data = new int[size];
        for (int i = 0; i < size; i++) data[i] = i * 10;
    }

    // Custom copy constructor: performs a deep copy
    Box(const Box& other) : size(other.size) {
        data = new int[size];
        for (int i = 0; i < size; i++) data[i] = other.data[i];
        cout << "Deep copy made" << endl;
    }

    void set(int index, int value) { data[index] = value; }
    int get(int index) const { return data[index]; }

    ~Box() { delete[] data; }
};

int main() {
    Box original(3);
    Box copy = original;

    copy.set(0, 999);

    cout << "original[0] = " << original.get(0) << endl;
    cout << "copy[0] = " << copy.get(0) << endl;
    return 0;
}

Output:

Deep copy made
original[0] = 0
copy[0] = 999

Box copy = original; invokes the copy constructor (this is initialization, not assignment, even though it uses =). Because the copy constructor allocates a brand new array and copies the values across, modifying copy afterward has no effect on original. If we had relied on the compiler-generated copy constructor instead, both objects would point at the same array, both destructors would eventually call delete[] on it, and the program would crash or corrupt memory.

How Constructors Work Step by Step

When you create an object, the following happens in order, regardless of which constructor overload is chosen:

  • 1. Memory is reserved for the object — on the stack for a local variable, or on the heap if created with new.
  • 2. Base class constructors run first, if the class inherits from another class (not shown in the examples above, but always true for derived classes).
  • 3. Members are constructed in declaration order. For each member, in the order it appears in the class definition, the compiler uses: the corresponding entry in the initializer list, if present; otherwise the member’s default member initializer, if it has one; otherwise the member’s own default constructor.
  • 4. The constructor body executes, now that every member is a fully constructed, valid object.
  • 5. The object is ready to use and can be passed around, have its methods called, and eventually be destroyed by its destructor.

This is also why the initializer list is more efficient than assigning inside the body for class-type members: the initializer list constructs the member directly with the right value, while assigning in the body first default-constructs the member and then overwrites it — two operations instead of one.

Common Mistakes

Mistake 1: Assigning to a const Member in the Constructor Body

const members must be given a value during initialization; they can never be assigned afterward, including inside the constructor body.

#include <iostream>
using namespace std;

class Config {
private:
    const int maxUsers;
public:
    Config(int m) {
        maxUsers = m; // ERROR: cannot assign to a const member here
    }
    int getMax() const { return maxUsers; }
};

int main() {
    Config c(100);
    cout << c.getMax() << endl;
    return 0;
}

This fails to compile because by the time the body runs, maxUsers already exists (uninitialized, since no initializer was given) and const objects cannot be assigned to. The fix is to initialize it in the member initializer list, which is the only place a const member can receive its value:

#include <iostream>
using namespace std;

class Config {
private:
    const int maxUsers;
public:
    Config(int m) : maxUsers(m) {}
    int getMax() const { return maxUsers; }
};

int main() {
    Config c(100);
    cout << c.getMax() << endl;
    return 0;
}

Output:

100

Mistake 2: Assuming the Initializer List Runs in the Order You Wrote It

Members always initialize in declaration order, not the order written in the initializer list. Writing the list out of order doesn’t change what runs first — it only makes the code misleading, and can hide bugs if one member’s initializer depends on another’s value.

#include <iostream>
#include <string>
using namespace std;

class Trace {
private:
    int first;
    int second;

    static int sideEffect(const string& label, int val) {
        cout << "initializing " << label << endl;
        return val;
    }

public:
    // Written as "second, then first" -- but that is NOT the real order!
    Trace(int x) : second(sideEffect("second", x)), first(sideEffect("first", x)) {}
};

int main() {
    Trace t(5);
    return 0;
}

Output:

initializing first
initializing second

Even though second is written first in the initializer list, first is declared first in the class, so it initializes first — the opposite of what the code visually suggests. The fix is to always write the initializer list in the same order the members are declared, so the code you read matches the code that actually runs:

#include <iostream>
#include <string>
using namespace std;

class Trace {
private:
    int first;
    int second;

    static int sideEffect(const string& label, int val) {
        cout << "initializing " << label << endl;
        return val;
    }

public:
    // Written in the same order as the member declarations -- matches
    // the real execution order, so the code is easy to reason about.
    Trace(int x) : first(sideEffect("first", x)), second(sideEffect("second", x)) {}
};

int main() {
    Trace t(5);
    return 0;
}

Output:

initializing first
initializing second

The output is identical here because neither member depends on the other’s value, but if second‘s initializer had read first‘s value while written out of declaration order, it would silently read an uninitialized member — undefined behavior that can produce garbage results or crashes. Most compilers warn about this mismatch with -Wreorder; treat that warning as a bug, not noise.

Best Practices

  • Prefer the member initializer list over assignment in the constructor body — it’s required for const and reference members, and more efficient for class-type members.
  • Write the initializer list in the same order the members are declared, to avoid -Wreorder warnings and confusing code.
  • Mark single-argument constructors explicit unless you deliberately want implicit conversions from that argument type.
  • Use delegating constructors to avoid duplicating initialization logic across overloads.
  • Use default member initializers (int age = 0;) for sensible defaults instead of repeating the same value in every constructor.
  • If a class manually manages a resource (raw pointer, file handle, etc.), write a proper copy constructor that performs a deep copy, or explicitly disable copying with ClassName(const ClassName&) = delete;.
  • Use = default to explicitly request the compiler-generated default or copy constructor when you want to keep it but also declare other constructors.
  • Keep constructor bodies focused on validation or setup logic; let the initializer list handle the actual member construction.

Practice Exercises

  • Exercise 1: Write a Circle class with a private double radius member. Give it a default constructor that sets the radius to 1.0 and a parameterized constructor that accepts a radius. Add a double area() const method (use 3.14159 for pi) and print the area for both a default-constructed circle and one built with radius 2.5.
  • Exercise 2: Write a Temperature class that stores a value in Celsius. Give it three constructors: one that takes no arguments (defaults to 0.0), one that takes a Celsius value, and a static-style pair of named factory-like constructors is not required — instead, add a second constructor overload that takes a Fahrenheit value and a bool flag indicating it’s Fahrenheit, and have it delegate to the Celsius constructor after converting the value. Print the stored Celsius value from each.
  • Exercise 3: Write a Playlist class that owns a dynamically allocated array of string song titles and a count. Implement a constructor that allocates the array, a custom copy constructor that performs a deep copy, and a destructor that frees the memory. In main, create one playlist, copy it, change a song title in the copy, and print both playlists to confirm they don’t affect each other.

Summary

  • A constructor shares its class’s name, has no return type, and runs automatically when an object is created.
  • The compiler generates a default constructor only if you define no constructors of your own.
  • The member initializer list (after the colon) is where members are actually constructed; it’s required for const and reference members and more efficient than assigning in the body.
  • Members always initialize in declaration order, never in the order written in the initializer list.
  • Constructor overloading lets a class offer several ways to create an object; delegating constructors let overloads reuse each other’s logic.
  • explicit prevents a single-argument constructor from being used as an implicit conversion.
  • The compiler-generated copy constructor performs a shallow copy; classes that own resources like dynamic memory need a custom copy constructor that performs a deep copy.
  • = default and = delete let you explicitly request or forbid compiler-generated constructors.