Object-oriented programming in C – part 1 (Objects)

Introduction

Background

I intend to write a small series of articles that explain how object-oriented programming (OOP) can be applied and used in C in embedded software projects. I realize that there already exist many articles that cover this subject. However, I found those that I’ve read either incomplete or unclean. Besides that, most of these articles seem to be written for an audience that is already familiar with the OOP programming paradigm and already knows when and how to use OOP. This series of articles is for software engineers that are familiar with programming embedded devices in C, but who may not be familiar with OOP. They may have a background in electronics engineering and not in computer science, like me.

C versus C++

Admittedly, it is also just for fun and to learn what a C++ compiler does behind the scenes, by programming it yourself in C. In the end, you can still drop C in favor of C++, because almost all manufacturers nowadays provide both C and C++ compilers for their microcontrollers. There’s seems to be a lot of debate about whether or not to use C++ in embedded software projects. What it boils down to I think is that with C the software engineer really has to know what he’s doing, while with C++ he really has to know what C++ is doing for you on top of that. In case you’re interested: the following two “Modern C in embedded systems” articles from www.embedded.com provide interesting insights in the matter: Part-1: Myth-and-Reality and Part-2: Evaluating C.

So, back to the article. In this part I will introduce the concept of objects. In my examples I’ll use an imaginary embedded software application of a room thermostat used in a central heating system.

Objects

The averaging module

Suppose the application contains averaging module that averages the temperature values read out from an indoor temperature sensor. The module is declared and implemented in the files averaging.h and averaging.c respectively and is used to average the values of the single temperature sensor in the system. The files might look like this:

averaging.h

#ifndef _AVERAGING_H_
#define _AVERAGING_H_

// Public variable
int number_of_averages = 10;

/**
 * @brief Update the average using the new value
 * @param value: the value to update the average with
 */
extern void update(float value);


/**
 * @brief Get the average.
 * @return The current average
 */
extern float average();

#endif // _AVERAGING_H_

averaging.c

// Private variable
float average_value = 0.0;


// Public function implementations
void 
update(float value) {
    // Calculate the moving average
    float sum = average * (number_of_averages - 1);
    average_value = (sum + sample_value) / number_of_averages;
}


float
average() {
    return average_value;
}

Encapsulation within module

As can be seen from the code, the averaging module implements the functionality of a moving average. The number of averages it uses to calculate this, is defined in the header file as a variable – for the sake of example. This way it can be changed by the user of the module: it is a publicly available variable. The average value _average_value stored in the implementation file of the module is not accessible by any user of the module: it is private to the module. The implementation of the module – the averaging.c file – encapsulates the private data (variables and functions). It is this encapsulation that is one of the important characteristics of an object.

Multiple instances

The second characteristic of an object is that multiple instances of them can be created and used independently of each other. Why is this useful? Well, suppose the company we create our room thermostat software for decides to add an advanced version of the thermostat to its product portfolio. One that is capable of measuring and displaying the (average) temperature of four additional rooms. How would we modify our averaging module to allow for this? Of course, we could change the private variable _average_value into and array of 5 elements (one for each temperature sensor value), and change all functions such that they work on a selected sensor. We could even have different number of averages for each of the temperature sensors by changing the public variable number_of_averages into a 5-element array as well.

The downside of this approach is that you have to modify the implementation of the averaging module whenever you want to add a temperature sensor, while your not actually change its behavior. Furthermore, it would be logical for the averaging module to be part of some code base that is common to the software applications of both the simple and the advances thermostat. So, when we modify the module for use in the advanced room thermostat, we have to take care to also modify parts of the simple thermostat’s code base to match the averaging module’s new interface. An even worse idea would be to not have the averaging module in a code base common to both thermostat’s applications, but to keep the original averaging module in the simple thermostat’s code base, and a modified copy of it in the advanced thermostat’s code base.

The Averaging object

Life would be easier if our averaging module would have been implemented as an object. Below is what the header file of the averaging module could look like when implemented as an object called Averaging.

averaging.h

#ifndef _AVERAGING_H_
#define _AVERAGING_H_

/**
 * @brief The Averaging object.
 */
