Week 10: Structure, Unions, Bit Manipulation and Enumerations

A structure is a composite data type that allows you to group together variables of different data types under a single name. This grouping enables you to organize related data elements efficiently.

Structure Definitions, Initialization, and Accesing Structure Members

// Defining a structure to represent a point in 2D space
struct Point {
    int x;
    int y;
};

The identifier Point is the structure tag, which you use with struct to declare variables of the structure type. Variables declared within a struct’s braces are the structure’s members. A struct’s members must have unique names, though separate structure types may contain members of the same name without conflict. Each structure definition ends with a semicolon.

Self-Referential Structures

A self-referential structure, also known as a recursive structure, is a structure type in C that contains a member that is a pointer to the same type of structure. This concept is commonly used in data structures like linked lists, trees, and graphs, where each element points to another element of the same type, creating a chain or hierarchy.

struct Point {
    int x;
    int y;
    struct Point *next; // Pointer to the next Point structure
};

Defining Variables of Structure Types

Defining variables of structure types in C involves declaring instances of the structure by specifying the structure's name, followed by the variable name. Here's how you can define variables of structure types:

struct Point p1;
struct Point p2 = {3, 4, NULL};

We can also define variables of a given structure type directly after the structure definition, all within the same line, separating the structure definition from the variable declaration by a semicolon.

struct Point {
    int x;
    int y;
    struct Point *next; // Pointer to the next Point structure
} p1,p2,p3;

Example

#include <stdio.h>

// Define the Point structure
struct Point {
    int x;
    int y;
    struct Point *next; // Pointer to the next Point structure
} p3;

int main() {
    // Declare some Point structures
    struct Point p1;
    p1.x = 1;
    p1.y = 2;
    p1.next = NULL;
    //
    struct Point p2 = {3, 4, NULL};
    //
    p3.x = 5;
    p3.y = 6;
    p3.next = NULL;

    // Link the Point structures together
    p1.next = &p2;
    p2.next = &p3;

    // Traverse the linked list of Points and print their coordinates
    struct Point *current = &p1; // Start at the first Point
    while (current != NULL) {
        printf("Point: (%d, %d)\n", current->x, current->y);
        current = current->next; // Move to the next Point
    }

    return 0;
}

Accessing Structure Members

From the above example, you see that we can access structure members using the dot (.) operator when dealing with structure variables directly, and you can use the arrow (->) operator when dealing with pointers to structure variables.

Here's how you can use both operators:

  1. Dot Operator (.):

    • It is used to access members of a structure when you have a direct instance of that structure.

    struct Point {
        int x;
        int y;
    };
    
    int main() {
        struct Point p1;
        p1.x = 10;
        p1.y = 20;
        printf("Coordinates of p1: (%d, %d)\n", p1.x, p1.y);
        return 0;
    }

  2. Arrow Operator (->):

    • The arrow operator (->) is used to access members of a structure when you have a pointer to that structure.

    • The expression ptr->x is equivalent to (*ptr).x which dereferences the pointer and accesses the member x using the structure member operator (.). The parentheses are needed here because the structure member operator (.) has higher precedence than the pointer dereferencing operator (*). The structure pointer operator and structure member operator have the highest precedence and group from left-to-right, along with parentheses (for calling functions) and brackets ([]) used for array indexing.

    struct Point {
        int x;
        int y;
    };
    
    int main() {
        struct Point p1 = {10, 20};
        struct Point *ptr = &p1;
        printf("Coordinates of p1: (%d, %d)\n", ptr->x, ptr->y);
        return 0;
    }

It is important to note that you do not put spaces around the -> and . operators to emphasize that the expressions the operators are contained in are essentially single variable names.

Array of Structure Objects

You can create arrays of structure objects to store multiple instances of a structure. This is useful when dealing with collections of related data. Each element of the array is a structure object, and you can access its members using the dot operator (.) or arrow operator (->) if dealing with pointers.

#include <stdio.h>

// Define the Point structure
struct Point {
    int x;
    int y;
};

int main() {
    // Declare an array of Point structures
    struct Point points[3] = {{1, 2}, {3, 4}, {5, 6}};

    // Access and print each Point structure in the array
    for (int i = 0; i < 3; i++) {
        printf("Point %d: (%d, %d)\n", i+1, points[i].x, points[i].y);
    }

    return 0;
}

