Lesson 06-C Language Structures and Unions

Definition and Usage of Structs

In the C language, a struct is a user-defined data type that allows you to combine data of different types to form a composite data type. A struct can contain various basic data types (such as int, float, char, etc.) and other complex types (such as arrays, pointers, or even other structs) as its members.

Defining a Struct

A struct is defined using the struct keyword, followed by the struct name and a pair of curly braces {}, which contain the declarations of the members. Each member declaration ends with a semicolon ;.

struct Student {
    char name[50];
    int age;
    float gpa;
};

Example: Defining a Student Struct

#include <stdio.h>
#include <string.h>

// Define a student struct (including student ID, name, age, score)
struct Student {
    char id[20];      // Student ID (string)
    char name[50];    // Name (string)
    int age;          // Age (integer)
    float score;      // Score (floating-point)
};

Using Structs

Once a struct type is defined, you can declare struct variables and access or modify their members.

Declaring Struct Variables:

You can declare variables at the same time as defining the struct type, or define the type first and then declare variables.

struct Student student1; // Declare a struct variable

Or

struct Student {
   char name[50];
   int age;
   float gpa;
};

struct Student student1;

Initializing Struct Variables:

Struct variables can be initialized at declaration or have their members initialized individually later.

struct Student student1 = {"John Doe", 20, 3.5}; // Initialize all members

Or

struct Student student1;
strcpy(student1.name, "John Doe");
student1.age = 20;
student1.gpa = 3.5;

Accessing Struct Members:

Use the . operator to access struct members.

printf("Name: %s\n", student1.name);
printf("Age: %d\n", student1.age);
printf("GPA: %.2f\n", student1.gpa);

Struct Pointers

A struct pointer is a pointer to a struct, used to indirectly access struct members. It points to the memory address of a struct variable and accesses members via the arrow operator (->).

struct Student *ptr = &student1; // Declare and initialize a struct pointer
printf("Name: %s\n", (*ptr).name); // Access member using pointer
printf("Age: %d\n", ptr->age); // Use arrow operator to access member

Defining a Struct Pointer

Student* p;  // Declare a struct pointer (uninitialized)
Student s = {"003", "Wang Wu", 19, 78.5};
p = &s;      // Pointer points to struct variable s

Accessing Members via Pointer

// Modify member values
p->age = 20;          // Equivalent to (*p).age = 20;
strcpy(p->name, "Wang Xiaowu");

// Read member values
printf("Student ID: %s, Name: %s\n", p->id, p->name);

Dynamically Allocating Struct Memory

Use malloc to dynamically allocate struct memory to avoid stack space limitations:

#include <stdlib.h>

Student* create_student(const char* id, const char* name, int age, float score) {
    Student* p = (Student*)malloc(sizeof(Student));  // Allocate memory
    if (p == NULL) {                                 // Check if allocation succeeded
        perror("malloc failed");
        exit(EXIT_FAILURE);
    }
    strcpy(p->id, id);
    strcpy(p->name, name);
    p->age = age;
    p->score = score;
    return p;  // Return dynamically allocated struct pointer
}

// Usage example
Student* s = create_student("004", "Zhao Liu", 22, 88.0);
printf("Dynamically allocated student: %s, %s\n", s->id, s->name);
free(s);  // Free memory (critical! avoid memory leaks)

Struct Arrays

Structs can also form arrays, where each array element is a complete struct.

Struct Arrays

A struct array is a contiguous storage of multiple struct variables, each element being a struct.

// Declare a struct array (3 students)
Student students[3] = {
    {"001", "Zhang San", 20, 85.5},
    {"002", "Li Si", 21, 92.0},
    {"003", "Wang Wu", 19, 78.5}
};

// Access array elements
for (int i = 0; i < 3; i++) {
    printf("Student %d: %s, Score %.2f\n", i+1, students[i].name, students[i].score);
}

Struct Pointers and Arrays

A struct pointer can point to the first element of an array and access elements via pointer arithmetic:

Student* p = students;  // Pointer points to the first element of the array (students[0])
for (int i = 0; i < 3; i++) {
    printf("Student %d: %s, Age %d\n", i+1, (p+i)->name, (p+i)->age);
    // Equivalent to p[i].name (pointer arithmetic automatically converts)
}

Dynamically Allocating Struct Arrays

Use malloc to allocate memory for a struct array, suitable for scenarios with unknown quantity:

int n = 3;  // Number of students
Student* arr = (Student*)malloc(n * sizeof(Student));  // Allocate space for n structs
if (arr == NULL) {
    perror("malloc failed");
    exit(EXIT_FAILURE);
}

// Initialize dynamic array
for (int i = 0; i < n; i++) {
    sprintf(arr[i].id, "00%d", i+1);
    strcpy(arr[i].name, "Student");
    arr[i].age = 18 + i;
    arr[i].score = 70.0 + i * 5.0;
}

// Free memory after use
free(arr);

Structs as Function Parameters

Structs can be passed as function parameters, allowing the function to access and modify struct members.

Structs can be passed to functions in two ways:

  • Pass by value: Copies the entire struct (suitable for small structs).
  • Pass by pointer: Passes the struct address (efficient, suitable for large structs).
// Pass by value (copies struct)
void print_student(Student s) {
    printf("Student ID: %s, Name: %s\n", s.id, s.name);
}

// Pass by pointer (efficient)
void modify_age(Student* p, int new_age) {
    p->age = new_age;  // Directly modify original struct member
}

int main() {
    Student s = {"001", "Zhang San", 20, 85.5};
    print_student(s);       // Pass by value
    modify_age(&s, 21);     // Pass by pointer
    print_student(s);       // Output age 21
    return 0;
}

Structs as Function Return Values

Functions can also return a struct, packaging multiple values into a single return.

As Function Return Value

Functions can return a struct or a struct pointer (avoid returning pointers to local variables):

// Return struct (recommended for small structs)
Student create_student(const char* id, const char* name, int age, float score) {
    Student s;
    strcpy(s.id, id);
    strcpy(s.name, name);
    s.age = age;
    s.score = score;
    return s;  // Return struct copy
}

// Return struct pointer (requires dynamic allocation)
Student* create_student_ptr(const char* id, const char* name, int age, float score) {
    Student* p = (Student*)malloc(sizeof(Student));
    if (p == NULL) exit(EXIT_FAILURE);
    strcpy(p->id, id);
    strcpy(p->name, name);
    p->age = age;
    p->score = score;
    return p;  // Return dynamically allocated pointer
}

int main() {
    Student s = create_student("001", "Zhang San", 20, 85.5);
    Student* p = create_student_ptr("002", "Li Si", 21, 92.0);
    printf("Struct return: %s, %s\n", s.id, s.name);
    printf("Pointer return: %s, %s\n", p->id, p->name);
    free(p);  // Free dynamically allocated memory
    return 0;
}

Struct Arrays and Pointers

Struct Arrays

A struct array is a collection of structs, where each element is a struct. This allows organizing and storing multiple records with the same fields.

Defining a Struct Array:

struct Student {
    int id;
    char name[50];
    float gpa;
};

struct Student students[3]; // Define an array containing 3 struct elements

Initializing a Struct Array:

struct Student students[3] = {
    {1, "Alice", 3.5},
    {2, "Bob", 3.7},
    {3, "Charlie", 3.6}
};

Accessing Elements in a Struct Array:

printf("Student ID: %d, Name: %s, GPA: %.2f\n", students[0].id, students[0].name, students[0].gpa);

Struct Pointers

A struct pointer is a pointer to a struct, used to indirectly access struct members. It can point to elements in a struct array or to a standalone struct variable.

Defining and Initializing a Struct Pointer:

struct Student *ptr; // Define a struct pointer
ptr = &students[0]; // Initialize pointer to point to the first element of the array

Accessing Struct Members Using a Pointer:

printf("Student ID: %d, Name: %s, GPA: %.2f\n", (*ptr).id, (*ptr).name, (*ptr).gpa);

Or using the arrow operator ->:

printf("Student ID: %d, Name: %s, GPA: %.2f\n", ptr->id, ptr->name, ptr->gpa);

