Next Up Previous Hi Index

Chapter 15

Object-oriented programming

Jonah Cohen

15.1 Programming languages and styles

There are many programming languages in the world, and almost as many programming styles (sometimes called paradigms). Three styles that have appeared in this book are procedural, functional, and object-oriented. Although C++ is usually thought of as an object-oriented language, it is possible to write C++ programs in any style. The style I have demonstrated in this book is pretty much procedural. Existing C++ programs and C++ system libraries are written in a mixture of all three styles, but they tend to be more object-oriented than the programs in this book.

It's not easy to define what object-oriented programming is, but here are some of its characteristics:

Recently object-oriented programming has become quite popular, and there are people who claim that it is superior to other styles in various ways. I hope that by exposing you to a variety of styles I have given you the tools you need to understand and evaluate these claims.


15.2 Member and nonmember functions

There are two types of functions in C++, called nonmember functions and member functions. So far, every function we have written has been a nonmember function. Member functions are declared inside class defintions. Any function declared outside of a class is a nonmember function.

Although we have not written any member functions, we have invoked some. Whenever you invoke a function "on" an object, it's a member function. Also, the functions we invoked on pstrings in Chapter 7 were member functions.

Anything that can be written as a nonmember function can also be written as a member function, and vice versa. Sometimes it is just more natural to use one or the other. For reasons that will be clear soon, member functions are often shorter than the corresponding nonmember functions.


15.3 The current object

When you invoke a function on an object, that object becomes the current object. Inside the function, you can refer to the instance variables of the current object by name, without having to specify the name of the object.

You can also refer to the current object through the keyword this. We have already seen this in an assignment operator in Section 11.2. However, the this keyword is implicit most of the time, so you will rarely find any need for it.


15.4 Complex numbers

Continuing the example from the previous chapter, we will consider a class definition for complex numbers. Complex numbers are useful for many branches of mathematics and engineering, and many computations are performed using complex arithmetic. A complex number is the sum of a real part and an imaginary part, and is usually written in the form x + yi, where x is the real part, y is the imaginary part, and i represents the square root of -1. Thus, i · i = -1.

The following is a class definition for a new object type called Complex:

class Complex
{
private:
  double real, imag;

public:
  Complex () {
    real = 0.0;  imag = 0.0;
  }

  Complex (double r, double i) {
    real = r;  imag = i;
  }
};

There should be nothing surprising here. The instance variables are two doubles that contain the real and imaginary parts. The two constructors are the usual kind: one takes no parameters and assigns default values to the instance variables, the other takes parameters that are identical to the instance variables.

In main, or anywhere else we want to create Complex objects, we have the option of creating the object and then setting the instance variables, or doing both at the same time:

    Complex x;
    x.real = 1.0;
    x.imag = 2.0;
    Complex y (3.0, 4.0);

15.5 A function on Complex numbers

Let's look at some of the operations we might want to perform on complex numbers. The absolute value of a complex number is defined to be sqrt(x2 + y2). The abs function is a pure function that computes the absolute value. Written as a nonmember function, it looks like this:

  // nonmember function
  double abs (Complex c) {
    return sqrt (c.real * c.real + c.imag * c.imag);
  }

This version of abs calculates the absolute value of c, the Complex object it receives as a parameter. The next version of abs is a member function; it calculates the absolute value of the current object (the object the function was invoked on). Thus, it does not receive any parameters:

class Complex
{
private:
  double real, image;

public:
  // ...constructors

  // member function
  double abs () {
    return sqrt (real*real + imag*imag);
  }
};

I removed the unnecessary parameter to indicate that this is a member function. Inside the function, I can refer to the instance variables real and imag by name without having to specify an object. C++ knows implicitly that I am referring to the instance variables of the current object. If I wanted to make it explicit, I could have used the keyword this:

class Complex
{
private:
  double real, image;

public:
  // ...constructors

  // member function
  double abs () {
    return sqrt (this->real * this->real + this->imag * this->imag);
  }
};

But that would be longer and not really any clearer. To invoke this function, we invoke it on an object, for example

    Complex y (3.0, 4.0);
    double result = y.abs ();

15.6 Another function on Complex numbers

Another operation we might want to perform on complex numbers is addition. You can add complex numbers by adding the real parts and adding the imaginary parts. Written as a nonmember function, that looks like:

  Complex Add (Complex& a, Complex& b) {
    return Complex (a.real + b.real, a.imag + b.imag);
  }

To invoke this function, we would pass both operands as arguments:

    Complex sum = Add (x, y);

Written as a member function, it would take only one argument, which it would add to the current object:

  Complex Add (Complex& b) {
    return Complex (real + b.real, imag + b.imag);
  }

Again, we can refer to the instance variables of the current object implicitly, but to refer to the instance variables of b we have to name b explicitly using dot notation. To invoke this function, you invoke it on one of the operands and pass the other as an argument.

    Complex sum = x.Add (y);

From these examples you can see that the current object (this) can take the place of one of the parameters. For this reason, the current object is sometimes called an implicit parameter.


15.7 A modifier

As yet another example, we'll look at conjugate, which is a modifier function that transforms a Complex number into its complex conjugate. The complex conjugate of x + yi is x - yi.

