SITERAW

Create Your Own Variable Types

The C language lets us do something incredibly powerful: it allows us to create our own variable types.

We're going to explore three kinds of custom variable types:

  • Structures: these let you build variables made up of several sub-variables.
  • Enumerations: they define a list of possible values for a variable.
  • Unions: kind of like structures, but they can only hold one of their variables at a time.

Creating new variable types becomes essential when you're aiming to write more complex programs. And by "more complex," I mean not just making a game of Hangman 😄

Luckily, this isn't difficult to understand or use. Still, stay sharp, because starting in the next chapter, we'll be using structures all the time. You should know that most libraries define their own types. So before long, you'll be dealing with variable types like File, and a bit later on, things like Window, Audio, Keyboard, Mouse, and so on.

Defining a Structure

A structure is a collection of variables that can each be of different types. Unlike arrays, which force you to stick to a single data type, a structure lets you combine long, char, int, double — all in one place. 🙂

You can even include pointers — or other structures! That's right: structure inception (yes, it's a thing 😉).

Structures are usually defined in .h files, just like your prototypes and #defines.

Here's a basic example:

struct YourStructureName
{
    int variable1;
    int variable2;
    int anotherVariable;
    double decimalNumber;
};

A structure definition starts with the struct keyword, followed by the name of your structure (like File or Screen, for example).

Personally, I name my structures the same way I name variables, except I capitalize the first letter. That way, when I see captainAge in my code, I know it's a variable (lowercase first letter). But if I see AudioClip, I know it's a structure — a custom type — because it starts with a capital.

Of course, you don't have to follow my style. The main thing is to stay consistent and organized. :)

After naming your structure, you open and close braces, just like with a function.

Heads up — this is important: you MUST put a semicolon after the closing brace. It's mandatory. Forget it, and your code won't compile.

So, what goes inside the braces?

That's easy: the variables that make up your structure. A structure usually has at least two sub-variables — otherwise, it doesn't really serve much purpose. ;)

As you can see, creating a custom variable type isn't complicated at all. Every structure you'll see is really just a bundle of basic variables like int, double, long, and so on. There's no magic. A File type is nothing more than a bunch of primitive values stitched together. 😄

Structure Example

Oh, I've got tons of structure ideas bouncing around in my head. Let's say you want to store the coordinates of a point on the screen. You'll definitely need something like that once we get into 2D game development later on — so we may as well get a head start. 😉

When working in 2D (two dimensions), there are two axes: the X-axis (left to right) and the Y-axis (bottom to top). By convention, the X value is usually stored in a variable called x, and the Y value in y.

Can you write a Coordinates structure that stores both the X (horizontal) and Y (vertical) position of a point?

Come on, it's not that hard :)

struct Coordinates
{
    int x; // X-axis
    int y; // Y-axis
};

Our structure is called Coordinates, and it contains two variables: x and y, which represent the horizontal and vertical positions.

If we wanted to, we could easily tweak this into a 3D version: just add a third variable (say, z) to represent height. Boom — now we've got a structure that can handle 3D points in space ;)

Arrays Inside a Structure

Structures can hold arrays too. That's great news — now we can include char arrays (a.k.a. strings) with no problem!

Let's build a Person structure that stores a bunch of information about someone:

struct Person
{
    char lastName[100];
    char firstName[100];
    char address[200];
    
    int age;
    int isMale; // Boolean: 1 = male, 0 = female
};

This structure has five sub-variables. The first three are strings, which store the person's last name, first name, and address.

The last two hold the age and gender. The gender is represented as a boolean: 1 = true = male, 0 = false = female.

This structure could be used in an address book program, for example. Of course, you can add more fields to your structure if you want. There's no hard limit to how many variables a structure can contain (though maybe don't cram a hundred into one — that might be overkill 😅).

Using a Structure

Now that we've defined our structure, it's time to use it inside a function in our .c file.

Here's how you create a variable of type Coordinates:

struct Coordinates
{
    int x; int y;
};

int main(int argc, char *argv[]) { struct Coordinates point; // Creating a variable called "point" of type Coordinates

return 0; }

