Inheritance is the C++ tool for taking an existing class and building a new one on top of it without copying the original's code. It's how the language expresses an "is-a" relationship between two types: a DigitalProduct is a Product, a PremiumCustomer is a Customer. This lesson covers what inheritance is for, the syntax for declaring a derived class, the memory model that makes it all work, and how to access inherited members. Access modes (public, protected, private inheritance), constructor order, virtual functions, and overriding each get their own dedicated chapters later in this section, so the examples here use only plain public inheritance and stick to non-virtual member functions.
Most online stores sell more than one kind of product. A physical product like a wireless mouse needs a name, a price, a stock count, and a weight for shipping. A digital product like an e-book needs a name, a price, a download URL, and a file size, but no shipping weight because nothing physical leaves the warehouse. Both kinds share most of their fields and most of their behavior; only a small slice of each is unique.
The naive way to handle this is to write two completely separate classes:
The two classes are 90% the same. The name, price, and stock fields are duplicated, and so is the entire body of print and applyDiscount. Three problems show up immediately. First, every change to the shared logic has to happen twice, with no compiler warning if one copy is forgotten. Adding a tax field tomorrow has to happen on both classes. Second, a function that wants to work on "any product" has to be written twice or use a generic template, because PhysicalProduct and DigitalProduct are unrelated types as far as the compiler is concerned. Even something as ordinary as a function applyHolidayDiscount would need two overloads. Third, the duplication tells a misleading story to anyone reading the code. Two classes that share five fields and two methods clearly describe the same kind of thing, but the code doesn't say so anywhere.
Inheritance fixes all three. The shared parts factor into one class and the specialized classes pull them in by saying "I am a kind of that". The specialized classes only spell out what's new or different. The fact that they share a base is then a fact the compiler knows about, not a comment.
Read the arrows as "is a kind of". A PhysicalProduct is a kind of Product that adds a weightKg. A DigitalProduct is a kind of Product that adds a downloadUrl. Both reuse Product's name, price, stock, print, and applyDiscount without copying a line.
Inheritance carries a strong meaning. When Derived inherits from Base, the declaration tells the compiler and every future reader of the code that a Derived object is a Base object, with possibly more bits attached. Anywhere code expects a Base, a Derived should be substitutable without surprises.
A test before using inheritance: would the sentence "a Derived is a Base" sound natural in plain English about the actual things being modeled?
| Relationship | Sounds right? | Use inheritance? |
|---|---|---|
A DigitalProduct is a Product | Yes | Yes |
A PremiumCustomer is a Customer | Yes | Yes |
An Order is a Customer | No | No |
A Cart is a list of Products | No, it has a list | No, use composition |
An Order has a Customer | "has a", not "is a" | No, use composition |
When the relationship is "has a" instead of "is a", the right tool is composition: store one object as a member of another. A Cart doesn't inherit from Product; it owns a std::vector<Product>. An Order doesn't inherit from Customer; it stores a Customer (or a customer ID) as a field. There's a short note on choosing between the two at the end of this lesson.
Two terms come up constantly in this section:
In the diagram above, Product is the base class. PhysicalProduct and DigitalProduct are both derived classes of Product. They're called siblings because they share a base.
A class can be both at once. If PremiumCustomer derives from Customer, and later LifetimePremiumCustomer derives from PremiumCustomer, then PremiumCustomer is the derived class in the first relationship and the base class in the second. Multi-level chains like that are valid C++ and behave as expected, with each level adding to the one below.
This lesson stays with one-level inheritance for clarity. Multi-level chains and multiple bases (a derived class with more than one direct parent) are covered later in this section.
The smallest possible derived class. The syntax is:
The colon and the keyword public between the derived name and the base name are what make this an inheritance declaration. The public part is the inheritance access mode, and swapping it for protected or private changes who can see the inherited members. Every example in this lesson uses public inheritance, the form that means "a Derived truly is a Base from the outside world's point of view".
A working example:
DigitalProduct only declares one new field, downloadUrl. Everything else, the name, price, stock data members and the print member function, comes from Product. The derived class doesn't have to mention them at all to use them. ebook.name = "..." works because name is a member of Product, and DigitalProduct is a Product.
A few things to flag while looking at this code:
public: label inside the class body and the public keyword in : public Product are different things. The label controls who can access members of DigitalProduct. The public in the inheritance declaration controls how Product's members appear from outside DigitalProduct. Both happen to use the same keyword.Product's full definition (size, members, layout) to lay out DigitalProduct, so a forward declaration like class Product; isn't enough on its own. The full Product definition has to be visible.In practice, when a class is split across header and implementation files, the derived class's header has to #include the base class's header so the full definition is in scope. The shape looks like this:
A forward declaration like class Product; would be enough for declaring a Product* member or a function returning Product, but it isn't enough for inheritance, because the compiler needs to know how big Product is to lay out DigitalProduct on top of it. A "base class has incomplete type" error from the compiler is almost always caused by a missing #include of the base header.
A derived object's storage starts with a complete copy of the base class's data members, and the derived class's own members come right after. The base part of a derived object is called the base subobject.
Consider this pair of classes:
The exact numbers depend on the compiler and the standard library implementation of std::string, but the relationship holds: sizeof(DigitalProduct) equals sizeof(Product) plus the size of the new downloadUrl member (with possible padding). The derived object literally contains the base object inside it.
The cyan block is the base subobject. The orange block is what DigitalProduct adds on top. The whole rectangle is what one DigitalProduct object actually occupies in memory. There's no pointer, no indirection, no separate allocation; the base part is embedded directly.
This layout has a few useful consequences. A pointer to the start of a DigitalProduct is also a valid pointer to its Product subobject, which is what makes upcasting (Product* p = &ebook;) free at runtime. Member access is an offset into the object: ebook.name reads from offset 0 (the start of the base subobject), and ebook.downloadUrl reads from offset sizeof(Product) plus any padding. There's no lookup table, no inheritance walk; the compiler resolves which subobject a member belongs to at compile time.
A small program makes the upcasting fact concrete:
asProduct and &ebook point at the same address, but the type system treats asProduct as if it sees only the Product subobject. Reading asProduct->name works because name is a base member; reading asProduct->downloadUrl would fail to compile because nothing in Product mentions a download URL. The derived data is still there in memory, but the type system has hidden it.
A derived object is exactly as cheap or expensive to construct, copy, and destroy as a base object plus a stand-alone object holding the derived class's extra members. Inheritance itself adds no per-object overhead in the non-virtual case. Virtual functions add one pointer per object (the vtable pointer), which the polymorphism section covers.
The "contains a base subobject" model also explains a behavior to know about up front. Copying a DigitalProduct into a plain Product (Product copy = ebook;) makes the compiler copy the base subobject and throw the downloadUrl away because the destination has no room for it. That's called object slicing.
From inside the derived class and from outside it, inherited public members are reached the same way as members declared directly in the derived class. The inheritance is invisible at the call site.
Several things happen here:
ebook.name, ebook.price, and ebook.stock work even though those fields aren't declared in DigitalProduct. They come from the Product subobject inside ebook.ebook.applyDiscount(15) calls a function defined in Product. Inside that call, price refers to the Product subobject's price, which is the same memory ebook.price reads. So when applyDiscount writes price, the change shows up when printWithLink later reads it.printWithLink is defined in DigitalProduct and calls print() without any qualification. The compiler looks up print in the derived class first, doesn't find it, then walks up to the base and finds it there. It's the same lookup it does for any name inside a member function.print() inside printWithLink uses the same object: there's only ever one object in play, and both the derived method and the base method operate on its data.To be explicit about which class a member came from, use the scope resolution operator ::. This is occasionally useful when names collide or when reading code, even if it's not strictly required:
That call does exactly the same thing as ebook.print() here. The qualified form becomes meaningful when method overriding lets a derived class define its own print that hides the base version.
The running E-Commerce theme stretched a little further. A base Product class holds the common fields and behavior, and two derived classes each add their own pieces. The goal is to see inheritance handling more than the trivial one-field case, and to see two siblings sharing a base.
A couple of things stand out. Both mouse and ebook reuse applyDiscount and describe without any duplication; the moment a bug shows up in applyDiscount, fixing it once fixes it for every derived class. Each derived class adds the methods that only make sense for its own kind of product: shipping weight on physical, file size and download URL on digital. The two derived classes don't know about each other and don't need to.
The trade-off is real. Adding a third kind of product, say a SubscriptionProduct with a billing interval, is cheap. Adding a fourth, fifth, and sixth starts to bake "this set of subclasses" into the design, and changing the base later (renaming a field, changing a function signature) ripples through every derived class. Inheritance is a sharper tool than composition, and that sharpness cuts both ways.
Another point in a multi-class example: each derived class is its own type, distinct from its siblings. A function that takes a PhysicalProduct won't accept a DigitalProduct, even though both are Products, because the relationship goes one direction: a PhysicalProduct is a Product, but a Product is not necessarily a PhysicalProduct. So a function that works on either kind has to take the base type:
Both calls are legal because both arguments are Products through their base subobject. The function never sees the derived parts and doesn't need to. That's the upcasting from the memory-model section, applied to a practical case.
The print() member function in Product always prints the same shape no matter which derived class calls it. There's no way for DigitalProduct to make describe() add the download URL automatically when called as ebook.describe(), not with the tools in this lesson. That's the job of virtual functions and method overriding, covered in the Method Overriding lesson and the Polymorphism section. The workaround for now is what printWithLink did earlier: define a separate method in the derived class that calls the inherited one and then adds whatever extra it needs.
A quick rule of thumb before closing the lesson. Both inheritance and composition build a class out of other classes, but they answer different questions:
DigitalProduct is a Product).Cart has a list of Products; an Order has a Customer).Most relationships in real code are "has-a", not "is-a", so composition is the right tool more often. A Cart doesn't inherit from Product; it stores std::vector<Product> as a member. An Order doesn't inherit from Customer; it stores a Customer (or a customer ID) as a member. Using inheritance to share helper methods is a common trap and produces hierarchies that resist requirement changes.
Rough guidance:
| Question | If yes, prefer |
|---|---|
| Is the relationship genuinely "is-a" in plain English? | Inheritance |
| Will code that takes a base also need to accept the derived class? | Inheritance |
| Is the relationship "has-a", "uses-a", or "is-implemented-using"? | Composition |
| Is the goal to share a few helper methods? | Composition (or a free function) |
This is a one-paragraph guideline, not a deep dive. Picking between inheritance and composition is its own design topic, and there's a fair amount of nuance once they get mixed in real systems. The takeaway for this lesson: inheritance is for "is-a", and most relationships aren't "is-a", so use it deliberately.
10 quizzes