Using Structure with Functions

With structures, you can pass to functions: individual structure members, entire structure objects, or pointers to structure objects. Structures can be passed to functions either by value or by reference:

  • Passing by value creates a copy of the entire structure.

  • Passing by reference (using pointers) passes the address of the structure, allowing modifications to the original structure. Passing structures by reference is often preferred when dealing with large structures to avoid the overhead of copying.

#include <stdio.h>

// Define the Point structure
struct Point {
    int x;
    int y;
};

// Function to modify a Point structure passed by reference
void modifyPoint(struct Point *p, int newX, int newY) {
    p->x = newX;
    p->y = newY;
}

int main() {
    // Declare a Point structure
    struct Point p = {3, 4};

    // Call function to modify the Point structure
    modifyPoint(&p, 10, 20);

    // Print the modified Point structure
    printf("Modified Point: (%d, %d)\n", p.x, p.y);

    return 0;
}

Typedef

typedef in C can be used with structure definitions to create aliases for complex structure types, providing clearer and more concise code. This approach simplifies the declaration of structure variables and enhances code readability.

#include <stdio.h>

// Define a structure for representing points in 2D space
typedef struct {
    int x;
    int y;
} Point;

int main() {
    // Declare variables of the Point structure type
    Point p1 = {1, 2}; // Declaration with initialization
    Point p2; // Declaration without initialization

    // Access and modify the members of the Point structures
    p2.x = 3;
    p2.y = 4;

    // Print the values of the Point structures
    printf("Point 1: (%d, %d)\n", p1.x, p1.y);
    printf("Point 2: (%d, %d)\n", p2.x, p2.y);

    return 0;
}

In the above example, typedef is used to create an alias Point for the structure struct { int x; int y; };. Then we can use the Point alias to declare variables p1 and p2 of the structure type.

Note that

typedef struct {
    int x;
    int y;
} Point;

is the same as

struct point {
    int x;
    int y;
};
typedef struct point Point;

Unions

Union is a user-defined data type that allows storing different types of data in the same memory location. Unlike structures, where each member has its own memory space, all members of a union share the same memory location. This means that modifying one member may affect the value of other members.

In most cases, unions contain two or more items of different types, and you can reference only one member (and thus only one type) at a time. It's your responsibility as a programmer to ensure that you reference the data with the proper type. Attempting to access or interpret the data stored in a union with the wrong type can lead to logic errors, and the result is implementation-dependent.

#include <stdio.h>

// Define a union to represent various types of values
union MyUnion {
    int intValue;
    float floatValue;
    char charValue;
};

int main() {
    // Declare a union variable and initialize its members
    union MyUnion u = { .intValue = 10 };

    // Access and print the value of the union members
    printf("Integer Value: %d\n", u.intValue);
    printf("Float Value: %f\n", u.floatValue); // Undefined behavior due to uninitialized access
    printf("Char Value: %c\n", u.charValue); // Undefined behavior due to uninitialized access

    return 0;
}

Try to set floatValue to 100 after initialization of intValue, see the result.

Using a Union Containing Struct

#include <stdio.h>
#include <math.h>

// Struct for Cartesian coordinates
struct Cartesian {
    double x;
    double y;
    double z;
};

// Struct for Spherical coordinates
struct Spherical {
    double rho;     // radial distance
    double theta;   // polar angle (in radians)
    double phi;     // azimuthal angle (in radians)
};

// Union to represent a point in space
union Point {
    struct Cartesian cartesian;
    struct Spherical spherical;
};

// Function to convert Cartesian coordinates to Spherical coordinates
void cartesian_to_spherical(union Point *point) {
    double x = point->cartesian.x;
    double y = point->cartesian.y;
    double z = point->cartesian.z;

    point->spherical.rho = sqrt(x*x + y*y + z*z);
    point->spherical.theta = atan2(sqrt(x*x + y*y), z);
    point->spherical.phi = atan2(y, x);
}

// Function to convert Spherical coordinates to Cartesian coordinates
void spherical_to_cartesian(union Point *point) {
    double rho = point->spherical.rho;
    double theta = point->spherical.theta;
    double phi = point->spherical.phi;

    point->cartesian.x = rho * sin(theta) * cos(phi);
    point->cartesian.y = rho * sin(theta) * sin(phi);
    point->cartesian.z = rho * cos(theta);
}