We've just created a variable named point, which is of type Coordinates. This variable automatically contains two sub-variables: x and y (its horizontal and vertical position).

Do we have to include the word struct when declaring a variable?

Yes, because it helps the compiler tell the difference between a built-in type like int and a custom type like Coordinates.

That said, programmers find it kind of annoying to write struct every time they declare a custom variable. To fix that, they invented a handy little trick: the typedef.

The typedef

Let's go back to the definition of our Coordinates structure.

We'll add a special instruction called typedef, which lets us create an alias — a shortcut that says "this thing is equivalent to that thing."

We'll insert a line starting with typedef just before the structure:

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

This line has three parts (and no, I didn't stutter with the word "Coordinates" 😆):

  • typedef: tells the compiler we're creating a type alias.
  • struct Coordinates: the name of the structure we want to alias.
  • Coordinates: the shortcut name.

In simple terms, this line says: "From now on, writing Coordinates is the same as writing struct Coordinates." That means you don't have to write struct every time anymore. Pretty neat!

An alternative declaration form is:

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

These two codes do the same thing. Pick whichever you like most (I personally use the second version more often).

So now, back in main, we can just write:

int main(int argc, char *argv[])
{
    Coordinates point; // Thanks to the typedef, this is the same as struct Coordinates
    return 0;
}

I definitely recommend using typedef like we did here for Coordinates. Most programmers do it this way. It saves them from typing "struct" all over the place, and since we're all lazy by nature, it works out just fine 😄

Modifying Structure Members

Now that we've created our point variable, we want to change its coordinates.

How do you access the x and y values of point? Like this:

int main(int argc, char *argv[])
{
    Coordinates point;
    
    point.x = 10;
    point.y = 30;

return 0; }

We've just modified the value of point by setting its x to 10 and its y to 30. Our point is now located at position (10, 30) — just like you'd write coordinates in math class 😛

To access any component of a structure, use the format:

variable.componentName;

The dot separates the structure variable from its sub-variable.

If we go back to the Person structure from earlier and we want to read someone's first and last name, we'd do it like this:

int main(int argc, char *argv[])
{
    Person user;

printf("What is your last name? "); scanf("%s", user.lastName); printf("Your first name? "); scanf("%s", user.firstName);

printf("Your name is %s %s", user.firstName, user.lastName);

return 0; }

Sample output:

What is your last name? SiteRaw
Your first name? Boss
Your name is Boss SiteRaw

We passed user.lastName to scanf, which directly writes into that sub-variable of user.

Same goes for firstName. We could do the same for address, age, and gender, but... I'm feeling lazy right now. Must be because I'm a programmer 😅

Of course, you could've done all this without using structures, just by creating a lastName variable and a firstName one separately. But the cool thing here is, you can create another Person variable and it'll have its own name, first name, etc.

So you could do:

Person player1, player2;

...and now you can store information about both players separately.

But wait — C can do even better! You can create an array of Person!

It's super simple:

Person players[2];

And to access the name of the first player, you just write:

players[0].lastName;

The big advantage of using an array here is that you can loop through the players instead of repeating the same code twice. Just go through the players array and ask for the name, first name, address, etc., one by one.

Initializing a Structure

Just like with variables, arrays, and pointers, it's a very good idea to initialize structures as soon as you declare them. Otherwise, they'll contain random junk. Remember: when a variable is created, it picks up whatever was already in memory. Sometimes it's zero, sometimes it's leftover nonsense from a previous program. You might end up with a value like -145788 out of nowhere.

Here's a quick recap of how to initialize:

  • Variable: set it to 0 (easy!).
  • Pointer: set it to NULL. NULL is defined in stdlib.h (usually as 0), but we use NULL by convention for pointers, just to make it obvious it's a pointer and not a plain variable.
  • Array: initialize each element to 0.

For structures, it's kind of like initializing an array. You can do this directly when declaring the variable:

Coordinates point = {0, 0};

This sets point.x = 0 and point.y = 0, in that order.

Let's go back to the Person structure (with its string fields). You're also allowed to initialize a string with "" (just empty quotes). I don't think I mentioned this trick in the strings chapter, but hey — it's never too late to learn something new. 😄

So you can initialize the name, first name, address, age, and gender like this:

Person user = {"", "", "", 0, 0};

That said, I don't personally use this technique much. I usually pass my variable to a function like initializeCoordinates, which handles the setup for me.

But to do that, I need to pass a pointer to the variable. If I just send the variable itself, the function will only get a copy, and any changes will affect that copy — not the original. Same behavior as with basic variables. If you're foggy on this, take a look back at the "red thread" in the pointers chapter. 😉

So... time to learn how to use pointers with structures. And guess what? That's exactly what we're about to do next!

Structure Pointers

A structure pointer is created just like a pointer to an int, double, or any other basic type:

Coordinates* point = NULL;

Here, we've got a pointer to a Coordinates structure called point.

Just as a quick reminder (it never hurts), you can also write the asterisk in front of the pointer name — it means exactly the same thing:

Coordinates *point = NULL;

That's actually how I usually do it, especially when declaring multiple pointers on the same line — you're forced to put an asterisk before each one:

Coordinates *point1 = NULL, *point2 = NULL;

But anyway, all of that's just general pointer stuff — not specific to structures ;)

Passing a Structure to a Function

What we're really interested in here is how to send a structure pointer to a function, so that the function can modify the contents of the variable.

Let's walk through a simple example: we'll create a Coordinates variable in main, then pass its address to a function called initializeCoordinates. That function will simply set all elements of the structure to 0.

This is just to illustrate the concept — in real life, you could just initialize the structure the easy way, like we saw earlier:

Coordinates point = {0};

That would automatically set all structure elements to 0.

Our initializeCoordinates function will take a parameter that's a pointer to a Coordinates structure — in other words, a Coordinates*.

void initializeCoordinates(Coordinates* point)
{
    // Initialize each member of the structure here
}

int main(int argc, char *argv[]) { Coordinates myPoint;

initializeCoordinates(&myPoint);

return 0; }

So, we create myPoint in main and send its address to initializeCoordinates. That function receives the address as a pointer named point (although really, we could've called it anything — myPoint, thing, whatever).

Alright, now that we're inside initializeCoordinates, we want to initialize each field one by one.

Important note: don't forget the asterisk when accessing the actual structure! If you leave it out, you might accidentally change the pointer's address — and that's not what we want ;)

Okay, here's where things get tricky. You might think you could do this:

void initializeCoordinates(Coordinates* point)
{
    *point.x = 0;
    *point.y = 0;
}

Ha! If only it were that simple :D

But it doesn't work like that. Why? Because the dot operator (.) binds more tightly than the asterisk (*), so it ends up trying to access the x field of point, not of *point.

What we really want is to access the structure that point points to. So, to make sure the dot applies to *point, we need to add parentheses:

void initializeCoordinates(Coordinates* point)
{
    (*point).x = 0;
    (*point).y = 0;
}

There we go :)

This code works — give it a try. We've passed a structure to the function, and it's initialized both x and y to 0.

Now, in C, we usually stick to the simpler initialization method we saw earlier. But in object-oriented languages like C++ or C#, you'll often see these kinds of initializations done inside functions instead. Objects are really just "super-structures" when you think about it — and all of this will come into play later when we move on to other concepts. For now, just keep focusing on C. Everything you're learning here will carry over to other languages too.

A Handy and Common Shortcut

You'll find yourself working with structure pointers all the time. Honestly, in C, we use structure pointers more than regular structures.

(See? I told you pointers would haunt you to the grave — I wasn't kidding ^^. There's no escaping it, it's your fate.)

... Okay okay, I'll be serious now :-°

Since we work with structure pointers so often, we end up writing this kind of thing a lot:

(*point).x = 0;

But surprise — programmers think that's too long. Those parentheses around *point? Ugh, what a hassle. So, since programmers are notoriously lazy (what, I've said that already? o_O), someone came up with a shortcut:

point->x = 0;

This little arrow, made from a hyphen and a greater-than sign, is a shortcut.

Writing point->x is EXACTLY the same as writing (*point).x. It'll make your code much cleaner, trust me ;)

Just remember — the arrow only works with pointers! If you're working directly with a structure variable, you have to use the dot, like we saw earlier.

Let's update our initializeCoordinates function using this shortcut:

void initializeCoordinates(Coordinates* point)
{
    point->x = 0;
    point->y = 0;
}

Burn that arrow syntax into your brain — we'll be using it a LOT ;)

And above all — don't confuse the arrow with the dot!

Use the arrow for pointers, and the dot for normal variables. Here's a quick little example to help you remember:

int main(int argc, char *argv[])
{
    Coordinates myPoint;
    Coordinates *pointer = &myPoint;

myPoint.x = 10; // We're working with a variable, so we use the dot pointer->x = 10; // We're using a pointer, so we use the arrow

return 0; }

Same result, two different ways: first by directly modifying the variable, then by modifying it through a pointer.

Enumerations

Enumerations are a slightly different way of creating your own variable types.

Unlike structures, an enumeration doesn't contain "sub-variables." Instead, it's a list of all the possible values a variable can take. An enum only takes up one space in memory, and that space can hold just one of the values you define — and only one at a time.

Here's an example of an enumeration:

typedef enum Volume Volume;
enum Volume
{
    LOW, MEDIUM, HIGH
};

As you can see, we're using typedef here again, just like we've done up to now.

To create an enumeration, you use the enum keyword. In this case, our enum is called Volume. It's a custom variable type that can take on one of three values: either LOW, MEDIUM, or HIGH.

Now we can create a variable of type Volume — let's call it music, which will store the current music volume. For example, we can initialize music to the value MEDIUM:

Volume music = MEDIUM;

And there you go! :) Later in the program, we could change the volume value to either LOW or HIGH.

