What About Data Members?
I think I'm feeling better about lifetime for individual objects, but what about data members?
What about them?
Like, what if one class has an instance of another class as one of its data members?
Mind. Blown. 🤯
Data members get their object-lifetime cues from the object that they're part of.
Let's walk through what happens!
Allocation Phase
The space for data members is allocated when the space for the object itself ls allocated.
In fact, the space for an object is the space for its data members!
Initialization Phase
The data members of a class are initialized as the very first step in the object's own initialization phase.
Crucially, remember that the data members have to be initialized in the member initialization list at the top of the constructor.
Anything not explicitly initialized in the initialization list will be default initialized/constructed!
The data members are initialized in the order they're declared in the class. The member initialization list should be in the same order. If it isn't, you'll probably get a compiler warning letting you know.
Use Phase
The data members of a class are used through the member functions of the class, or sometimes directly if they are public.
Destruction Phase
The data members of an object are destroyed when the object itself is destroyed, specifically, at the closing }
of the destructor.
It destroys the data members in reverse order from the order they're declared in the class, thus the last data member constructed is the first one destroyed.
Crucially, the destructor that the compiler synthesizes is identical to a destructor with no code in the body. Given the rule above, it “does nothing” (the empty body) and then calls the destructor for each data member.
If that behavior is not sufficient, then you need to write a custom destructor.
We'll see a situation where that's necessary in a future lesson!
Deallocation Phase
The data members are deallocated when the object is deallocated, since the memory for the data members is the memory for the object.
Practice Exercise
Suppose we have the Cow
class from before, which has print statements in its constructors and destructor.
We'll make another class, Barn
, that has a Cow
as its only data member:
class Barn {
public:
Barn() = delete;
Barn(const Cow& cow);
Barn(const Barn& other) = default;
~Barn() = default;
private:
Cow residentCow_;
};
Barn::Barn(const Cow& cow) : residentCow_{cow} {
// Nothing (else) to do
}
We then use the class in the following program:
int main() {
Cow bessie;
Barn olin{bessie};
Barn moogregor{olin};
return 0;
}
We'll get the following output:
Cow default constructor called
Cow copy constructor called
Cow copy constructor called
Cow destructor called
Cow destructor called
Cow destructor called
We can reason about the output like this:
- The default constructor call is for
bessie
. - The first copy constructor call is for initializing the
residentCow_
data member ofolin
as a copy ofbessie
. - The second copy constructor call is for initializing the
residentCow_
data member ofmoogregor
as a copy ofolin
'sresidentCow_
. - The three destructor calls are for
bessie
,olin
'sresidentCow_
, andmoogregor
'sresidentCow_
.
Okay, we've made a lot of progress. Let's head up to the parent page to see what we've learned, and maybe take a break before we move on to our final chunk of material for this lesson.
(When logged in, completion status appears here.)