int main() {
    // Create a point in Cartesian coordinates
    union Point cartesian_point = {.cartesian = {1.0, 2.0, 3.0}};

    // Convert Cartesian to Spherical
    cartesian_to_spherical(&cartesian_point);

    // Print Spherical coordinates
    printf("Spherical Coordinates: (rho=%.2f, theta=%.2f, phi=%.2f)\n",
           cartesian_point.spherical.rho,
           cartesian_point.spherical.theta,
           cartesian_point.spherical.phi);

    // Create a point in Spherical coordinates
    union Point spherical_point = {.spherical = {5.0, M_PI/3, M_PI/4}};

    // Convert Spherical to Cartesian
    spherical_to_cartesian(&spherical_point);

    // Print Cartesian coordinates
    printf("Cartesian Coordinates: (x=%.2f, y=%.2f, z=%.2f)\n",
           spherical_point.cartesian.x,
           spherical_point.cartesian.y,
           spherical_point.cartesian.z);

    return 0;
}

Bitwise Operators

In computing, a bit (short for binary digit) is the smallest unit of data and can have one of two values: 0 or 1. Multiple bits are combined to represent more complex data types, such as integers, characters, or boolean values. On most systems, a sequence of eight bits forms a byte, the typical storage unit for a char variable.

Bitwise operators are used to perform manipulation and comparison operations on individual bits within binary representations of data.

#include <stdio.h>

int main() {
    unsigned int a = 5; // Binary: 0000 0101
    unsigned int b = 3; // Binary: 0000 0011
    
    // Bitwise AND
    unsigned int result_and = a & b; // 0000 0001 (Decimal: 1)
    printf("Bitwise AND: %u\n", result_and);
    
    // Bitwise OR
    unsigned int result_or = a | b; // 0000 0111 (Decimal: 7)
    printf("Bitwise OR: %u\n", result_or);
    
    // Bitwise XOR
    unsigned int result_xor = a ^ b; // 0000 0110 (Decimal: 6)
    printf("Bitwise XOR: %u\n", result_xor);
    
    // Bitwise NOT
    unsigned int result_not_a = ~a; // 1111 1010 (Decimal: 4294967290)
    printf("Bitwise NOT of a: %u\n", result_not_a);
    
    // Left Shift
    unsigned int result_left_shift = a << 2; // 0001 0100 (Decimal: 20)
    printf("Left Shift of a: %u\n", result_left_shift);
    
    // Right Shift
    unsigned int result_right_shift = a >> 1; // 0000 0010 (Decimal: 2)
    printf("Right Shift of a: %u\n", result_right_shift);
    
    return 0;
}

Bitwise Assignment Operators

Bitwise assignment operators combine bitwise operations with assignment, allowing for more concise code when performing bitwise operations on variables and assigning the result back to the same variable. These operators modify the value of a variable in-place using bitwise operations.

#include <stdio.h>

int main() {
    unsigned int a = 5; // Binary: 0000 0101
    unsigned int b = 3; // Binary: 0000 0011
    
    // Bitwise AND Assignment
    a &= b; // Equivalent to: a = a & b;
    printf("Bitwise AND Assignment: %u\n", a); // Output: 1 (Binary: 0000 0001)
    
    // Bitwise OR Assignment
    a |= b; // Equivalent to: a = a | b;
    printf("Bitwise OR Assignment: %u\n", a); // Output: 3 (Binary: 0000 0011)
    
    // Bitwise XOR Assignment
    a ^= b; // Equivalent to: a = a ^ b;
    printf("Bitwise XOR Assignment: %u\n", a); // Output: 2 (Binary: 0000 0010)
    
    // Bitwise Left Shift Assignment
    a <<= 2; // Equivalent to: a = a << 2;
    printf("Bitwise Left Shift Assignment: %u\n", a); // Output: 8 (Binary: 0000 1000)
    
    // Bitwise Right Shift Assignment
    a >>= 1; // Equivalent to: a = a >> 1;
    printf("Bitwise Right Shift Assignment: %u\n", a); // Output: 4 (Binary: 0000 0100)
    
    return 0;
}

Bitwise operators are commonly used in low-level programming, device drivers, cryptography, and optimization algorithms. They are used to manipulate and extract specific bits from binary representations of data, perform bitwise arithmetic, and optimize memory usage.

Bit Fields

