Journal Articles
Browse in : |
All
> Journals
> CVu
> 142
(12)
All > Topics > Programming (877) Any of these categories - All of these categories |
Note: when you create a new publication type, the articles module will automatically use the templates user-display-[publicationtype].xt and user-summary-[publicationtype].xt. If those templates do not exist when you try to preview or display a new article, you'll get this warning :-) Please place your own templates in themes/yourtheme/modules/articles . The templates will get the extension .xt there.
Title: Applying OO to C
Author: Administrator
Date: 03 April 2002 13:15:50 +01:00 or Wed, 03 April 2002 13:15:50 +01:00
Summary:
It is possible to apply OO techniques to make better C programs and that is the thrust of this article.
Body:
C of itself lacks support for object-orientated programming (OOP). There are no classes, overloading, inheritance or other nice OOP features that make C++ and Java OO languages and as we all know OO is a good thing. Unfortunately there are a large number of applications still to be written in C because there is no alternative. Real-time developments soldier on using C and assembler, and some products will not be developed in C++ or Java because of safety considerations.
But it is possible to apply OO techniques to make better C programs and that is the thrust of this article.
The first building block is the opaque type. An application programmer is presented with a set of services that manipulate objects of type T but because of how T is defined she unaware of the construction of the type. Basically the programmer is not given any information about T so cannot rely on T's construction, i.e., fiddle with it.
The standard C library has the FILE type which is almost opaque-all the services only use pointers to a FILE object-but the construction of FILE is visible, you can find out what it is made of and that means you can write code that assumes something about it.
A true opaque type reveals nothing and this is how it is done; I use typedefs to make the code more understandable.
In a header file declare the type and services that use pointers to type:
typedef struct Ttag T; /* T is the opaque type */ extern T* get_a_T(void); /* create on free store/retrieve a new T */ extern void finish_with_a_T(T*); /* delete from free store/put back a T */ //... other services
In the source file defining the services, the definition of T is completed and the services defined:
struct Ttag { //.. elements as required, e.g. int thingy; }; T* get_a_T(void) {return malloc(sizeof T); void finish_with_a_T(T* t) {if(t) free(t);} //... other services
The user of these services cannot determine the data structure of a T! This is because the definition is completed inside the C source file. With this approach we can encapsulate services and operations because we have hidden all data details from the user. Best bit - we can change the implementation and the user will be unaware. Stage 1 of OO C achieved.
In the code above I ignored the problem of safe cleanup of objects, partly because in C the only mechanisms available are:
-
use a garbage collection library to replace the malloc/free calls;
-
use atexit() to register a clean up function for objects created by get_a_T(). The function will get called when the program exits because of a completion of main() or calls to exit(). The price is nothing gets cleared up until exit. Installing the function could be carried out within the call to get_a_T() - see lazy evaluation example below;
-
require the user match creation with destruction, i.e., for every T created (?) with get_a_T() there must be a corresponding call to finish_with_a_T() for that object. The user may have to guarantee the destruction under all circumstances by using the atexit() mechanism.
The classic iteration in C is to use a for loop:
for(i = 0; i < N; i++) {}
C++ iteration could use a for loop, but the Standard Template Library for_each function can be employed too - it scans a range calling the user defined handler:
for_each (x.begin(), x.end(), doSomething); //where x is some sort of container
Java has its own idioms (Java 1.1 has the Enumeration class and 1.2 adds the Iterator class):
Enumeration e = x.enumerate(); while(e.hasMoreElements()){ MyClass mc = (MyClass) e.nextElement(); mc.doSomething(); }
Imagine we want a simple directory lister that can be used on a number of platforms. How directory information is acquired is intimately related to the operating system, so as good OO practitioners, we want to hide the details from the user and contain the variances by linking in platform dependent source files. We define a universal enumeration interface header file, Enumerate.h, and one of many enumerations, for example FileEnum.h:
// Enumerate.h #ifndef ENUMERATE_H_ #define ENUMERATE_H_ typedef struct EnumerationTag Enumeration; #endif // FileEnum.h // a simple minded example, lists files on the // drive at its current directory #ifndef FILEENUM_H_ #define FILEENUM_H_ #include "enumerate.h" extern Enumeration * FileEnum_get_enumeration(char drive); extern int FileEnum_hasMoreElements(Enumeration *); extern char const * FileEnum_nextElement(Enumeration *); extern void FileEnum_delete(Enumeration *); #endif
The source for FileEnum, based on Borland's dir.h (DOS). Of course none of this is visible to the user of FileEnum services:
// FileEnum.c #include "FileEnum.h" #include <dir.h> #include <string.h> #define NAME_LEN 256 // for 32 dos bit name is 256 bytes struct EnumerationTag { // completion of the opaque type struct ffblk ffblk; int exists; char name[NAME_LEN]; }; Enumeration * FileEnum_get_enumeration(char drive) { Enumeration * e = malloc(sizeof(Enumeration)); if(e != NULL) { char buf[6] = " :*.*"; buf[0] = drive; /* should check if the drive exists, simple example though */ if(findfirst(buf, &e->ffblk, 0 /*normal files*/) == 0) e->exists = 1; else e->exists = 0; } return e; } int FileEnum_hasMoreElements(Enumeration * e){ if(e != NULL) return e->exists; return 0; } char const * FileEnum_nextElement( Enumeration * e) { if(e != NULL) { // copy filename before overwriting ffblk strcpy(e->name, e->ffblk.ff_name); if(findnext(&e->ffblk) == 0) e->exists = 1; else e->exists = 0; return(char const *) e->name; } return ""; } void FileEnum_delete(Enumeration * e) { if(e != NULL) free(e); }
Finally user code looks like this:
//printDir.c #include "FileEnum.h" #include <stdio.h> static void printDirectory(char drive) { Enumeration * e = FileEnum_get_enumeration(drive); while(FileEnum_hasMoreElements(e)) printf("%s\n", FileEnum_nextElement(e)); FileEnum_delete(e); }
The implementation of FileEnum can completely change without user code changing, and without having to re-compile the user code. The user code cannot break if the members of Enumeration change. The local variable e in printDirectory can be re-used for another type of iteration because it has no definition within the user's domain.
It's a shame we have to use specific calls when the Enumeration type is anonymous. This can be solved at a price and that price is a partial definition of Enumeration in "enumerate.h", something like:
/* Enumerate.h */ typedef struct EnumerateTagInternals EnumerateInternals; // opaque type /* function types */ typedef int (hasMoreElements_fn)(EnumerateInternals *); typedef void * (nextElement_fn)(EnumerateInternals *); typedef void (enumerationDeleter_fn)(void); typedef struct { hasMoreElements_fn * hasMoreElements; nextElement_fn * nextElement; enumerationDeleter_fn * deleter; EnumerateInternals * internals; } Enumeration; /* generic functions */ extern int hasMoreElements(Enumeration *); extern void * nextElement(Enumeration *); extern void enumerationDeleter(Enumeration *);
We need an Enumerate.c:
int hasMoreElements(Enumeration * e) {return(e->hasMoreElements)(e->internals); } void * nextElement(Enumeration * e){ { return(e->nextElement)(e->internals); } void enumerationDeleter(Enumeration * e) {(e->deleter)(); }
FileEnum's header and source would be:
/* FileEnum.h */ #ifndef FILEENUM_H_ #define FILEENUM_H_ #include "enumerate.h" extern Enumeration * FileEnum_get_enumeration(char drive); extern void FileEnum_delete(Enumeration *); #endif /* FileEnum.c */ #include "fileenum.h" #include <dir.h> #include <string.h> #include <stdlib.h> #define NAME_LEN 256 struct EnumerateInternals_tag { // completion of opaque type struct ffblk ffblk; int exists; char name[NAME_LEN]; }; static int FileEnum_hasMoreElements(EnumerateInternals* i){ if(i != NULL) return i->exists; return 0; } static void * FileEnum_nextElement(EnumerateInternals * i){ if(i != NULL){ strcpy(i->name, i->ffblk.ff_name); if(findnext(&i->ffblk) == 0)i->exists = 1; else i->exists = 0; return i->name; } return ""; } void FileEnum_delete(Enumeration * e){ if(e != NULL) { if(e->internals != NULL) free(e->internals); free(e); } } Enumeration * FileEnum_get_enumeration(char drive){ Enumeration * e=malloc(sizeof(Enumeration)); if(e != NULL){ e->internals = malloc(sizeof(EnumerateInternals)); e->hasMoreElements = FileEnum_hasMoreElements; e->deleter = FileEnum_delete; e->nextElement = FileEnum_nextElement; if(e->internals != NULL) { char buf[] = " :*.*"; buf[0] = drive; if(!findfirst(buf, &e->internals->ffblk, 0) e->internals->exists = 1; else e->internals->exists = 0; } } return e; }
mplementers of Enumeration services use the above as a template, but users are then more-or-less forced to use a consistent approach when they use these services - see user code below. This makes for consistency in large projects and that's a good thing for maintenance.
/* printDir.c */ #include "Enumerate.h" #include "FileEnum.h" #include <stdio.h> static void printDirectory(char drive){ Enumeration * e = FileEnum_get_enumeration(drive); while (hasMoreElements(e)){ printf("%s\n", (char*)nextElement(e)); } enumerationDeleter(e); }
Note that only the creation of the Enumeration object is specific, the rest of the code is generic.
Why is this polymorphic? Imagine we had defined a number of different Enumeration types and created some objects of these types. Now if they were held in a table we could scan the table and process all of them the same way- they meet the Liskov substitution principle because the interface is the same and (if the implementers have got it right) they behave the same.
Caveat. Not quite guaranteed because the return type of nextElement_fn is void * so it is possible to return a different type to the one you are expecting - however I leave that problem as an exercise for you to solve (hint: time for another opaque type).
This is here to show a classic pattern in C, often found in C++ and Java, and to fill in the details for a reference I made earlier.
This technique is used quite often to hide initialisation from the calling software. For example, supposing we wish to register a cleanup function to be called on exit. The function should be registered once only. On exit the function must then cleanup everything in its jurisdiction. Ignoring the issue of how it knows what to clean, let's see this in action:
#include <stdlib.h> // atexit static int cleanme = 0; // flag static void cleanup(void) { /* tidy up loose ends: files open, deal wih malloc'ed and not freed in this module etc. */ } void someFunctionCalledByTheUserThatMallocs(void){ if(cleanme ==0) { cleanme = 1; atexit(cleanup); } // rest of function including optional malloc }
Although this does not look interesting, it allows the user to implement safe memory and resource handling schemes, and can be used to get rid of delete functions at the cost of, say, allocated memory not freed until program closure.
Lazy evaluation can be used anywhere resources have to be set up in advance of access. I have used it successfully to set up reading parameters from battery backed ram where there was no control over the user calls - they could happen anytime including during system initialisation.
The OO bit is the user has no idea that something special is happening apart from the first call taking somewhat longer.
An extension to lazy evaluation is the concept of the singleton whereby a single instance of data is created on demand and no further instances are allowed:
static T * singleton = NULL; static void cleanupSingleton(void); // tidies up and frees the singleton at exit T * getSharedT(void){ if(singleton == NULL){ atexit(cleanupSingleton); singleton = malloc(sizeof(T)); // set up the singleton.. } return singleton; }
Note how creation and cleanup of the singleton T object are contained within the call to getSharedT.
There is more to say on the subject of OO C. Unfortunately I have run out of time to do so. I would have liked to try to cover the problem of separating out the what from the how, e.g. a queue object that can hold any type of object in a type safe manner - much easier in C++ and Java than C.
If you have found this article useful or have any comments please let me know. Enough encouragement and I might write some more.
Notes:
More fields may be available via dynamicdata ..