Skip to content

C Structures

While arrays group items of the same data type, structures (struct) are user-defined types that allow you to group items of different data types under a single name. They are fundamental for organizing related data.

Imagine you need to represent information about a point in a 2D plane. You need an x-coordinate and a y-coordinate, possibly of different types (e.g., double). A structure is perfect for this:

  • X-coordinate (e.g., double x;)
  • Y-coordinate (e.g., double y;)

You define a structure using the struct keyword, followed by an optional tag name and a block {} containing member definitions.

struct [structure_tag] {
type member1_name;
type member2_name;
// ... other members
} [optional_variable_declarations];

The structure_tag names the structure type. Each member_definition is a standard variable declaration. You can declare variables of the structure type immediately after the closing brace, but it’s more common to declare them separately using struct structure_tag variable_name;.

Example: Defining a Point structure.

// Define the structure named 'Point'
struct Point {
double x;
double y;
};
// Define a structure for a Book (using typedef for convenience later)
typedef struct {
char title[100];
char author[100];
int year_published;
double price;
} Book;

To access a member of a structure variable, use the member access operator, which is a dot (.).

Syntax: structure_variable.member_name

#include <stdio.h>
#include <string.h> // For strncpy
// Define the Point structure
struct Point {
double x;
double y;
};
// Use the Book typedef from the previous example
typedef struct {
char title[100];
char author[100];
int year_published;
double price;
} Book;
int main(void) {
// Declare structure variables
struct Point p1, p2;
Book book1;
// --- Initialize p1 ---
p1.x = 10.5;
p1.y = -3.7;
// --- Initialize p2 using designated initializers (C99+) ---
p2 = (struct Point){ .x = 0.0, .y = 5.0 }; // Compound literal initialization
// --- Initialize book1 ---
strncpy(book1.title, "The C Programming Language", sizeof(book1.title) - 1);
book1.title[sizeof(book1.title) - 1] = '\0'; // Ensure null termination
strncpy(book1.author, "Kernighan & Ritchie", sizeof(book1.author) - 1);
book1.author[sizeof(book1.author) - 1] = '\0';
book1.year_published = 1978;
book1.price = 25.99;
// --- Access and print members ---
printf("Point p1: (%.2f, %.2f)\n", p1.x, p1.y);
printf("Point p2: (%.2f, %.2f)\n", p2.x, p2.y);
printf("\nBook 1 Details:\n");
printf(" Title: %s\n", book1.title);
printf(" Author: %s\n", book1.author);
printf(" Year: %d\n", book1.year_published);
printf(" Price: $%.2f\n", book1.price);
return 0;
}

Output:

Point p1: (10.50, -3.70)
Point p2: (0.00, 5.00)
Book 1 Details:
Title: The C Programming Language
Author: Kernighan & Ritchie
Year: 1978
Price: $25.99

Structures can be passed to functions in two ways:

  • Pass by Value: A copy of the entire structure is passed to the function. Changes made inside the function do not affect the original structure. This can be inefficient for large structures due to the copying overhead.
  • Pass by Pointer (Address): The address of the structure is passed. The function works directly with the original structure via the pointer. This is more efficient for large structures and allows the function to modify the original structure.

Example: Pass by Value vs. Pass by Pointer

#include <stdio.h>
// Define the Point structure
struct Point {
double x;
double y;
};
// Function taking Point by value (makes a copy)
void print_point_value(struct Point p) {
printf("Value: (%.2f, %.2f)\n", p.x, p.y);
// p.x = 0; // Change here would NOT affect the original p1
}
// Function taking Point by pointer (accesses original)
void print_point_pointer(const struct Point *p_ptr) { // Use const if not modifying
// Access members using the arrow operator (->)
printf("Pointer: (%.2f, %.2f)\n", p_ptr->x, p_ptr->y);
// p_ptr->x = 0; // Change here WOULD affect the original p1 (if not const)
}
// Function to modify Point via pointer
void move_point(struct Point *p_ptr, double dx, double dy) {
p_ptr->x += dx;
p_ptr->y += dy;
}
int main(void) {
struct Point p1 = { 3.0, 4.0 };
printf("Original p1: ");
print_point_value(p1); // Pass by value
printf("Original p1 via pointer: ");
print_point_pointer(&p1); // Pass address (pointer)
move_point(&p1, 1.0, -2.0); // Modify original p1
printf("Moved p1: ");
print_point_pointer(&p1); // Print modified p1
return 0;
}

Output:

Original p1: Value: (3.00, 4.00)
Original p1 via pointer: Pointer: (3.00, 4.00)
Moved p1: Pointer: (4.00, 2.00)

You can declare pointers that hold the memory address of structure variables.

struct Point *ptr_p1; // Declares a pointer to a Point structure

Assign the address of a structure variable using the address-of operator (&).

struct Point p1 = {1.0, 2.0};
ptr_p1 = &p1; // ptr_p1 now holds the address of p1

To access members of the structure through the pointer, use the arrow operator (->).

ptr_p1->x = 10.0; // Equivalent to (*ptr_p1).x = 10.0
ptr_p1->y = 20.0; // Equivalent to (*ptr_p1).y = 20.0
printf("Point via pointer: (%.2f, %.2f)\n", ptr_p1->x, ptr_p1->y);

The expression (*ptr_p1).x first dereferences the pointer (*ptr_p1) to get the structure itself, and then uses the dot operator (.) to access the member. The arrow operator (->) is convenient shorthand for this common operation.

Bit fields allow you to define structure members that occupy a specific number of bits, rather than a full byte or word. This is useful for:

  • Memory Saving: Packing multiple boolean flags or small integer values into a single integer member.
  • Hardware Interfacing: Matching data structures directly to hardware registers or specific binary data formats where fields aren’t byte-aligned.

You define a bit field by specifying the member type (usually unsigned int or int), the member name, a colon :, and the number of bits.

struct HardwareRegister {
unsigned int enable : 1; // 1 bit for enable flag
unsigned int error_flag : 1; // 1 bit for error flag
unsigned int mode : 3; // 3 bits for mode (0-7)
unsigned int reserved : 3; // 3 unused bits (padding)
// Total: 1+1+3+3 = 8 bits (likely fits in 1 byte or int)
};
int main(void) {
struct HardwareRegister reg = {0}; // Initialize all bits to 0
reg.enable = 1; // Set the enable bit
reg.mode = 5; // Set mode to 5 (binary 101)
if (reg.error_flag == 0) {
printf("No error detected.\n");
}
printf("Mode: %u\n", reg.mode);
printf("Size of register struct: %zu bytes\n", sizeof(reg));
return 0;
}

Important Considerations for Bit Fields:

  • Portability: The exact layout (order, padding) of bit fields within memory is implementation-defined (depends on the compiler and architecture). Code relying on a specific bit field layout might not be portable.
  • Addressability: You cannot take the address (&) of a bit field member.
  • Performance: Accessing bit fields might be slower than accessing regular members due to the extra masking and shifting operations required.
  • Type: While int and unsigned int are common, other integer types might be allowed depending on the implementation.

Use bit fields judiciously when memory conservation or hardware mapping is a primary concern, but be aware of the portability implications.