You’ve
seen how important it is in C++ to guarantee proper initialization, and
it’s no different during composition and inheritance. When an object is
created, the compiler guarantees that constructors for all its subobjects are
called. In the examples so far, all the subobjects have default constructors,
and that’s what the compiler automatically calls. But what happens if
your subobjects don’t
have default constructors, or if you want to change a default argument in a
constructor? This is a problem because the new class constructor doesn’t
have permission to access the
private
data elements of the subobject, so it can’t initialize them directly.
The
solution is simple: Call the constructor for the subobject. C++ provides a
special syntax for this, the
constructor
initializer list.
The form of the constructor initializer list echoes the act of inheritance.
With inheritance, you put the base classes after a colon and before the opening
brace of the class body. In the constructor initializer list, you put the calls
to subobject constructors after the constructor argument list and a colon, but
before the opening brace of the function body. For a class
MyType,
inherited from
Bar,
this might look like
MyType::MyType(int
i) : Bar(i) { // ...
if
Bar
has a constructor that takes a single
int
argument.
Member
object initialization
It
turns out that you use this very same syntax for member object initialization
when using composition. For composition, you give the names of the objects
rather than the class names. If you have more than one constructor call in the
initializer list, you separate the calls with commas:
This
is the beginning of a constructor for class
MyType2,
which is inherited from
Bar
and
contains a member object called
memb.
Note that while you can see the type of the base class in the constructor
initializer list, you only see the member object identifier.
Built-in
types in the initializer list
The
constructor initializer list allows you to explicitly call the constructors for
member objects. In fact, there’s no other way to call those constructors.
The idea is that the constructors are all called before you get into the body
of the new class’s constructor. That way, any calls you make to member
functions of subobjects will always go to initialized objects. There’s no
way to get to the opening brace of the constructor without
some
constructor being called for all the member objects and base-class objects,
even if the compiler must make a hidden call to a default constructor.
This is a further enforcement of the C++ guarantee that no object (or part of
an object) can get out of the starting gate without its constructor being called.
This
idea that all the member objects are initialized by the opening brace of the
constructor is a convenient programming aid, as well. Once you hit the opening
brace, you can assume all subobjects are properly initialized and focus on
specific tasks you want to accomplish in the constructor. However,
there’s a hitch: What about embedded objects of built-in types, which
don’t
have
constructors?
To
make the syntax consistent, you’re allowed to treat built-in types as if
they have a single constructor, which takes a single argument: a variable of
the same type as the variable you’re initializing. Thus, you can say
class X {
int i;
float f;
char c;
char* s;
public:
X() : i(7), f(1.4), c(‘x’), s("howdy") {}
// ...
The
action of these “pseudoconstructor calls”
is to perform a simple assignment. It’s a convenient technique and a good
coding style, so you’ll often see it used.
It’s
even possible to use the pseudoconstructor syntax when creating a variable of
this type outside of a class:
int
i(100);
This
makes built-in types act a little bit more like objects. Remember, though, that
these are not real constructors. In particular, if you don’t explicitly
make a pseudo-constructor call, no initialization is performed.