Features


Object-Oriented Programming As A Programming Style

Eric White


Eric White is a software engineer at Advanced Programming Institute, Ltd. He is working on a character-based version of XVT. XVT is a common programming interface with implementations for various window systems, including Macintosh, Microsoft Windows, Presentation Manager, OSF/Motif, and character based on UNIX and MS-DOS. He can be reached at API at (303) 443-4223.

Object-oriented programming is a programming style that can be used in many languages, including C and C++. Some programmers think that C++ gives them the ability to do object-oriented programming. This isn't accurate -- C programmers can already do object-oriented programming. I will demonstrate by showing two identically structured object-oriented programs, one in C and the other in C++.

Even though one can do object-oriented programming in C, C++ offers several advantages: C++ supplies syntactic support for object-oriented programming and C++ provides type checking where not possible in C.

I am assuming the reader has already read one of the numerous magazine articles that introduce object-oriented programming. A good article is "Mapping Object Oriented Concepts Into C++ Language Facilities", CUJ July '89 by Tsvi BarDavid. If you already know C, an example of object-oriented programming in C can clarify exactly what is goes on in object-oriented programming. Once you understand the C example, the identical example in C++ can make learning C++ easier. You can even imagine how the code generated by a C++ translator looks.

The Example

I'll develop the comparison using a graphical application that could be the beginnings of a drawing program such as Mac Draw. This example is constructed with four classes of objects: graph_obj, circle, square, and double_circle. Three instructions can be given to any one of these objects:

The Listings

Listing 1 is the pseudo-code for the example. The code in Listing 2 (obj.h) and Listing 3 (obj.c) facilitates object-oriented programming in C, allowing the creation of classes, methods, objects, and implementing inheritance. Listing 4 (drawc.c) and Listing 5 (drawcxx.cxx) are two examples of object-oriented code in C and C++ respectively. They perform identically.

In the pseudo-code, you can see:

For portability, I isolate the graphics functions in a utility module. Listing 6 (utility.h) is the interface to the utility module. Listing 7 (utility.c) contains fatal() and the graphical functions. The utility module is compiled and linked with either the C or C++ code. The isolation also makes it easier to compare the two object-oriented examples.

Object-Oriented Programming In C

This system implements classes, methods, objects, inheritance, and messages. The entire module that facilitates object-oriented programming is less than 90 lines of code.

I'll start with a simple data abstraction mechanism, then develop it into a system that supports classes, inheritance, and messages.

The most natural means of creating an object and associating methods with it is to put pointers to the methods (pointers to functions) in a structure along with the data. A structure for an instance of a circle might look like this:

struct {
    int y;
    int x;
    int radius;
    void (*init)();
    void (*draw)();
    void (*move)();
} circle;
This implements an object that knows how to initialize itself, draw itself, and move itself. The implementation could vary for different types (such as a double circle). However, we might get tired of setting up the methods every time we create a new instance of a circle. A solution is to design another structure (called a class) that contains the pointers to the functions, and place only a pointer to the class in each object. With this technique we may create a class once, then create several objects and have them point to that class.

To make the class structure more generic, we define an array of pointers to functions, and by convention, define the methods as an index into this array. The code now looks like

/* defines for methods */
#define INIT 0
#define DRAW 1
#define MOVE 2

struct class {
    int nbr_methods;
    void (**method)();
};

typedef struct class CLASS;

struct {
    CLASS *class;
    int y;
    int x;
    int radius; }
circle;
When creating a class, we need to initialize the array of pointers to functions after allocating memory for it. If the method is implemented in the class itself, then the pointer is set to the function address. If the method is inherited from the super-class, then the pointer is loaded from the super-class.

To make an object more generic, we'll take the definition of the data out of the object and replace it with a pointer to the data. Space for the data is allocated when the object is created and freed when the object is no longer needed. Listing 2 contains the final definitions of structures for class and object.

Classes

To define a class:

Methods