Traversing a Struct Array:

for (int i = 0; i < 3; i++) {
    struct Student *currentStudent = &students[i];
    printf("Student ID: %d, Name: %s, GPA: %.2f\n", currentStudent->id, currentStudent->name, currentStudent->gpa);
}

Combined Use of Struct Arrays and Pointers

Combining struct arrays and pointers provides more flexible data access and processing. For example, you can define a pointer to a struct array and access its elements through the pointer.

Defining a Pointer to a Struct Array:

struct Student (*arrayPtr)[3]; // Define a pointer to an array containing 3 struct elements
arrayPtr = &students; // Initialize pointer to point to the entire struct array

Accessing Elements Using a Pointer to a Struct Array:

printf("Student ID: %d, Name: %s, GPA: %.2f\n", (*arrayPtr)[0].id, (*arrayPtr)[0].name, (*arrayPtr)[0].gpa);

Or using pointer arithmetic directly:

printf("Student ID: %d, Name: %s, GPA: %.2f\n", arrayPtr->id, arrayPtr->name, arrayPtr->gpa);

Note that using arrayPtr->id directly is incorrect because the -> operator accesses members of the struct pointed to, while arrayPtr points to a struct array, not a single struct.

The correct approach is to first dereference arrayPtr to get the array, then use array indexing or pointer arithmetic to access a specific struct element, and finally use -> or . to access struct members.

// Correctly access elements using a pointer to a struct array
printf("Student ID: %d, Name: %s, GPA: %.2f\n", (*arrayPtr)[0].id, (*arrayPtr)[0].name, (*arrayPtr)[0].gpa);

// Or using pointer arithmetic
struct Student *studentPtr = (*arrayPtr);
printf("Student ID: %d, Name: %s, GPA: %.2f\n", studentPtr[0].id, studentPtr[0].name, studentPtr[0].gpa);

Dynamically Allocating Struct Arrays

In addition to statically defining struct arrays, you can use the malloc() function to dynamically allocate space for a struct array. This is particularly useful when the array size is not known until runtime.

int numStudents = 5; // Assume student count is known at runtime as 5
struct Student *dynamicStudents = malloc(numStudents * sizeof(struct Student));

if (dynamicStudents != NULL) {
    // Initialize dynamically allocated struct array
    for (int i = 0; i < numStudents; i++) {
        dynamicStudents[i].id = i + 1;
        strcpy(dynamicStudents[i].name, "Student");
        dynamicStudents[i].gpa = 3.0 + (float)i / 10;
    }

    // Use dynamically allocated struct array
    for (int i = 0; i < numStudents; i++) {
        printf("Student ID: %d, Name: %s, GPA: %.2f\n", dynamicStudents[i].id, dynamicStudents[i].name, dynamicStudents[i].gpa);
    }

    // Do not forget to free dynamically allocated memory
    free(dynamicStudents);
}

Union

A union is a special composite data type where all members share the same memory space, and only one member is valid at a time. It is suitable for saving memory or handling data of different types.

Defining a Union

A union is defined using the union keyword, followed by the union name and a pair of curly braces {}, containing the member declarations.

union Data {
    int i;
    float f;
    char str[20];
};

Example: Defining a Union with Shared Memory

union Data {
    int i;       // 4 bytes (assuming int is 4 bytes)
    float f;     // 4 bytes (assuming float is 4 bytes)
    char str[8]; // 8 bytes (character array occupies 8 bytes)
};  // The total size of the union is determined by the largest member (8 bytes here)

Using Unions

Accessing Union Members

Access members using the dot operator (.), but only one member is valid at a time:

union Data d;

d.i = 100;       // Write to int member (occupies first 4 bytes)
printf("int value: %d\n", d.i);

d.f = 3.14f;     // Write to float member (overwrites first 4 bytes)
printf("float value: %f\n", d.f);

strcpy(d.str, "hello");  // Write to char array (overwrites first 8 bytes)
printf("string: %s\n", d.str);

Union Size and Alignment

  • Size: The total size of the union equals the size of its largest member (must satisfy memory alignment).
  • Alignment: The alignment requirement of the union matches that of its largest member (ensures efficient access).
