Constructors and Initialization
A constructor is the mechanism that arranges for that starting value to be set at the moment an object comes into existence.
Why a Program Must Know How Many Values It Holds
A program that works with a group of numbers needs to know how many numbers it is holding. In C++, once you invent a type of your own, you can package that group inside one variable and let the type answer questions about it. This article follows a single running example: a Vector variable that holds a collection of double values.
The article is built around one question that you should keep in mind as you read: when a Vector is created, where does its information live, and which parts of it can the rest of the program touch? Trace the code step by step rather than reading it all at once. The place where your expectation first differs from the actual behavior is the place to look when something goes wrong.
A Vector Is a Small Handle
A Vector object is best understood as a small handle. The handle has two members. The first, elem, is a pointer to where the actual elements are stored. The second, sz, records how many elements there are. The elements themselves are not stored inside the handle. They sit in a separate block of memory that the object refers to.
Notice in the diagram that the fixed-size handle is drawn on the left and the element cells are drawn separately on the right, linked only by the pointer. The handle always consists of the same two members. A Vector of 6 elements and a Vector of 600 elements have the same handle; what changes is how much data the handle points to.
The handle's size is fixed, but the amount of data it describes is not. That amount can differ from one Vector to the next, and can even differ for the same Vector at different times.
Creating a Vector with an Initial Size
Defining a variable of a user-defined type looks just like defining a variable of a built-in type: the type name comes first and the variable name comes after it. The part in parentheses is passed to the type so that the new object can set itself up. In the declaration below, the object is created with room for six elements.
- Vector is the type name, which comes first, as with a built-in type
- v is the variable name
- (6) is the argument list, passed to the type so the object can set itself up with six elements
The same syntax works with any size you choose. The number is not fixed to six. Because a Vector can be created with a size that is only known while the program runs, the general form uses a size variable in place of the literal number.
- n holds a size that could be read from input or computed
- Vector v(n) creates an object whose element count is the value of n at this moment
Reading and Writing Elements
Elements are reached through a subscript function named operator[]. You write it the same way you would index an array, as v[i]. The function does not hand back a copy of the element. It returns a reference to the appropriate element, which means that reading and writing through v[i] both act on the stored value itself.
What do you think happens?
After v[2] = 5; runs, which stored element has changed, and why can the left side of the assignment be written at all?
Reveal answer
The element at index 2 changes to 5. Because operator[] returns a reference to the stored element rather than a copy, the assignment writes into that stored value.
A copy would be a temporary that disappears after the statement, so assigning to it would not affect the Vector. Returning a reference is what makes the write visible through v afterwards.
- v[0] = 1.5 writes through the reference returned by operator[]
- v[2] = 5 changes the stored element at index 2
- double x = v[2] reads the stored value into x; x is a copy, but v is unchanged by it
Looping Over Elements with size()
A loop over the elements needs to know where to stop. The size() function is supplied for this purpose: it gives the number of elements in the object. Calling it means writing the parentheses after the name, as in v.size(). The result is the count the object holds at that moment, so the loop stops at the right place even though the program never states the number six again.
- The loop index i starts at 0
- The condition i <
v.size()stops the loop once i reaches the element count - The body writes through v[i], so each iteration changes one stored element
Tracing a loop over a six-element Vector
Vector v(6); followed by the loop above. What does the loop write, and how many iterations run?
- 1Start
i is 0, and
v.size()is 6, so 0 < 6 is true and the body runs. - 2Iteration with i = 0
v[0] = 2 * 0, so element 0 becomes 0.
- 3Iteration with i = 2
v[2] = 2 * 2, so element 2 becomes 4.
- 4Last iteration with i = 5
v[5] = 2 * 5, so element 5 becomes 10.
- 5Stop
i becomes 6, the condition 6 < 6 is false, and the loop ends. The loop ran six times.
The loop runs six times and writes 0, 2, 4, 6, 8, 10 into elements 0 through 5.
What a Constructor Does
Every object of a class holds data, and that data has to start somewhere. Consider a complex number class that stores a real part and an imaginary part. Before any arithmetic can happen, each stored part needs a value. A constructor is the mechanism that arranges for that starting value to be set at the moment an object comes into existence. It is a member function that initializes objects of a class, and it is selected by the arguments supplied at the declaration.
A constructor is a member function that initializes objects of a class. It has the same name as the class, has no return type written in front of it, and is chosen by the argument list given in the declaration.
Read a constructor's definition by looking at its parts. The member initializer list after the colon, such as :re{r}, im{i}, is what assigns the incoming values into the stored parts. The parameter names r and i are only placeholders for the values passed in. They are not the stored members themselves.
Matching Arguments to Constructors
The same name can appear on several constructors. This is the feature called overloading: one name, several constructors, with the compiler choosing among them by the arguments present. The source states that constructors can be overloaded to accept different numbers and types of arguments, and the complex class has three constructors that show this rule in action.
| Declaration | Argument list | Constructor that is selected |
|---|---|---|
| Vector v(6); | one value, 6 | the Vector constructor that takes a size |
| complex a; | none | complex(), the default constructor |
| complex b{3.0, 4.0}; | two values, 3.0 and 4.0 | the complex constructor that takes a real part and an imaginary part |
The compiler picks the constructor whose parameter list matches the arguments in the declaration.
The rule is simple to apply. Count and type the arguments in the declaration, then find the constructor whose parameter list accepts exactly those arguments. The arguments map to the parameters in order: the first argument supplies the first parameter, and so on.
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(). 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 complex() 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 {0,0}.
The guarantee is about the class as a whole. Any variable of type complex that is declared without arguments starts with a defined value. Code that reads its parts therefore does not depend on luck about what memory happened to contain.
Tracing a Complex Number Step by Step
When a value from a constructed object is not what you expected, the fastest route is to trace which constructor ran and what it assigned. Work the following case the way a debugger would, one declaration at a time, and check your expectation against each step.
- complex a; has no argument list, so the default constructor
complex()runs - complex b{3.0, 4.0}; has two arguments, so the constructor with a real and an imaginary parameter runs
Suppose a learner expects b to hold 0 and 0 because it was declared near a default object, and prints b and sees 3.0 and 4.0. The trace shows the cause: the declaration complex b{3.0, 4.0}; supplied two arguments, so the constructor with the real and imaginary parameters ran and its initializer list stored 3.0 and 4.0 into the parts. The default constructor was never called for b. Matching the argument list to the constructor finds the source of the value immediately.
Mistakes Beginners Make with Construction
Common mistake Assuming every declaration calls the default constructor
What it looks likecomplex b{3.0, 4.0}; then expecting b to hold 0 and 0
Why it failsThe argument list selects the constructor. Two arguments select the constructor that takes two parameters, not
complex().Do this insteadMatch the argument list to a constructor's parameter list before predicting the stored values.
Common mistake Treating the parameter names as the stored members
What it looks likeAssuming that changing r inside the body changes what the object holds
Why it failsr and i are placeholders for incoming values. The initializer list copies them into the stored parts re and im.
Do this insteadLook at the member initializer list to see where each incoming value is stored.
Common mistake Thinking the Vector handle holds the elements
What it looks likeExpecting that a larger Vector makes the handle itself bigger
Why it failsThe handle has a fixed pair of members. The elements sit in a separate block that elem points to.
Do this insteadThink of the handle as fixed in size and the element storage as the part that varies.
Common mistake Assigning through a copy of an element
What it looks likedouble x = v[2]; x = 5; expecting v[2] to become 5Why it failsdouble x = v[2]; copies the value into x. Only assigning through v[2] itself writes to the stored element.
Do this insteadWrite v[2] = 5; directly, because operator[] returns a reference to the stored element.
Common mistake Hard-coding the element count in a loop
What it looks likefor (int i = 0; i < 6; ++i) on a Vector built with a different sizeWhy it failsThe loop stops at 6 whatever the object holds, so it can run past the end or stop early.
Do this insteadUse i < v.size() so the loop follows the count the object holds at that moment.
Practice Tracing Construction
A program declares Vector v(4); and then runs a loop over v.size() that sets v[i] = i + 1 for each index. Before you check, predict: how many iterations run, and what does the element at index 3 hold at the end? Explain which part of the code decided the answer.
Hints
- Start with the value that
v.size()returns right after Vector v(4); runs. - Remember that operator[] returns a reference, so each assignment writes into the stored element.
- The last valid index is
size()minus one.
Given the declarations complex p; and complex q{1.5, -2.0};, state which constructor runs for each one, the values p holds afterwards, and the values q holds afterwards. Then describe how you would locate the source of a wrong value in q.
Hints
- Match each argument list to a constructor's parameter list.
- Argument-free declarations are the ones the default constructor guarantees.
Key Takeaways
- A Vector is a fixed-size handle with an elem pointer and a sz count; the elements live in separate storage that the handle refers to.
- A declaration such as Vector v(6); passes its argument list to the type so the object can set itself up with that size.
- operator[] returns a reference, so v[i] reads and writes the stored element, and
size()is the correct stopping point for loops. - A constructor is a member function that initializes an object, and the argument list in the declaration selects which constructor runs.
- A default constructor is callable with no arguments, and defining one guarantees that argument-free declarations start at a defined value such as {0,0}.