A method is a function written specifically to go with the class. In this example, methods don't return a value.

All methods should be aware that obj->data is a pointer to the data allocated on the heap. For a particular class, this data is of an assumed structure type. By casting obj->data to a pointer to a structure, the method can access the object data correctly.

All methods receive the argument arg_ptr, which can be used with the macro va_arg() if there are arguments to the method. See your documentation on stdarg.h.

Objects

The structure that holds what we need to know about an object is:

typedef struct {
    void *data; 
    CLASS *class;
} OBJECT;
To create and use an object:

Inheritance

Inheritance of methods is demonstrated here. circle inherits MOVE from class graph_obj. double_circle inherits INIT and MOVE from its super-classes.

I implement inheritance of data structures by having a sub-class allocate more memory than the super-class. The sub-class data consists of the parent-class data followed by the data specific to the subclass.

Messages

There is a distinction between a message and a method. A message gets sent to an object, and then something decides which method to invoke. Invoking a method means that the function that is part of the class is called. In C++, the translator decides which method to invoke. In the system implemented in C, the function message() (Listing 3) decides, based on the class of the object.

Summary Of OOP In C

One disadvantage of doing object-oriented programming in C is that there is no function prototyping. We have no idea what the arguments to a method are when we declare the pointers to functions in the class structure. Programmers are responsible for sending the correct parameters to a message.

Another disadvantage is that when writing methods, the programmer must access the data in the object correctly. The pointer to the data in the object structure must be cast as a pointer to the correct structure type.

Object-Oriented Programming In C++

The C++ example also demonstrates classes, methods, objects, inheritance, and messages.

I'll explain a small subset of the syntax of C++, only what is essential to do object-oriented programming. There are many features of C++ that have nothing to do with object-oriented programming, and the object-oriented programming part of C++ is elaborate, with useful but nonessential features. The subset is:

Classes

The three essential pieces of a class are:

The definition of a class in C++ looks like:

class graph_obj {
public:
    int y;
    int x;
    void init(int y, int x);
    void move(int y, int x);
    virtual void draw(int color){};
};
y and x are the data that will be contained in an object of class graph_obj. To define methods, you put the function prototype for the methods in the definition of the class.

The class graph_obj doesn't have a super-class. When defining a class where there is a super-class, you follow the name of the class by a colon (:), the keyword public, and the name of the super-class. For example:

class circle : public graph_obj {
public:
    int radius;
    void init(int y, int x, int
radius);
    void draw(int color);
};
Members of a class may be private or public. For simplicity's sake, all members of all classes in this example are public. I'm not attempting to do data-hiding in this example. Hiding data is a separate (and important) issue, but is beyond the scope of this article. The keyword public before the name of the super-class means that all the public members of the super-class are public members of the sub-class.

Methods

The definition of a method looks similar to that of a function. To define the name of the function, you follow the class name with the scope resolution operator :: and the name of the method. For example, the draw method for class circle would look like this:

void circle::draw(int color)
{
    /* code to draw a circle */
    ...
}
Here is an important note about coding a method. A hidden argument to every method is the object. When a method gets invoked for a particular object, by definition you get access to that object. You can access the members of that object just by using the names of the members.

Methods are invoked much as functions are called in C. Sometimes, when writing code for a method, we want to force a method to be invoked for a super-class, and the class for which we are writing the method has a method of the same name as the one in the super-class that we want to invoke. In this case, we can use the scope resolution operator (::) and force the method to be invoked for the super-class. For the init method for class circle, to invoke the init method for class graph_obj, we specify the name of the class, followed by the scope resolution operator, followed by the name of the method.

Sometimes the method to invoke at run time can't be determined because a particular section of code could be operating on many types of objects. In C++, code such as this must be operating on objects of a certain class, or of a sub-class of that class. If you declare a method of a class highest in the class hierarchy virtual, C++ will wait until run time to make the decision of which method to invoke, and will invoke the correct method for the object being operated on. To do this, C++ puts something in the object that indicates which class it is. Resolution of the method to invoke at run time is called late binding.