#include <stddef.h>  // For offsetof macro

union Data {
    char c;   // 1 byte
    int i;    // 4 bytes (alignment requirement 4)
    double d; // 8 bytes (alignment requirement 8)
};  // Largest member is double (8 bytes), union size is 8 bytes (aligned to 8)

int main() {
    printf("Union size: %zu\n", sizeof(union Data));        // Output 8
    printf("int member offset: %zu\n", offsetof(union Data, i)); // Output 0 (starts at 0)
    printf("double member offset: %zu\n", offsetof(union Data, d)); // Output 0
    return 0;
}

Notes

  • Type Compatibility: Since all members in a union share the same memory segment, ensure type compatibility when accessing different members; otherwise, it may lead to data corruption or undefined behavior.
  • Initialization: It is best to initialize one member before using the union to avoid using indeterminate values.
  • Memory Alignment: The size of a union is at least the size of its largest member due to memory alignment. The compiler adjusts the union size according to platform alignment requirements to ensure all members can be accessed correctly.

Union Size and Alignment

The size of a union depends on its largest member because all members share the same memory segment. For example, if a union contains int, float, and char[20], and char[20] is the largest member, the union size is at least 20 bytes (assuming char is 1 byte). However, the actual size may be larger to satisfy memory alignment requirements.

union MyUnion {
    int i;
    long long ll;
    char str[10];
};

int main() {
    union MyUnion u;
    printf("Size of union: %zu\n", sizeof(u));
    return 0;
}

In this example, the size of union MyUnion may be larger than 10 bytes, depending on the size of long long and memory alignment rules.

Enumeration (enum)

Defining an Enumeration

An enumeration type is defined using the enum keyword, followed by the enumeration name and a pair of curly braces {}, containing a series of enumeration members.

enum Weekday {
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday,
    Sunday
};

By default, enumeration members are counted starting from 0, with each subsequent member incremented by 1. However, you can explicitly specify values for enumeration members.

enum Color {
    Red = 1,
    Green,
    Blue
};

In this example, Green’s value will automatically be set to 2 because it follows Red (value 1).

Using Enumerations

Once an enumeration type is defined, you can declare enumeration variables and assign enumeration members to them.

enum Weekday today;
today = Monday;

You can also initialize it directly when defining the enumeration variable.

enum Weekday today = Friday;

Declaring Enumeration Variables

Enumeration variables must be declared before use; type aliases can simplify:

// Method 1: Direct declaration
enum Weekday today;

// Method 2: Using type alias (typedef)
typedef enum Weekday Weekday;
Weekday tomorrow;

Initializing Enumeration Variables

Enumeration variables can be initialized at declaration (using member name or integer value):

enum Weekday today = MON;    // Initialize using member name (value 0)
Weekday tomorrow = TUE;      // Value 1
Weekday weekend = (enum Weekday)6;  // Explicitly specify integer value (value 6)

Values of Enumeration Members

Enumeration member values are integers, defaulting to increment from 0, or can have explicit initial values:

enum Color {
    RED = 1,    // Explicitly set to 1
    GREEN = 3,  // Next member is 4 (3+1)
    BLUE = 5    // Next member is 6 (5+1)
};

int main() {
    printf("RED: %d\n", RED);    // Output 1
    printf("GREEN: %d\n", GREEN); // Output 3
    printf("BLUE: %d\n", BLUE);   // Output 5
    return 0;
}

Enumeration Member Values

Enumeration member values are integers; you can use these values directly or compare enumeration variables with members.

enum Weekday today = Wednesday;
if (today == Wednesday) {
    printf("Today is Wednesday.\n");
}

Implicit Conversion of Enumerations

Enumeration members can be implicitly converted to integers, meaning you can use enumeration members where integer values are needed.

enum Color color = Red;
printf("Color value: %d\n", color);

Implicit Conversion

Enumeration variables can be implicitly converted to int type (assigned to int variables or used in integer operations):

enum Weekday today = WED;  // Value 2
int num = today;           // Implicitly converted to int (num=2)
printf("Wednesday corresponds to value: %d\n", num);

