Concepts / Constructors and Initialization in C++

Constructors and Initialization in C++

Constructors can be overloaded, which means one name can name several constructors that accept different numbers and types of arguments.

  • Programming

Why an Object Needs a Starting Value

Every object of a class holds data, and that data has to start somewhere. Imagine a complex number class that stores a real part and an imaginary part. Before any arithmetic can happen, each stored part needs a value. This article answers two questions: how a class arranges for a starting value to be set at the moment an object comes into existence, and why that arrangement matters for correctness. The same mechanism also handles the other end of an object's life, where memory the object obtained must be given back.

Before you read the explanation, try to predict the output of the small program below. It defines a class whose constructor and destructor each print one line, and it creates one object inside an inner block. Think about the order in which the lines should appear, then compare your answer with the reveal.

What do you think happens?

In what order do the five lines print: before block, constructor, inside block, destructor, after block?

  • before block, inside block, constructor, destructor, after block

  • before block, constructor, inside block, destructor, after block

  • constructor, before block, inside block, after block, destructor

Reveal answer
Answer:

before block, constructor, inside block, destructor, after block

The constructor runs when the object Noisy n is created, which happens after the first line and inside the inner block. The destructor runs when the inner block ends, so it prints after inside block and before after block.

cpp
Output
before block
constructor
inside block
destructor
after block

What a Constructor Does

Definition

A constructor is a member function that initializes objects of a class. It is selected by the arguments supplied at the point of declaration. Its job is to put every object of the class into a well-defined starting state before any other code touches that object.

A constructor has the same name as its class and no return type written in front of it. It is invoked automatically when an object is created. The programmer does not call it as an ordinary function after the object exists; the declaration itself triggers it.

Two things happen inside a constructor. First, it can allocate resources the object needs, such as memory from the free store. Second, it initializes the members of the object. The member initializer list, written after a colon, is the place where incoming values are assigned into the stored parts. In a constructor written as Complex(double r, double i) : re{r}, im{i}, the parameters r and i are only placeholders for the values passed in. The stored members are re and im, and the initializer list is what copies the values into them.

Following a Complex Number Through Construction

A complex number has two stored parts, so it is a good object for tracing what construction actually produces. The sketch below is an illustrative version of such a class with three constructors. All three share the class name, differ only in their parameter lists, and assign their values through initializer lists.

cpp
Notes on the code
  • Complex() takes no arguments and is the default constructor.
  • Complex(double r) and Complex(double r, double i) are overloads chosen by argument count.
  • The initializer list after the colon assigns each stored member.
Worked example

Tracing the values of a constructed complex number

What values does each member hold after the declaration Complex z{3.0, 4.0}; is executed?

  1. 1
    Identify the declaration

    The declaration supplies two double arguments, 3.0 and 4.0.

  2. 2
    Match the argument list

    Two arguments of type double match the constructor Complex(double r, double i), so that constructor runs.

  3. 3
    Apply the initializer list

    The initializer list sets re to r, which is 3.0, and im to i, which is 4.0.

  4. 4
    Read the result

    z.re holds 3.0 and z.im holds 4.0. Nothing about the constructor is left to chance.

Result

After construction, z.re is 3.0 and z.im is 4.0.

z (Complex)one objectre3.0im4.0has memberhas member
What value does each member of z hold after the declaration Complex z{3.0, 4.0} runs, and where does a value that was never assigned come from?

Choosing a Constructor by Argument List

Constructors can be overloaded, which means one name can name several constructors that accept different numbers and types of arguments. The compiler picks among them by looking at the arguments present in the declaration. The three Complex constructors above are a concrete instance of this rule. The name Complex is the same in all three, and the parameter list is the only thing that distinguishes them.

The selection process can be followed one decision at a time, the same way a debugger would step through it. Start with the declaration. If no arguments are given, the constructor that takes no arguments is the one that matches. If arguments are given, the compiler checks whether their count and types match one of the constructors. When one matches, that constructor runs. When none does, the declaration is rejected at compile time, so the object is never built.

