TIGCC Programming Lessons
Lesson 7: Structures, Unions, and Enumerations
Step 7 - An Introduction to Unions
Well, we spent a lot of time covering Structures, the first and probably
most popular way to group data members together in C. But it is not the
only way of grouping data together. Suppose we want to use either an
integer, or a floating point number to store a number, but we don't want
to use both. Well, instead of declaring two variables, we could put them
together in what is called a union in C. Unions are like structures in
that they combine data members together in a single block. But the
difference is that we can only use one variable in the union at a time.
This is because the variable space for all the variables are defined on
top of one another. So using any one of the variables will overwrite the
value of all the other values.
You may be asking yourself, why would anyone want to create a structure
where you could only use one variable? Well, the answer is easy: memory
management. If we need to store two different kinds of data, there is no
reason to reserve space for both kinds. We can just reserve space for
the largest one, and save the space used by the smaller one. It is also
used to allowing certain kind of data to be defined in two different
ways. For example, the SCR_RECT union defined by AMS will let you define
a rectangle on the screen either using 4 characters (4 bytes), or a
single long integer (4 bytes). This doesn't seem to have a big
advantage, but what if you had a flag variable, where each bit
represented a different flag. So, we could define it either as a single
byte, and just remember internally what each bit represents, or we could
define it as a whole byte and as a segment of 8 bits, each with their
own name. In this way, we could pass the the entire flag byte when we
need to give it to a function, or manipulate several bits at once, or
we could use a single bit flag to modify each flag one at a time. This
explanation makes more sense when you see the VAT Symbol Entry table, so
let's take a look at that real quick:
typedef struct {
char name[8];
unsigned short compat;
union {
unsigned short flags_n;
struct {
unsigned short busy:1,local:1,flag1_5:1,flag1_4:1,
collapsed:1,twin:1,archived:1,in_view:1;
unsigned short folder:1,overwritten:1,checked:1,hidden:1,
locked:1,statvar:1,graph_ref_1:1,graph_ref_0:1;
} bits;
} flags;
unsigned short handle;
} SYM_ENTRY;
In the Symbol Entry table here, used by the AMS to keep track of files,
we can see that the structure has an embedded union. I didn't mention
this before when we talked about structures, but you can create an
unnamed union or structure and declare an instance of the structure by
putting the name you want to call the structure or union before the
closing semi-colon after the ending brace. So, in this example, the
unnamed structure inside the union will have a variable called bits,
which is the structure inside the union. Now, we haven't covered bit
operations yet, but the variables inside the structure use one bit each.
This is a common use for unions, because we can now access each of the
flags as part of the union or we can address the entire flags_n variable
at once, if we need to.
So, instead of using 16 integers to keep track of the different parts of
a symbol entry, we can use just a single integer and address it one bit
at a time, and we still have the luxury of addressing the entire flag
variable as a single piece. This is a very powerful concept in memory
management, because rather than wasting 32 bytes of memory on this one
entry, we only use 2 bytes.
As far as using unions, they are used in exactly the same manner as
structures. The dot operator accesses their member variables, or in this
case their member variable, singular. Remember we can only use one piece
of the union at any one time. We will get to an example program after we
talk about the next concept, and you will see how easy unions are. So,
let's hold off on any more union talk for now.
Step 8 - Enumerations, the Pseudo-Structures
The last kind of 'structures' we will be talking about here are called
enumerations. An enumeration literally means a series, like 0, 1, 2,
..., etc. So, using enumerations in C is just an easy way to declare
constants.
For example, instead of declaring 4 #define constants that all have
arbitrary values, we could define them inside an enumeration, like this:
enum Suits {SPADES, DIAMONDS, HEARTS, CLUBS};
This is the same thing as using 4 define statements, like this:
#define SPADES 0
#define DIAMONDS 1
#define HEARTS 2
#define CLUBS 3
But I think you can see the space saving constraints of using an
enumeration. It's customary to name enumerations so it's clear what kind
of constants we are referring to, but there is no requirement that
enumerations be named. In fact, we can get the same results by doing
this:
enum {SPADES, DIAMONDS, HEARTS, CLUBS};
We can assign specific values to the constants in enumerations, if it is
necessary to do so, but if we are using arbitrary constants, and just
want to use names to represent values that just need to be different,
like card suits, we can do this. But for sport, let's examine how we
would assign values to the enumeration constants:
enum {SPADES = 1, DIAMONDS = 2, HEARTS = 3, CLUBS = 4};
By using the assignment operator =, we can assign values to our
constants different than the defaults. This is good if we have an
enumeration whose values are specific, like defining common key code
values, like in the TIGCC CommonKeys enumeration:
enum CommonKeys {KEY_F1 = 268, KEY_F2 = 269, KEY_F3 = 270, KEY_F4 = 271,
KEY_F5 = 272, KEY_F6 = 273, KEY_F7 = 274, KEY_F8 = 275,
KEY_ESC = 264, KEY_QUIT = 4360, KEY_APPS = 265, KEY_SWITCH = 4361,
KEY_MODE = 266, KEY_BACKSPACE = 257, KEY_INS = 4353, KEY_CLEAR = 263,
KEY_VARLNK = 4141, KEY_CHAR = 4139, KEY_ENTER = 13, KEY_ENTRY = 4109,
KEY_STO = 258, KEY_RCL = 4354, KEY_SIGN = 173, KEY_MATH = 4149,
KEY_MEM = 4150, KEY_ON = 267, KEY_OFF = 4363
};
Each of the key codes is unique, and we can still use our constants
names in place of the values, which is easier for us to remember.
Computers may like numbers, but humans like names.
The last property of enumerations is that, if we define a value, but
don't define other values, the other values will be assigned sequential
values, so, if we did this:
enum {SPADES = 1, DIAMONDS, CLUBS, HEARTS};
Then, the SPADES constant would be 1, DIAMONDS would be 2, CLUBS would
be 3, and HEARTS would be 4. This is just one of the many flexibilities
of the enumeration verses #define constants, along with their ease of
use in defining many constants at once.
There is one more thing about enumerations, and that is that we can
define an enum variable type when we name our enumerations.
enum Suits suit = SPADES;
This is not often done though because C treats enumerations the same as
integers. There is no prohibition on assigning suit some random integer
value like 12. The one reason you might do something like this is to
make your program more readable.
Step 9 - Some More Concrete Code using Unions and Enumerations
Start TIGCC and create a new project. Create a new C Source File called
days. Modify the file so that it looks like this:
#include <tigcclib.h>
#define YEAR 2002
enum {JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC};
typedef union {
char date[25];
struct {
short int month;
short int day;
} dayValues;
} DAY;
void pickDays(DAY *days) {
int loop, randNum;
for (loop = 0; loop < 7; loop++) {
// select a random month
randNum = rand() % 12;
days[loop].dayValues.month = randNum;
// get a random day taking into account the max number of
// days in the random month we just selected
switch (randNum) {
case JAN:
case MAR:
case MAY:
case JUL:
case AUG:
case OCT:
case DEC:
randNum = rand() % 31;
break;
case FEB:
randNum = rand() % 28;
break;
case APR:
case JUN:
case SEP:
case NOV:
randNum = rand() % 30;
break;
}
// assign our randomly selected day to our structure
days[loop].dayValues.day = randNum + 1;
}
}
void convertDays(DAY *days) {
int loop;
short int day, month;
const char *monthnames[] = {"January","February","March","April","May","June",
"July","August","September","October",
"November","December"};
// loop through the days and assign a date string value
for (loop = 0; loop < 7; loop++) {
day = days[loop].dayValues.day;
month = days[loop].dayValues.month;
sprintf(days[loop].date,"%s %d, %d",monthnames[month],day,YEAR);
}
}
void printDays(DAY *days) {
int loop;
// clear the screen
clrscr();
// print each day in our days array
for (loop = 0; loop < 7; loop++) {
printf("%s\n",days[loop].date);
}
}
void _main(void) {
DAY days[7];
// seed the random number generator
randomize();
// pick 7 random days in the year 2002
pickDays(days);
// convert the numeric dates to a string
convertDays(days);
// print our randomly selected days
printDays(days);
// wait for user input before exiting the program
ngetchx();
}
Step 9a - Compile and Run the Program
Save the project and build it. Send the program to TiEmu and run it. It
will look something like this:
Step 9b - Program Analysis
I realize this program wasn't very useful, but I couldn't find any
helpful examples that would illustrate these concepts in a useful
manner. These concepts are so unique and specific, the only examples are
either extremely complex and powerful, or extremely trivial. But, if it
makes you feel any better, this example is much better than any of the
ones I found in my C programming books, and at least I put some thought
into it. Anyway, let's start the analysis.
typedef union {
char date[25];
struct {
short int month;
short int day;
} dayValues;
} DAY;
The first thing to take a look at is the definition of the union we will
be using. Remember that unions can be used exactly like structures,
except that we can only use one of their variables. In this case, we can
either use the character array, or the dayValues structure. I don't
like using the union and struct keywords, so I always type define my
structures and unions.
enum {JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC};
Here we define our enumeration. The constants are the month
abbreviations, with JAN being month 0, and DEC being month 11 (1 - 12).
I don't really see the need for a name, since we will be using the
constants directly. Okay, now let's take a look at the _main() function.
DAY days[7];
Here is our DAY union array. We could have allocated it dynamically, but
there is nothing wrong with doing it this way, and I don't want you to
forget that we have other methods than dynamic memory allocation for
using memory. Remember that we use unions just like we use structures.
// pick 7 random days in the year 2002
pickDays(days);
// convert the numeric dates to a string
convertDays(days);
// print our randomly selected days
printDays(days);
The three functions we use in this program are pickDays(),
convertDays(), and printDays(). We choose 7 random days in the
pickDays() function. We convert the numeric values we randomly selected
into a date string in the convertDays() function. Finally, we print our
date strings in the printDays() function.
void pickDays(DAY *days) {
int loop, randNum;
for (loop = 0; loop < 7; loop++) {
// select a random month
randNum = rand() % 12;
days[loop].dayValues.month = randNum;
// get a random day taking into account the max number of
// days in the random month we just selected
switch (randNum) {
case JAN:
case MAR:
case MAY:
case JUL:
case AUG:
case OCT:
case DEC:
randNum = rand() % 31;
break;
case FEB:
randNum = rand() % 28;
break;
case APR:
case JUN:
case SEP:
case NOV:
randNum = rand() % 30;
break;
}
// assign our randomly selected day to our structure
days[loop].dayValues.day = randNum + 1;
}
}
This function is not difficult. We loop through the union array and
select a random month number and random day of that month. To determine
how many days are in each month, we use the enumeration to see which
month we selected, and select from the appropriate random number range,
either 0-30, 0-27, or 0-29. To that number, we add one to adjust the
date to something printable. I hope you remember how to use the switch
statement. If not, take a look back at lesson 4 where we introduced
them.
Although we cannot use more than one variable inside the union at a
time, we can group variables using structures and access all the pieces
of a structure inside the union, because a structure is considered as
one piece. This is one of the advantages of using structures.
void convertDays(DAY *days) {
int loop;
short int day, month;
const char *monthnames[] = {"January","February","March","April","May","June",
"July","August","September","October",
"November","December"};
// loop through the days and assign a date string value
for (loop = 0; loop < 7; loop++) {
day = days[loop].dayValues.day;
month = days[loop].dayValues.month;
sprintf(days[loop].date,"%s %d, %d",monthnames[month],day,YEAR);
}
}
The convert days function is just as easy as the pickDays() function.
We loop through the union array and create a date string based on the
integer values stored in the union's internal structure. It is a good
idea to make a copy of the month and day variables from our structure
before we run the sprintf() function, because it's possible we would
start overwriting them when copying the month name before we got to the
day value. So, we make a copy of the month and day, then create a string
of MONTH NAME DAY, YEAR. The year is a constant defined to be 2002.
Remember that once we write the date string, we have overwritten and
lost the values contained in the dayValues structure.
void printDays(DAY *days) {
int loop;
// clear the screen
clrscr();
// print each day in our days array
for (loop = 0; loop < 7; loop++) {
printf("%s\n",days[loop].date);
}
}
This function is so simple, it doesn't even really need explanation. All
we do is clear the screen, and print the date strings by looping through
the day union array. Since the _main() method pauses the program before
exiting, we don't need to do that here.
Remember that since we have the date string in use, the dayValues member
variables are gone. They have been overwritten.
Well, I know that wasn't the most enthralling, or even useful example
program, but I think it demonstrated the concept. We will revisit these
concepts later when we are doing more complex programs. For now, just
keep these concepts in mind, because we will revisit them and use them
in much more detail later.
Step 10 - Conclusion
We learned some very useful concepts today. We also learned about unions
and enumerations. I'm kidding of course. Unions and enumerations are
very useful, even if it doesn't seem so right now. They are reserved for
tasks that are a bit more advanced than a simple example can cover.
You have been using enumerations since lesson 2. The KEY_XX constants
you have been using to check for key presses are all defined in
enumerations. They are located in the kbd.h file which is included
automatically by including the tigcclib.h include header which we have
been using for all our programs thus far.
You should be able to see how useful structures can be in programs you
want to write right now. Unions and enumerations will become more
important later on. Enumerations are very useful for defining arbitrary
constant names, which you have probably already had need of, so you may
be able to replace some of the things you are writing in your own
programs with them already. They are a little less bulky than #define
constants.
Have fun with structures. You are well on your way to becoming an expert
C programmer. But experience is the key. Try to do things you are
interested in. Break the program into small parts and write them as
functions. Then string all the functions together. That's what I do.
Lesson 7: Structures, Unions, and Enumerations
Questions or Comments? Feel free to
contact us.
|