All About Initialization
Let's learn a bit more about initialization.
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 (int
s, char
s, float
s, size_t
s, 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!!!
So is it just random?
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.
In some cases, such as with
bool
values, the indeterminate value could be invalid (and thus cause undefined behavior).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
.
Meh. So we save, like, a fraction of a nanosecond not redundantly initializing it.
What happens when we do want to initialize our primitive with a specific value?
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.
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.
What are some types of constructor you've seen?
A default constructor is one that takes no arguments, such as
Cat()
.And a parameterized constructor is any other constructor that takes one or more arguments as input, such as
Koala(int eucalyptusSnackBudget)
!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
So… primitive function parameters are copy initialized when the function is called.
That's right.
Is the same true for object function parameters?
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 ofbuttercup
, so nothing actually happens tobuttercup
.
- It's also the case that
oldercup
is a copy ofbuttercopy
.- It has to be! By the time
oldercup
is initialized,buttercopy
has been destroyed and deallocated!
- It has to be! By the time
- Now we know that
buttercopy
andoldercup
are initialized using a specific constructor that we are in charge of writing: the copy constructor.
For every class we write, it's our responsibility to figure out what it means to copy an object of that class.
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| }
(When logged in, completion status appears here.)