Declarationobject is being createdAny arguments given?Argument-freeconstructor runsfor example Complex()That constructor runsits initializer listassigns membersCount and types matcha constructor?Rejected at compiletimeno object is builtnoyesyesno
Given a declaration with a particular argument list, how does the compiler decide which constructor runs, and what happens when no constructor matches?

The Default Constructor and Its Guarantee

Definition

A constructor that can be invoked without an argument is the default constructor. In the complex class, that role belongs to complex(). Note that the definition describes how a constructor is called, not what it looks like. A default constructor can sit among other constructors that do take arguments. What makes it the default is simply that a caller can write the declaration with nothing supplied.

Key point

Defining a default constructor eliminates the possibility of uninitialized variables of that type, because every argument-free declaration starts at the defined value {0,0}. Any variable of the class declared without arguments begins with a defined value, so code that reads its parts does not depend on luck.

Acquiring and Releasing a Resource

Construction is not only about setting member values. A constructor can also acquire a resource, such as an array allocated with new. The destructor is the matching function that releases it. Its name is a tilde followed by the class name, so a class named Vector has the constructor Vector and the destructor ~Vector. The destructor releases the resource with delete. The two functions work as a pair: whatever the constructor takes from the free store, the destructor is responsible for returning.

The Vector class is the running example. A container is an object that holds a collection of elements, and Vector is simple to understand, provides range-checked access, and can report its size so that you can loop over its elements. It has one serious flaw: it obtains memory for its elements with new, and nothing in the class gives that memory back until a destructor is added. The sketch below is a simplified version of the class, with error handling left out on purpose so that the core idea stays visible.

cpp
Notes on the code
  • The constructor Vector(int s) asks the free store for an array of s doubles.
  • elem stores the pointer to that array and sz records the count.
  • Every slot is filled with zero inside the constructor.
  • The destructor ~Vector() returns the same array with delete[].

Users of Vector never call the destructor themselves. They create and use Vector objects much as they would variables of built-in types, and the cleanup happens without their intervention. The rules for naming, scope, allocation, and lifetime that apply to an int also apply to a Vector, so the destructor runs as the object's lifetime ends.

A language-level garbage collector would be one alternative, but C++ does not depend on one. Some environments have no collector, and sometimes a programmer wants precise control over when memory is freed for logical or performance reasons. The destructor is the mechanism the language provides to guarantee that memory obtained in a constructor is eventually released.

v (Vector)small handleelempointerelements0.0, 0.0, 0.0sz3holdsholdsrefers to
The Vector object is a small handle that holds a pointer and a count, while the elements themselves live on the free store and are reached through that pointer.

Tracing a Vector Through Its Lifetime

Follow a single example through its life. Suppose a program creates a Vector with three elements inside a function, uses it, and then the function returns. The states happen in a fixed order, and each one has a specific effect on the resource.

Worked example

Creating, using, and leaving scope with a Vector

A function declares Vector v(3), uses it, and returns. When is the array acquired and when is it released?

  1. 1
    Declaration

    Vector v(3) selects the constructor Vector(int s) because one int argument is supplied.

  2. 2
    Acquisition

    The constructor asks the free store for three doubles, stores the pointer in elem, sets sz to 3, and fills the slots with zero.

  3. 3
    Use

    The function reads and writes through the object. The handle stays the same while the data it refers to is available.

  4. 4
    Scope ends

    The function returns, so the object's lifetime ends and the destructor runs automatically.

  5. 5
    Release

    The destructor calls delete[] on elem, which gives the three-element array back to the free store.

Result

The array is acquired in the constructor and released in the destructor, with no manual cleanup in the calling code.

Enter blockVector v(3) createdconstructor acquires threedoublesUse velements are read andwrittenLeave blockscope endsDestructor runsdelete[] releases elementsnextnextnextnext
In what order do the constructor and destructor run as an object is created, used, and leaves scope, and when is the resource acquired and released?

RAII and Avoiding Naked new

