C Programming Basics: Part 7 – Structures and Unions
Welcome to the seventh part of our C programming tutorial series! In previous articles, we’ve covered the fundamentals of C, variables, operators, control flow, functions, arrays, and strings. Now, let’s dive into structures and unions – powerful features that allow you to create custom data types by grouping related variables together.
Understanding Structures in C
In real-world programming, you often need to represent complex entities with multiple attributes. For example, a person has a name, age, and address; a point in 2D space has x and y coordinates. C structures allow you to group related variables of different types under a single name.
Defining a Structure
The syntax for defining a structure is:
struct structure_name {
data_type member1;
data_type member2;
// More members...
};
For example, a structure to represent a person:
struct Person {
char name[50];
int age;
float height;
};
Declaring Structure Variables
You can declare variables of a structure type in several ways:
// Method 1: Declare after defining the structure
struct Person person1;
// Method 2: Declare with definition
struct Person {
char name[50];
int age;
float height;
} person1, person2;
// Method 3: Using typedef to create an alias
typedef struct {
char name[50];
int age;
float height;
} Person;
Person person1; // No need for 'struct' keyword
Initializing Structure Variables
You can initialize a structure at declaration:
struct Person person1 = {"John Doe", 30, 5.9};
Or initialize member by member:
struct Person person1;
strcpy(person1.name, "John Doe");
person1.age = 30;
person1.height = 5.9;
Accessing Structure Members
You access structure members using the dot operator (.
):
#include <stdio.h>
#include <string.h>
struct Person {
char name[50];
int age;
float height;
};
int main() {
struct Person person1;
// Assign values to members
strcpy(person1.name, "John Doe");
person1.age = 30;
person1.height = 5.9;
// Access and print members
printf("Name: %s\n", person1.name);
printf("Age: %d\n", person1.age);
printf("Height: %.1f ft\n", person1.height);
return 0;
}
Structures and Memory Layout
A structure’s memory layout is typically a sequential arrangement of its members:
graph LR A["name[50]<br>50 bytes"] ---> B["age<br>4 bytes"] ---> C["height<br>4 bytes"]
Note: The actual memory layout might include padding bytes for alignment, depending on the compiler and architecture.
Structures as Function Arguments
You can pass structures to functions in several ways:
1. Pass by Value
When a structure is passed by value, a copy of the entire structure is made:
#include <stdio.h>
struct Point {
int x;
int y;
};
// Function taking a structure by value
void printPoint(struct Point p) {
printf("Point coordinates: (%d, %d)\n", p.x, p.y);
// Changes to p here do not affect the original structure
p.x = 100; // This change is local to the function
}
int main() {
struct Point myPoint = {10, 20};
printPoint(myPoint);
// Original structure is unchanged
printf("Original point: (%d, %d)\n", myPoint.x, myPoint.y);
return 0;
}
2. Pass by Reference (using pointers)
For large structures or when you need to modify the original structure, pass a pointer:
#include <stdio.h>
struct Point {
int x;
int y;
};
// Function taking a pointer to a structure
void movePoint(struct Point *p, int dx, int dy) {
// Use arrow operator (->) to access members through a pointer
p->x += dx;
p->y += dy;
// Equivalent to (*p).x += dx; (*p).y += dy;
}
int main() {
struct Point myPoint = {10, 20};
printf("Original point: (%d, %d)\n", myPoint.x, myPoint.y);
movePoint(&myPoint, 5, -3);
// Original structure is modified
printf("Moved point: (%d, %d)\n", myPoint.x, myPoint.y);
return 0;
}
The arrow operator (->
) is a shorthand for dereferencing a pointer and accessing a structure member: p->x
is equivalent to (*p).x
.
3. Returning Structures from Functions
A function can return a structure:
#include <stdio.h>
struct Point {
int x;
int y;
};
// Function returning a structure
struct Point createPoint(int x, int y) {
struct Point newPoint;
newPoint.x = x;
newPoint.y = y;
return newPoint;
}
int main() {
struct Point myPoint = createPoint(15, 25);
printf("Created point: (%d, %d)\n", myPoint.x, myPoint.y);
return 0;
}
Arrays of Structures
Structures and arrays can be combined to create arrays of structures, which are ideal for representing collections of entities:
#include <stdio.h>
#include <string.h>
struct Student {
char name[50];
int id;
float gpa;
};
int main() {
// Array of 3 Student structures
struct Student students[3];
// Initialize the first student
strcpy(students[0].name, "Alice");
students[0].id = 1001;
students[0].gpa = 3.8;
// Initialize the second student
strcpy(students[1].name, "Bob");
students[1].id = 1002;
students[1].gpa = 3.5;
// Initialize the third student
strcpy(students[2].name, "Charlie");
students[2].id = 1003;
students[2].gpa = 3.9;
// Print all students using a loop
printf("Student Information:\n");
printf("-------------------\n");
for (int i = 0; i < 3; i++) {
printf("Name: %s\n", students[i].name);
printf("ID: %d\n", students[i].id);
printf("GPA: %.1f\n\n", students[i].gpa);
}
return 0;
}
This approach is more organized than maintaining separate arrays for names, IDs, and GPAs.
Nested Structures
Structures can contain other structures as members, allowing you to build more complex data representations:
#include <stdio.h>
#include <string.h>
// Address structure
struct Address {
char street[100];
char city[50];
char state[20];
char zipCode[10];
};
// Person structure containing an Address
struct Person {
char name[50];
int age;
struct Address address; // Nested structure
};
int main() {
struct Person person;
// Initialize person
strcpy(person.name, "John Doe");
person.age = 30;
// Initialize the nested address structure
strcpy(person.address.street, "123 Main St");
strcpy(person.address.city, "Anytown");
strcpy(person.address.state, "CA");
strcpy(person.address.zipCode, "12345");
// Print information
printf("Person Information:\n");
printf("Name: %s\n", person.name);
printf("Age: %d\n", person.age);
printf("Address: %s, %s, %s %s\n",
person.address.street,
person.address.city,
person.address.state,
person.address.zipCode);
return 0;
}
classDiagram class Address { char street[100] char city[50] char state[20] char zipCode[10] } class Person { char name[50] int age Address address } Person *-- Address : contains note for Person "Outer structure" note for Address "Nested structure"
Self-Referential Structures
A structure can contain a pointer to its own type, which is essential for creating linked data structures like linked lists:
#include <stdio.h>
#include <stdlib.h>
// Node structure for a linked list
struct Node {
int data;
struct Node *next; // Pointer to the same structure type
};
int main() {
// Create and link three nodes
struct Node *head = malloc(sizeof(struct Node));
struct Node *second = malloc(sizeof(struct Node));
struct Node *third = malloc(sizeof(struct Node));
if (!head || !second || !third) {
printf("Memory allocation failed\n");
return 1;
}
// Initialize and link the first node
head->data = 10;
head->next = second;
// Initialize and link the second node
second->data = 20;
second->next = third;
// Initialize the third node (end of list)
third->data = 30;
third->next = NULL;
// Traverse and print the linked list
struct Node *current = head;
printf("Linked List: ");
while (current != NULL) {
printf("%d ", current->data);
current = current->next;
}
printf("\n");
// Free allocated memory
free(head);
free(second);
free(third);
return 0;
}
Here’s a visual representation of this linked list:
graph LR A[head<br>data: 10] --> B[second<br>data: 20] --> C[third<br>data: 30] --> D[NULL]
Structure Padding and Memory Alignment
The compiler may add padding bytes between structure members to ensure proper memory alignment for efficient CPU access:
#include <stdio.h>
struct Example1 {
char c; // 1 byte
int i; // 4 bytes
double d; // 8 bytes
};
struct Example2 {
int i; // 4 bytes
double d; // 8 bytes
char c; // 1 byte
};
int main() {
printf("Size of Example1: %lu bytes\n", sizeof(struct Example1));
printf("Size of Example2: %lu bytes\n", sizeof(struct Example2));
return 0;
}
You might expect both structures to use 13 bytes (1 + 4 + 8), but due to padding, they’ll likely be larger. The order of members can affect the total size, so arranging members by size (largest to smallest) can sometimes reduce padding.
You can control padding with compiler-specific directives:
// GCC example to pack the structure (minimize padding)
#pragma pack(1)
struct Packed {
char c;
int i;
double d;
};
#pragma pack()
// Check the size
printf("Size of Packed: %lu bytes\n", sizeof(struct Packed));
Bit Fields in Structures
Bit fields allow you to specify the exact number of bits for structure members, which is useful for memory-constrained systems or when working with hardware registers:
#include <stdio.h>
struct Date {
unsigned int day : 5; // 5 bits for day (0-31)
unsigned int month : 4; // 4 bits for month (0-15)
unsigned int year : 12; // 12 bits for year (0-4095)
};
int main() {
struct Date today = {26, 3, 2025};
printf("Date: %d/%d/%d\n", today.day, today.month, today.year);
printf("Size of Date: %lu bytes\n", sizeof(struct Date));
return 0;
}
Instead of using 4 bytes for each field (12 bytes total), this structure uses only 21 bits (3 bytes) in total.
Limitations of bit fields:
- You can’t take the address of a bit field
- Bit fields can’t be arrays
- The ordering of bits within a word is implementation-dependent
Unions in C
A union is similar to a structure, but all members share the same memory location. This means a union variable can hold different types of data at different times, but only one type at any given moment.
Defining a Union
union union_name {
data_type member1;
data_type member2;
// More members...
};
For example:
union Data {
int i;
float f;
char str[20];
};
Using Unions
#include <stdio.h>
#include <string.h>
union Data {
int i;
float f;
char str[20];
};
int main() {
union Data data;
// Using the integer member
data.i = 10;
printf("data.i: %d\n", data.i);
// Using the float member (overwrites the integer value)
data.f = 220.5;
printf("data.f: %.1f\n", data.f);
// Using the string member (overwrites the float value)
strcpy(data.str, "C Programming");
printf("data.str: %s\n", data.str);
// The integer and float values are now corrupted
printf("data.i: %d\n", data.i);
printf("data.f: %.1f\n", data.f);
printf("Size of union: %lu bytes\n", sizeof(union Data));
return 0;
}
The size of the union is determined by its largest member (in this case, str[20]
).
Memory Layout of Unions
Unlike structures, where members are stored sequentially, all members of a union share the same memory:
graph TD subgraph "Union Data" A["Memory block (20 bytes)"] end B["data.i (4 bytes)"] --- A C["data.f (4 bytes)"] --- A D["data.str (20 bytes)"] --- A
Practical Uses of Unions
- Type Punning: Reinterpreting memory from one type to another
#include <stdio.h>
union FloatIntUnion {
float f;
unsigned int i;
};
int main() {
union FloatIntUnion converter;
converter.f = 3.14;
printf("Float value: %f\n", converter.f);
printf("As integer bits: 0x%08X\n", converter.i);
return 0;
}
- Tagged Unions: Combining a union with a structure to safely interpret data:
#include <stdio.h>
#include <string.h>
// Define a tagged union
struct Variant {
enum { INT, FLOAT, STRING } type; // Tag to track the current type
union {
int i;
float f;
char str[20];
} data;
};
void printVariant(struct Variant v) {
switch(v.type) {
case INT:
printf("Integer: %d\n", v.data.i);
break;
case FLOAT:
printf("Float: %.2f\n", v.data.f);
break;
case STRING:
printf("String: %s\n", v.data.str);
break;
default:
printf("Unknown type\n");
}
}
int main() {
struct Variant v1, v2, v3;
// Set v1 as an integer
v1.type = INT;
v1.data.i = 42;
// Set v2 as a float
v2.type = FLOAT;
v2.data.f = 3.14;
// Set v3 as a string
v3.type = STRING;
strcpy(v3.data.str, "Hello");
// Print all variants
printVariant(v1);
printVariant(v2);
printVariant(v3);
return 0;
}
- Memory Conservation: When memory is tight, a union can save space by reusing the same memory area for different purposes at different times.
Structures vs. Unions
Let’s compare structures and unions:
Feature | Structure | Union |
---|---|---|
Memory allocation | Each member has its own memory area | All members share the same memory area |
Size | Sum of all members (plus padding) | Size of the largest member |
Access | All members can be used simultaneously | Only one member should be used at a time |
Use case | Representing entities with multiple attributes | Handling multiple data types in the same memory location |
Memory efficiency | Less efficient | More efficient when only one member is needed at a time |
Practical Example: Student Records System
Let’s create a practical example that puts structures, arrays, and functions together – a simple student records system:
#include <stdio.h>
#include <string.h>
// Define structures
struct Date {
int day;
int month;
int year;
};
struct Course {
char code[10];
char name[50];
float grade;
};
struct Student {
int id;
char name[50];
struct Date birthdate;
int numCourses;
struct Course courses[5]; // Maximum 5 courses per student
float gpa;
};
// Function prototypes
void addStudent(struct Student *students, int *count);
void displayStudent(struct Student student);
void displayAllStudents(struct Student *students, int count);
void calculateGPA(struct Student *student);
int findStudentById(struct Student *students, int count, int id);
int main() {
struct Student students[100]; // Array to store up to 100 students
int studentCount = 0;
int choice;
int searchId, foundIndex;
do {
printf("\n===== Student Records System =====\n");
printf("1. Add Student\n");
printf("2. Display All Students\n");
printf("3. Search Student by ID\n");
printf("4. Exit\n");
printf("Enter your choice: ");
scanf("%d", &choice);
switch(choice) {
case 1:
addStudent(students, &studentCount);
break;
case 2:
displayAllStudents(students, studentCount);
break;
case 3:
printf("Enter student ID to search: ");
scanf("%d", &searchId);
foundIndex = findStudentById(students, studentCount, searchId);
if (foundIndex != -1) {
displayStudent(students[foundIndex]);
} else {
printf("Student with ID %d not found.\n", searchId);
}
break;
case 4:
printf("Exiting program. Goodbye!\n");
break;
default:
printf("Invalid choice. Please try again.\n");
}
} while (choice != 4);
return 0;
}
void addStudent(struct Student *students, int *count) {
struct Student newStudent;
int i;
if (*count >= 100) {
printf("Error: Maximum number of students reached.\n");
return;
}
printf("\nEnter Student Details:\n");
printf("ID: ");
scanf("%d", &newStudent.id);
// Check if ID already exists
if (findStudentById(students, *count, newStudent.id) != -1) {
printf("Error: Student with ID %d already exists.\n", newStudent.id);
return;
}
printf("Name: ");
scanf(" %[^\n]", newStudent.name); // Read full name including spaces
printf("Birthdate (DD MM YYYY): ");
scanf("%d %d %d", &newStudent.birthdate.day,
&newStudent.birthdate.month,
&newStudent.birthdate.year);
printf("Number of courses (max 5): ");
scanf("%d", &newStudent.numCourses);
if (newStudent.numCourses > 5) {
newStudent.numCourses = 5;
printf("Warning: Maximum 5 courses allowed. Only first 5 will be recorded.\n");
}
for (i = 0; i < newStudent.numCourses; i++) {
printf("Course %d Code: ", i+1);
scanf(" %[^\n]", newStudent.courses[i].code);
printf("Course %d Name: ", i+1);
scanf(" %[^\n]", newStudent.courses[i].name);
printf("Course %d Grade: ", i+1);
scanf("%f", &newStudent.courses[i].grade);
}
calculateGPA(&newStudent);
students[*count] = newStudent;
(*count)++;
printf("Student added successfully!\n");
}
void displayStudent(struct Student student) {
int i;
printf("\n===== Student Information =====\n");
printf("ID: %d\n", student.id);
printf("Name: %s\n", student.name);
printf("Birthdate: %02d/%02d/%d\n", student.birthdate.day,
student.birthdate.month,
student.birthdate.year);
printf("Courses:\n");
for (i = 0; i < student.numCourses; i++) {
printf(" %s - %s: %.1f\n", student.courses[i].code,
student.courses[i].name,
student.courses[i].grade);
}
printf("Overall GPA: %.2f\n", student.gpa);
}
void displayAllStudents(struct Student *students, int count) {
int i;
if (count == 0) {
printf("No students in the database.\n");
return;
}
printf("\n===== All Students =====\n");
for (i = 0; i < count; i++) {
printf("%d. %s (ID: %d) - GPA: %.2f\n",
i+1,
students[i].name,
students[i].id,
students[i].gpa);
}
}
void calculateGPA(struct Student *student) {
float sum = 0;
int i;
if (student->numCourses == 0) {
student->gpa = 0;
return;
}
for (i = 0; i < student->numCourses; i++) {
sum += student->courses[i].grade;
}
student->gpa = sum / student->numCourses;
}
int findStudentById(struct Student *students, int count, int id) {
int i;
for (i = 0; i < count; i++) {
if (students[i].id == id) {
return i; // Return the index of the found student
}
}
return -1; // Return -1 if student not found
}
This program demonstrates many of the concepts we’ve covered:
- Nested structures (
Student
containsDate
andCourse
) - Arrays of structures (
Course courses[5]
andStudent students[100]
) - Passing structures to functions
- Modifying structures with pointers
- Using structures to organize related data
flowchart TD A[Start Program] --> B[Display Menu] B --> C{User Choice} C -->|1| D[Add Student] D --> D1[Enter Student Details] D1 --> D2[Enter Course Information] D2 --> D3[Calculate GPA] D3 --> D4[Add to Database] D4 --> B C -->|2| E[Display All Students] E --> E1[List Students with ID and GPA] E1 --> B C -->|3| F[Search Student by ID] F --> F1[Enter ID to Search] F1 --> F2{Student Found?} F2 -->|Yes| F3[Display Student Details] F2 -->|No| F4[Display Not Found Message] F3 --> B F4 --> B C -->|4| G[Exit Program] G --> H[End] C -->|Invalid| I[Display Error Message] I --> B classDef process fill:#e9ecef,stroke:#495057,stroke-width:1px; classDef decision fill:#fff4dd,stroke:#fd7e14,stroke-width:1px; classDef io fill:#d1e7dd,stroke:#20c997,stroke-width:1px; classDef terminal fill:#cfe2ff,stroke:#0d6efd,stroke-width:1px; class A,H terminal class B,D,D1,D2,D3,D4,E,E1,F,F1,F3,F4,I process class C,F2 decision class G io
Advanced Topics
Advanced Structure and Union Techniques in C
Flexible Array Members (C99)
C99 introduced flexible array members, which allow a structure to include an array of variable length:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct DynamicArray {
int size;
int data[]; // Flexible array member (must be last)
};
int main() {
int arraySize = 5;
// Allocate memory for the structure plus the array elements
struct DynamicArray *arr = (struct DynamicArray*)
malloc(sizeof(struct DynamicArray) + arraySize * sizeof(int));
if (arr == NULL) {
printf("Memory allocation failed\n");
return 1;
}
arr->size = arraySize;
// Initialize the array
for (int i = 0; i < arr->size; i++) {
arr->data[i] = i * 10;
}
// Print the array
printf("Dynamic array values:\n");
for (int i = 0; i < arr->size; i++) {
printf("%d ", arr->data[i]);
}
printf("\n");
// Free the allocated memory
free(arr);
return 0;
}
Anonymous Structures and Unions (C11)
C11 introduced anonymous structures and unions, which allow for cleaner member access:
#include <stdio.h>
struct Point2D {
int x;
int y;
};
struct Point3D {
struct { // Anonymous structure
int x;
int y;
};
int z;
};
int main() {
struct Point3D point = {10, 20, 30};
// Direct access to x and y without using an intermediate name
printf("Point: (%d, %d, %d)\n", point.x, point.y, point.z);
return 0;
}
This feature is particularly useful with unions:
#include <stdio.h>
struct Value {
enum { INT_TYPE, FLOAT_TYPE } type;
union { // Anonymous union
int i;
float f;
};
};
int main() {
struct Value v;
v.type = INT_TYPE;
v.i = 42; // Direct access without using an intermediate name
printf("Value: %d\n", v.i);
return 0;
}
Common Pitfalls and Best Practices
Header: Avoid These Common Structure and Union Mistakes
1. Structure Assignment and Comparison
Unlike arrays, entire structures can be assigned with the =
operator:
struct Point p1 = {10, 20};
struct Point p2;
p2 = p1; // Copies all members from p1 to p2
However, you can’t directly compare structures using ==
or !=
. Instead, compare individual members:
// Incorrect
if (p1 == p2) { /* ... */ } // Compilation error
// Correct
if (p1.x == p2.x && p1.y == p2.y) { /* ... */ }
2. Structure Padding and Portability
Structure padding can differ between compilers and architectures, affecting size and memory layout. For portable code:
- Don’t rely on the exact size of a structure
- Don’t perform pointer arithmetic based on assumed structure layout
- Use serialization functions when storing structures in files or sending over networks
3. Using Unions Safely
When using unions, always track which member contains valid data to avoid misinterpreting the bits:
union Data {
int i;
float f;
};
// Bad practice:
union Data data;
data.i = 42;
printf("%f\n", data.f); // Undefined behavior
// Good practice (using a tagged union):
struct TaggedData {
enum { INT_TYPE, FLOAT_TYPE } type;
union Data data;
};
struct TaggedData safe;
safe.type = INT_TYPE;
safe.data.i = 42;
if (safe.type == INT_TYPE) {
printf("%d\n", safe.data.i);
} else {
printf("%f\n", safe.data.f);
}
4. Copying Structures with Pointers
Be careful when copying structures that contain pointers, as a simple assignment only copies the pointer values, not the data they point to:
struct StringHolder {
char *text;
};
// Shallow copy (pointers point to the same memory)
struct StringHolder s1, s2;
s1.text = strdup("Hello");
s2 = s1; // Both s1.text and s2.text now point to the same string
// For deep copy:
s2.text = strdup(s1.text); // Create a separate copy of the string
Conclusion
Structures and unions are powerful tools that allow you to create custom data types tailored to your specific needs. By grouping related data together, they make your code more organized, readable, and maintainable.
In this article, we’ve covered:
- Structure definition and declaration
- Accessing structure members
- Passing structures to functions
- Arrays of structures
- Nested structures
- Self-referential structures for linked data structures
- Structure memory layout and bit fields
- Unions and their memory-sharing characteristics
- Tagged unions for type-safe access
- Practical examples showing how to use these features effectively
In the next part of our series, we’ll explore pointers and memory management in C, which will deepen your understanding of memory operations and dynamic data structures.
Practice Exercises
- Create a structure to represent a bank account with account number, holder name, and balance. Write functions to deposit, withdraw, and display account information.
- Implement a simple address book using an array of structures. Include functions to add, search, and display contacts.
- Design a library management system with structures for books, authors, and borrowers. Include functionality for checking out and returning books.
- Create a structure to represent a fraction with numerator and denominator. Implement functions for addition, subtraction, multiplication, and division of fractions.
- Design a tagged union that can hold different geometric shapes (circle, rectangle, triangle) and calculate their areas.
- Implement a simple employee management system with nested structures for personal details, salary information, and department data.
Happy coding!