Assigning Numbers to Values

You might have noticed I wrote the enum values in all caps. That probably reminds you of constants or #defines, right? ;)

And yeah, it's pretty similar — but not exactly the same. With enums, the compiler automatically assigns a number to each possible value in the list.

So in our Volume enum, LOW is 0, MEDIUM is 1, and HIGH is 2. This assignment happens automatically and always starts at 0.

Unlike with #define, where the preprocessor does the replacement, here it's the compiler that associates MEDIUM with 1, for example. In the end, the result is similar ;) When we initialized music to MEDIUM, we were actually storing the number 1 in memory.

But here's the real question: is it useful to know that MEDIUM equals 1, or that HIGH is 2?

Nope. Most of the time, you honestly couldn't care less ;) The compiler handles the number associations behind the scenes. All you have to do is something like:

if (music == MEDIUM)
{
    // Play the music at medium volume
}

You don't even need to know what value MEDIUM has — just let the compiler deal with that.

The real benefit? It makes your code super readable. Anyone looking at that if statement instantly knows what's going on — we're checking if the music is set to medium volume.

Now technically, we could've written this without using an enum at all. We could've just made music an int, and remembered that 1 means medium volume, for example:

if (music == 1)
{
    // medium volume?
}

But as you can see, that version is way less clear for the poor programmer reading the code ;)

