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.
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?
Reveal 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.
before block
constructor
inside block
destructor
after blockWhat a Constructor Does
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.
Complex()takes no arguments and is the default constructor.Complex(double r)andComplex(double r, double i)are overloads chosen by argument count.- The initializer list after the colon assigns each stored member.
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?
- 1Identify the declaration
The declaration supplies two double arguments, 3.0 and 4.0.
- 2Match the argument list
Two arguments of type double match the constructor
Complex(double r, double i), so that constructor runs. - 3Apply the initializer list
The initializer list sets re to r, which is 3.0, and im to i, which is 4.0.
- 4Read the result
z.re holds 3.0 and z.im holds 4.0. Nothing about the constructor is left to chance.
After construction, z.re is 3.0 and z.im is 4.0.
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.
The Default Constructor and Its Guarantee
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.
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.
- 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.
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.
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?
- 1Declaration
Vector v(3) selects the constructor
Vector(int s)because one int argument is supplied. - 2Acquisition
The constructor asks the free store for three doubles, stores the pointer in elem, sets sz to 3, and fills the slots with zero.
- 3Use
The function reads and writes through the object. The handle stays the same while the data it refers to is available.
- 4Scope ends
The function returns, so the object's lifetime ends and the destructor runs automatically.
- 5Release
The destructor calls delete[] on elem, which gives the three-element array back to the free store.
The array is acquired in the constructor and released in the destructor, with no manual cleanup in the calling code.
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.
| Aspect | Manual acquire and release | RAII |
|---|---|---|
| Where the release is written | In the general-purpose code that uses the resource | Inside the destructor of the abstraction |
| Who must remember to release | Every programmer who uses the resource | Nobody in everyday code; the destructor runs automatically |
| Risk when a path exits early | The release can be skipped on that path | The destructor still runs when the object's lifetime ends |
| Typical result | Higher chance of resource leaks | Fewer 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 likeComplex z; followed by z.Complex(3.0, 4.0); to set the values
Why it failsA 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 insteadSupply 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 likestruct Broken { double re; double im; Broken(double r) : re{r} {} }; Then reading b.im after Broken b{2.0};Why it failsNo 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 insteadAdd 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 likeAssuming a Vector inside a block stays alive until the end of the whole function
Why it failsThe destructor runs as the object's lifetime ends, which follows the scope the object was declared in.
Do this insteadTrace 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 likeA function allocates an array with new and returns early before any delete[] is written
Why it failsThe release depends on the programmer reaching it on every path, which is exactly what RAII avoids.
Do this insteadMove 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
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.
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.