Initializing Heap Memory in Member Functions
Right now, our
Barn
class declaration looks like
class Barn {
public:
Barn(size_t numCows);
void feed();
// Disable default constructor, copying and assignment
Barn() = delete;
Barn(const Barn& other) = delete;
Barn& operator=(const Barn& rhs) = delete;
private:
size_t cowCapacity_;
size_t cowCount_;
Cow** residentCows_; // Points to an array of Cow pointers on the heap
};
And our Barn
class's parameterized constructor looks like
Barn::Barn(size_t numCows) :
cowCapacity_{numCows},
cowCount_{0},
residentCows_{new Cow*[cowCapacity_]}
{
// Nothing (else) to do here!
}
What about the other constructors? Don't we have to do something with them, too?
We sure do! We're going to hold off on those for a little bit, though, and get through all the steps with just the parameterized constructor. So for now we've disabled the other constructors (by using
= delete;
).We promise we'll come back to the default and copy constructors by the end of today's lesson!
Adding Cows to the Barn
We're going to add a new public member function for adding Cows to our Barn. That member function will have to do the following:
- Create a new
Cow
on the Heap - Store the location of the newly-created
Cow
in the first available space inresidentCows_
.
Let's call the new member function addCow
.
Imagine that we've added one Cow
. Then we want our memory to look like this:
If we call addCow
again, it should create a Cow
on the heap, and then should store the location of that new Cow
in memory location h43 (the next entry in the array), like this:
Now we can implement
addCow()
. What information doesaddCow
need to be able to create the newCow
?Ooh! Our
Cow
constructor needs astring
representing theCow
's name and asize_t
representing the number of spots theCow
has. So it seems like we need both of those to be able to create aCow
.That works!
Let's create a version of addCow
that has all the information it needs to create a new Cow
. After it puts the new Cow
in, it can update the number of Cow
s we have:
void Barn::addCow(string cowName, size_t numSpots) {
// Body goes here...
++cowCount_;
}
We want to create a new Cow
, get its location on the heap, and store that location in the first empty spot in the array that residentCows_
points to. We can do all of that with
residentCows_[cowCount_] = new Cow{cowName, numSpots};
Hay, won't this break if the
Barn
is already full?It will! On Homework 4, you'll work through a class that grabs more memory when it runs out of space. For now, let's just check and throw an exception if our
Barn
is full.
void Barn::addCow(std::string cowName, size_t numSpots) {
if (cowCount_ >= cowCapacity_) {
throw std::length_error("Barn is full!");
}
residentCows_[cowCount_] = new Cow{cowName, numSpots};
++cowCount_;
}
Adding an Existing Cow
What if we've already created a
Cow
, and then we want to put it into ourBarn
?Ooh! Good idea!
Let's write another version of addCow
that takes a reference to a Cow
as its parameter and puts a copy of that Cow
in the Barn
: Barn::addCow(const Cow& existingCow)
.
We can use the copy constructor to make the copy!
void Barn::addCow(const Cow& existingCow) {
if (cowCount_ >= cowCapacity_) {
throw std::length_error("Barn is full!");
}
residentCows_[cowCount_] = new Cow{existingCow};
++cowCount_;
}
Why did we put the
Cow
copy on the heap?Because we want it to persist after this function returns! If we had made a copy on the stack, it would be deallocated as soon as the function returns. Let's be super clear about this…
// This code WILL NOT WORK correctly
void Barn::addCow(const Cow& existingCow) {
if (cowCount_ >= cowCapacity_) {
throw std::length_error("Barn is full!");
}
Cow newCow{existingCow};
residentCows_[cowCount_] = &newCow;
++cowCount_;
}
// When the function exists, newCow will be deallocated, and
// residentCows_[cowCount_] will be a dangling pointer to stack memory
// that no longer belongs to us and does not contain a Cow any more.
Our pointers in the residentCows_
array should always Cow
s on the heap (allocated using new
), not on the stack. If we put a pointer to a stack-allocated Cow
in the array, we're asking for trouble!
It's really important to grasp the difference between
Cow newCow{existingCow};
(which creates a newCow
on the stack referred to by the variable namenewCow
), andnew Cow{existingCow};
(which creates a newCow
on the heap and returns a pointer to it).
Try It Out
You can check out the code here (as always, when you run the code be sure to expand the output window at the bottom to see all the output printed).
Hopefully you can see why Duck's idea of trying to store a pointer to the original “Daisy”
Cow
would be a bad idea.
The Principle of Ownership
Why are we making a copy at all? Why not just put a pointer to the
Cow
we were given in the array?It doesn't belong to us. (We don't even have write access to it!)
Exactly. Some other code elsewhere is responsible for that
Cow
. We have no idea where it is allocated or when it will be destroyed.By making a copy, our
Barn
class has full responsibility for theCow
s it stores. It owns them.
The principle of ownership is a key concept in C++ programming practice (and applies in other languages, too). In our code here, each instance of the Barn
class owns the Cow
s in its Barn
. In other words, it's responsible for creating them, and for destroying them.
The C++ language doesn't force us to follow the principle of ownership. We could do things differently if we wanted, but experience has shown that when we don't have a clear sense of which parts of a program hold responsibility for resources, programmers become confused and make mistakes. The idea of classes owning their data is a simple rule that reduces these kinds of mistakes.
(When logged in, completion status appears here.)