typedef struct Averaging_struct {
    int number_of_averages; /** Public object variable. */
    void* _private;         /** Private object data. */
} Averaging;

/**
 * @brief Initialization parameters for the Averaging object
 */
typedef struct Averaging_params_struct {
    int number_of_averages; /** The number of samples to average over. */
} Averaging_params;


/**
 * @brief  Create an Averaging instance on the heap
 * @param  in_params: set of initialization parameters for the Averaging object
 * @return A pointer to the created object or NULL in case the object could not
 *         be created.
 */
extern Averaging*
Averaging__create_on_heap(Averaging_params* in_params);


/**
 * @brief  Destroy the Averaging instance
 * @param  inout_self: the Averaging instance to destroy. Pointer will be set to
 *         NULL after the object has been destroyed
 */
extern void
Averaging__destroy(Averaging* inout_self);


/**
 * @brief  Update the average based on the given value
 * @param  in_self: an Averaging instance
 * @param  value: the value to update the average with
 */
extern void
Averaging__update(Averaging* in_self, float value);


/**
 * @brief  Get the average.
 * @param  in_self: an Averaging instance
 * @return The current average
 */
extern float
Averaging__average(Averaging* in_self);


#endif // _AVERAGING_H_

The biggest difference with the averaging module‘s header file is that the update() and average() functions now take an instance of an Averaging object as an argument. This instance contains the variables on which these functions operate. Note that I added the prefix Averaging__ to each of the (public) functions. This is to make it explicit that the functions belong to the Averaging interface. This way, if module/object B also has an update() function, the compiler won’t be confused if Averaging and B are both included and used by module/object C. It corresponds to what is done in C++ implementation files as Averaging::update() and Averaging::average().

In order to create an instance of the Averaging object, heap memory is to be allocated. The Averaging__create_on_heap() function takes care of this. The interface also provides an Averaging__destroy() function which will free up the memory used to store the Averaging instance in. If needed you can also choose to change the code such that it allocates the instance on the stack instead of on the heap. For this you will have to give up on having private data for the object, since it is the void-pointer to the private data in the Averaging struct that forces us to use the heap. To my opinion there’s nothing wrong with always using the heap to create your object instances on. For embedded software applications (and in fact also for non-embedded ones) my advice is to allocate object instances on the heap during the initialization phase of the application where possible. Always keep the amount of dynamic memory allocation/deallocations in an application to a minimum.

See below for a typical usage example of the Averaging object:


#include <stdio.h>;

// Create an Averaging instance
Averaging_params params = {
    .number_of_averages = 10
};
Averaging* instance = Averaging__create_on_heap(params);


// Do something with it
for(int i=0; i<100 ; i++) {
    Averaging__update(instance, i);
}
printf("Calculated average is: %f\n", Averaging__average(instance));


// Destroy the instance when done
Averaging__destroy(instance);

Last but not least, here’s also the implementation of the Averaging object:

averaging.c

#include "averaging.h"

// We need malloc()/free() to construct the object on the heap
#include <stdlib.h>;

// Private data
typedef struct Averaging_private_struct {
    float average_value;
} Averaging_private;


// Private function declarations
static void initialize_data(Averaging* in_self, Averaging_params* in_params);
static inline Averaging_private* get_private_data(Averaging* in_self);


Averaging* 
Averaging__create_on_heap(Averaging_params* in_params) {
    Averaging* object = (Averaging*) malloc(sizeof(Averaging));

    if(NULL != object) {
        Averaging_private* private_data = (Averaging_private*) malloc(
            sizeof(Averaging_private)
        );

        if(NULL != private_data) {
            object->_private = private_data;
            initialize_data(object, in_params);
        } else {
            free(object);
            object = NULL;
        }
    }

    return object;
}


static void 
initialize_data(Averaging* in_self, Averaging_params* in_params) {
    // Initialization of public variables
    in_self->number_of_averages = in_params->number_of_averages;

    // Initialization of private variables
    get_private_data(in_self)->average_value = 0.0;
}


static inline
Averaging_private* get_private_data(Averaging* in_self) {
    return (Averaging_private*) in_self->_private;
}