Assigning Specific Values

So far, the compiler's been in charge of assigning numbers: 0 to the first value, then 1, 2, 3, and so on.

But guess what? You can assign specific values to each enum item yourself. So, what's the point of doing that, you ask? ^^

Well, imagine your computer handles volume levels from 0 to 100 (0 being silent, and 100 being full blast). In that case, it makes total sense to assign real volume levels to your enum values:

typedef enum Volume Volume;
enum Volume
{
    LOW = 10, MEDIUM = 50, HIGH = 100
};

Now LOW corresponds to 10% volume, MEDIUM to 50%, and so on. Much more meaningful, right?

And of course, you could easily add new options like MUTE. In that case, you'd assign MUTE the value... 0! Nailed it! You're getting the hang of it :D

Unions in C

What is a Union?

A union, much like a structure, is a grouping of different types of variables.

But there's a big difference: a union is an aggregate that can only hold one of its members at any given time. In other words, a union can store the value of any of its members — but just one at a time.

The way you define a union is pretty much identical to how you'd define a struct, except that you use the union keyword instead of struct.

union type {
    int integer;
    double floating;
    void *pointer;
    char letter;
};

The code above defines a union called type that can hold either an int, a double, a void* pointer, or a char.

This idea — that it can only store one value at a time — is reflected in the result of the sizeof operator:

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

