Data structures¶
Learning Objectives: Using data structures in C and their use cases in embedded systems programming.
A C
struct (English struct) is a logical structure/combination/aggregation of several variables, where related information can be grouped for unified handling. Data structures are a very common concept in programming, and they also have use cases in embedded programming, so let's take a look at how they are used in C.In C, the word reserved for defining a data structure is
struct, and a structure is defined as a block as follows. The members of a data structure in C can be of any variable types.struct structure_type_name {
member declarations;
};
For example, let's define a data structure of type
point, which has x and y coordinates as members and a point name as a string. Then, we define another structure rect, which consists of point type structures.struct point {
uint16_t x;
uint16_t y;
char name[16];
};
struct rect {
struct point max;
struct point min;
struct point all_points[10];
};
The definition above describes a new structure type called
struct point; it does not yet create a variable. Later, in your program, you can create variables of this type and initialize them. struct point p1;
struct point p2 = {100, 200, "Point"};
Data Structures in a C Program¶
To use the members of a data structure in a program, two new operators are needed:
- Members are referenced using the
.operator, similar to referencing any other variable. - If we have a pointer to the structure, we use the
->operator.
Example:
// Defining the structures
struct point {
uint16_t x;
uint16_t y;
};
struct rect {
struct point max;
struct point min;
struct point all_points[10];
};
// Variables and initialization
struct point min_pt;
min_pt.x = 100;
min_pt.y = 100;
struct rect box;
box.max.x = 320;
box.max.y = 200;
printf("%d,%d", box.max.x, box.max.y);
Declaration and Initialization¶
Syntactically, the declaration and initialization of a data structure can be done like any variable. In this, C language syntax is flexible, and there are several ways to initialize a structure.
// Variable definition immediately after structure declaration. Followed by initialization.
// here point1
struct point {
uint16_t x;
uint16_t y;
char name[20];
} point1;
point1.x = 320;
point1.y = 200;
char midpoint[] = "Midpoint";
// Use string library functions!
strncpy(point1.name, midpoint, strlen(midpoint)+1);
// As a separate variable
struct point point2;
point2.x = 320;
point2.y = 200;
strncpy(point2.name, midpoint, strlen(midpoint)+1);
struct point point3 = { 320, 200, "Midpoint" };
Please note that after a structure variable has been defined, we cannot assign a new string directly to a character-array member using the
= operator. Instead, we can copy the string into the array using strncpy. strncpy allows us to copy the contents of one string (such as "Midpoint") into the character array (name).Note!
strlen returns the number of characters in the string, but it does not include the terminating null character '\0'. Therefore, we use strlen(midpoint) + 1 so that strncpy also copies the null character at the end of the string. In this example, we know that the destination array is large enough to contain the complete string.There are other ways to declare and initialize data structures, but the ones above are the clearest.
Data structures can also be "assigned" to each other, in which case each member is copied.
min_pt = max_pt;
Pointers and Data Structures¶
When used as function parameters, as we explained before, data structures are typically passed by pointers because when we pass an argument to a function, a copy of the data structure will be created, and we want to save memory.
To access a structure member through a pointer, we use the
-> operator. It combines two operations: dereferencing the pointer and accessing the structure member.For example:
p1_ptr->point.x; // Easier notation
(*p1_ptr).point.x; // Equivalent expression
Below is a code example demonstrating its usage.
#include <stdio.h>
#include <inttypes.h>
struct point {
uint16_t x;
uint16_t y;
} point = { 160, 100 };
int main() {
struct point *p1_ptr = &point;
printf("x=%d y=%d\n", point.x, point.y);
printf("x=%d y=%d\n", p1_ptr->x, p1_ptr->y);
// Assign to the y member via pointer
p1_ptr->y = 103;
printf("x=%d y=%d\n", point.x, point.y);
return 0;
}
When accessing members of nested structures through a pointer, remember that the operator depends on what we are accessing. In this example,
p1_ptr is a pointer to struct rect, so we first use ->. The member point is itself a structure, so we then use . to access its members.#include <stdio.h>
#include <inttypes.h>
struct rect {
struct point {
uint16_t x;
uint16_t y;
} point;
} r = { .point.x = 101, .point.y = 102 };
int main() {
struct rect *p1_ptr = &r;
// Access members of the struct through p1_ptr using ->
// Access members of the nested point struct using .
printf("x=%d y=%d\n", p1_ptr->point.x, p1_ptr->point.y);
return 0;
}
Note! Also check the string library function
memcpy, which can be used to copy entire blocks of memory. In other words, the content of a data structure can be copied to another by copying the memory area allocated for the structure using pointers. C's low-level memory model also allows us to work with data in this way.#include <stdio.h>
#include <stdint.h>
#include <string.h> // For memcpy
// Definition of the struct point
struct point {
uint16_t x;
uint16_t y;
char name[20];
};
int main() {
// Declare and initialize p1
struct point p1 = {320, 200, "Midpoint"};
// Declare another struct to copy into
struct point p2;
// Copy the contents of p1 into p2 using memcpy
memcpy(&p2, &p1, sizeof(struct point));
// Print both structs to show they are now identical
printf("p1: x = %d, y = %d, name = %s\n", p1.x, p1.y, p1.name);
printf("p2: x = %d, y = %d, name = %s\n", p2.x, p2.y, p2.name);
return 0;
}
Note!
memcpy copies the memory representation of the structure. If the structure contains pointers, the pointer values are copied, but the data they point to is not.Comparing Data Structures¶
Data structures cannot be compared directly as a whole using relational operators. Instead, the relevant members can be compared individually. In this example, we use pointers to access the structures and compare their
x members.Example: The function returns a pointer to the data structure that has the larger value in the
x member.#include <stdio.h>
#include <inttypes.h>
struct point {
uint16_t x;
uint16_t y;
};
// Function that returns a pointer to the data structure
struct point *which_is_larger(struct point *a, struct point *b);
int main() {
struct point first = { 320, 200}, second = { 321, 201 };
struct point *larger = which_is_larger(&first, &second);
printf("x=%d y=%d\n", larger->x, larger->y);
return 0;
}
struct point *which_is_larger(struct point *a, struct point *b) {
if (a->x > b->x) {
return a;
} else {
return b;
}
}
This pointer stuff is starting to get pretty advanced...
Embedded Data Structures¶
As stated earlier, a data structure is a convenient way to logically group related information into a cohesive package. In embedded systems, such a package could consist of sensor data collected at the same moment or perhaps the configuration parameters of a peripheral device or the transmission settings for wireless communication. Since sensor data is often analyzed as a time series, it is essential to know the exact time of measurement. Many applications also use data collected simultaneously from multiple sensors in the implementation of more intelligent services, where the timestamp ties together different types of data.
struct sensor_data {
uint32_t timestamp;
float temperature;
float humidity;
};
And, when data is neatly packaged in a structure, it can easily be converted into a CSV-formatted string, which can then be wirelessly transmitted to an IoT backend system.
When we get to programming the hardware in this course, you will notice that many of the pre-made software components in the device's operating system actually expect their configuration parameters in the form of a data structure.
Note! The compiler may add unused padding bytes between structure members to satisfy memory-alignment requirements. Therefore, the size of a structure is not always exactly the sum of the sizes of its members.
Custom Data Types¶
In addition to data structures, C allows us to define our own type names using
typedef. A typedef can give a new name to an existing type, including simple types, arrays, pointers, and structures. This is useful for giving meaningful names to types used in the program.For example, we can define a type representing an array of 10 bytes:
typedef uint8_t RegisterBytes[10];
RegisterBytes reg;
Here,
RegisterBytes is an alternative name for an array containing 10 uint8_t values.It also allows to create alias for other types, for example:
// Define Length as an alias for uint16_t
typedef uint16_t Length;
Length len = 100;
printf("%d\n", len);
typedef is very commonly combined with structures. For example:typedef struct {
uint16_t x;
uint16_t y;
} Point;
Point p1 = {100, 200};
Now
Point is a type name for this structure, so we do not need to write struct point when declaring variables.In embedded systems (and sometimes also in C programs on workstations),
typedef is often used to make code easier to port between hardware platforms. By using application-specific type names, platform-dependent type choices can be concentrated in one place. When moving the code to another platform, the corresponding underlying types can then be adjusted if necessary. This feature is especially useful when working with more complex data structures, but sometimes renaming standardized data types can also be beneficial. For example, uint16_t provides a guaranteed 16-bit unsigned integer on all platforms.We will use the same approach later in the device programming part of the course. Many hardware libraries and APIs define their configuration data using structures and custom type names, so understanding
struct and typedef will help you read and use those interfaces. The reason for this is, of course, compatibility and ease of transferring code from the manufacturer's devices to others, ensuring that all code works as expected in the manufacturer's embedded operating system.In Conclusion¶
You don't need to memorize every way of declaring and initializing structures shown in this material. Learn one clear method first and use it consistently. The other forms are useful to recognize when you encounter them in existing C code.
Give feedback on this content
Comments about this material