Object-oriented programming in C

by Paul Field

paul.field@dial.pipex.com

This is one of the first programming articles I wrote. It was published in C Vu 4:1 (November 1991) and, although my knowledge of object-oriented programming has improved immensely since then, I thought it was still useful.

Do you think that to program in an object-oriented style you need an object-oriented language? Well, you're wrong. It seems to be a common myth that you need an object-oriented language to implement an object-oriented design and, although languages such as C++ and Smalltalk provide many features that encourage this style of design, you can benefit equally well from the use of objects in imperative languages such as C.

The first thing to do is to learn some of the jargon of object-oriented programming. I've given some of the most important definitions below. Don't worry if you don't understand them at first, the main example that follows should make everything clear.

An object-oriented program is made up of objects. An object has an internal state (i.e. variables that are local to the object) and operations to modify that state. For example, Julie's car is an object. It has an internal state consisting of 'amount of fuel', 'make of car', 'number of miles driven' and 'colour' and it responds to the operations 'drive n miles', 'how many miles have you driven?' and 'respray with colour c'. Where relevant, objects know how to display themselves on the screen and how to load and save themselves.

Every object is an instance of (belongs to) a class. In simple terms a class is just a template for the object. It contains details of all the behaviour of an object along with what state information the object has (i.e. it has the code and data structure definitions). 'Car' is an example of a class. It describes what it is like to be a car but it isn't actually a car itself. To make a 'real' car you have to instantiate the class, and then you get an object such as Julie's car which is a green Metro or Fred's car which is a red Volvo.

The difference between object and class is like the difference between a car and its technical manual. The manual tells you everything you need to know about the car so that you can even go and build one, but you'll look pretty silly sitting in the manual and expecting it to transport you to Edinburgh.

We can put this into practice by starting with part of an informal description of a game-playing program:

The game uses a pack which initially contains 52 playing cards. A card can be identified by its suit and its value. The pack may be shuffled, and a single card may be dealt from the pack or replaced in the pack.

To keep things simple, I have not said anything about what suit or value mean. The first thing we have to do is identify the classes in this problem. A good rule of thumb is that the nouns in the description are objects and the verbs are the operations. This gives us the following classes:

Strictly speaking 'Game' is also an object, but its operations are not described in the text above, so we'll ignore it for the moment.

In an object based world the only way for anything to happen is by objects communicating with each other and acting on the results. This communication is called message-passing and involves one object sending a message to another and (possibly) receiving a result. Remember that every object's state is private - the only way you can access it is by asking the object to tell you what it is or to change it for you.

We can implement message passing in C by function call. If we want to send a message to an object and get a result we simply call a function with the following declaration:

  result message(object, parameters...);

If you look in the header files 'pack.h' and 'card.h' given below you can see how the operations for pack and card have been turned into function declarations. Notice that each class name is defined as a type (which is a pointer to some unknown internal data) so there is now a mapping where variables represent objects and types represent classes.

