From C to C++: For Dummies

A practical guide for Embedded Software engineers

Leonardo Cavagnis
8 min readJan 9, 2023

--

Change is inevitable. Growth is optional.

C is the most popular programming language used in the development of embedded systems due to its efficiency and flexibility.

C++ is also a popular choice for embedded systems development because it is an extension of the C programming language and includes many additional features: object-oriented programming, code reuse with templates, etc.

While C and C++ have many similarities, switching from C to C++ can be challenging because it may require forgetting certain C habits (such as writing lots of global functions or using many pointers and casts) and learning new concepts.

This practical guide is intended for embedded software and firmware engineers with a C background who are interested in switching to the dark side of programming embedded systems using C++.

Class & Objects

C is a procedural programming language, while C++ is an object-oriented programming (OOP) language in which everything is associated with classes and objects.

  • Object is the virtual representation of a real-world entity such as a car, pen, person, etc. In embedded software programming the “entity” could be a peripheral (e.g., UART, SPI), a sensor (e.g., temperature, humidity), etc.
  • Class represents a group of similar objects and serves as a template from which objects are created.

In C, a peripheral is usually implemented using modular programming. Modular programming groups related sets of functions and parameters into a module consisting of an interface (.h) and an implementation (.c).

/* uart.c */

#include "uart.h"

int uart_baudrate;

void uart_init(int baudrate)
{
// Configure UART peripheral
}

void uart_putc(char c)
{
// Transmit single character over UART
}

void uart_puts(char *s)
{
// Transmit string of characters over UART
}

char uart_getc(void)
{
// Receive single character over UART
}

char *uart_gets(char *s, int size)
{
// Receive string of characters over UART
}
/* uart.h */

void uart_init(int baudrate);

void uart_putc(char c);

void uart_puts(char *s);

char uart_getc(void);

char *uart_gets(char *s, int size);

The module encapsulates the functionality of an entity in a way that creates a level of abstraction. In C++, an object is an evolution of a module. Both modular programming and OOP are approaches to logically organizing a program and allowing for the reuse of components, but programming with objects includes additional features such as polymorphism, overloading, inheritance, data hiding…

class UART
{
public:
UART(int baudrate)
{
// Configure UART peripheral
}

void putc(char c)
{
// Transmit single character over UART
}

void puts(const char *s)
{
// Transmit string of characters over UART
}

char getc()
{
// Receive single character over UART
}

char *gets(char *s, int size)
{
// Receive string of characters over UART
}

private:
int baudrate;
};

Standard Library

A standard library is a collection of functions, data structures, and other constructs that are included in a programming language. These libraries provide a set of functions that programmers can be used to perform common tasks such as input/output management, string manipulation, …

In the C Standard Library, there are modules for input/output management (stdio.h), string manipulation (string.h), and mathematical operations (math.h).

The C++ Standard Library is a set of C++ classes that incorporates C standard libraries and provides complex data structures and functions such as lists, stacks, arrays, algorithms, iterators, and many other components.

A linked list is a linear data structure where elements are not stored at a contiguous location and are linked using pointers. Each node stores the data and the address of the next node

In C, there is no implementation of a linked list inside the standard libraries, so programmers have to implement it by themselves using structs, pointers, and manipulation functions:

#include <stdlib.h>

typedef struct node
{
int data;
struct node *next;
} node_t;

node_t *head = NULL;

void add_to_list(int item)
{
node_t *new_node = (node_t *)malloc(sizeof(node_t));
new_node->data = item;
new_node->next = head;
head = new_node;
}

void remove_from_list(int item)
{
node_t *curr = head;
node_t *prev = NULL;
while (curr != NULL)
{
if (curr->data == item)
{
if (prev == NULL)
{
head = curr->next;
}
else
{
prev->next = curr->next;
}
free(curr);
return;
}
prev = curr;
curr = curr->next;
}
}

int get_item(int index)
{
node_t *curr = head;
for (int i = 0; i < index; i++)
{
if (curr == NULL)
{
return -1;
}
curr = curr->next;
}
return curr->data;
}

In C++, the standard library implements it for you.
Here is an example of a program that uses a std::list to perform the operations of adding an element, removing an element, and getting the element at the 2nd position:

#include <list>

int main() {
// Create a list of integers
std::list<int> numbers;

// Add some elements to the list
numbers.push_back(1);
numbers.push_back(2);
numbers.push_back(3);

// Remove the element at the front of the list
numbers.pop_front();

// Get the element at the 2nd position (index 1)
std::list<int>::iterator it = numbers.begin();
std::advance(it, 1);
int element = *it;

return 0;
}

Namespace

