CS 70

All About Initialization

  • LHS Cow speaking

    Let's learn a bit more about initialization.

  • Pig speaking

    MORE?? Oh, yeah!!

Initialization of Primitive Types

Primitives types are fundamental building-block types that are not instances of classes. They include all of the numeric types we learned about at the beginning of the semester (ints, chars, floats, size_ts, etc.).

When we create a new value that's a primitive type, we can initialize it with an explicit value or just let it be “default initialized”, which results in it having an indeterminate value. So, for example,

int x;    // Default-initialize x -- danger indeterminate (possibly weird) int value!
float f;  // Default-initialize y -- danger indeterminate (possibly weird) float value!
bool b;   // Default-initialize b -- DANGER indeterminate (possibly INVALID) bool value!!!
  • Duck speaking

    So is it just random?

  • LHS Cow speaking

    Default-initialized primitives (and pointers) officially have an “indeterminate value”, which means we're not told to expect anything specific, but it's pretty common to just get whatever bits happen to be in the space that was allocated for it.

  • RHS Cow speaking

    In some cases, such as with bool values, the indeterminate value could be invalid (and thus cause undefined behavior).

  • Hedgehog speaking

    So why does it work like this if it's not great?

The main reason C++ allows this behavior is so you don't have to “pay the cost” of initializing a variable with a value if you don't have to. An example is setting a value inside an if statement, like

int pagesReadToday;
if (library.isOpen()) {
    Book borrowedBook = library.checkOutBook();
    pagesReadToday = borrowedBook.read();
} else {
    pagesReadToday = myKindle.readCurrentBook();
}

where pagesReadToday is default initialized and then immediately set to a value inside the if.

  • Goat speaking

    Meh. So we save, like, a fraction of a nanosecond not redundantly initializing it.

  • Duck speaking

    What happens when we do want to initialize our primitive with a specific value?

  • LHS Cow speaking

    Great question!

Primitives can be also be copy initialized:

int x = 5;     // Copy-initialize x with 5   -- Looks like assignment but isn't
float f{4.2};  // Copy-initialize y with 4.2 -- Alternative brace-based syntax

During copy initialization, the given value is copied into the memory representing the variable, ensuring that it has the same value.

  • RHS Cow speaking

    The same thing happens when we initialize primitive function parameters.

If we have the function

float myFun(int x, float f) {
    return x + f;
}

and we call it as

float result = myFun(5, 4.2);

then the parameters x and f in myFun are copy initialized with the values of the arguments (5 and 4.2 in this case).

Default and Direct Initialization of Objects

Instances of classes are always initialized with a constructor.

A class can define one or more constructors that users are allowed to use to initialize an instance of that class.

  • LHS Cow speaking

    What are some types of constructor you've seen?

  • Cat speaking

    A default constructor is one that takes no arguments, such as Cat().

  • Koala speaking

    And a parameterized constructor is any other constructor that takes one or more arguments as input, such as Koala(int eucalyptusSnackBudget)!

  • LHS Cow speaking

    Yes!

Remember that every object is initialized exactly once! Exactly one constructor will be called for every instance of a class that exists in memory.

For example, you can default initialize an object using the default constructor, as in

Sheep mead;     // A default-constructed sheep
Sheep fluffy{}; // Also a default-constructed sheep

When you initialize an object using a parameterized constructor, it's called direct initialization. It looks like

Cow buttercup{5, 3}; // A Cow initialized to have 5 spots and be 3 years old

Copy Initialization of Objects

There's one more special type of constructor that is specifically for copy initialization of objects.

The copy constructor is a parameterized constructor that takes a const reference to an object of the same class. For example,

Cow(const Cow& other);

The purpose of the copy constructor is to initialize the new Cow object to be just like the one that was passed in!

For our Cow class, we might have

Cow::Cow(const Cow& other) :
    age_{other.age_},
    numSpots_{other.numSpots_} {
    // Nothing else to do
}

You can see that the Cow being constructed will end up with all the same properties as other. That was a pretty simple one, but sometimes copy constructors need to be more complicated.

The copy constructor allows you to copy initialize an object; for example,

Sheep dolly{fluffy};   // dolly is initialized to be a copy of fluffy,
                       // (much like the real Dolly the sheep, cloned in 1996)
Sheep denise = dolly;  // Yes, you can use this syntax too!!!
                       // (Denise is the name of one of the four subsequent clones of Dolly)

Objects as Function Parameters and Return Values

  • Duck speaking

    So… primitive function parameters are copy initialized when the function is called.

  • LHS Cow speaking

    That's right.

  • Duck speaking

    Is the same true for object function parameters?

  • LHS Cow speaking

    Yes!

When a function takes an object as a parameter, that object is copy constructed when the function is called.

When a function returns an object, that return value is copy constructed.

For example, consider this program:

Cow ageCow(Cow buttercopy) {
    buttercopy.setAge(buttercopy.getAge() + 10);
    return buttercopy;
}

int main() {
    Cow buttercup{5, 3};
    Cow oldercup = ageCow(buttercup);
    cout << buttercup.getAge() << " " << oldercup.getAge() << endl;
    return 0;
}

Key Points:

  • We've seen in past lessons that this code will print 3 13.
    • buttercopy is a copy of buttercup, so nothing actually happens to buttercup.
  • It's also the case that oldercup is a copy of buttercopy.
    • It has to be! By the time oldercup is initialized, buttercopy has been destroyed and deallocated!
  • Now we know that buttercopy and oldercup are initialized using a specific constructor that we are in charge of writing: the copy constructor.

  • RHS Cow speaking

    For every class we write, it's our responsibility to figure out what it means to copy an object of that class.

  • LHS Cow speaking

    That determines what it means for objects to be passed as parameters and returned from functions.

Practice Exercise

Consider the following code:

 1| void bothMoo(Cow c1, Cow c2, int numMoos) {
 2|     c1.moo(numMoos);
 3|     c2.moo(numMoos);
 4| }
 5|
 6| int main() {
 7|     Cow adeline;
 8|     Cow bessie = adeline;
 9|     Cow cadewyn{bessie};
10|     Cow daisy{5, 3};
11|     bothMoos(adeline, bessie, 4);
12|     return 0;
13| }

How many times will the default constructor of Cow be invoked in this program? (Just the number.)

How many times will the copy constructor of Cow be invoked in this program? (Just the number.)

(When logged in, completion status appears here.)