Resource Acquisition Is Initialization, usually abbreviated RAII, is the technique of acquiring a resource in a constructor and releasing it in a destructor. It is the best-known basis for most C++ general resource management techniques. Its payoff is that it lets code avoid what the source calls naked new and naked delete operations. A naked new is an allocation sitting in general-purpose code, and a naked delete is a release written by hand in that same code.

RAII keeps allocations buried inside the implementation of a well-behaved abstraction, such as Vector, so that everyday code does not have to remember to clean up. Avoiding naked new and naked delete makes code far less error-prone and far easier to keep free of resource leaks.

AspectManual acquire and releaseRAII
Where the release is writtenIn the general-purpose code that uses the resourceInside the destructor of the abstraction
Who must remember to releaseEvery programmer who uses the resourceNobody in everyday code; the destructor runs automatically
Risk when a path exits earlyThe release can be skipped on that pathThe destructor still runs when the object's lifetime ends
Typical resultHigher chance of resource leaksFewer leaks and less cleanup code to write

Manual cleanup compared with RAII

Mistakes Beginners Make with Constructors

  • Common mistake

    Assuming a constructor is called like an ordinary function after the object exists

    What it looks like

    Complex z; followed by z.Complex(3.0, 4.0); to set the values

    Why it fails

    A constructor is invoked as part of creating the object. It is not a member you call later to reset an existing object in this way.

    Do this instead

    Supply the arguments in the declaration, such as Complex z{3.0, 4.0};, so the matching constructor runs when the object is created.

  • Common mistake

    Leaving a member out of the initializer list

    What it looks like
    struct Broken { double re; double im; Broken(double r) : re{r} {} };  Then reading b.im after Broken b{2.0};
    Why it fails

    No value is assigned to im in the constructor, so the member is left without a defined value. Reading it does not produce a guaranteed result.

    Do this instead

    Add im{0} to the initializer list, or provide a constructor that assigns every stored part.

  • Common mistake

    Expecting the destructor to run at an arbitrary moment

    What it looks like

    Assuming a Vector inside a block stays alive until the end of the whole function

    Why it fails

    The destructor runs as the object's lifetime ends, which follows the scope the object was declared in.

    Do this instead

    Trace the scope: the destructor runs when the block or function that declared the object finishes.

  • Common mistake

    Writing new in general code and forgetting the matching delete

    What it looks like

    A function allocates an array with new and returns early before any delete[] is written

    Why it fails

    The release depends on the programmer reaching it on every path, which is exactly what RAII avoids.

    Do this instead

    Move the allocation into a class whose constructor acquires and whose destructor releases.

  • Common mistake

    Believing a class without a default constructor can still be declared without arguments

Practice Tracing a Constructor

EASY

A class Complex has the three constructors Complex(), Complex(double r), and Complex(double r, double i). For each declaration below, name the constructor that runs and state the resulting value of re and im: (a) Complex a; (b) Complex b{5.0, -2.0};. Then explain why a declaration of the form Complex c{1.0, 2.0, 3.0}; would be rejected.

Hints
  • Count the arguments in each declaration and match the count and types against the parameter lists.
  • A declaration with no arguments selects the default constructor, which is the one that takes no parameters.
  • No constructor in the list takes three double arguments, so the argument list matches nothing.
MEDIUM

Explain, in your own words, how the Vector class would behave if its destructor were removed. Identify which step of the lifetime timeline would no longer release the memory obtained by the constructor, and what the handle would still refer to after the object's lifetime ends.

Hints
  • Look at which function is responsible for delete[].
  • Think about what the elem pointer still points to once the handle is gone.

Key Takeaways

  • A constructor is a member function that initializes objects of a class, and the arguments in a declaration select which constructor runs.
  • Constructors can be overloaded, and a default constructor is one that can be called with no arguments; it guarantees that argument-free declarations start with defined values.
  • A constructor can acquire a resource such as an array from the free store, and the destructor, named with a tilde and the class name, releases it with delete.
  • RAII acquires resources in constructors and releases them in destructors, which avoids naked new and naked delete and reduces resource leaks.

Related concepts

More concepts starting with C