Object Oriented Programming in C

Version 0.5
Copyright (C) 2008 by Zack Smith
All rights reserved.

News

For actual downloadable code, click here.

Introduction

Object-oriented programming languages (OOPLs) became dominant years ago over older procedural languages like C, Pascal and the like. With these languages come fundamental problems and complexities that are not necessarily desired by a software architect.

The case of C++ exemplifies the pitfalls of using OO languages: its templates and multiple inheritance and ever-shifting class libraries make software design, implementation, and debugging more difficult than it needs to be. While some C++ code is well-written, some C++ code is an illegible tangle of derived classes worse than any "goto" based problem from the distant past. And yet, nevertheless a typical C++ partisan will defend such a C++ tangle as being perfectly valid and appropriate.

Due to the problems of common object-oriented languages, it is sometimes preferable for a programmer to use a non-OOP language such as C augmented to use object-oriented practices.

Key techniques of object-oriented C programming

Struct with pointers

A simple technique for implementing an object in C is to imitate C++, and provide a struct with pointers to methods inside.
typedef struct obj {
    int a;
    char *b;

    // Class methods
    struct obj (*new)();
    void (*delete)(struct obj *);

    // Object methods
    void (*set_a)(int);
    void (*set_b)(char *);
} Object; 
In use:
Object *obj = class_struct->new();
obj->set_a (123);
A possible pitfall of this approach is that methods in the struct might be NULL due to lack of initialization or may have been erroneously overwritten, in either case resulting in a program crash when used. And if a function pointer is overwritten, there will be no easy way to ascertain where the pointer was overwritten.

Struct and message-passer

A better alternative is to use an approach similar to Objective C's, and pass messages to objects based on a method name.
typedef struct obj {
   int class_id;
   int a;
   char *b;
} Object;
A send_message function is used to send messages to objects and is implemented to take variable numbers of arguments. The class initializer is responsible for loading all of its methods into a hash of functions. Note that it's necessary to include "stdarg.h".
int send_message(int count, ...)
{
   int tmp;
   va_list ap;
   va_start(ap, count);
   Object *this = va_arg(ap, Object*);
   char *msg = va_arg(ap, char*);

   // Here, look up method in hash and call appropriate function...

   va_end(ap);
} 
In use:
Object *obj;
obj = send_message (class_struct, "new");
send_message (obj, "set_a", 123);
Of course, in practice "send_message" would be shortened to something more useable, e.g. "S". In Objective C, one simply encloses the message passing in brackets e.g.
[ object message: parameters ];

An advantage to this approach is that method pointers are less likely to be overwritten since they are not in the data struct.

This approach can also be extended to include the hashing of object data, to provide further protection against program errors.

A further refinement might be to include some non-textual predefined numeric method names, e.g. implemented using enum or #define, which index into a table to avoid doing any string comparison. For instance:

enum { 
   MESSAGE_NEW=1, 
   MESSAGE_DESTROY=2, 
   MESSAGE_DUMP=3 
};
Object *obj = send_message (rectangle_class_struct, MESSAGE_NEW);
send_message (obj, "set_width", 456);

Container classes

In C++, the template functionlity permits the creation of containers of objects, for instance an ordered list of string objects. However the use of templates can quickly make code unreadable.

In C, an equivalent functionality can be implemented by simply having all containers deal with only one object type, which is a 32-bit or 64-bit value, and by providing the container with any functions that it may need to process that value, e.g. to compare two of them in order to sort a list. The application logic would then have to deal with the values themselves to decide if it is a basic type, e.g. int or float, or an object pointer.

An alternative is to literally pound-define the type in question e.g. #define SOMETYPE int, just before pound-including the container class, e.g. write the container class to contain things of type SOMEOBJECT, which is defined in another file.

Download

My implementation of the message-passing approach is a work in progress.

Links