Classes and Objects

Constructors

Constructing an object does not happen in one step. The compiler allocates a chunk of raw memory large enough to contain each of the data members of the class, and since the chunk is fresh and new, it is filled with whatever happened to be in the memory when the compiler comes along with it. At this point, the chunk is not a valid object.

Just after the memory has been allocated, the compiler searches for an appropriate constructor, and if found, calls it.

Once the constructor has finished, the object is available for use in the program.

A constructor is a member function with exactly the same name as the class and no return type. The omission is not a mistake; the constructor is no more asked to generate a value. Its only task is to transform the raw memory into a well-formed object.

Since it is the compiler which takes care of calling it, there are no parentheses written manually at the object creation site.

The language inserts them by itself.

If you want to see the automatic invocation in action, you can add a simple output statement in the constructor of the following program.

#include <iostream>

// Define the members that belong to this type
class Tracer
{
public:
    Tracer()
    {
        std::cout << "constructor running\n";
    }
};

int main()
{
    std::cout << "before object\n";
    Tracer t{};
    std::cout << "after object\n";

    return 0;
}

Somewhere in the entry to main is a call to write the string "before object". When the statement Tracer t{}; is encountered, the compiler first has to allocate a block of memory to hold the object, then it calls the constructor method for the object type, Tracer(). The constructor then executes its body, writing the string "constructor running" to the console. Only after the constructor has returned can the call within main to write the string "after object" be executed.

The identical automatic call takes place when the constructor takes arguments. The arguments that are listed in the braced initializer list are passed to the constructor.

#include <iostream>

class Point
{
// Members below this label are private
private:
    int x{};
    int y{};

public:
    Point(int xVal, int yVal)
    {
        x = xVal;
        y = yVal;
        std::cout << "Point constructed at " << x << ',' << y << '\n';
    }

    void print()
    {
        std::cout << x << ',' << y << '\n';
    }
};

int main()
{
    Point p{ 3, 5 };
    p.print();

    return 0;
}

The statement Point p{ 3, 5 }; again begins by allocating memory. The compiler then finds the constructor for a Point that takes two ints and calls it, giving 3 for xVal and 5 for yVal. When the constructor executes, it copies those values into the private members, and prints out a message. At the end of the constructor's execution, the object p is complete and the subsequent call to print is just to read the members that were initialized by the constructor.

If the class declares any constructor, then the compiler will not generate an object unless it can invoke a constructor with a parameter list that exactly matches the arguments at the point of the new statement. In the previous class definition, if you then place Point q{}; in the code it will generate a compiler error because there is no matching constructor with an empty argument list. This is always the case: the language always accepts the constructor you have written, never invents one.

The constructor operates on uninitialized storage. That's why the body of the constructor is the first, and frequently the only, place where data members are given initial values. Reading a member before the constructor has finished would entail reading uninitialized storage, which is undefined behavior. The language guarantees that the constructor finishes before the new object is made available to the rest of the program.

A constructor can be defined outside the class once a matching one is declared inside the class body. The declaration simply advises the compiler that the constructor exists; the definition provides the body. The constructor is called in the same way regardless of where it is defined.

#include <iostream>

class Counter
{
private:
    int value{};

public:
    Counter(int start);
    // Declare print before code needs to call it
    void print();
};

Counter::Counter(int start)
{
    value = start;
    std::cout << "Counter starts at " << value << '\n';
}

void Counter::print()
{
    std::cout << value << '\n';
}

int main()
{
    Counter c{ 10 };
    c.print();

    return 0;
}

The class contains the line Counter(int start);. The following line defines the class and the use of the scope resolution operator attaches the definition of the constructor to the declaration inside the class. When main runs Counter c{ 10 };, the compiler still reserves memory and calls the constructor; the only alteration is that the constructor code now lives outside the class. The output confirms this, showing that the constructor has run and that the subsequent call to print finds the value stored in the constructor.

Exercise: ConstructorsStarter code, hints and solutionOpenClose
Guided practiceBeginner · 10–15 min

Practice: Constructors

Work through the mission, reveal hints only when needed, and compare your result with the model solution.

Your challenge

Create another Counter initialized with 4 and call its print function. Predict when its constructor message appears relative to its print.

Mission checklist

  1. Run the starter as a baseline, then predict the change requested in Your challenge.
  2. Make that change in your editor; use the lesson-specific hints if you need help.
  3. Compile and run your answer, then compare with the worked solution and explain the result.

Starter program

This is the working baseline, not the completed answer. Compile as C++20, then make the change described above.

#include <iostream>

class Counter
{
private:
    int value{};

public:
    Counter(int start);
    // Declare print before code needs to call it
    void print();
};

Counter::Counter(int start)
{
    value = start;
    std::cout << "Counter starts at " << value << '\n';
}

void Counter::print()
{
    std::cout << value << '\n';
}

int main()
{
    Counter c{ 10 };
    c.print();

    return 0;
}

Hints

Hint 1

Counter c{10} calls the constructor before c.print(). The constructor assigns start to value and prints its own message.

Hint 2

Add a second Counter with a different argument and follow construction in declaration order. A constructor has no return type, unlike an ordinary member function.

Worked solution

A second Counter is constructed with 4. Its constructor message precedes second.print(), and the original counter retains its separate value of 10.

#include <iostream>

class Counter
{
private:
    int value{};

public:
    Counter(int start);
    // Declare print before code needs to call it
    void print();
};

Counter::Counter(int start)
{
    value = start;
    std::cout << "Counter starts at " << value << '\n';
}

void Counter::print()
{
    std::cout << value << '\n';
}

int main()
{
    Counter c{ 10 };
    c.print();

    Counter second{4};
    second.print();
    return 0;
}

Example output

Counter starts at 10
10
Counter starts at 4
4

0 comments

No comments yet. Be the first.

Add a comment

About CppLearn

CppLearn is an interactive, free reference for modern C++. In each lesson, you'll find out how the language works, rather than just lines of code that you can copy and paste. We cover concepts starting from beginner lessons on fundamentals and proceed to templates, move semantics, smart pointers, and areas that usually trap or scare C++ learners.