void
Averaging__destroy(Averaging* inout_self) {
    // Destroy object's private data first
    free(get_private_data(inout_self));

    // Only now destroy object
    free(inout_self);

    // Let the pointer value reflect that the object no longer exists
    inout_self = NULL;
}

void
Averaging__update(Averaging* in_self, float value) {
    float sum = get_private_data(in_self)->average_value *
                (in_self->number_of_averages - 1);

    get_private_data(in_self)->average_value = (sum + value) /
                                               in_self->number_of_averages;
}


float
Averaging__average(Averaging* in_self) {
    return get_private_data(in_self)->average_value;
}

The first thing to notice is that the Averaging module’s private variables have moved to a struct:

// Private data
typedef struct Averaging_private_struct {
    float average_value;
} Averaging_private;

This way, each Averaging instance can have its own private variable. The instance is created on the heap and initialized when calling the Averaging__create_on_heap() function:

Averaging*
Averaging__create_on_heap(Averaging_params* in_params) {
    Averaging* object = (Averaging*) malloc(sizeof(Averaging));

    if(NULL != object) {
        Averaging_private* private_data = (Averaging_private*) malloc(
            sizeof(Averaging_private)
        );

        if(NULL != private_data) {
            object->_private = private_data;
            initialize_data(object, in_params);
        } else {
            free(object);
            object = NULL;
        }
    }

    return object;
}

The Averaging__update() and Averaging__average() functions take a pointer to an Averaging instance to operate on. As you can see this instance is consequently referred to as in_self:

void Averaging__update(Averaging* in_self, float value) {
    float sum = get_private_data(in_self)->average_value *
                (in_self->number_of_averages - 1);
    get_private_data(in_self)->average_value = (sum + value) / 
                                               in_self->number_of_averages; 

When it comes to passing pointers to functions I prefix their names with in_ to indicate to the user of the function that the data structure pointed to is only read, but never written to by the function. A prefix out_ implies that the previous value of the data structure is of no interest to the function. Lastly, the inout_ prefix indicates that the data structure pointed to will be modified by the function, but that the function also reads from it.

The self in in_self is a keyword I borrowed from Python, the language I program in mostly nowadays. You could equally well use the keyword this from C++ (and other languages).

For the sake of completeness I also provide the implementation of the function Averaging__create_on_stack():

Averaging*
Averaging__create_on_stack(Averaging* inout_self, 
                           Averaging_params* in_params) {
    initialize_data(inout_self, in_params);

    return object;
}

Since an Averaging instance is already created by the caller, all the above function has to do is to initialize it with the given initialization parameters. Note that no real private data can be created anymore, since that was our compromise when creating objects on the stack. You can choose to move the average_value variable to the Averaging struct defined in the header file averaging.h. You could also keep the Averaging_private struct, but move it to its own header file averaging_PRIVATE.h, which is then included in averaging.h. This way you indicate to the user that the user should treat the variables defined in the Averaging_private struct as if they were private – in other words: the user should leave them alone! When going for this second option, the void-pointer variable _private in the Averaging struct is replaced by an actual Averaging_private variable.

Conclusion

In this article I explained how to add reusability, scalability and flexibility to your code by applying the concept of objects in the C programming language. The whole idea behind objects in C (or any other language for that matter) is that there’s a set of functions, defined as part of an object, that can operate on the data of instances of that object (that is: if the object actually contains data). The example object given in this article clearly reflects that: the data of the object is defined by the Averaging struct, and apart from that, there’s set of functions that can operate on instances of them – declared as extern in the header file. Encapsulation of data can be obtained with object implementations, just as with normal modules. Of course, object functions can also operate on data (structures) defined elsewhere.

The concept of objects goes beyond being able to instantiate a multiple of a specific object. It would be helpful for our C implementation of objects to support polymorphism and inheritance. I’ll discuss these subjects in later articles.

I hope you found this article useful! If you have any questions, remarks, (or improvements!) don’t hesitate to contact me.




No Comments


You can leave the first : )



Leave a Reply

Your email address will not be published. Required fields are marked *