C programmers usually prefix their identifiers (e.g., names of variables, functions, etc.) to prevent name conflict between modules.
For example, when two or more peripherals implement the same functions, the functions are usually prefixed with the module name:

// uart1.h
void uart1_init();
void uart1_start();
void uart1_stop();

// uart2.h
void uart2_init();
void uart2_start();
void uart2_stop();

In C++, it is not necessary to do this, as they can be placed in a namespace.
A namespace is a region of code in which identifiers can be defined and used. They are used to organize code and prevent name collisions between identifiers defined in different parts of a program.

namespace uart1 {
void init()
{
//...
}

void start()
{
//...
}

void stop()
{
//...
}
}

namespace uart2 {
void init()
{
//...
}

void start()
{
//...
}

void stop()
{
//...
}
}

Error Handling

Error handling is the process of managing and responding to errors that occur during the execution of a program.

C language does not provide direct support for error handling.
A C programmer has to handle errors by taking appropriate action without compromising the stability of the embedded system, such as: checking that the divisor is not zero in a division.

Here is an example of a function in C that takes three arguments: dividend, divisor, and a pointer where the result will be stored. If the divisor is zero, the function returns false to indicate that an error occurred. Otherwise, it stores the result in the result variable and returns true.

bool divide(float a, float b, float *result) {
if (b == 0) {
// Return false to indicate an error occurred
return false;
}
*result = a / b;
return true;
}

In C++, error handling is accomplished through the use of exceptions.
Exceptions are special objects that are called when an error condition occurs in a program. When an exception is raised, the normal flow is halted and the program looks for a way to handle the exception.

C++ exception handling is built upon three keywords: try, catch, and throw.

#include <cstdint>
#include <iostream>
#include <stdexcept>

float divide(float a, float b) {
if (b == 0) {
// Throw an exception to indicate an error occurred
throw std::invalid_argument("Error: Cannot divide by zero");
}
return a / b;
}

int main() {
try {
float result = divide(10, 5);
} catch (const std::invalid_argument& e) {
// Catch the exception if it is thrown
return -1;
}
return 0;
}

Template

In C++, a template is a feature that allows programmers to create generic functions that can work with different data types.

Here is an example of a template function that returns the maximum of the two generic numbers:

template <typename T>
T max(T a, T b) {
return (a > b) ? a : b;
}

The type T can be any data type (int, float, etc.). When the max function is called, you specify the type of T by providing the type in angle brackets:

int m1    = max<int>(5, 7); 
float m2 = max<float>(3.14, 2.71);

Templates can be very useful when you need to create similar functions working with different types.

C does not support templates and programmers have to write as many functions as data types they need:

int maxint(int a, int b) {
return (a > b) ? a : b;
}

float maxfloat(float a, float b) {
return (a > b) ? a : b;
}

Function overloading

C++ supports function overloading: allowing the programmer to create multiple functions with the same name, but with different sets of parameters.

int add(int a, int b) {
return a + b;
}

int add(int a, int b, int c) {
return a + b + c;
}

int add(int a, int b, int c, int d) {
return a + b + c + d;
}

It is a useful technique for writing flexible and reusable code. It allows you to write functions that can handle a wide range of inputs without having to change the identifier of the function for each type of input, as would occur in C:

int add(int a, int b) {
return a + b;
}

int add_3(int a, int b, int c) {
return a + b + c;
}

int add_4(int a, int b, int c, int d) {
return a + b + c + d;
}

Function arguments

In C, there are two ways to pass arguments to a function:

  • By value: the function receives a copy of the value of the variable.
  • By pointer: the function receives a pointer to the original variable, rather than a copy of its value. So, any changes made to the content are reflected in the original variable.

C++ adds an additional way:

  • By reference: the function receives a reference to the original variable, rather than a copy of its value. The change result is similar to the pointer case.

In C++, there is no real difference between pass by reference and pass by pointer. The advantage of pass by reference is that it is easier to read and understand the code.

// By value
void increment_v(int x) {
x++;
}

// By pointer
void increment_p(int* x) {
(*x)++;
}

// By reference
void increment_r(int& x) {
x++;
}

int main() {
int a = 5;

increment_v(a);
/* a is still 5, because the value of a was copied
* and any changes did not affect a */

increment_p(&a);
/* a is now 6, because the function was able to modify
* the original variable through the pointer */

increment_r(a);
/* a is now 7, because the function was able to modify
* the original variable through the reference*/
}

References

Some useful links:

--

--

Leonardo Cavagnis
Leonardo Cavagnis

Written by Leonardo Cavagnis

Passionate Embedded Software Engineer, IOT Enthusiast and Open source addicted. Proudly FW Dev @ Arduino

No responses yet