Object-oriented C
Object-oriented languages align more closely with human thinking and often reduce code complexity while improving readability and maintainability. Traditional C can also be organized to produce readable, maintainable, and low-complexity code. This article illustrates that with a practical example.
Basics
Structures
In addition to basic data types, C allows user-defined data types via structures. A structure can represent any entity. Structures are precursors to the class concept in object-oriented languages. For example:
typedef struct{
float x;
float y;
}Point;
This defines a point in a plane with two fields, x and y.
Fields in a structure are called members. Members can be simple data types, other structures, or nested structures. For example, a typical linked list node can be defined as:
typedef struct node{
void *data; // data pointer
int dataLength; // data length
struct node *next; // pointer to the next node
}Node;
Here the next pointer in node is of type node.
Function pointers
Pointers are a core feature of C and give the language flexibility and power. Function pointers point to a function's entry address in memory. They allow passing functions as parameters and invoking them later, enabling callbacks and asynchronous communication.
For example, the signal registration function prototype in UNIX/Linux is:
void (*signal(int signo,void (*func)(int))) (int)
To use it, define a signal handler externally and register it with signal(sigNo, handler). When the signal occurs, the process can invoke the handler.
Function pointers as struct members
Structure members can include pointers, including function pointers. When function pointers in a structure operate only on that structure's data, the combined data and operations form a self-contained entity that resembles a class.
Object-oriented language features
Inheritance, encapsulation, and polymorphism are commonly regarded as essential object-oriented features. These features highlight the advantages of object-oriented over procedural programming.
Object orientation is a design paradigm independent of specific languages. Although many languages offer built-in object-oriented constructs that improve readability and match natural thinking, the conceptual approach can be applied in C using available mechanisms.
Language-level object orientation
When describing an object, we usually specify its attributes and behaviors. For example, a box has six faces, a color, a weight, and whether it is empty; it can contain objects and allow retrieval.
In object-oriented languages this is typically represented as a class:
class Box{
color color;
int weight;
boolean empty;
put(something);
something get();
}
Operations would look like:
Box.put(cake);
Box.get();
In procedural languages, operations are often global functions that take the entity as a parameter:
Put(Box, cake);
Get(Box);
The first form is more intuitive, and object-oriented languages provide syntax-level support for it. C is flexible enough to emulate this style using its basic features.
Object-oriented style in C
Object orientation is a design approach independent of language. Below is an example using a linked list to show how to design C code with an object-oriented flavor.
Defining the interface
An interface specifies the operations an implementing entity provides without exposing implementation details. This allows implementers to change internals without affecting users of the interface.
Here is a linked list interface definition:
#ifndef _ILIST_H
#define _ILIST_H
// Define a node in the list
typedef struct node{
void *data;
struct node *next;
}Node;
// Define the list structure
typedef struct list{
struct list *_this;
Node *head;
int size;
void (*insert)(void *node); // function pointer
void (*drop)(void *node);
void (*clear)();
int (*getSize)();
void* (*get)(int index);
void (*print)();
}List;
void insert(void *node);
void drop(void *node);
void clear();
int getSize();
void* get(int index);
void print();
#endif /* _ILIST_H */
The IList interface makes clear that a list entity supports insert, drop, clear, getSize, get(index), and print operations.
Interface implementation
Listing 2. Constructor
Node *node = NULL;
List *list = NULL;
void insert(void *node);
void drop(void *node);
void clear();
int getSize();
void print();
void* get(int index);
List *ListConstruction(){
list = (List*)malloc(sizeof(List));
node = (Node*)malloc(sizeof(Node));
list->head = node;
list->insert = insert; // register insert implementation on the list entity
list->drop = drop;
list->clear = clear;
list->size = 0;
list->getSize = getSize;
list->get = get;
list->print = print;
list->_this = list; // store the list itself in _this
return (List*)list;
}
Note the _this pointer which ensures external operations on list are mapped to operations on _this, simplifying the code.
Listing 3. Insert and drop
// Insert a node into a list object
void insert(void *node){
Node *current = (Node*)malloc(sizeof(Node));
current->data = node;
current->next = list->_this->head->next;
list->_this->head->next = current;
(list->_this->size)++;
}
// Remove a specified node
void drop(void *node){
Node *t = list->_this->head;
Node *d = NULL;
int i = 0;
for(i; i < list->_this->size; i++){
d = list->_this->head->next;
if(d->data == ((Node*)node)->data){
list->_this->head->next = d->next;
free(d);
(list->_this->size)--;
break;
}else{
list->_this->head = list->_this->head->next;
}
}
list->_this->head = t;
}
Other implementation details are omitted for brevity.
Testing
Test code
The previous work aims to provide a clean API for users. The following test demonstrates usage.
int main(int argc, char** argv) {
List *list = (List*)ListConstruction(); // construct a new list
// Insert some values for testing
list->insert("Apple");
list->insert("Borland");
list->insert("Cisco");
list->insert("Dell");
list->insert("Electrolux");
list->insert("FireFox");
list->insert("Google");
list->print(); // print the entire list
printf("list size = %d\n",list->getSize());
Node node;
node.data = "Electrolux";
node.next = NULL;
list->drop(&node); // remove a node
node.data = "Cisco";
node.next = NULL;
list->drop(&node); // remove another node
list->print(); // print again
printf("list size = %d\n",list->getSize());
list->clear(); // clear the list
return 0;
}
Conclusion
The Unix design philosophy emphasizes simple building blocks that users can combine to build powerful applications. C inherits this philosophy: it is concise and powerful. Although many early C applications were procedural, C provides simple, powerful, and general mechanisms that let developers assemble higher-level abstractions as needed.