CS 70

Initializing Heap Memory in Member Functions

  • LHS Cow speaking

    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!
}
  • Duck speaking

    What about the other constructors? Don't we have to do something with them, too?

  • LHS Cow speaking

    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;).

  • RHS Cow speaking

    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 in residentCows_.

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:

  • LHS Cow speaking

    Now we can implement addCow(). What information does addCowneed to be able to create the new Cow?

  • Cat speaking

    Ooh! Our Cow constructor needs a string representing the Cow's name and a size_t representing the number of spots the Cow has. So it seems like we need both of those to be able to create a Cow.

  • LHS Cow speaking

    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 Cows we have:

void Barn::addCow(string cowName, size_t numSpots) {
    // Body goes here...
    ++cowCount_;
}

What should go in the missing line in the function?

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};
  • Horse speaking

    Hay, won't this break if the Barn is already full?

  • LHS Cow speaking

    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

  • Duck speaking

    What if we've already created a Cow, and then we want to put it into our Barn?

  • LHS Cow speaking

    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_;
}
  • Duck speaking

    Why did we put the Cow copy on the heap?

  • LHS Cow speaking

    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 Cows 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 new Cow on the stack referred to by the variable name newCow), and
  • new Cow{existingCow}; (which creates a new Cow 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).

  • RHS Cow speaking

    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

  • Duck speaking

    Why are we making a copy at all? Why not just put a pointer to the Cow we were given in the array?

  • LHS Cow speaking

    It doesn't belong to us. (We don't even have write access to it!)

  • RHS Cow speaking

    Exactly. Some other code elsewhere is responsible for that Cow. We have no idea where it is allocated or when it will be destroyed.

  • LHS Cow speaking

    By making a copy, our Barn class has full responsibility for the Cows 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 Cows 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.)