AlgoMaster Logo

Constructors

High Priority19 min readUpdated June 6, 2026
Listen to this chapter
Unlock Audio

A constructor is a special member function that runs automatically when an object is created. It exists so that an instance starts life in a valid, fully-initialized state instead of holding whatever bits happened to be in memory. This lesson covers default constructors, parameterized constructors, overloading them, delegating one constructor to another, and the = default and = delete controls that decide which constructors the compiler generates.

Why Constructors Exist

A class that only exposes data members and relies on the caller to set them is fragile. If a caller forgets one field, the object is half-initialized. If two callers disagree on what "valid" looks like, every site that touches the type has to repeat the same logic.

The stock field was never set, so reading it is undefined behavior. The same problem returns every time someone creates a Product. A constructor fixes this by making "create an instance" and "set its fields to valid values" the same step.

The constructor's job is to take whatever arguments the caller supplies and put the object into a known good state. After the constructor finishes, the object is ready to use. Nothing can create a Product without going through some constructor, which means the type controls its own invariants.

Initialization vs Assignment

Inside a constructor body, statements like name = productName; look like initialization but they are actually assignment. The data members were already constructed before the body started running, using their default constructors. The body then overwrites them.

For built-in types like int and double, the cost of one assignment is the same as one initialization, so this works. For class-type members like std::string, the body-assignment pattern does extra work: the string is first default-constructed (allocating an empty state), then assigned to (potentially freeing the empty state and allocating again).

Body-assignment of class-type members does two operations: default-construct, then assign. Member initializer lists (covered in the C++ Advanced section) construct each member with its final value in one step, skipping the default-construct.

This lesson uses body-assignment because it is the most common pattern beginners read in real code. A later chapter shows the initializer list syntax (Product(...) : name(n), price(p), stock(s) {}) which is the preferred form once you are comfortable with it.

The Default Constructor

A constructor that takes no arguments is called a default constructor. If a class has one, you can create an instance without supplying any values.

The line Product placeholder; calls the default constructor. No parentheses, no arguments. After it returns, every field has a meaningful value.

A common pitfall: writing Product placeholder(); does not create an object. It declares a function named placeholder that takes no arguments and returns a Product. This is called the most vexing parse. The fix is to drop the parentheses or use braces:

Compiler-Generated Default Constructor

If you write no constructors at all, the compiler generates one for you. This implicit default constructor does the minimum: it default-constructs each data member. For class-type members like std::string, that means calling their default constructor. For built-in types like int and double, it does nothing, leaving them uninitialized.

The name came out empty because std::string defaults to empty. The price and stock show garbage because the compiler-generated default constructor never touched them. Many bugs in early C++ code come from assuming the compiler will zero things out. It will not.

The fix is to make the compiler-generated constructor produce zeroed numerics by giving each data member an in-class initializer:

In-class initializers act as the default value whenever a constructor does not set the field some other way. With these in place, the compiler-generated default constructor produces a clean, fully-initialized object every time.

When the Compiler Removes the Default Constructor

The implicit default constructor is generated only when the class does not define any other constructor. As soon as you write any constructor of your own, the implicit default disappears.

Compiler error (g++):

The moment you wrote Product(const std::string&, double, int), the compiler stopped generating a free Product(). The reasoning is that if you cared enough to write a constructor, you are also responsible for deciding whether a no-argument version makes sense. If you want both, you have to write the default one yourself or ask the compiler to put it back (covered later in this lesson with = default).

Parameterized Constructors

A constructor that takes one or more arguments lets the caller supply data at construction time. You have already seen this pattern, but it is worth pinning down a few details.

Strings are passed by const reference because copying a std::string allocates memory, and the constructor only needs to read from the argument. Integers are passed by value because copying a 4-byte int is essentially free.

Three syntaxes call a parameterized constructor:

All three call the same constructor. Braces are the most modern choice and avoid the most vexing parse, so many style guides prefer them. The parentheses form appears in older code and in most textbooks.

Default Arguments in Constructors

A constructor can declare default values for its parameters, just like any other function. This lets the same constructor act as a default constructor when called with fewer arguments.

One constructor with default arguments is often enough to cover three or four usage patterns. The same rules as for any other function apply: defaults must appear on the trailing parameters, you cannot have a gap in the middle, and the defaults are picked left-to-right.

Constructor Overloading

A class can have more than one constructor as long as their parameter lists are different. The compiler picks which constructor to call based on the types and number of arguments at the call site. This is constructor overloading.

Four constructors, each one a different way to build a Product. The compiler matches the arguments to whichever overload fits best. If two overloads match equally well, the call is ambiguous and the compiler refuses to compile it.

A common ambiguity trap shows up when default arguments and overloading mix:

What is wrong with this code?

Order() and Order(int count = 0) are both callable with zero arguments, so the compiler cannot pick. Either remove the default value from the second constructor or remove the first constructor.

Fix:

Overloading is the right tool when each constructor does something genuinely different. Default arguments are the right tool when one constructor body covers all cases and needs flexible inputs. Mixing them creates ambiguities.

Constructor overload resolution happens at compile time, not runtime. Once compiled, calling an overloaded constructor is no slower than calling a single constructor. The cost is paid in code complexity and compile time, not by the program.

Delegating Constructors

When you have several overloaded constructors, the bodies often repeat the same logic. C++11 introduced delegating constructors, which let one constructor call another constructor of the same class to do the actual work. This removes the duplication.

The syntax is OtherConstructor(args) in the position where a member initializer list would normally go. The delegated-to constructor runs first, fully constructs the object, and then the delegating constructor's body runs (if it has one).

A few rules worth knowing:

RuleMeaning
One delegation per constructorA constructor can delegate to at most one other constructor
No mixing with member initA delegating constructor cannot also initialize members directly in the same list
No infinite loopsIf constructor A delegates to B and B delegates back to A, you get undefined behavior
Delegated-to runs firstThe target constructor completes fully before the delegator's body runs

Delegation shares construction logic across overloads. Without it, every overload would duplicate the same body, and a change to validation or default values would mean editing every constructor by hand.

The : name(productName), price(productPrice), stock(productStock) {} syntax used in the primary constructor above is a member initializer list. It initializes each member directly with the given value, skipping the default-construct-then-assign sequence. We cover the full mechanics in a later chapter; for now, just notice that the primary constructor uses it because it pairs naturally with delegation.

= default and = delete

C++11 gave you two keywords that let you control which constructors the compiler generates: = default asks for the implicit version explicitly, and = delete removes a constructor entirely.

= default: Bring Back the Implicit Constructor

When you write a constructor, the compiler stops generating the default one. If you still want the implicit default constructor alongside your custom one, you can ask for it with = default:

The compiler-generated default constructor uses the in-class initializers, so placeholder ends up with "Unnamed", 0.0, and 0 without you writing any constructor body. This is cleaner than writing a manual default constructor that does the same thing.

= default is also a signal to readers of your code. "I thought about this, I want the standard default behavior, and I am making that choice explicit." A manually-written Product() {} looks identical at the call site, but = default tells the compiler to generate whatever the standard rules say, which is safer if the class grows new members later.

= delete: Forbid a Constructor

Sometimes you want a class that genuinely cannot be created with no arguments, or cannot be created at all. = delete makes a constructor unavailable, and any code that tries to use it fails to compile.

If you uncomment the Product placeholder; line, the compiler refuses with a message like:

= delete is useful when the type does not have a sensible no-argument state. A Customer with no name and no email is not really a customer, so deleting the default constructor forces every caller to provide real values.

You can delete any constructor, not just the default one. We will see this again in later lessons when we discuss preventing copies of unique resources, but the syntax is the same: ConstructorSignature = delete;.

FormMeaning
Type() = default;"Compiler, please generate the implicit version of this constructor"
Type() = delete;"Compiler, do not generate it, and reject any code that tries to call it"

Object Construction Sequence

When you write Product mouse("Wireless Mouse", 24.99, 50);, the compiler runs a sequence of steps. Knowing this sequence helps explain why certain rules exist.

Each step matters:

  1. Memory allocation. Storage for the whole object is reserved. For a local variable, that's on the stack. For a new expression, that's on the heap.
  2. Data members are constructed in declaration order. This is the order they appear inside the class, top to bottom. It is not the order they are listed in a member initializer list, and it is not the order arguments arrive in. If you list them in a different order in an initializer list, the compiler still constructs them in declaration order and may warn you.
  3. The constructor body runs. By the time the first line of the body executes, every member is already constructed. The body then assigns or modifies them.
  4. The object is fully alive and can be used by the rest of the program.

The same sequence runs in reverse when the object is destroyed: body runs first (in the destructor), then members are destroyed in reverse declaration order, then memory is freed.

A small example shows the declaration-order rule in action:

The three Logged members are constructed top to bottom, and only then does the Order constructor's body run. If you reordered the members so third came before second in the class body, the output would change to reflect the new declaration order.

There is one more piece worth flagging now without a deep dive. A single-argument constructor like Product(const std::string&) can be triggered by accident when you pass a string where a Product is expected, because the compiler tries to convert. The explicit keyword turns off this implicit conversion. The "Explicit Constructors" chapter later in the section covers when to use it and what bugs it prevents.

Quiz

Constructors Quiz

10 quizzes