Panel For Example Panel For Example Panel For Example

Improving Readability of Embedded C Code

Author : Adrian September 16, 2025

Object-oriented C

Object-oriented languages map more closely to human reasoning and can significantly reduce code complexity while improving readability and maintainability. Traditional C code can also be designed to be readable, maintainable, and of low complexity. This article illustrates that point with a practical example.

Basics

Structs

In addition to basic data types, C allows users to define custom data types via structs. In C, you can represent any entity with a struct. Structs are an early form of the class concept found 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 inside a struct are called members. Members may be basic types, other structs, or nested structs. For example, a typical linked list node may be defined as:

typedef struct node{ void *data; // data pointer int dataLength; // data length struct node *next; // pointer to next node }Node;

Here, the next pointer in node is of type node.

Function pointers

Pointers are a key feature of C and give it flexibility and power. Function pointers point to a function's entry in memory. With function pointers, functions can be passed as parameters to other functions and invoked when appropriate, enabling patterns such as callbacks.

For example, the signal registration function in UNIX/Linux has a prototype like:

void (*signal(int signo, void (*func)(int)))(int);

To use it, define a signal handler externally and register it with signal(sigNo, handler) so the process can invoke the handler when the signal occurs.

Using function pointers as struct members

As mentioned, struct members can be simple types, other structs, or pointers. When function pointers are members of a struct and those functions operate only on that struct's data, you can form an entity that contains both data and operations on that data. This naturally leads to a class-like concept.

Object-oriented language features

Inheritance, encapsulation, and polymorphism are often cited as the three core features of object-oriented languages. These features explain many of the advantages of object-oriented approaches over procedural ones.

Object-oriented design is a software design approach independent of any particular language. Although languages may provide direct syntax support, object-oriented thinking can be applied in languages without built-in OOP constructs.

Nevertheless, languages designed specifically for object orientation tend to produce code that more closely matches natural human reasoning, improving readability and understandability compared with some procedural code.

Language-level object orientation

When describing an object, we typically enumerate its properties. For example, a box has six faces, a color, a weight, an empty flag, and supports operations such as putting things in and taking things out.

In an object-oriented language, such an object might be abstracted as a class:

class Box{ color color; int weight; boolean empty; put(something); something get(); }

Operations on the box might look like:

Box.put(cake); Box.get(); // retrieve an item from the box

In procedural code, the entity is often passed to global functions, for example:

Put(Box, cake); // put a cake into the box Get(Box); // get an item from the box

The first form is typically easier to read and reason about, so object-oriented languages provide language-level support that enhances code readability and comprehension.

C as a flexible, simple language

Object-oriented design is a language-independent software design approach. The following linked list example shows how to design C code with an object-oriented style.

Defining an interface

Interfaces are important in object-oriented design. An interface promises what implementing entities can do without exposing how they do it. This allows implementers to modify implementations without touching code that uses the interface.

Consider this linked list interface definition:

#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 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 clearly shows operations available on a list object: insert, drop, clear, getSize, get(index), and print.

Interface implementation

Constructor example

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 object 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 via _this pointer return (List*)list; }

Note the _this pointer. The _this pointer ensures external operations on the list map to operations on _this, simplifying the code.

Insertion and deletion

// 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)++; } // delete the 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 here for brevity.

Testing

Test code

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); // delete a node node.data = "Cisco"; node.next = NULL; list->drop(&node); // delete another node list->print(); // print again printf("list size = %d\n", list->getSize()); list->clear(); // clear the list return 0; }

Conclusion

The UNIX platform where C originated promoted a design philosophy of composing simple tools into powerful applications. C inherits that philosophy: it is concise and powerful. Because C predates widespread adoption of object-oriented ideas, many C applications are procedural, which can give the false impression that C is strictly a procedural language. In reality, C provides simple, powerful, and general capabilities, and how those building blocks are composed is up to the developer.