Enumeration Range

Enumeration member values should fit within the underlying integer type. By default, the underlying type of an enumeration is the smallest integer type sufficient to hold all members. If member values exceed the default integer type range, the compiler may choose a larger integer type.

The valid range of an enumeration is the integers between the minimum and maximum member values (determined by the compiler, usually the int range):

enum Color {
    RED = -10,
    GREEN = 20,
    BLUE = 30
};  // Valid range: integers from -10 to 30

Enumeration Restrictions

While enumerations provide the convenience of named integer constants, they have some limitations. For example, you cannot perform mathematical operations on enumeration members, nor assign arbitrary integer values to enumeration variables (unless the value corresponds to an enumeration member).

  • Member Type: Enumeration members can only be integer constants (cannot be expressions or functions).
  • Member Uniqueness: Enumeration member values cannot be duplicated (otherwise compilation warning).
  • Scope: Enumeration members have the same scope as the enumeration type (avoid naming conflicts).

Comprehensive Comparison and Application Scenarios

TypeFeaturesTypical Application Scenarios
StructCombines data of different types, memory not sharedStudent information, employee records, 2D coordinates
UnionMembers share memory, saves spaceFlag bits and data sharing space, protocol parsing
EnumNamed integer constants, improves readabilityState machine, weekday/month representation, error codes

Practical Example: Student Grade Management System (Struct + Enum)

Requirements Description

Implement a student grade management system that supports:

  • Recording basic student information (student ID, name, gender).
  • Recording course grades (math, Chinese, English).
  • Calculating student average score (using enum to represent course types).

Implementation Code

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

// Define gender enum
enum Gender {
    MALE,   // Male (0)
    FEMALE  // Female (1)
};

// Define course enum
enum Course {
    MATH,    // Math (0)
    CHINESE, // Chinese (1)
    ENGLISH  // English (2)
};

// Define student struct
typedef struct {
    char id[20];      // Student ID
    char name[50];    // Name
    enum Gender gender; // Gender
    float scores[3];  // Score array (math, Chinese, English)
} Student;

// Print student information
void print_student(Student s) {
    const char* gender_str[] = {"Male", "Female"};  // Enum to string
    printf("Student ID: %s, Name: %s, Gender: %s\n", 
           s.id, s.name, gender_str[s.gender]);
    printf("Scores: Math %.2f, Chinese %.2f, English %.2f\n",
           s.scores[MATH], s.scores[CHINESE], s.scores[ENGLISH]);
}

// Calculate average score
float calculate_average(Student s) {
    return (s.scores[MATH] + s.scores[CHINESE] + s.scores[ENGLISH]) / 3.0;
}

int main() {
    // Create student array (dynamically allocated)
    int n = 2;
    Student* students = (Student*)malloc(n * sizeof(Student));
    if (students == NULL) {
        perror("malloc failed");
        exit(EXIT_FAILURE);
    }

    // Initialize student data. 
    strcpy(students[0].id, "001");
    strcpy(students[0].name, "Zhang San");
    students[0].gender = MALE;
    students[0].scores[MATH] = 85.5;
    students[0].scores[CHINESE] = 92.0;
    students[0].scores[ENGLISH] = 88.5;

    strcpy(students[1].id, "002");
    strcpy(students[1].name, "Li Si");
    students[1].gender = FEMALE;
    students[1].scores[MATH] = 90.0;
    students[1].scores[CHINESE] = 88.0;
    students[1].scores[ENGLISH] = 95.0;

    // Print student information and average score
    for (int i = 0; i < n; i++) {
        print_student(students[i]);
        printf("Average score: %.2f\n\n", calculate_average(students[i]));
    }

    free(students);  // Free memory
    return 0;
}

Compile and Run:

gcc -o score_management score_management.c
./score_management

Output Result:

Student ID: 001, Name: Zhang San, Gender: Male
Scores: Math 85.50, Chinese 92.00, English 88.50
Average score: 88.67

Student ID: 002, Name: Li Si, Gender: Female
Scores: Math 90.00, Chinese 88.00, English 95.00
Average score: 91.00
Share your love