Bit fields are a feature that allows for the packing of data structures in a more memory-efficient manner. They enable the allocation of specific numbers of bits within a structure, providing precise control over memory usage and alignment.

To define, we use a syntax: type fieldName : width;, where type is the data type of the field, fieldName is the name of the field, and width is the number of bits allocated to the field.

#include <stdio.h>

// Define a structure to represent colors using bit fields
struct Color {
    unsigned int red   : 8; // 8 bits for red component
    unsigned int green : 8; // 8 bits for green component
    unsigned int blue  : 8; // 8 bits for blue component
    unsigned int alpha : 8; // 8 bits for alpha (transparency) component
};

int main() {
    // Declare a variable of type Color
    struct Color c;

    // Initialize color components using bit fields
    c.red = 255;    // Maximum intensity for red
    c.green = 128;  // Moderate intensity for green
    c.blue = 0;     // No blue
    c.alpha = 255;  // Fully opaque

    // Print the color components
    printf("Color components: R=%u, G=%u, B=%u, A=%u\n",
           c.red, c.green, c.blue, c.alpha);

    return 0;
}

It is important to note that due to their compact nature, individual bit fields are not assigned unique memory addresses like regular structure members. Attempting to take the address of a bit field can lead to errors or misleading results. For example,

#include <stdio.h>

struct Example {
    unsigned int flag : 1;  // Bit field with 1 bit
};

int main() {
    struct Example ex;
    ex.flag = 1;
    unsigned int *ptr = &ex.flag;
    return 0;
}

You may see an error during compilation

main.c: In function ‘main’:
main.c:11:25: error: cannot take address of bit-field ‘flag’
   11 |     unsigned int *ptr = &ex.flag;
      |     

Unnamed Bit Fields

Unnamed bit fields are bit fields that do not have a specified field name. They are used primarily for padding or alignment purposes within a structure.

#include <stdio.h>

// Structure with unnamed bit fields for padding
struct Example {
    unsigned int a : 16;  // 16-bit field
    unsigned int : 0;    // unnamed 0-bit field for alignment
    unsigned int b : 16;  // 16-bit field
};

int main() {
    printf("Size of struct Example: %lu bytes\n", sizeof(struct Example));
    
    struct Example ex;
    ex.a = 0b1000000000000000; 
    ex.b = 0b0000000000000001;

    printf("Value of 'a': %u\n", ex.a);
    printf("Value of 'b': %u\n", ex.b);
    
    // Printing individual bits of 'a' and 'b'
    printf("Bits of 'a': ");
    for (int i = 15; i >= 0; i--) {
        printf("%u", (ex.a >> i) & 1);
    }
    printf("\n");

    printf("Bits of 'b': ");
    for (int i = 15; i >= 0; i--) {
        printf("%u", (ex.b >> i) & 1);
    }
    printf("\n");

    return 0;
}

Note that, in C, 0b is the prefix used to indicate that the following digits represent a binary number. In the above example, 32768 is represented by 0b1000000000000000

Enumeration Constants

Enumerations, often referred to as enums, are user-defined data types in C that consist of a set of named integer constants. They provide a way to create symbolic names for integral values, improving code readability and maintainability.

enum Day {SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY};

To number the days 1 to 7, uses

enum Day {SUNDAY = 1, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY};

The example of using enumeration variable d in a for statement to print the day of the week from the array dayName.

#include <stdio.h>

// Define enum for days of the week
enum Day { SUNDAY = 1, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY };

int main() {
    // Array of strings representing days of the week
    const char* dayNames[] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};

    // Loop through each day of the week
    for (enum Day d = SUNDAY; d <= SATURDAY; d++) {
        // Print the day using its integer value to index into the array
        printf("%d%11s\n", d, dayNames[d - 1]);
    }

    return 0;
}

Classwork - Week 10

Create a simple student record management system for this course using structures and enums. The program should allow users to perform the following tasks:

  1. Define a structure Student with the following fields:

    • Name (string)

    • Student ID (integer)

    • Grade (enum: A, B, C, D, F)

  2. Implement the following menu-driven functionalities:

    • Add a new student record

    • Display all student records

    • Delete a student record by student id

    • Exit

  3. Ensure input validation for user inputs (e.g., handle incorrect menu choices gracefully).

For simplicity, you can limit the number of student to be 10 students max.

Last updated