There are two operations that are usually needed although they are not explicitly mentioned in the problem text, they are 'create' and 'destroy'. Technically, 'create' is a class operation; an object cannot be asked to create itself since it does not exist, so the object's class must handle the operation. The distinction between class and object operations is not particularly important unless you are using an object-oriented language, suffice to say that operations that concern objects in general (e.g. 'how many cars have been created?', 'what's the average size of a whale?') are class operations whereas operations that concern a particular object and its state (e.g. what colour is Harold's hamster?) are object operations.

The next stage of our implementation involves determining what makes up the internal state of an object. The problem text says that a pack of cards begins with 52 cards but that this number could change because of dealing and replacing cards. I have chosen to use an array of cards along with a variable containing the number of cards in the pack as the internal state information for a 'pack'. Notice that when the code for pack needs to access a card in some way, it only uses function calls. There are no assumptions about the way cards are implemented - if you change the data structures and code that make up a card everything should still work (as long as the card functions appear to do the same things as before).

Once all of the classes, their operations and their data structures have been determined, the code can be developed. As I've pointed out earlier, every object has an internal state. To ensure that this is truly private we must keep every class's code in a separate file and use techniques to hide certain information from other modules. Firstly, we can use the keyword static to hide variables in a module. If, for example, we wanted to keep track of how many 'pack' objects existed in our game playing program, we could add the following line to the source file 'pack.c':

  static int numberofpacks;

along with suitable code to initialise and update it. Since static has been used, no outside module can directly access this variable, so it is private to the class. A different technique is used to hide the data structures. If you look at the file 'pack.c' below, you can see that every pack's internal state is represented by the structure 'pack_str'. To stop any outside modules from using this structure directly, we can use a 'partial declaration' of pack_str in the header file 'pack.h':

  typedef struct pack_str *pack;

This defines a new type 'pack' which is a pointer to a structure called 'pack_str'. This is the only reference to 'pack_str' that the outside modules have and, since they know nothing about the fields inside 'pack_str', they can't access it directly.

Here is the complete source code for the classes 'pack' and 'card', together with some definitions of 'value' and 'suit'. I have defined these as enumerated types - but note that neither card nor pack make any assumptions about this, they could equally well be implemented as strings (containing the name of the suit or value) - as long as the functions that they provide appear to do the same thing.


/**** pack.h ****/

#include "card.h"

typedef struct pack_str *pack;
 /* Defines 'pack' as a pointer to a structure that is not
  * defined here and so is not accessible to the outside world
  */

#define PACK_MAXCARDS 52   /* Maximum number of cards in a pack */

pack pack_create(void);
 /* Returns a new pack, or NULL if there isn't enough memory.
  */

void pack_destroy(pack);
 /* Frees the memory associated with the pack. Do not use the pack after this operation.
  */

void pack_shuffle(pack);
 /* Places the cards in the pack in a random order.
  */

card pack_deal(pack);
 /* Removes a card from the pack.
  * Returns NULL if the pack is empty.
  */

void pack_replace(pack, card);
 /* Replaces a card in the pack.
  */

/**** pack.c ****/

#include "pack.h"

#include <assert.h>
#include <stdlib.h>

struct pack_str
 { int  numberofcards;
   card cards[PACK_MAXCARDS];
 }pack_str;


pack pack_create(void)
 { pack p;

   p = malloc(sizeof(struct pack_str));
   if (p != NULL)
    { suit  s;
      value v;
      card  c;

      p->numberofcards = 0;
      for (s = suit_lowest(); s <= suit_highest(); s = suit_next(s))
       { for (v = value_lowest(); v <= value_highest(); v = value_next(v))
          { if ((c = card_create(s,v)) == NULL)
             { pack_destroy(p);
               return(NULL);
             }
            pack_replace(p, c);
          }
       }
    }
   return(p);
 }


void pack_destroy(pack p)
 { int n;

   for (n = p->numberofcards-1; n >= 0; n--)
    { card_destroy(p->cards[n]);
    }
   free(p);
 }


void pack_shuffle(pack p)
 { card tempcard;
   int  n;
   int  swappos;

   /**** Based closely on 'The Harpist's card shuffle ****/

   for (n = p->numberofcards-1; n >= 0; n--)
    { swappos = rand()%(n+1);   /* Random number in the range 0-n */
      tempcard = p->cards[n];
      p->cards[n] = p->cards[swappos];
      p->cards[swappos] = tempcard;
    }
 }


card pack_deal(pack p)
 { if (p->numberofcards == 0)
    { return(NULL);
    }
   else
    { return(p->cards[--p->numberofcards]);
    }
 }


void pack_replace(pack p, card c)
 { assert(p->numberofcards < PACK_MAXCARDS);  /* There will be too many cards */
   p->cards[p->numberofcards++] = c;
 }

/**** card.h ****/

#include "suit.h"
#include "value.h"

typedef struct card_str *card;
 /* Defines 'card' as a pointer to a structure that
  * is not defined here and so is not accessible to the outside world
  */


card card_create(suit, value);
 /* Returns a new card, or NULL if there isn't enough memory.
  */

void card_destroy(card);
 /* Frees the memory associated with 'card'. Do not use the card after this operation.
  */

suit card_identifysuit(card);
value card_identifyvalue(card);
/* void card_display(card); or similar would be useful */

/**** card.c ****/

#include "card.h"
#include <stdlib.h>

struct card_str
 { suit  s;
   value v;
 }card_str;


card card_create(suit s, value v)
 { card c;

    c = malloc(sizeof(struct card_str));
    if (c != NULL)
     { c->s = s;
       c->v = v;
     }
    return(c);
 }

void card_destroy(card c)
 { free(c);
 }

suit card_identifysuit(card c)
 { return(c->s);
 }

value card_identifyvalue(card c)
 { return(c->v);
 }

/**** suit.h ****/

typedef enum suit
 { hearts, clubs, diamonds, spades
 }suit;

#define suit_lowest()   hearts
#define suit_highest()  spades
#define suit_next(s)    ((s)+1)

/**** value.h ****/

typedef enum value
 { two, three, four, five, six, seven, eight, nine, ten, jack, queen, king, ace
 }value;

#define value_lowest()  two
#define value_highest() ace
#define value_next(v)   ((v)+1)

So, what are the advantages of this approach? Firstly, the code is maintainable. If you suddenly want to add a extra card to every suit or add another suit then you simply change the definition of the 'value' or 'suit' class and everything should work. Equally, you may discover a better (less memory or faster) way of implementing one of the classes. If you had 'dictionary' objects that used a linked list structure to store words, you might change this to a binary tree to improve the speed of access. Since no other objects make assumptions about that linked list structure, only the code in the 'dictionary' class needs to be changed - you will not have to search through 20 other source files changing references to linked lists and introducing untold numbers of bugs.

The second advantage is reusability of the code. The classes 'pack' and 'card' could be used in any card game program and an added benefit is that the more you use them without any problems, the more you can be confident that they are bug-free.

The readability of the program should also be improved by using object-oriented techniques. Every time you access an object the function call acts as its own comment. So, instead of:

  p->cards[p->numberofcards++] = c;

you have:

  pack_replace(p, c);

which is much easier to understand, easier to remember and also less prone to errors when you type it in.

There are, however, disadvantages. Many of an object's functions are very simple, often only one or two commands, but because they are functions they incur the overhead of a function call and return every time they are used. There is a way around this. Write your program using objects and then, if it is too slow, use profiling tools to see which functions are called the most. If these are 'simple' functions then you can turn them into macros and put them into the class' header file (see suit.h and value.h which use macros instead of functions). The unfortunate side of this is that you may have to make some of the internal declarations global to the program. Usually, only a small percentage of the code is involved in most of the processing so this 'macroising' technique should not be too time consuming and should not corrupt the (hopefully) elegant style of your program.

Although this article has concentrated on the use of C to implement an object-oriented design, there are advantages of using an object-oriented language. Many of these languages handle memory allocation for you and have automatic garbage-collection so that you don't have to free memory explicitly. A powerful feature of object-oriented languages is inheritance, which allows classes to be arranged in a hierarchy and inherit behaviour from classes above them. If you are interested in object-oriented programming I would suggest playing with a real object-oriented language such as Smalltalk (there is a PD version called 'Little Smalltalk' that I believe is available on various micros including the Archimedes and probably the IBM PC and Macintosh) - even if you still use C to implement your object-oriented designs you will have a much better feel for defining objects from the problem requirements in the first place.

Finally, and perhaps most importantly, beware the dogma of object-oriented design. Many problems have 'natural' object-oriented solutions, but equally many are best solved in a functional way. I think that many projects need a mixture of both. Remember that object-oriented programming is a powerful tool and so is a screwdriver but neither can unbolt nuts.


Last modified 19th July 1998       Comments to caug@accu.org


Mirrored from http://www.accu.org/