What are the "three pillars" of object-oriented programming (OOP)?

The "three pillars" of OOP are generally taken to be:

Use of encapsulation alone (i.e., defining and using classes, but making no use of either inheritance or polymorphism) is often called object-based programming. To be truly practicing object-oriented programming, you must be using all three of the "pillars", i.e., encapsulation, inheritance, and polymorphism.

What is encapsulation?

In C++ the term encapsulation refers to the placing of both data and operations within a class definition to implement the conceptual notion of an abstract data type (ADT).

Closely related are the notions of abstraction and information hiding:

What is inheritance?

In C++ there are two forms of inheritance, which is also called derivation:

The following UML diagram (UML = Unified Modeling Language) illustrates the difference, in general, between single and multiple inheritance:

So, a base class is one from which others are derived, while a derived class is one defined as an extension of another class.

Other terminology includes child class or subclass for a derived class, and parent class or superclass for the base class.

If there are one or more classes separating the two derivation-related classes in the class hierarchy, we say that the class at the higher level is an ancestor of the class at the lower level, which in turn is a descendant of the class at the higher level. We also say that the class at the lower level inherits indirectly from the one at the higher level. If there are no intermediate classes, the inheritance is direct.

A derived class often overrides one or more of the member functions in the base class, thereby changing the behavior (but not the interface) of that member function. A derived class will often also extend the base class by adding new member functions, with or without overriding base class functions.

Sometimes a derived class can also be used for realization. That is, sometimes a derived class actually needs to implement one or more member functions that have not been implemented in the base class. This gives us the notion of an abstract base class (or, simply, an abstract class), which is a class used only to derive other classes, and which cannot be used to create an object. In order to be an abstract class, the class must contain at least one pure virtual function.

A pure virtual function is a function declared with the following special syntax:

virtual return_type name(parameter_list) = 0;

Such a function has no implementation in the (abstract) class. It specifies the what, but not the how, of object behavior, and at least one such function must be contained in a class if that class is to be an abstract base class.

Any class that inherits from an abstract base class must implement (define bodies for) all pure virtual functions in that abstract base class, or the derived class itself will become an abstract class and cannot be used to create objects. Only a class for which all functions have been implemented can be used to instantiate an object.

The syntax for class inheritance is

class Derived : [optional_access_qualifier] Base

The access qualifier for inheritance can be

We can summarize this by saying that the access keyword applied to the base class specifies the minimum encapsulation that derived members get, from the perspective of the derived class [Josuttis].

The default access qualifier is private. However, public inheritance is the one most often used, so the syntax would normally look like this:

class Derived : public Base

In any case, it does not matter whether the base class itself is a derived class.

If a derived class provides a new definition for one of the member functions of its base class, there are two possibilities:

  1. First is the case where you provide the exact same function signature and return type in the derived class as in the base (or ancestor) class. In this case, the function at the higher level is said to be shadowed by the one at the lower level, and this, in turn, admits two further possibilities:
  2. Second is the case where the name of the function in the derived class is the same as the name of the function in the higher-level class, but you change either the parameter list or the return type (or both). In this case, any and all versions of the function with that name in the higher-level class are hidden from view in the derived class. This really shouldn't be done, because it means that you are probably using the class in a way that is different from the way the inheritance is meant to support, and you have in fact compromised the "is-a-kind-of" nature of the inheritance relationship. Another way of saying this is that you have failed to live up to the "Principle of Substitutability".

An object of a derived class can be assigned to a variable whose type is the base class (or an ancestor class), or copied into such a variable, as would happen when passing a derived object to a parameter that has the base type, for example. However, either of these actions will cause slicing (i.e., the so-called slicing problem. Here is the reason for this: In general, a derived class object will have more data members than its base or ancestor class objects, so the compiler, without complaining, simply "slices off" the extra data members of the derived class object and uses only the members inherited from the base or ancestor class for the base or ancestor class variable. However, when references or pointers to base arguments are used, this "slicing" does not occur, which is one of the reasons that references and pointers are so important in C++ object-oriented programming.

What is polymorphism?

The term polymorphism (from the Greek for "many forms") refers to the ability to use the same name for what may be different actions on objects of different data types. Polymorphism may be achieved (or at least approximated) in several ways:

A member function "found" at run-time (i.e., bound to an object at run-time, or dynamically bound is called a virtual member function (or, more simply, a virtual function). To mark a member function for selection at run-time, its declaration in the header file must be preceded by the keyword virtual. Once a function has been declared virtual, it remains virtual in any and all derived classes, without repetition of the virtual keyword, though it is generally regarded as a best practice to repeat the keyword virtual in the derived classes, for readability.