Inheritance and Polymorphism
The course is part of this learning path
This course is designed to enhance your object-oriented programming skills by focusing on two concepts: inheritance and polymorphism. We'll cover the key concepts and then put them to practice with a couple of demo projects towards the end of the course.
- Learn about base classes and derived classes and how they are related
- Understand how different base classes can be used to control how derived classes inherit data and behaviors from their base classes
- Understand the fundamentals of polymorphism
- Learn about enumerated types in C++
- Beginner coders, new to C++
- Developers looking to upskill by adding C++ to their CV
- College students and anyone studying C++
To get the most out of this course, you should have a basic understanding of the fundamentals of C++.
In the previous lecture, we discussed inheritance and saw how derived classes can inherit behaviors from their base classes. We modified the dog class so that it overrides the makeNoise method of the animal class to make a dog specific noise such as woof!. In this lecture, we will explore the third primary principle of object-oriented programming that is polymorphism. A lot of students and even seasoned developers have trouble articulating what polymorphism is or how it works. When you're done with this lecture however, I think you will have a good grasp on the basics of polymorphism and how it relates to inheritance. The proper context to discuss polymorphism involves dynamic memory and pointers as well. So, thankfully, we have experience with those now. So, let's continue with our AnimalFun project. So, let's take a look at the main file and modify it to use some dynamic memory allocation. But this time we will do something a little more interesting. We'll see that since a dog is an animal, we can use an animal pointer to point to a dog object and this will be called a polymorphic reference. All right, so here we go, animal pointer, dogPtr = new Dog and it's going to be "Fido", 115, who's a "Golden Retriever." All right. And to make noise since we're using a pointer this time in dynamic memory, I'm going to put a couple of spaces, dogPtr-> makeNoise and endl. And then of course at the very end before return 0, we're going to delete dogPtr because we have to return the memory and then for good practice, we set dogPtr = nullptr. All right, so let's see what we have here. We have an animal pointer but a dog object. So, they both have to makeNoise member function method. So, here's the big question. Which version will be called? Will animals version be called since that's the type of the pointer? Or will dogs version be called since that's the type of the object? So, let's run the project and find out. Debug, start without debugging. Interesting. So, when it doesn't make noise right here we get unknown, interesting. So, instead of woof!, we see unknown. That's really, really interesting. Let's clear it up a little bit to make it obvious what we're doing. I will comment these out, 'Ctrl K + Ctrl C' and then let's run it again so you can see it more clearly maybe, makeNoise is unknown, wow. So, animals version is run. The reason for this is that C++ uses by default what's called static binding or early binding. In order to understand this, we should take a step back and define binding. So, do you remember how we can have function definitions and also function calls or function invocations? These are two separate but of course related things. Binding refers to the process of associating a definition of a function with a function call, that's for member functions or regular function. The runtime system needs to know which definition to use when a function call is made. So, that's what binding is. So, if a function call is bound to a function definition at compile time, it is called static or early binding. This is again the default for C++. For practical purposes, what does this mean for our AnimalFun project? Well, with an animal pointer we can point to any of its descendants. So, we have a dog right now as its only derived class but could have a cat, bird or any number of animal descendants. And remember, one of the advantages we gained by using pointers and dynamic memory is that we can decide what kind of object such as what descendant of animal at runtime or as needed, right? However, without making any modifications, we are stuck with this general unknown noise that the animal class makes when makeNoise is called. So, is there a way to force the dog's version of makeNoise to be called even when we are using an animal pointer. The answer is yes. In order to tell the compiler not to do the binding, in other words, that we want to hold off until runtime to do the binding, we place the virtual keyword in front of the member function in question. This causes dynamic binding also known as late binding to occur. For us, this means that the object pointed to at runtime will be used to determine the version of makeNoise in our case, dog's version of makeNoise rather than the compile time association made with the pointers version in our case, animal, which is being used right now. So, let's update the code and test it out. The only thing we need to change is the makeNoise declaration in Animal.h. We don't put the virtual keyword on the definition, you just need it in the declaration. So, Animal.h, makeNoise we put virtual, right there. That's all we have to do. Now let's run it and give it a try. With everything else still commented out, no other changes made, our dog is going woof, and that's exactly what we wanted to happen. Nice, so we're getting the results we want. The virtual keyword has told the compiler not to do early binding and the runtime takes care of the binding for us. So, whenever you want to make a function that has some default behavior in the base class but you want that behavior to be able to be overridden in a derived class, and for the derived version of the function to be called even when a base class pointer is used like our animal pointer, you just need to put the virtual keyword in front of the declaration in the base class. It's fairly simple. Because of the ability of the animal pointer to point to different derived class types, assuming we made more derived classes of course, this means it can take on many different forms, which is what the word polymorphism actually means in Greek. Poly means many and morph is form. So, late binding enables polymorphism. Now, maybe another question comes up. What if we don't know or don't want a default behavior in our animal class for makeNoise? I mean what does a generic animal sound like anyway? However, we want anything derived from animal to be required to have an implementation for makeNoise. In other words, we can say goodbye to the unknown noise which is a bit silly anyway, and make all of the derived classes like our dog class implement makeNoise. The way we do this is with pure virtual functions. A pure virtual function also has the virtual keyword just like any virtual function, but instead of an implementation, you won't have curly braces or a body in the base class. You just need an equal sign and the number zero. The equal zero is called the pure specifier in this context. So, let's change the code in animal and see what happens. All right, so Animal.h, we're going to make, set makeNoise equal to zero. That's how you do that with the pure specifier right there, and then in Animal.cpp, we're going to remove the implementation entirely. All right. Now let's try running this and see what happens. You're going to find there's something interesting happens. So, start without debugging. my goodness, we got a pop up. There's build errors. I don't want to continue, let's see what went wrong. So, it's 'animal cannot instantiate abstract class'. What does that mean? So, it seems there is a side effect of having a pure virtual function in our animal class right here. Not a virtual function, but a pure virtual function. Now we can no longer, we're getting an error here, this is where the underline is coming from right there. We can no longer create an instance of animal directly, so we can't declare an animal object or use the new keyword followed by the animal constructor. So, if we were to say animal pointer, dogPtr = newAnimal, that wouldn't work either. That's because the animal class is now what's called an abstract class. We can use its type for the pointer so this is allowed. Remember there's a distinction between the pointer and the object it's pointing to. The animal type is now abstract because we have a pure virtual function. As soon as you have one pure virtual function, you can no longer instantiate that class directly. So, I can still use a pointer to that type though, since dog is not an abstract class, I'm able to create an instance of it here. All right. Let's be clear. You can still create variables of type animal pointer, but you cannot create instances or objects of type animal anymore. So, we can use the animal pointer still to point to the dog object. That's still pretty cool. But let's fix the code, all we have to do is comment out our little, my animal declaration and since we have the other stuff commented out, that should be okay. So, shouldn't have any more problems. All right, so let's see if it runs now. And it's back to working again. Good. So, now I have a challenge for you. Continue with the same AnimalFun project. I want you to modify animal and dog so that animal has another pure virtual function that returns a string and this function will be called eat. The eat function will return a phrase like, "I love dog food" from the dog class so that's its implementation in the dog class. Since it's pure virtual in the animal class, there is no definition in the animal class, just the declaration in the specification or Animal.h file, but you must implement it in the dog class. Make sure to test this function in the main function. So, pause the video and give this your best shot. Come back when you're done or if you need some help. How did that work out for you? Did you solve this polymorphic challenge? Let's work on it together. In Animal.h, we will add virtual string eat const = 0. It's const because it doesn't modify any of the internal data. Now you might have an implementation where it causes the animal to gain weight or something like that, but for our purposes it doesn't do that. Now in Dog.h, we need string eat. So, no virtual keyword because we're not making it virtual here, and then finally we have the dog.cpp or not finally, but the next step. Okay, there we go. And return, "I love dog food." All right, looks good to me. Okay, error went away. And now of course, in main.cpp, let's just give it a test here, "Eating", give it a couple spaces and then say dogPtr->eat. Since it's returning a string, we can use it with cout. And of course, let's give this one a run. Start without debugging. Here we go. So, it's woofing and it's saying, "I love dog food." Awesome. So, dog is required to implement the eat function because it is virtual, pure virtual in the base class. And even though the dog pointer is actually of type animal pointer, it calls the correct eat method. In the case of pure virtual functions, the base class doesn't even have an implementation, so someone has to come up with one and that falls to the derived classes to do that. Make sure to keep the AnimalFun project handy because you'll be coming back to it for your project. In the next lecture, we will explore a brief side topic to prepare you for the projects at the end of this section and also it's a useful topic in general. The topic of enumerated types. Let's get going.
John has a Ph.D. in Computer Science and is a professional software engineer and consultant, as well as a computer science university professor and department chair.