Building more complex data structures with struct

We have already seen most of C’s basic data types. However, we will frequently want to store more complicated data that cannot be represented by one of these basic types. For our first example, let’s say we want to store complex numbers. A complex number is made up of two real-valued components: the real part and the complex part. We could store complex numbers as arrays of type float or double, but we can actually do better than that. C provides a concept called a struct or structure that allows the programmer to make their own types out of the basic types provided. To define a complex number, we could put this at the top of our file (outside of any function definitions):

struct complex {
    double real;
    double imaginary;
};

This defines a new type: struct complex that represents a complex number. A structure definition starts with the word struct followed by a name, then a series of variable declarations surrounded by braces, and finally a semicolon. Each of the variables inside a structure is called a member or a field. In order to access members of a structure, the . operator is used. So if you have a variable z of type struct complex, you can access the real component of z with z.real.

Let’s say we want to do complex arithmetic. We couldn’t multiply or add these types like we could with double because the computer doesn’t know what the data represents or what multiplication and addition would even look like. However, if we wanted to make complex arithmetic easier, we could write some functions to do it for us:

/* Computes x + y */
struct complex complex_add(struct complex x, struct complex y)
{
    struct complex z;

    c.real = x.real + y.real;
    z.imaginary = x.imaginary + y.imaginary;
    
    return z;
}

/* Computes x - y */
struct complex complex_subtract(struct complex x, struct complex y)
{
    struct complex z;

    c.real = x.real - y.real;
    z.imaginary = x.imaginary - y.imaginary;
    
    return z;
}

/* Computes x * y */
struct complex complex_multiply(struct complex x, struct complex y)
{
    struct complex z;

    c.real = x.real * y.real - x.imaginary * y.imaginary;
    z.imaginary = x.real * y.imaginary + x.imaginary * y.real;
    
    return z;
}

If we wanted to multiply two complex numbers, we could just write c = complex_multiply(a, b);. When you use the assignment (=) operator on a structure, it simply assigns each of the members of the structure the value of the corresponding member of the expression. So x = y; is equivalent to

x.real = y.real;
x.complex = y.complex;

Let’s go for a more complicate structure and try to store a date and time. We could do that in one structure, but because it might be useful to just use the date or just use the time, we will do it in three:

struct date {
    unsigned char day;
    unsigned char month;
    unsigned int year;
};

struct time {
    unsigned char second;
    unsigned char minute;
    unsigned char hour;
};

struct date_time {
    struct date date;
    struct time time;
};

Then we could write functions to work with these date and time structures just like we did for struct complex. For instance, we could add two times:

struct time time_add(struct time t1, struct time t2)
{
    struct time t3;

    t3.second = t1.second + t2.second;
    t3.minute = t1.minute + t2.minute;
    t3.hour = t1.hour + t2.hour;

    /* Normalize seconds and minutes to [0, 60) */
    t3.minute += t3.second / 60;
    t3.second = t3.second % 60;
    t3.hour += t3.minute / 60;
    t3.minute = t3.minute % 60;

    return t3;
}

We could also write functions to convert a struct time to a number of seconds or compute the number of days since January 1st for a given struct date. The exact functions to write depend on what you want to do with our new-found structure. One very useful function would be one that prints it to the terminal:

void time_print(FILE *stream, struct time time)
{
    char *month_string;

    switch(time.date.month) {
    case 1:
        fprintf(stream, "January");
        break;
    case 2:
        fprintf(stream, "February");
        break;
    case 3:
        fprintf(stream, "March");
        break;
    case 4:
        fprintf(stream, "April");
        break;
    case 5:
        fprintf(stream, "May");
        break;
    case 6:
        fprintf(stream, "June");
        break;
    case 7:
        fprintf(stream, "July");
        break;
    case 8:
        fprintf(stream, "August");
        break;
    case 9:
        fprintf(stream, "September");
        break;
    case 10:
        fprintf(stream, "October");
        break;
    case 11:
        fprintf(stream, "November");
        break;
    case 12:
        fprintf(stream, "December");
        break;
    default:
        fprintf(stream, "Unknown Month");
    }

    printf(" %hhd, $hhd at %hhd:%02hhd:%02hhd",
            time.date.day, time.date.year,
            time.time.hour, time.time.minute, time.time.second);
}