Introduction
A structure is wrapper which allows definition of custom data types containing multiple primary data types. Unlike an array, a struct allows a combination of different types. The items inside the struct are called members and they can be referenced in multiple ways.
Definition
A struct can be defined using the struct keyword in any part of the code. But, it is preferred to define a struct in the global scope.
Warning
A struct cannot be initialized with values in the definition. It’s definition is a new data type creation. So to use the type, new variable of this type should be declared and initialized.
Example
The following snippet of code defines a struct for a sensor’s data register.
struct sensor_reg {
int8_t id;
int16_t temp;
int16_t pres;
int16_t hum;
};Declaration
Declaration of struct refers to the use of our newly defined data type by allocating memory to it. It is similar to a standard variable declaration. Memory allocation process can be automatic if on stack or dynamic (manual) if on heap.
There are 2 unique methods to declare a struct:
1. Standard Declaration
The most basic declaration method is like defining an auto variable in any scope. Here, struct struct_name is the data type of interest.
Stack Allocation (auto)
// Definition
struct sensor_reg {
int8_t id;
int16_t temp;
int16_t pres;
int16_t hum;
};
// Declaration
struct sensor_reg reg;Heap Allocation (dynamic)
// Definition
struct sensor_reg {
int8_t id;
int16_t temp;
int16_t pres;
int16_t hum;
};
// Declaration & Allocation
struct sensor_reg *reg = (struct sensor_reg*)malloc(sizeof(reg));2. Immediate Declaration
Immediate Declaration declares an entity of the struct immediately after definition. It is a more cleaner method than standard declaration and saves many LOC (Lines of code).
Stack Allocation (auto)
// Definition
struct sensor_reg {
int8_t id;
int16_t temp;
int16_t pres;
int16_t hum;
} reg; // DeclarationHeap Allocation (dynamic)
// Definition
struct sensor_reg {
int8_t id;
int16_t temp;
int16_t pres;
int16_t hum;
} *reg; // Declaration
reg = (struct sensor_reg*)malloc(sizeof(reg)); // AllocationInitialization
Initialization refers to the process of assigning values to the members of a struct at the time of its declaration or soon after. A struct in C can be initialized using multiple methods based on readability, maintainability, and use-case.
There are 4 primary methods of struct initialization:
1. Positional Initialization
Initialize members in the exact order of declaration within the struct.
struct sensor_reg reg = {1, 320, 1012, 62};
// id = 1, temp = 320, pres = 1012, hum = 62Order Matters
This form of initialization is concise, but all values must be provided in the correct sequence. If fewer values are provided, the remaining fields will be initialized to
0.
2. Labelled Initialization (Designated Initializers)
Explicitly assign values to individual members by name, using a .member = value syntax.
struct sensor_reg reg = {
.pres = 1012,
.id = 1,
.hum = 62,
.temp = 320
};Flexible & Readable
This method is especially helpful when working with long or complex structs where field order is hard to remember. Unspecified members are automatically zero-initialized.
3. Partial Initialization (Zero-Filling)
Initialize only a subset of fields; the remaining fields are set to 0 implicitly.
struct sensor_reg reg = {1};
// id = 1, temp = 0, pres = 0, hum = 0This is a special case of positional initialization, and helps quickly zero out a struct while setting only a few important fields.
Implicit Zeroing
While convenient, it can be easy to forget which field is being initialized — so it is best used with small or well-known structs.
4. Pointer (Block-Based Initialization)
Initialize a struct by overlaying it on a raw memory block using pointer casting. This is commonly used in low-level or embedded programming. It is commonly used with bitfields and packed structs.
uint8_t raw_data[] = {1, 0x40, 0x01, 0xF4, 0x03, 0x1E, 0x00};
struct sensor_reg *reg = (struct sensor_reg*)raw_data;
// Now reg->id = 1, reg->temp = 320, etc.Handle With Care
This form of initialization assumes the data in memory is already valid for the struct layout. Alignment, endianness, and padding must be considered.
Access
Struct variables can be allocated either on the stack (automatic storage) or on the heap (dynamic storage). The way you access their members differs slightly depending on where they reside.
Accessing by Value (Stack Allocation)
When a struct is declared as a normal variable, it lives on the stack. Access members using the dot (.) operator.
struct sensor_reg {
int id;
int temp;
};
struct sensor_reg reg; // Allocated on stack
reg.id = 1; // Access using dot operator
reg.temp = 25;Accessing by Pointer (Heap Allocation)
When a struct is allocated dynamically using malloc(), it lives on the heap. You get a pointer to the struct, so you must use the arrow (→) operator to access members.
struct sensor_reg *reg_ptr = (struct sensor_reg*)malloc(sizeof(struct sensor_reg));
reg_ptr->id = 1; // Access via pointer arrow operator
reg_ptr->temp = 25;Types of Structs
C allows a variety of struct layouts based on the use-case. Below are the three most common types:
1. Nested Structs
A struct can contain other structs as members. This allows grouping logically related sub-structures under a parent struct.
struct date {
int day;
int month;
int year;
};
struct person {
char name[32];
struct date birthdate;
};Accessing a nested member:
struct person p = {"Mithil", {4, 6, 2025}};
printf("%d", p.birthdate.year); // 2025Useful when a component is repeated or modular (e.g., time, coordinates, color).
2. Self-Referential Structs
A struct that contains a pointer to its own type. This is the foundation for dynamic data structures like linked lists and trees.
struct node {
int data;
struct node* next;
};This allows construction of chains or trees of nodes at runtime.
A struct cannot contain itself directly — only a pointer to itself, to avoid infinite size.
3. Bitfield Structs
Used to store data in tightly packed formats where memory is constrained or when dealing with hardware registers.
struct status_reg {
uint8_t mode : 3;
uint8_t error : 1;
uint8_t reserved : 4;
};Each field occupies a defined number of bits. Ideal for flags or low-level I/O mapping.
Bitfields behave like regular members:
struct status_reg s = {5, 1, 0};
printf("%d", s.mode); // 5Bitfield layout may vary by compiler, architecture, and endianness. Avoid them in portable network data formats.
Refer to 4. Bitfields and Packed Structs & 5. Alignment & Memory Mapping