Object Oriented Programming
Contents
Basics
The motivation behind object oriented programming (OOP) is to provide three things:
- encapsulation (abstraction)
- inheritance
- polymorphism
In CSC160, we learned about the first, encapsulation. Encapsulation provides a layer of abstraction for data and operations on that data. Through classes, we are able to expose methods and hide the implementation. We'll talk about the logistics of encapsulation in much of this document. Inheritance (referring to organizing classes in a hierarchy) and polymorphism (referring to the generalization of data types) will be introduced at the end.
You've already seen many of the basics of OOP. In this section, we're going to talk about some new syntax that should make your life easier. To start with, here is the class specification and implementation for Person
that we used in the C++ Review:
No side effects
One thing that is useful to know is whether or not a method will change the internal state of an instance, e.g., its data members. If a method can guarantee that it will not, then this guarantee is provided by following the method signature with the keyword const
. For example, the print
method only outputs the data members; it does not modify them. So, we can explicitly indicate this by making the signature: void print() const
. Our declaration will look like:
and our implementation will look like:
const
methods are important because, if a function (or method) takes a const
class instance as a parameter, it can only access const
members! For example, the following function declared in our main file will work with our newest version of print()
, but not with the former (we'll get a compiler error):
So, if a method doesn't change the values of any data members, make it const
.
Member initializer list
Did you notice in the implementation, the constructor takes parameter names that are not quite the same as the Person
data members? E.g., we use theName
instead of name
. That's because we refer to the data member as name
. So if the parameter was also name
, then we'd have the assignment name = name
. Now which name are we talking about? The C++ compiler will assume we're talking about the parameter and trying to reassign it to itself (confusing, eh?).
It's pretty annoying to have to worry about making the parameters for methods different from data members we want to access within the method. Luckily, there are two ways to get around this! The first is to use a member initializer list. Just after the signature of our method in the implementation, we use the syntax : member(param), member(param), ...
, where member
is just a placeholder for a data member of the class and param
is whichever parameter you want to assign to the data member. Here's the implementation of our constructor using this technique:
Because the syntax is specific about where data members go (outside of the parentheses) and where parameters go (inside of the parentheses), the compiler knows exactly what to do. This is the preferred way to set data members from parameter values—it's very concise.
(Back to top)The this
keyword
The second technique requires the introduction of a special keyword: this
. Within any class methods, the this
keyword is a pointer that refers to the current instance of the class. Just like an instance declared elsewhere, we can access data member and methods using the arrow notation (->
). Unlike instances declared elsewhere, the this
instance has access to private members, since it can only ever be used from within the class itself. That means that we can get around our naming confusion by just preceding data members with this->
, e.g., this->name
inside of the constructor will refer to our name
data member, and not the name
parameter. Here's what it looks like in our example:
this
can also be used to access methods, e.g., this->print()
is equivalent to print()
from within the class implementation.
Operator overloading
A really useful feature of C++ is it's ability to overload operators. That means you can control what happens when ++
is used with your instance. Let's suppose that for our Person
class, when someone tries to increment an instance of Person
, we want the age of that instance to go up by one. E.g., here's what we want to see happen:
How can we make this happen? Well, we need to add a new method declaration to the interface: Person& operator++(int num);
. Notice that the method returns a Person
. In fact, it will return the same instance of the person. The parameter num
is not used, but an int
parameter is required. Here's what the implementation looks like:
This actually isn't the canonical way; the post-increment is actually supposed to make a copy of the instance, increment the desired value in the original, and return the copy (the return type should really be Person
and not a reference to it). But that requires we implement a few other things here, such as a copy function, which is more than I want to cover right now. There are many operators you can overload, including +, -, *, /, %, [], (), ^, !, &, <, <=, >, >=, +=, -=, *=, /=, %=, and =. This page has a nice overview of operator overloading, as does the top of this page.
Access: private, public, and protected
There are three types of access: private, public, and protected. You have already seen the first two, but I'll summarize them here again.
Private
Any data members or methods that are declared under private:
can only be accessed within a class, or by friend functions and classes (more on this in a minute). For example, for the Person
class defined above, the following lines in main
would result in a compiler error:
Using the friend
keyword, a class interface can list functions and classes defined outside of the class that have access to its private members and methods. For example, to designated an externally defined function (e.g., not a method of the class), include a declaration somewhere in the class interface that consists of friend
followed by the function signature. E.g., designating a function with the signature void changeName(Person &person, string newName)
would look like this in our Person
class interface:
We can define this function outside of the class. It will look just like a regular function—it's not a method of Person
, so we don't need the Person::
prefix. This function will change the name
member (which is private) of the instance that is passed in by reference:
We can invoke this function from any other function and pass in a Person
instance. To give other classes access, we need only specify the friendship at the top of our class interface. We need to also make sure that our class knows the other class exists, so if the class we're "friending" won't appear until later in the code, add an empty class declaration above the current class' interface. Let's say we want to give the class Dog
access to our Person
class' private members. We'd do the following:
Now if Dog
is given or creates an instance of Person
, it can access the private data members name
, age
, and gender
.
Note that with classes, all members and methods that are not declared below any explicit access keyword are automatically private. For instance, the following version of the Person
interface without the private:
keyword is equivalent to the version from earlier:
Public
Public members and methods can be accessed by everyone everywhere: both within a class and outside. In our examples above, we can call the print
method from main
. Note that in structs, any members or methods that are not declared below an access keyword are automatically public.
Protected
Protected members and methods are only available to certain other entities, including friends (as discussed above) as well as subclasses or derived classes. We'll talk about subclasses/derived classes in the next section when we go over inheritance. One thing to keep in mind is that, friendship is explicit, so while the friend
keyword gives access to private data members and methods (which is the most access anyone can have), only those functions and classes that are known ahead of time are given that access. With protected data members/methods, any class that decides to inherit can have access to it—the base class does not have a say.
Inheritance
Inheritance refers to organizing classes into a hierarchy, where classes further down (called sub classes or derived classes) get or inherit data and methods from their antecedents (called super classes or base classes). A classic example is to think about a program that draws shapes. There are certain properties and functionalities that all shapes we intend to draw should have, such as the x
and y
coordinates of where the center of the shape should be and a draw
method that actually draws the shape. Each particular shape will have a different implementation of the draw
method, as well as some additional properties. For instance, a circle has a radius and circumference. A rectangle has a height and a width. A square is actually the same thing as a rectangle, but with the constraint that the height and width must be the same. Our class hierarchy may look something like what's in \ref.
For this example, lets suppose we define a class Shape
with this interface:
I'm not going to worry about the implementation of the methods; let's just suppose they all do something. Now, let's see how to make a subclass of Shape
, say Rectangle
. The key to inheritance in C++ is in the way we declare our class:
From main
(or any other function), we can do:
This will print:
10, 20 30, 40
Note that we can invoke myRectangle.getX()
, even though we never defined getX()
or getY()
for class Rectangle
. That's because Rectangle
inherited all public and protected methods/members from Shape
(though Shape didn't have any protected members). Note that private data members/methods were not inherited. That means Rectangle
cannot access x
and y
. We can only print their values because the getX()
and getY()
methods are defined in Shape
, which has access to its own private members. If we want Rectangle
to have access to x
and y
, then we should make them protected in the Shape
interface:
We can redefine the draw()
method in Rectangle
, if we want—this is called overriding the superclass' method. We can also update the constructor however we'd like. For example:
Now when we execute myRectangle.draw()
, it will use the version defined in Rectangle
, not in Shape
, as it would have before. We don't need to both redefining the getX()
and getY()
methods—we could, but they'd just do the same thing.
Polymorphism
The term polymorphism can refer to many things, but two core types of polymorphism are subtype (also called run-time polymorphism) and parametric (also called compile-time polymorphism). We'll cover these each in turn below.
Subtype / Run-time polymorphism (C++: Virtual functions)
Subtype polymorphism lets us declare a variable of one type, but assign it an instance of a subtype (or derived type). For example, say I have a function (not an instance method) that takes a Shape
pointer. Let's say this function prints out the x
and y
coordinates and then invokes the draw
method. We'll call it verboseDraw
:
We can send this function a pointer to a Shape
instance. But, through the beauty of polymorphism, we can actual send it any subtype, or sub-subtype. So, any class represented in \ref. Like:
(A technical note: because verboseDraw()
takes a pointer, I can pass it an address, which you'll recall is done using the &
keyword). Now, this won't actually do what you think it will do given our current Shape
interface—even if each of the subclasses override the draw() method. In order to force the subclass version to be invoked, we need to make the draw
method virtual. We can do this by adding the keyword virtual
before the declaration in the interface (you don't need the virtual
keyword in front of the definition):
You can use virtual with any method where you expect it to be overridden by subclasses. There's another kind of virtual method, as well, called a pure virtual method. These are not actually defined by the superclass at all. They look like a regular virtual method, except you set it equal to 0:
Any class that has one or more pure virtual methods is called an abstract class. Instances of abstract classes cannot be created, e.g., we can no longer do: Shape myShape(3, 5);
.
Parametric / Compile-time polymorphism (C++: Templates)
This is going to be the most useful kind of polymorphism for us in this class. C++ provides this kind of polymorphism through templates. Let's suppose that I want to create a class that acts similar to a regular array, but keeps track of the size. We don't want the array to only hold int
s or char
s. We want it to hold any arbitrary type. We need to specify some type, and we'll do this using the template <class T>
notation (T
can be replaced by whatever letter you want, but convention is to use T
for "template"). Here's what our Array
class interface looks like:
Note that we specify that we're using a template just above the interface. We then use T
everywhere we need to refer to the type being stored by our array. The member data
is our array—it's a pointer because we don't know the size of our array until the constructor is called, requiring us to use dynamic memory. Here's the method definitions:
CAUTION: when using templates, the class definition and implementation need to go together. You can do this by any of the following approaches:
- including the method definitions within the class definition (in the
.h
file) - adding the method definitions below the class definition (in the
.h
file) - put the implementation in its own file with the extension
.hpp
, then include that file at the bottom of the.h
file
Notice that we have to have template <class T>
above each method implementation. Furthermore, we need to refer to the class as Array<T>
, rather than just Array
.
Other than that, it looks pretty typical. Here's an example of using our array class in main
:
You can also make special implementations for specific type, which can be done with specialized templates. The very bottom of this page describe those in more detail.
(Back to top)Additional readings
- Operator overloading,
this
keyword, static and constant members and functions, and templates - More on friends, inheritance, multiple inheritance
- Parametric polymorphism (virtual functions)
- A blob post discussing four types of polymorphism