As a nonmember function, this looks like:

  void conjugate (Complex& c) {
    c.imag = -c.imag;
  }

As a member function, it looks like

  void conjugate () {
    imag = -imag;
  }

By now you should be getting the sense that converting a function from one kind to another is a mechanical process. With a little practice, you will be able to do it without giving it much thought, which is good because you should not be constrained to writing one kind of function or the other. You should be equally familiar with both so that you can choose whichever one seems most appropriate for the operation you are writing.

For example, I think that Add should be written as a nonmember function because it is a symmetric operation of two operands, and it makes sense for both operands to appear as parameters. It just seems odd to invoke the function on one of the operands and pass the other as an argument. (Actually, in the next section you'll learn of a method called operator overloading which eliminates the need for explicitly calling functions like Add.)

On the other hand, simple operations that apply to a single object can be written most concisely as member functions (even if they take some additional arguments).


15.8 Operator overloading and <<

There are two operators that are common to many object types: << and =. << converts the object to some reasonable string representation so it can be outputted, and = is used to copy objects.

When you output an object using cout, C++ checks to see whether you have provided a << definition for that object. If it can't find one, it will refuse to compile and give an error such as

  complex.cpp:11: no match for `_IO_ostream_withassign & << Complex &'

Here is what << might look like for the Complex class:

  ostream& operator << (ostream& os, Complex& num) {
    os << num.real << " + " << num.imag << "i";
    return os;
  }

Whenever you pass an object to an output stream such as cout, C++ invokes the << operator on that object and outputs the result. In this case, the output is 1 + 2i.

The return type for << is ostream&, which is the datatype of a cout object. By returning the os object (which, like ostream, is just an abbreviation of output stream), you can string together multiple << commands such as

  cout << "Your two numbers are " << num1 << " and " << num2;

To illustrate why that's a good thing, consider what you would be forced to do if you didn't return the ostream object:

  cout << "Your two numbers are ";
  cout << num1;
  cout << " and ";
  cout << num2;

Because the first example allows stringing << statements together, all the display code fits easily on one line. The output from both statements is the same, displaying "Your two numbers are 3 + 2i and 1 + 5i".

This version of << does not look good if the imaginary part is negative. As an exercise, fix it.


15.9 The = operator

Unlike the << operator, which refuses to output classes that haven't defined their own definition of that function, every class comes with its own =, or assignment, operator. This default operator simply copies every data member from one class instance to the other by using the = operator on each member variable.

When you create a new object type, you can provide your own definition of = by including a member function called operator =. For the Complex class, this looks like:

  const Complex& operator = (Complex& b) {
    real = b.real;
    imag = b.imag;
    return *this;
  }

By convention, = is always a member function. It returns the current object. (Remember this from Section 11.2?) This is similar to how << returns the ostream object, because it allows you to string together several = statements:

  Complex a, b, c;
  c.real = 1.0;
  c.imag = 2.0;
  a = b = c;

In the above example, c is copied to b, and then b is copied to a. The result is that all three variables contain the data originally stored in c. While not used as often as stringing together << statements, this is still a useful feature of C++.

The purpose of the const in the return type is to prevent assignments such as:

  (a = b) = c;

This is a tricky statement, because you may think it should just assign c to a and b like the earlier example. However, in this case the parentheses actually mean that the result of the statement a = b is being assigned a new value, which would actually assign it to a and bypass b altogether. By making the return type const, we prevent this from happening.


15.10 Invoking one member function from another

As you might expect, it is legal and common to invoke one member function from another. For example, to normalize a complex number, you divide through (both parts) by the absolute value. It may not be obvious why this is useful, but it is.

Let's write the function normalize as a member function, and let's make it a modifier.

  void normalize () {
    double d = this->abs();
    real = real/d;
    imag = imag/d;
  }

The first line finds the absolute value of the current object by invoking abs on the current object. In this case I named the current object explicitly, but I could have left it out. If you invoke one member function within another, C++ assumes that you are invoking it on the current object.

As an exercise, rewrite normalize as a pure function. Then rewrite it as a nonmember function.


15.11 Oddities and errors

If you have both member functions and nonmember functions in the same class definition, it is easy to get confused. A common way to organize a class definition is to put all the constructors at the beginning, followed by all the member functions and then all the nonmember functions.

You can have a member function and a nonmember function with the same name, as long as they do not have the same number and types of parameters. As with other kinds of overloading, C++ decides which version to invoke by looking at the arguments you provide.

Since there is no current object in a nonmember function, it is an error to use the keyword this. If you try, you might get an error message like: "Undefined variable: this." Also, you cannot refer to instance variables without using dot notation and providing an object name. If you try, you might get "Can't make a static reference to nonstatic variable..." This is not one of the better error messages, since it uses some non-standard language. For example, by "nonstatic variable" it means "instance variable." But once you know what it means, you know what it means.


15.12 Inheritance

The language feature that is most often associated with object-oriented programming is inheritance. Inheritance is the ability to define a new class that is a modified version of a previously-defined class (including built-in classes).

The primary advantage of this feature is that you can add new functions or instance variables to an existing class without modifying the existing class. This is particularly useful for built-in classes, since you can't modify them even if you want to.

The reason inheritance is called "inheritance" is that the new class inherits all the instance variables and functions of the existing class. Extending this metaphor, the existing class is sometimes called the parent class and the new class is called the subclass.


15.13 Message class

An an example of inheritance, we are going to take a message class and create a subclass of error messages. That is, we are going to create a new class called ErrorMessage that will have all the instance variables and functions of a Message, plus an additional member variable, errorCode, which will be displayed when the object is outputted.

The Message class definition looks like this:

class Message
{
  protected:
    pstring source;      //source of message
    pstring message;     //text in message

  public:
    //constructor
    Message(const pstring& src, const pstring& msg) {
      source = src;      //initialize source
      message = msg;     //initialize message
    }

    //convert message to pstring
    virtual pstring getMessage() const {
      return source + ": " + message;
    }
};

And that's all there is in the whole class definition. A Message has two protected member variables: the source of the message and the text of the message. The constructor initializes these member variables from the two pstrings passed as arguments.

You probably noticed that there is a const floating in free-space after the getMessage function declaration. When variables are declared const (such as in the Message constructor), it indicates that the function can't modify their values. However, a const after a function declaration means that the function itself is const! Only member functions can use this feature, because what it means is that the function can't modify any member variables of its class. Think of it as if *this is marked const.

The virtual indicator at the beginning of getMessage is a very important feature of inheritance. When a function is marked virtual, it allows that function to be redefined in subclasses. We will use this feature to change the behavior of getMessage in the ErrorMessage class.

Now here is an example of an ErrorMessage class which extends the functionality of a basic Message:

class ErrorMessage : public Message
{
  protected:
    pstring errorCode;        //error messages have error codes

  public:
    //constructor
    ErrorMessage(const pstring& ec, const pstring& src, const pstring& msg) {
      errorCode = ec;         //initialize error code
      source = src;           //initialize source
      message = msg;          //initialize message
    }

    //convert message to pstring
    virtual pstring getMessage() const {
      return "ERROR " + errorCode + ": " + source + ": " + message;
    }
};

The class declaration indicates that ErrorMessage inherits from Message. A colon followed by the keyword public is used to identify the parent class.

The ErrorMessage class has one additional member variable for an error code, which is added to the string returned from the getMessage function. It would serve to notify a user of the error code associated with whatever message they received. The constructor of ErrorMessage initializes both the original member variables, source and message, and the new errorCode variable.

An important thing to note is that the getMessage function has been redefined in ErrorMessage. Now the returned string includes the error code of the message. Suppose we want to overload the << operator to call getMessage in order to display messages.

ostream& operator << (ostream& os, const Message& msg) {
  return os << msg.getMessage();
}

This function will take any Message object and display it by calling its getMessage function. Since the ErrorMessage class is inherited from the Message class, what that means is that every ErrorMessage object is also a Message object! This allows you to use displayMessage like this:

ErrorMessage error ("1234", "Hard drive", "Out of space");
cout << error << endl;

The code first creates an ErrorMessage object with three strings for the source, message, and error code. Then the error message is passed to <<. Inside <<, the Message object's getMessage function is called in order to get a string representation of the object for output. The resulting output is:

ERROR 1234: Hard drive: Out of space

Even though the function thinks the object is a Message, and has probably never even heard of the ErrorMessage class, it is still calling a function defined in ErrorMessage. This is all because of the virtual keyword used in the getMessage declaration. All functions that are ever going to be redefined in subclasses must be declared virtual. Otherwise, << would not realize the object is an ErrorMessage and would go ahead and call the getMessage defined in Message instead.

As an excercise, remove the virtuals and recompile the program. See if you can predict the output before running it.


15.14 Object-oriented design

Inheritance is a powerful feature. Some programs that would be complicated without inheritance can be written concisely and simply with it. Also, inheritance can facilitate code reuse, since you can customize the behavior of build-in classes without having to modify them.

On the other hand, inheritance can make programs difficult to read, since it is sometimes not clear, when a function is invoked, where to find the definition. For example, in a GUI environment you could call the Redraw function on a Scrollbar object, yet that particular function was defined in WindowObject, the parent of the parent of the parent of the parent of Scrollbar.

Also, many of the things that can be done using inheritance can be done almost as elegantly (or more so) without it.


15.15 Glossary

dot notation
The method C++ uses to refer to member variables and functions. The format is className.memberName.
member function
A function that is declared within the class defintion of an object. It is invoked directly on an object using dot notation.
nonmember function
A function defined outside any class defintion. Nonmember functions are not invoked on objects and they do not have a current object.
current object
The object on which a member function is invoked. Inside the function, the current object is referred to by the pointer this.
this
The keyword that refers to a pointer to the current object.
virtual
The keyword that is used by any function defined in a parent class that can be overloaded in subclasses.
implicit
Anything that is left unsaid or implied. Within a member function, you can refer to the instance variables implicitly (without naming the object).
explicit
Anything that is spelled out completely. Within a nonmember function, all references to the instance variables have to be explicit.


Next Up Previous Hi Index