Home > Articles > Programming > C/C++

  • Print
  • + Share This
This chapter is from the book

2.6 Object-Oriented Programming

Data abstraction is fundamental to good design and will remain a focus of design throughout this book. However, user-defined types by themselves are not flexible enough to serve our needs. This section first demonstrates a problem with simple user-defined data types and then shows how to overcome that problem by using class hierarchies.

2.6.1 Problems with Concrete Types

A concrete type, like a "fake type" defined through a module, defines a sort of black box. Once the black box has been defined, it does not really interact with the rest of the program. There is no way of adapting it to new uses except by modifying its definition. This situation can be ideal, but it can also lead to severe inflexibility. Consider defining a type Shape for use in a graphics system.

Assume for the moment that the system has to support circles, triangles, and squares. Assume also that we have

class Point{ /* ... */ };
class Color{ /* ... */ };

The /* and */ specify the beginning and end, respectively, of a comment. This comment notation can be used for multi-line comments and comments that end before the end of a line.

We might define a shape like this:

enum Kind { circle, triangle, square }; // enumeration

class Shape {
  Kind k;   // type field
  Point center;
  Color col;
  // ...

public:
  void draw();
  void rotate(int);
// ...
};

The "type field" k is necessary to allow operations such as draw() and rotate() to determine what kind of shape they are dealing with (in a Pascal-like language, one might use a variant record with tag k). The function draw() might be defined like this:

void Shape::draw()
{
  switch (k) {
  case circle:
   // draw a circle
   break;
  case triangle:
   // draw a triangle
   break;
  case square:
   // draw a square
   break;
  }
}

This is a mess. Functions such as draw() must "know about" all the kinds of shapes there are. Therefore, the code for any such function grows each time a new shape is added to the system. If we define a new shape, every operation on a shape must be examined and (possibly) modified. We are not able to add a new shape to a system unless we have access to the source code for every operation. Because adding a new shape involves "touching" the code of every important operation on shapes, doing so requires great skill and potentially introduces bugs into the code that handles other (older) shapes. The choice of representation of particular shapes can get severely cramped by the requirement that (at least some of) their representation must fit into the typically fixed-sized framework presented by the definition of the general type Shape.

2.6.2 Class Hierarchies

The problem is that there is no distinction between the general properties of every shape (that is, a shape has a color, it can be drawn, etc.) and the properties of a specific kind of shape (a circle is a shape that has a radius, is drawn by a circle-drawing function, and so on). Expressing this distinction and taking advantage of it defines object-oriented programming. Languages with constructs that allow this distinction to be expressed and used support object-oriented programming. Other languages don't.

The inheritance mechanism (borrowed for C++ from Simula) provides a solution. First, we specify a class that defines the general properties of all shapes:

class Shape {
  Point center;
  Color col;
  // ...
public:
  Point where() { return center; }
  void move(Point to) { center = to; /* ... */ draw(); }

  virtual void draw() = 0;
  virtual void rotate(int angle) = 0;
  // ...
};

As in the abstract type Stack in §2.5.4, the functions for which the calling interface can be defined—but where the implementation cannot be defined yet—are virtual. In particular, the functions draw() and rotate() can be defined only for specific shapes, so they are declared virtual.

Given this definition, we can write general functions manipulating vectors of pointers to shapes:

void rotate_all(vector<Shape*>& v, int angle) // rotate v's elements angle degrees
{
  for (int i = 0; i<v.size(); ++i) v[i]->rotate(angle);
}

To define a particular shape, we must say that it is a shape and specify its particular properties (including the virtual functions):

class Circle : public Shape {
  int radius;
public:
  void draw() { /* ... */ }
  void rotate(int) {}  // yes, the null function
};

In C++, class Circle is said to be derived from class Shape, and class Shape is said to be a base of class Circle. An alternative terminology calls Circle and Shape subclass and superclass, respectively. The derived class is said to inherit members from its base class, so the use of base and derived classes is commonly referred to as inheritance.

The programming paradigm is now as follows:

Decide which classes you want; provide a full set of operations for each class; make commonality explicit by using inheritance.

Where there is no such commonality, data abstraction suffices. The amount of commonality between types that can be exploited by using inheritance and virtual functions is the litmus test of the applicability of object-oriented programming to a problem. In some areas, such as interactive graphics, there is clearly enormous scope for object-oriented programming. In other areas, such as classical arithmetic types and computations based on them, there appears to be hardly any scope for more than data abstraction, and the facilities needed for the support of object-oriented programming seem unnecessary.

Finding commonality among types in a system is not a trivial process. The amount of commonality to be exploited is affected by the way the system is designed. When a system is designed—and even when the requirements for the system are written—commonality must be actively sought. Classes can be designed specifically as building blocks for other types, and existing classes can be examined to see if they exhibit similarities that can be exploited in a common base class.

Class hierarchies and abstract classes (§2.5.4) complement each other instead of being mutually exclusive. In general, the paradigms listed here tend to be complementary and often mutually supportive. For example, classes and modules contain functions, while modules contain classes and functions. The experienced designer applies a variety of paradigms as need dictates.

  • + Share This
  • 🔖 Save To Your Account