Concepts / Constructors and Initialization

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.

  • Programming

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.

Vector handlefixed sizeElement storageseparate block of memoryv[0]elementelempointer to element storagev[1]elementsz6v[2]elementv[3]elementv[4]elementv[5]elementrefers tocontains
Where does the fixed-size handle live, where does the variable-size element data live, and how is the handle connected to that data? This diagram shows a six-element Vector.

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.

Key point

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.

Notes on the code
  • 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.

Notes on the code
  • 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.

v[0]stored valuev[1]stored valueindex 2v[2]v[2]5v[3]stored valuev[4]stored valuev[5]stored valueoperator[] returns a reference to
When v[2] = 5 executes, which element's storage changes? The subscript yields a location that can be assigned to, so the write lands in the stored element.

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
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.

Notes on the code
  • 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.

Notes on the code
  • 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
Worked example

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?

  1. 1
    Start

    i is 0, and v.size() is 6, so 0 < 6 is true and the body runs.

  2. 2
    Iteration with i = 0

    v[0] = 2 * 0, so element 0 becomes 0.

  3. 3
    Iteration with i = 2

    v[2] = 2 * 2, so element 2 becomes 4.

  4. 4
    Last iteration with i = 5

    v[5] = 2 * 5, so element 5 becomes 10.

  5. 5
    Stop

    i becomes 6, the condition 6 < 6 is false, and the loop ends. The loop ran six times.

Result

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.

Definition

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.

DeclarationArgument listConstructor that is selected
Vector v(6);one value, 6the Vector constructor that takes a size
complex a;nonecomplex(), the default constructor
complex b{3.0, 4.0};two values, 3.0 and 4.0the 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.

Key point

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.

Notes on the code
  • 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
complex are 0, im 0complex bre 3.0, im 4.0next declaration
After each declaration, what real and imaginary values does each complex object hold? Read the steps in order.
Debugging

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 like

    complex b{3.0, 4.0}; then expecting b to hold 0 and 0

    Why it fails

    The argument list selects the constructor. Two arguments select the constructor that takes two parameters, not complex().

    Do this instead

    Match 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 like

    Assuming that changing r inside the body changes what the object holds

    Why it fails

    r and i are placeholders for incoming values. The initializer list copies them into the stored parts re and im.

    Do this instead

    Look 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 like

    Expecting that a larger Vector makes the handle itself bigger

    Why it fails

    The handle has a fixed pair of members. The elements sit in a separate block that elem points to.

    Do this instead

    Think 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 like
    double x = v[2]; x = 5; expecting v[2] to become 5
    Why it fails

    double x = v[2]; copies the value into x. Only assigning through v[2] itself writes to the stored element.

    Do this instead

    Write 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 like
    for (int i = 0; i < 6; ++i) on a Vector built with a different size
    Why it fails

    The loop stops at 6 whatever the object holds, so it can run past the end or stop early.

    Do this instead

    Use i < v.size() so the loop follows the count the object holds at that moment.

Practice Tracing Construction

EASY

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.
MEDIUM

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}.

Related concepts

More concepts starting with C