Object-oriented C
Object-oriented languages more closely match human reasoning and often reduce code complexity while improving readability and maintainability. Traditional C can also be designed to produce readable, maintainable code with low complexity. This article demonstrates that with a practical example.
Basics
Structures
Beyond basic data types, C allows users to define custom data types using structures. In C, you can represent any entity with a struct. Structures are the forerunner of 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.
Struct fields are called members. Member types can be simple types, other structs, or even nested structs. 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 next node
} Node;
Note that the next pointer in node is of type struct node.
Function pointers
Pointers are a core part of C and provide flexibility and power. Function pointers point to a function's entry in memory. Through function pointers, functions can be passed as parameters and invoked later, enabling callbacks and similar patterns.
For example, the signal registration function in UNIX/Linux has the prototype:
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 will invoke the registered handler.
Function pointers as struct members
Struct members can be simple data, other structs, or pointers. When function pointers are members and those functions operate only on the struct's data, the struct becomes an entity that contains both data and operations—introducing a class-like pattern.
Characteristics of object-oriented design
Inheritance, encapsulation, and polymorphism are commonly cited features of object-oriented languages. These concepts highlight the differences between object-oriented and procedural approaches.
Object-oriented principles are design ideas and are not tied to any specific language. Although some languages provide built-in object-oriented features that improve readability and alignment with natural thinking, the underlying design ideas can be applied in C using its basic mechanisms.
Language-level object modeling
When describing an object, we typically define its attributes and operations. For example, a box has six faces, color, weight, and whether it is empty; it can accept items and provide items.
In an object-oriented language, such an object could be modeled as a class:
class Box {
color color;
int weight;
boolean empty;
void put(something);
something get();
}
Operations might be invoked as:
Box.put(cake);
Box.get();
In procedural code, the entity is commonly passed to global functions:
Put(Box, cake);
Get(Box);
The first form is typically more intuitive. C, being flexible and simple, can be used to emulate this style and produce clearer code.
Object-oriented style in C
Object-oriented design is independent of language. The following example shows how to design a linked list in C with an object-oriented style.
Defining the interface
An interface specifies what functionalities an entity must provide without exposing implementation details. This allows implementers to change internals without affecting users of the interface.
Example list interface:
#ifndef _ILIST_H
#define _ILIST_H
// Define node structure for the list
typedef struct node{
void *data;
struct node *next;
} Node;
// Define list structure
typedef struct list{
struct list *_this;
Node *head;
int size;
void (*insert)(void *node); // function pointers
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 */
In this IList-style interface, the list object supports operations such as insert, drop, clear, getSize, get(index), and print.
Interface implementation
Construction function and global references:
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 list
list->drop = drop;
list->clear = clear;
list->size = 0;
list->getSize = getSize;
list->get = get;
list->print = print;
list->_this = list; // store list itself in _this
return (List*)list;
}
Note the _this pointer. It maps external operations on list to operations on _this, simplifying code.
Insert and drop implementations
// Insert a node into the 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 program:
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 whole 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 that influenced C emphasizes simple building blocks that can be connected to form powerful applications. C inherits this philosophy: it is concise and powerful. Because object-oriented concepts were not yet widespread when C was developed, many C applications are written in a procedural style, leading to the perception that C is strictly procedural. In fact, C provides simple, powerful, and general mechanisms; how they are combined is up to the developer.