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:

C
struct structure_name {
    data_type member1;
    data_type member2;
    // More members...
};

For example, a structure to represent a person:

C
struct Person {
    char name[50];
    int age;
    float height;
};

Declaring Structure Variables

You can declare variables of a structure type in several ways:

C
// 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:

C
struct Person person1 = {"John Doe", 30, 5.9};

Or initialize member by member:

C
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 (.):

C
#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:

C
#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:

C
#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:

C
#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:

C
#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:

C
#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:

C
#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:

C
#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:

C
// 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:

C
#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

C
union union_name {
    data_type member1;
    data_type member2;
    // More members...
};

For example:

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

Using Unions

C
#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

  1. Type Punning: Reinterpreting memory from one type to another
C
#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;
}
  1. Tagged Unions: Combining a union with a structure to safely interpret data:
C
#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;
}
  1. 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:

FeatureStructureUnion
Memory allocationEach member has its own memory areaAll members share the same memory area
SizeSum of all members (plus padding)Size of the largest member
AccessAll members can be used simultaneouslyOnly one member should be used at a time
Use caseRepresenting entities with multiple attributesHandling multiple data types in the same memory location
Memory efficiencyLess efficientMore 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:

C
#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 contains Date and Course)
  • Arrays of structures (Course courses[5] and Student 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:

C
#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:

C
#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:

C
#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:

C
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:

C
// 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:

C
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:

C
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

  1. Create a structure to represent a bank account with account number, holder name, and balance. Write functions to deposit, withdraw, and display account information.
  2. Implement a simple address book using an array of structures. Include functions to add, search, and display contacts.
  3. Design a library management system with structures for books, authors, and borrowers. Include functionality for checking out and returning books.
  4. Create a structure to represent a fraction with numerator and denominator. Implement functions for addition, subtraction, multiplication, and division of fractions.
  5. Design a tagged union that can hold different geometric shapes (circle, rectangle, triangle) and calculate their areas.
  6. Implement a simple employee management system with nested structures for personal details, salary information, and department data.

Happy coding!

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top