Introduction
In earlier lessons you learned how to declare, initialize, and use structures, as well as how they compare conceptually with unions. In this lesson, you will apply those ideas in a realistic mini‑project. We’ll model a simple student management scenario to practice:
- Designing and using structures for organized data
- Accessing members with the dot (
.) operator - Using designated initializers for readable setup
- Passing structures to functions to keep code modular
- Contrasting structures and unions for memory efficiency
Designing a Practical Struct: Student Records
We’ll build a small program that stores and prints information about students in a course. Each student has:
- An ID
- A name
- A current grade
- A flag indicating if they have a scholarship
Defining the Structure
#include <stdio.h>
#include <string.h>
#define MAX_NAME_LEN 50
struct Student {
int id;
char name[MAX_NAME_LEN];
float grade;
int has_scholarship; // 0 = no, 1 = yes
};
// Function prototypes
void print_student(struct Student s);
void update_grade(struct Student *s, float new_grade);
Here, struct Student groups related fields into one logical unit, making it easier to pass around complete student records instead of many separate variables.
Initializing Structs with Designated Initializers
Instead of relying on position-based initialization, designated initializers clearly show which field gets which value.
int main(void) {
struct Student s1 = {
.id = 1001,
.name = "Alice Johnson",
.grade = 89.5f,
.has_scholarship = 1
};
struct Student s2 = {0}; // zero-initialize all members
// Later, we can set s2 fields explicitly
s2.id = 1002;
strcpy(s2.name, "Bob Smith");
s2.grade = 72.0f;
s2.has_scholarship = 0;
print_student(s1);
print_student(s2);
update_grade(&s2, 78.0f);
print_student(s2);
return 0;
}
Explanation
struct Student s1 = { .id = 1001, ... };uses designated initializers to explicitly map values to members, improving readability and reducing mistakes when the struct changes over time.struct Student s2 = {0};sets all bytes to zero, which is a common pattern for “empty/default” records.strcpy(s2.name, "Bob Smith");copies a string into thenamearray.- Functions
print_studentandupdate_grade(defined next) operate on entire student records.
Output
// Output:
// ID: 1001, Name: Alice Johnson, Grade: 89.50, Scholarship: YES
// ID: 1002, Name: Bob Smith, Grade: 72.00, Scholarship: NO
// Updated grade for Bob Smith to 78.00
// ID: 1002, Name: Bob Smith, Grade: 78.00, Scholarship: NO
Accessing Structure Members with the Dot Operator
To work with individual fields of a struct variable (not a pointer), use the dot (.) operator.
void print_student(struct Student s) {
printf("ID: %d, Name: %s, Grade: %.2f, Scholarship: %s\n",
s.id, // dot operator
s.name,
s.grade,
s.has_scholarship ? "YES" : "NO");
}
Here, s.id, s.name, and s.grade access each member directly.
When you have a pointer to a struct (like in update_grade), you will typically use ->, which is syntactic sugar for (*pointer).member.
void update_grade(struct Student *s, float new_grade) {
s->grade = new_grade; // same as (*s).grade = new_grade;
printf("Updated grade for %s to %.2f\n", s->name, s->grade);
}
Passing Structures to Functions
Passing by Value vs Passing by Pointer
In print_student, we pass struct Student s by value. The function receives a copy of the struct:
- Safe: the original
Studentinmaincannot be modified insideprint_student. - Cost: copying large structs repeatedly can be less efficient.
In update_grade, we pass struct Student *s by pointer:
- More efficient: only an address is passed.
- Can modify original data:
s->gradechanges the actual student inmain.
This combination is common: use by value when only reading data, and by pointer when updates are required.
Arrays of Structures in a Practical Context
Let’s extend the example to handle multiple students and compute an average grade.
#include <stdio.h>
#include <string.h>
#define MAX_NAME_LEN 50
#define MAX_STUDENTS 3
struct Student {
int id;
char name[MAX_NAME_LEN];
float grade;
int has_scholarship;
};
void print_student(struct Student s) {
printf("ID: %d, Name: %s, Grade: %.2f, Scholarship: %s\n",
s.id, s.name, s.grade,
s.has_scholarship ? "YES" : "NO");
}
float average_grade(const struct Student students[], int count) {
float sum = 0.0f;
for (int i = 0; i < count; i++) {
sum += students[i].grade; // dot via array element
}
return (count > 0) ? sum / count : 0.0f;
}
int main(void) {
struct Student students[MAX_STUDENTS] = {
[0] = { .id = 1001, .name = "Alice Johnson", .grade = 89.5f, .has_scholarship = 1 },
[1] = { .id = 1002, .name = "Bob Smith", .grade = 78.0f, .has_scholarship = 0 },
[2] = { .id = 1003, .name = "Carol Lee", .grade = 92.0f, .has_scholarship = 1 }
};
for (int i = 0; i < MAX_STUDENTS; i++) {
print_student(students[i]);
}
float avg = average_grade(students, MAX_STUDENTS);
printf("Average grade: %.2f\n", avg);
return 0;
}
Explanation and Output
- We use an array of structures to store multiple students.
- Designated initializers like
[0] = { ... }make it explicit which index is being initialized. average_gradetakes aconst struct Student[]to promise it will not modify the array.
// Output:
// ID: 1001, Name: Alice Johnson, Grade: 89.50, Scholarship: YES
// ID: 1002, Name: Bob Smith, Grade: 78.00, Scholarship: NO
// ID: 1003, Name: Carol Lee, Grade: 92.00, Scholarship: YES
// Average grade: 86.50
Structures vs Unions: Memory Efficiency in Practice
Imagine we also want to store extra info for each person in a system, but this extra info can be either a GPA or years of experience — never both at the same time.
Using a struct:
struct PersonStructInfo {
float gpa;
int experience_years;
};
This always allocates memory for both gpa and experience_years, even if only one is relevant.
Using a union:
union PersonUnionInfo {
float gpa;
int experience_years;
};
Here, gpa and experience_years share the same memory location. The size of the union is roughly the size of its largest member, making it more memory‑efficient when only one field is logically active at a time.
In a real application, you’d typically combine these ideas with an additional flag to indicate which union member is valid:
struct Person {
char name[50];
int is_student; // 1 = student, 0 = worker
union {
float gpa; // used when is_student == 1
int experience_years; // used when is_student == 0
} extra;
};
Here, the struct organizes related data, while the union optimizes memory for mutually exclusive values.
Summary / Key Takeaways
In this lesson Practical Example with Structures, you have learned:
- Practical struct design: How to model real-world entities (like students or people) using structures for clear, organized data storage.
- Member access: How to access and modify structure members using the dot operator for variables and the arrow operator for pointers.
- Designated initializers: How to use field-name-based initialization to make struct setup safer, clearer, and less error-prone.
- Passing structs to functions: When to pass structures by value for read-only operations and by pointer for efficient, in-place modification.
- Arrays of structures: How to manage collections of related records and process them in functions (e.g., computing averages).
- Combining structs with unions: How to nest unions inside structures to store mutually exclusive data efficiently.
- Memory efficiency considerations: How choosing between structs and unions impacts memory usage, especially when only one of several fields is needed at a time.
- Clean, modular organization: How grouping related fields into structs simplifies function interfaces and improves program maintainability.