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

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

Encapsulation

Inheritance

Polymorphism

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:

The term abstraction refers to the process of extracting
the "essence" of a real world thing or concept and modeling it with
the data (data abstraction) and operations (procedural abstraction)
of the ADT. The data portion of this duo is generally placed in the
"private" part of the class, while the operations form the public
interface to the ADT and are therefore placed in the "public"
part of the class definition.

The term information hiding refers to the fact that we
prevent a client of the class from having access to the data in the
class implementation. The client should not need any such information.
We say that we are practicing information hiding by defining
our classes in this way. We should be careful, however, not to
confuse "inaccessibility" with "invisibility", since (at least in the
case of C++) the private (data) part of a class is merely
inaccessible, not invisible.

What is inheritance?

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

single inheritance, which is the mechanism by which one
class (called the derived class) acquires the properties (data and
operations) of another class (called the base class)

multiple inheritance, which is the mechanism by which
one class acquires the properties of two or more base classes

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
{
//declarations
}

The access qualifier for inheritance can be

public With public inheritance there are no additional
limitations. That is, public members of the base class are also
public in the derived class, protected members remain protected, and
private members remain private.

protected With protected inheritance, some
restrictions apply. All public members of the base class become
members of the derived class that cannot be accessed by the client.
However, derived classes do have access to these members. Thus,
public becomes protected, protected remains protected, and private
remains private.

private With private inheritance, all members of the
base class become private members in the derived class. Therefore,
neither the client nor derived classes have access to any of the
members.

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:

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:

In the case of an ordinary (i.e., non-virtual) function from
the higher-level class, it is called redefining that
function in the lower-level derived class.

In the case of a virtual function from the higher-level
class, it is called overriding that function in the
lower-level derived class. This the usual case, the main reason
why we would do this in the first place, and the case where
polymorphism kicks in.

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:

via function overloading and operator overloading

via function templates

via virtual functions with dynamic binding (i.e. run-time
binding)

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.