C Structures
C - Structures (struct)
Section titled “C - Structures (struct)”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;)
Defining a Structure
Section titled “Defining a Structure”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;Accessing Structure Members
Section titled “Accessing Structure Members”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 structurestruct Point { double x; double y;};
// Use the Book typedef from the previous exampletypedef 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.99Structures as Function Arguments
Section titled “Structures as Function Arguments”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 structurestruct 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 pointervoid 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)Pointers to Structures
Section titled “Pointers to Structures”You can declare pointers that hold the memory address of structure variables.
struct Point *ptr_p1; // Declares a pointer to a Point structureAssign 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 p1To access members of the structure through the pointer, use the arrow operator (->).
ptr_p1->x = 10.0; // Equivalent to (*ptr_p1).x = 10.0ptr_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
Section titled “Bit Fields”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
intandunsigned intare 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.