This is useful when you send messages to pointers to objects, where the pointer could point to one of several classes of objects. It's also useful in a method that serves a class and its subclasses. draw is virtual because the method move (which uses the method draw) in class graph_obj also serves classes circle, double_circle and square.

In C++, each class can have two special methods: the constructor and destructor. Essentially the constructor is called automatically when an object comes into scope, and the destructor is called when an object goes out of scope. For example, if you declare an automatic object at the start of a function, the constructor is called at the time of declaration, and the destructor is called before the function returns to its calling function.

Constructors and destructors are not essential to object-oriented programming. In other systems, programmers make a method specifically for initializing an object when they need one, then send that message to the object after creating it. In the C++ example that accompanies this article, I don't use the built-in constructors and destructors. In both the C and C++ examples, I have a method that initializes the values of the graphical object. I call this method INIT.

In the C example, I use a function that allocates memory for the object before use and frees the memory after use. These functions aren't defined as part of a class and should not be confused with methods.

Objects

An object declaration looks like a declaration of something for which there is a typedef in C. A declaration of an object of class circle looks like:

circle c1;
In the graphics example, immediately after declaring a graphical object the init message is sent to the new object. This gives the object its starting position and size, and draws it on the screen.
Listing 7, line 99 shows initialization of a circle at position (40, 40), with a radius of 20.

After sending the init message, we can send a move message to the object, causing it to move on the screen. (Listing 7, line 103-105).

In the C example, we use a pointer in an object to point to the data specific to that instance of the object. new_object() allocates that data on the heap, and the function free_obj () frees it.

In contrast, the C++ translator actually creates a structure that contains the data. In our example, this structure is an automatic structure. Space for it gets deallocated when main() returns. We don't need to free any data on the heap as we needed to do in the C example.

Inheritance

Just as in the C example, the C++ example demonstrates inheritance of methods. double_circle inherits init and move from class circle.

Messages

Sending a message in C looks like:

message(&c1, MOVE, 1, 1);
Sending a message in C++ looks like:

c1.move(1, 1);
We specify the same essential elements in both cases. They are:

Summary Of OOP In C++

Data hiding and modularity are important issues in C++ as in other languages. I am not addressing these issues and have put the entire program in one source file. I want to focus on the object-oriented aspect and keep it simple.

Often in C++, when a message is sent to an object of a known type, the compiler resolves the particular method to invoke at compilation time. This is called early binding. In contrast, the function message() in the C scheme presented here resolves the issue of which method to invoke at run time. This is called late binding.

Because the C methodology always does late binding, a little more code must always be executed at run time. The C code may be a bit slower than the code generated by the C++ translator. However, when using virtual functions, I believe that the speed of sending a message in C is comparable to C++.

C++ inherits many of the characteristics of C. In C++, you have the ability to corrupt memory in the same ways that you can corrupt memory in C. This causes temporal and referential non-localization of bugs. C++ offers the same beneficial characteristics of C such as speed, compactness, and the possibility of portability.

Portability

The C code is quite portable and runs on:

The C++ code runs on:

The graphics code works on CGA, EGA, Hercules and VGA.

The utility module can use either the graphics library that accompanies Microsoft C v5.0 or the graphics library that comes with the Zortech C++ compiler.

If you are using the Microsoft graphics library and Hercules graphics, before you can run these programs you need to run MSHERC.COM.

The Zortech graphics library has its origin at the lower-left corner. Microsoft has its origin at the upper-left corner. Also, because pixels are not square, neither the Zortech nor the Microsoft libraries create perfectly round circles. Because this article is focusing on object-oriented techniques and not on graphical techniques, I didn't address any of these problems.

Exercises

A few valuable exercises might be:

Acknowledgements

I thank Marc Rochkind and Tom Cargill, who taught me much of what I know about object-oriented programming.