union uPlayer { char name[28]; int Hp; int Mp; } uPlayer;

struct structPlayer { char name[28]; int Hp; int Mp; } structPlayer;

int main() { printf("size of union = %d bytes", sizeof(uPlayer)); printf("\nsize of structure = %d bytes", sizeof(structPlayer)); return 0; }

Output:

size of union = 28 bytes  
size of structure = 36 bytes

But why are the sizes different if the variables are the same?

Here's the deal: the structure takes up 36 bytes because:

  • name[28] is 28 bytes
  • Hp is 4 bytes
  • Mp is 4 bytes

Simple math: 28 + 4 + 4 = 36 bytes.

The union, on the other hand, is only 28 bytes. That's because the size of a union is always equal to the size of its largest member. In this case, the biggest one is name[28], so the union's total size is 28 bytes.

With unions, all members share the same memory space.

Also, just like structs, unions can contain padding bits — but only at the end of the union.

When to Use a Union

Because of how they work, unions are used less frequently than structs. Their main appeal is memory optimization — something structs can't do as efficiently.

So what's the point of using them?

They're super useful when you know that you'll only ever need one of the variables at a time.

Here's a super simple example:

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

// Define a union for RPG character stats union Stats { int strength; // For Warriors float magic; // For Wizards };

// Define a struct for an RPG character struct Character { char name[50]; char class[10]; // Either "Warrior" or "Wizard" union Stats stats; };

int main() { struct Character hero;

// Make a Wizard strcpy(hero.name, "Gandalf"); strcpy(hero.class, "Wizard"); hero.stats.magic = 97.5;

printf("%s the %s has a magic power of %.1f!\n", hero.name, hero.class, hero.stats.magic);

// Make a Warrior strcpy(hero.name, "Boromir"); strcpy(hero.class, "Warrior"); hero.stats.strength = 85;

printf("%s the %s has a strength of %d!\n", hero.name, hero.class, hero.stats.strength);

return 0; }

Output:

Gandalf the Wizard has a magic power of 97.5!  
Boromir the Warrior has a strength of 85!

One annoying thing with this syntax is that you need to use the . operator twice: once to access the struct field (stats), and once again to access the union member (strength or magic).

Also, you're forced to give the union a name when embedding it in the struct, which isn't always ideal.

Anonymous Unions

Lucky for us, there's a way to make a union anonymous (and no, this has nothing to do with the AA! 😄). That means you can include it inside a struct without giving it a name.

When you do that, the union's fields behave just like regular fields of the struct — they're directly accessible.

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

// Define a struct for RPG characters struct Character { char name[50]; unsigned magician : 1; // bit field unsigned warrior : 1; // bit field union { // anonymous union int strength; double magic; }; };

static void show_power(struct Character c) { if (c.warrior) printf("%s has a power of %d\n", c.name, c.strength); else if (c.magician) printf("%s has a power of %f\n", c.name, c.magic); }

int main() { struct Character hero1, hero2;

strcpy(hero1.name, "Gandalf"); hero1.magician = 1; hero1.magic = 97.5;

strcpy(hero2.name, "Boromir"); hero2.warrior = 1; hero2.strength = 85; show_power(hero1); show_power(hero2);

return 0; }

Output:

Gandalf has a power of 97.500000  
Boromir has a power of 85

You'll notice the use of bit fields — we'll talk more about those later in this course.

Also note that anonymous unions aren't exclusive to unions — you can use the same trick with structures too.

Summary:

  • A union is an aggregate of variables of different types, but it can only store one of them at a time;
  • Like structs, unions can have padding bits, but only at the end;
  • If a union is part of another struct or union, you can make it anonymous to simplify how you access its fields.

Learn C Programing for Beginners

Enjoyed this C / C++ course?

If you liked this lesson, you can find the book "Learn C Programing for Beginners" from the same authors, available on SiteRaw, in bookstores and in online libraries in either digital or paperback format. You will find a complete C / C++ workshop with many exclusive bonus chapters.

More information