
Inheritance from the compiler perspective
We can picture the Rectangle class we declared earlier in the following way:
When we declare the rect object in the main() function, the space that's required for the local objects of the function is allocated in the stack. The same logic follows for the make_big_rectangle() function when it's called. It doesn't have local arguments; instead, it has an argument of the Rectangle& type, which behaves in a similar fashion to a pointer: it takes the memory space required to store a memory address (4 or 8 bytes in 32- and 64-bit systems, respectively). The rect object is passed to make_big_rectangle() by reference, which means the ref argument refers to the local object in main():
Here is an illustration of the Square class:
As shown in the preceding diagram, the Square object contains a subobject of Rectangle; it partially represents a Rectangle. In this particular example, the Square class doesn't extend the rectangle with new data members.
The Square object is passed to make_big_rectangle(), though the latter takes an argument of the Rectangle& type. We know that the type of the pointer (reference) is required when accessing the underlying object. The type defines how many bytes should be read from the starting address pointed to by the pointer. In this case, ref stores the copy of the starting address of the local rect object declared in main(). When make_big_rectangle() accesses the member functions via ref, it actually calls global functions that take a Rectangle reference as its first parameter. The function is translated into the following (again, we slightly modified it for the sake of simplicity):
void make_big_rectangle(Rectangle * const ref) {
Rectangle_set_width(*ref, 870);
Rectangle_set_height(*ref, 940);
}
Dereferencing ref implies reading sizeof(Rectangle) bytes, starting from the memory location pointed to by ref. When we pass a Square object to make_big_rectangle(), we assign the starting address of sq (the Square object) to ref. This will work fine because the Square object actually contains a Rectangle subobject. When the make_big_rectangle() function dereferences ref, it is only able to access the sizeof(Rectangle) bytes of the object and doesn't see the additional bytes of the actual Square object. The following diagram illustrates the part of the subobject ref points to:
Inheriting the Square from the Rectangle is almost the same as declaring two structs, one of which (the child) contains the other (the parent):
struct Rectangle {
int width_;
int height_;
};
void Rectangle_set_width(Rectangle& this, int w) {
this.width_ = w;
}
void Rectangle_set_height(Rectangle& this, int h) {
this.height_ = h;
}
int Rectangle_area(const Rectangle& this) {
return this.width_ * this.height_;
}
struct Square {
Rectangle _parent_subobject_;
int area_;
};
void Square_set_side(Square& this, int side) {
// Rectangle_set_width(static_cast<Rectangle&>(this), side);
Rectangle_set_width(this._parent_subobject_, side);
// Rectangle_set_height(static_cast<Rectangle&>(this), side);
Rectangle_set_height(this._parent_subobject_, side);
}
int Square_area(Square& this) {
// this.area_ = Rectangle_area(static_cast<Rectangle&>(this));
this.area_ = Rectangle_area(this._parent_subobject_);
return this.area_;
}
The preceding code demonstrates the compiler's way of supporting inheritance. Take a look at the commented lines of code for the Square_set_side and Square_area functions. We don't actually insist on this implementation, but it expresses the full idea of how the compiler processes OOP code.