You may have heard that C++ is object oriented. You may wonder what that means exactly. We’ve already talked about structs and classes as a way to organize code, but without inheritance and virtual functions, classes would just be a minor convenience rather than a programming paradigm.
Let’s say we have a Fruit class:
class Fruit {
public:
// This is an Fruit constructor. Constructors are
// methods which are called when you create
// a new "Fruit" instance. Constructors have
// the same name as the class they in and have
// no return type.
Fruit(float weight, Color color) {
weight_ = weight;
}
float weight() { return weight_; }
private:
float weight_;
};
Note that this class doesn’t really tell us what kind of fruit it is. We could obviously add a method to Fruit which returns “apple”, “pear” or “orange”, but in an object oriented program, we also have the option of using inheritance.
class Apple : public Fruit {
public:
// In a constructor, we can initialize object variables
// and inherited classes after the argument list, but
// before the {} by using a :. In this example, we use
// use this to call the Fruit constructor.
Apple(float weight) : Fruit(weight) {}
};
Now we have an Apple class, which inherits from Fruit. In object oriented vernacular, this makes sense because Apple IS a fruit. That means that for the most part, any functions that expect a Fruit, can also be called with an Apple.
Example:
// In this example, all fruit costs the same per kg.
float cost_per_kg = 0.1;
float calculate_price(Fruit* fruit) {
return fruit->weight() * cost_per_kg;
}
void main() {
Apple apple(0.1); // this calls the constructor with 0.1
printf("This apple costs %f\n", calculate_price(&apple));
}
This example works. Since Apple IS a Fruit, Apple* is compatible with Fruit*, which means I can call calculate_price(). The pointer is important though. Pointers or references (which we haven’t talked about yet) are needed for this compatibility to work. If calculate_price took a Fruit
as input instead of a Fruit*
, then we could not call it with an Apple. The reason for this is that when you call a function, it normally makes a copy of whatever argument you send it. calculate_price would know how to make a copy of a Fruit, but it wouldn’t really know how to make a copy of an Apple. The clever part about inheritance is a chunk of the Apple memory is laid out exactly like the memory for a Fruit, so when you call a function that requires a Fruit, you can give it a pointer to that memory, and it doesn’t have to know that it’s a part of something else.
Let’s add a color to our apples:
class Apple : public Fruit {
public:
Apple(float weight) : Fruit(weight) {}
enum Color { UNKNOWN, RED, GREEN, YELLOW };
virtual Color color() { return UNKNOWN; }
};
I could have used a variable to hold the color, similar to how the weight works, but that’s not very object oriented. Instead I made the Apple class return UNKNOWN, now we implement some apple types:
class RedDelicious : public Apple {
public:
RedDelicious(float weight) : Apple(weight) {}
Color color() override { return RED; }
};
class GrannySmith : public Apple {
public:
GrannySmith(float weight) : Apple(weight) {}
Color color() override { return GREEN; }
};
Did you see the “virtual” above? Without that, this wouldn’t work:
void print_price_and_color(Apple* apple) {
float price = calculate_price(apple);
Color color = apple->color();
const char* color_name = "unknown";
switch (color) {
case Apple::RED: color_name="red"; break;
case Apple::GREEN: color_name="green"; break;
case Apple::YELLOW: color_name="yellow"; break;
}
printf("This %s apple costs %f\n", color_name, price);
}
Without virtual, the compiler would always call the “color” function inside the Apple class, and the color would always be UNKNOWN. But with virtual, a special memory structure is created called a “vtable”, with an entry for the “color” method. Every class which inherits from Apple will have it’s own vtable, and may have a different “color” method. When color
is called, the calling function doesn’t have to know what kind of Apple it is, it just looks up the right method in the vtable and calls that.
This also applies to the Apple class itself. When a virtual method is called from inside Apple, it will go through the vtable, and the right method.
You may have also have noticed the “override” keyword above. “override” is used to help the programmer prevent typos. Let’s say I created an apple class with a “colour”. Without “override” this would compile just fine, but the method would not override the virtual method in the Apple class, which is what I actually wanted. With “override” the compiler would complain if the method doesn’t override some method in some base class. (Base class = some class your class is inheriting from.)
This whole concept of overriding is where object oriented programming gets most of its usefulness and strength from. Basically these virtual functions become hooks which can be changed in subclasses to do things differently.
In ProffieOS inheritance is most obvious in the Prop classes. They all inherit from PropBase, which has a lot of virtual functions you can override.