Author: Sven Gregori / Source: Hackaday

Programming C without variables is like, well, programming C without variables. They are so essential to the language that it doesn’t even require an analogy here. We can declare and use them as wildly as we please, but it often makes sense to have a little bit more structure, and combine data that belongs together in a common collection.
Arrays are a good start to bundle data of the same type, especially when there is no specific meaning of the array’s index other than the value’s position, but as soon as you want a more meaningful association of each value, arrays will become limiting. And they’re useless if you want to combine different data types together. Luckily, C provides us with proper alternatives out of the box.This write-up will introduce structures and union
s in C, how to declare and use them, and how union
s can be (ab)used as an alternative approach for pointer and bitwise operations.
Structs

Before we dive into union
s, though, we will start this off with a more common joint variable type — the struct
. A struct
is a collection of an arbitrary amount of variables of any data type, including other struct
s, wrapped together as a data type of its own. Let’s say we want to store three 16-bit integers representing the values of a temperature, humidity, and light sensor.
Yes, we could use an array, but then we always have to remember which index represents what value, while with a struct
, we can give each value its own identifier. To ensure we end up with an unsigned 16-bit integer variable regardless of the underlying system, we’ll be using the C standard library’s type definitions from stdint.h
.
1234567 | #include <stdint.h> struct sensor_data { uint16_t temperature; uint16_t humidity; uint16_t brightness; }; |
We now have a new data type that contains three integers arranged next to each other in the memory. Let’s declare a variable of this new type and assign values to each of the struct
‘s field.
12345 | struct sensor_data data; data.temperature = 123; data.humidity = 456; data.brightness = 789; |
Alternatively, the struct
can be initialized directly while declaring it. C offers two different ways to do so: pretending it was an array or using designated initializers. Treating it like an array assigns each value to the sub-variable in the same order as the struct
was defined. Designated initializers can be arbitrarily assigned by name. Once initialized, we can access each individual field the same way we just assigned values to it.
123456789101112131415 | struct sensor_data array_style = { 123, /* temperature */ 456, /* humidity */ 789 /* brightness */ }; struct sensor_data designated_initializers = { .humidity = 456, .temperature = 123, .brightness = 789 }; printf ( "Temperature: 0x%d\n" , array_style.temperature); printf ( "Humidity: 0x%d\n" , array_style.humidity); printf ( "Brightness: 0x%d\n" , array_style.brightness); |
Notice how the fields in the designated initializers are not in their original order, and we could even omit individual fields and leave them simply uninitialized. This allows us to modify the struct
itself later on, without worrying much about adjusting every place it was used before — unless of course we rename or remove a field.
Bitfields

The bitfield is a special-case struct
that lets us split up a portion of an integer into its own variable of arbitrary bit length. To stick with the sensor data example, let’s assume each sensor value is read by an analog-to-digital converter (ADC) with 10-bit resolution.
Storing the results in 16-bit integers will therefore waste 6 bits for each value, which is more than one third. Using bitfields will let us use a single 32-bit integer and split it up in three 10-bit variables instead, leaving only 2 bits unused altogether.
12345 | struct sensor_data_bitfield { uint32_t temperature:10; uint32_t humidity:10; uint32_t brightness:10; }; |
We could also add a 2-bit wide fourth field to use the remaining space at no extra cost. And this is pretty much all there is to know about bitfields. Other than adding the bit length, bitfields are still just struct
s, and are therefore handled as if they were just any other regular struct
. Bitfields can be somewhat architecture and compiler dependent, so some caution is required.
Unions
Which brings us to today’s often overlooked topic, the union
. From the outside, they look and behave just like a struct
, and are in fact declared, initialized and accessed the exact same way. So to turn our struct sensor_data
into a union
, we simply have to change the keyword and we are done.
12345 | union sensor_data { uint16_t temperature; uint16_t humidity; uint16_t brightness; }; |

However, unlike a struct
, the fields inside a union
are not arranged in sequential order in the memory, but are all located at the same address. So if a struct sensor_data
variable starts at memory address 0x1000
, the temperature
field will be located at 0x1000
, the humidity
field at 0x1010
, and the brightness
field at address 0x1020
. With a union
, all three fields will be located at address 0x1000
.
What this means in practice is easily shown once we assign values to all the fields like we did in the struct
example earlier.
1234567 | union sensor_data data; data.temperature = 123; data.humidity = 456; data.brightness = 789; printf ( "Temperature: 0x%d\n" , data.temperature); |
Unlike the struct
example,…
The post Unionize Your Variables – An Introduction to Advanced Data Types in C appeared first on FeedBox.