TIGCC Programming Lessons
The following is part of a series of lessons designed to help teach
people to program in C for the TI-89, 92+, and V200 calculators
using the TIGCC development environment.
If you wish, you can download the program source code, project
files, and binaries
here.
Lesson 7: Structures, Unions, and Enumerations
Step 1 - An Introduction to Structures
In nearly every programming language, it becomes necessary to group data
together to form larger constructs. We do this for many reasons, but
mainly because it's easier for us to think of a group of information as
part of a larger item. For example, when you think of someone's date of
birth, you are really thinking of at least 3 numbers, the day of the
month, the month, and the year. But encapsulating all this data into a
single place, we can start thinking of it as a single unit, and that's
very advantageous over keeping track of variables that are related by
yourself.
So, now that we have some belief that it's a good idea to put data
members together, how do we go about accomplishing this task? Easy! C
has a built-in method for working with sets of data, and they are called
structures. Structures, as the name implies, are containers for varying
kinds of data. You can put any kind of data within a structure:
integers, characters, pointers, arrays, even other structures if you
like. Let's take a look at a small example to see how structures work:
struct dob {
int month;
int day;
int year;
};
struct dob birthday;
birthday.month = 6;
birthday.day = 2;
birthday.year = 1973;
This may look at little complicated at first, but trust me, once we get
to the explanation, everything will become very simple. So, let's take a
look at our various parts. All structures have 2 parts, the definition
and the declaration. Once they are defined and declared, we can use
them. So, let's examine them piece by piece. I highlighted the various
parts in different colors. The definition is in red, the declaration is
in blue, and the usage is in teal. We'll start with the definition,
since we must first define a structure before we can declare it or use
it.
Structures are defined using the struct keyword. That keyword is
followed by the name of the structure. The name of a structure is just
like a variable type name, like int or char, so it must be unique. The
name of the structure we defined is called 'dob', short for date of
birth. Once we have the name of the structure, we need to open a brace
{ to enclose the members of the structure. This is where we put all the
variables we will have as members of the structure. We use the same
procedure for declaring variables inside a structure as we do for
declaring variables in a function, except we don't initialize any of the
values. This is because these variables do not actually exist yet,
because we haven't created any structures yet, we're only telling the
compiler how to create a structure so that when we tell it to build one,
it will know how. Finally, when we are finished declaring the members of
the structure, we close the brace } and signify the end with a
semi-colon ;. The semi-colon is very important, for reasons we will
discuss later on.
Now that we have defined a structure, the compiler will know how to
build one when we ask it to. To actually declare a structure, like we
declare a variable, we use the struct keyword again with the name of the
structure (so the compiler knows which structure to create), and then
give it a variable name. So, in the blue above, we used struct dob to
tell the compiler we want it to create a dob structure, and then give it
a variable name we can use to access the structure, which we called
birthday. This is just like a standard variable declaration (like int or
char), except we need to add the struct keyword because the compiler
won't know what a 'dob' is without it.
Finally, we get down to the how of structures: How do we access the
members of a structure? Well, that's easy. C has a built-in operator,
called the dot operator. The dot operator is just that, a dot, or more
properly, a period. So, we use the name of the structure (the variable
name, not the structure name), followed by the dot operator, and then
the name of the variable structure member we want to access. This is
just like using any other variable, so you can do anything with a
structure member that you can do with a variable.
See, structures are easy! So let's take it a step further and work
through a small example program.
Step 2 - Using Structures in C
Start TIGCC and create a new project. Create a new C Source File named
struct. Modify the file so that it looks like this:
struct.c
#include <tigcclib.h>
struct dob {
int month, day, year;
};
struct Person {
char name[20];
int height;
struct dob birthday;
};
void printPerson(struct Person person) {
// clear the screen
clrscr();
// print the structure information
printf("Name: %s\n", person.name);
printf("Height: %d inches\n", person.height);
printf("Birthday: %d-%d-%d\n", person.birthday.month,
person.birthday.day, person.birthday.year);
// wait for user to press a key to see the results
ngetchx();
}
void _main(void) {
struct Person person;
short int result = 1;
const char *title = "DMA Error", *error = "Not enough free memory";
char *input = NULL;
HANDLE dlg = H_NULL;
// allocate memory for the input buffer
if ((input = (char *)calloc(35,sizeof(char))) == NULL) {
DlgMessage(title,error,BT_OK,BT_NONE);
return;
}
// allocate memory for the dialog box
if ((dlg = DialogNewSimple(140,85)) == H_NULL) {
DlgMessage(title,error,BT_OK,BT_NONE);
// free input buffer memory before exiting
free(input);
return;
}
// design the dialog box
DialogAddTitle(dlg,"Personal Information",BT_OK,BT_NONE);
DialogAddRequest(dlg,5,15,"Name:",0,20,14);
DialogAddRequest(dlg,5,25,"Height (inches):",21,2,3);
DialogAddRequest(dlg,5,35,"Birth Month:",24,2,3);
DialogAddRequest(dlg,5,45,"Day of Month:",27,2,3);
DialogAddRequest(dlg,5,55,"Birth Year:",30,4,5);
// prompt the user for dialog input
while (result != KEY_ENTER && result > 0) {
result = DialogDo(dlg,CENTER,CENTER,input,NULL);
}
// free the memory used by the dialog box
HeapFree(dlg);
// copy information from the input buffer
sprintf(person.name,"%s",input);
person.height = atoi(input+21);
person.birthday.month = atoi(input+24);
person.birthday.day = atoi(input+27);
person.birthday.year = atoi(input+30);
// free the memory used by the input buffer
free(input);
// print the structure information
printPerson(person);
}
Step 2a - Compile and Run the Program
Save the program source and build the project. Send the program to TiEmu
and run it. It will look something like this:
Step 2b - Program Analysis
We are getting past the point of writing completely trivial programs.
The programs we start writing now will be a little more complex, but you
should be reaching the level where you no longer need explanations for
most of the code we write. If you haven't started yet, it would be a
really good idea to start taking at look at the TIGCC library
documentation. It has tons of useful information in it, and any function
that I would use in a program that we haven't already seen is probably
documented there.
struct Person person;
short int result = 1;
const char *title = "DMA Error", *error = "Not enough free memory";
char *input = NULL;
HANDLE dlg = H_NULL;
Our variable declaration section is quite diverse this time. Integers,
character arrays with and without string literals, dialog handles, and
of course structures. The only thing of note in this list is the
structure defined at the time. Recall from our little intro above that
we declare structures with the keyword struct, the name of the
structure, and the variable name we want to use. If we haven't mentioned
it before, we should now: C is a case sensitive language. This means
that 'person' and 'Person' are two different things to the C compiler,
so we can name our struct Person and name our variable person and the C
compiler will have no trouble differentiating between them.
// allocate memory for the input buffer
if ((input = (char *)calloc(35,sizeof(char))) == NULL) {
DlgMessage(title,error,BT_OK,BT_NONE);
return;
}
// allocate memory for the dialog box
if ((dlg = DialogNewSimple(140,85)) == H_NULL) {
DlgMessage(title,error,BT_OK,BT_NONE);
// free input buffer memory before exiting
free(input);
return;
}
I hope you are getting used to dynamic memory allocation by now. It's a
very powerful tool when used properly. We probably went slightly
overboard with the small character array we have, but it's never a bad
idea to try and save memory.
Remember that in allocating space for a dialog box, we use the
DialogNewSimple() function, and instead of checking to see if it
returned NULL, we make sure it didn't return H_NULL, or HANDLE NULL.
Remember that if we allocate the memory for the input array, but cannot
allocate the dialog box, we need to free the input array before we exit
the program.
// design the dialog box
DialogAddTitle(dlg,"Personal Information",BT_OK,BT_NONE);
DialogAddRequest(dlg,5,15,"Name:",0,20,14);
DialogAddRequest(dlg,5,25,"Height (inches):",21,2,3);
DialogAddRequest(dlg,5,35,"Birth Month:",24,2,3);
DialogAddRequest(dlg,5,45,"Day of Month:",27,2,3);
DialogAddRequest(dlg,5,55,"Birth Year:",30,4,5);
Remember from lesson 6, dialog boxes need to be created, designed, and
then displayed. We only had a single input box on the last dialog box,
but this demonstrates how we can have more than one request box using
the offset parameter we talked about before. Remember that we always
need one more character than the maximum length of a string to account
for the terminator. Also keep in mind that the AMS dialog boxes can only
input strings, they can't input integers or other kinds of data, so we
will need to convert our strings to integers later.
It's not too hard to see how the offset parameter works. Note that for
the first input box, the offset is 0. This is because we are going to
add data to the beginning of our input string. Now see that the next
field has an offset of the max length + 1, or 21. We repeat this process
going down the request boxes, adding the max length + 1 to the following
offset field, plus the offset position of the current field. So the
first field is 0 with a max length of 20, so the next field will be max
length of the previous field + 1 plus the previous field offset, or 20 +
1 + 0, or 21. So, our offset here is 21, and our max length is 2, so the
next fields offset should be 2 (max length) + 1 (terminator) + 21
(offset), or 24. I think you can see the mathematical pattern.
The last parameter, the width just shows us how many characters can be
displayed in the text field at once. The first field was chosen fairly
arbitrarily (I took the value from the TIGCC library docs), the rest of
the values are one character more in length than the max length of the
field, so we can see all the characters at once.
// prompt the user for dialog input
while (result != KEY_ENTER && result > 0) {
result = DialogDo(dlg,CENTER,CENTER,input,NULL);
}
Last time we did dialog boxes, I had the while loop keep looping until
we got the ENTER key returned, but I said that was a bad idea, because
if there was insufficient memory to display the dialog box, and an error
was returned, that would create an infinite loop. So, instead of that,
this time we check to see either if we got the enter key returned, or if
an error was returned. An error return is any number less than zero.
Remember from our variable declaration section, we defined the result to
be 1 initially, so both of these conditions will be false initially, so
the while loop will happen at least once.
// copy information from the input buffer
sprintf(person.name,"%s",input);
The next part of our code assigns values to our structure. We talked
about the sprintf() function briefly in lesson 5 when we created a
limited facsimile of it, but I think this may be the first time we have
used the function. Remember that sprintf() is like printf(), but instead
of printing to the screen, we print to a string. In this case, we are
printing to the name member of our person structure. Since we cannot
copy an array using the assignment operator =, we must copy the array
character by character. This is where the sprintf() function can help us
out, by copying our input string (until it reaches the terminator of the
first string) to our name member.
person.height = atoi(input+21);
person.birthday.month = atoi(input+24);
person.birthday.day = atoi(input+27);
person.birthday.year = atoi(input+30);
The next set of assignments are a little less complex. Since they are
integers, we can use the assignment operator, but we must convert the
strings to integers. Remember that the dialog request box can only input
string values, so to get integers, we need to convert to integers. To
convert a string to an integer, we use the atoi() function, which I
believe is short for array to integer, but don't quote me on that. In
any event, the atoi() function converts a string (character array) to an
integer value, and sends that integer value back as its return type.
Keep in mind since the strings we are looking for are not at the
beginning of the string, we need to use pointer arithmetic to find the
correct place. Remember the offsets we used above when declaring the
request boxes? These are the values we need to increment by to find the
correct place to start the conversion.
Remember that input + 21 is the beginning of the 21st character in the
array, so by advancing by the offset, we start looking at the proper
string. The Request Box takes care of adding the terminating character
for us, but since we used calloc(), all the non-used spaces are zero
anyway, and zero is the terminating character.
The other thing to notice here is that we used a structure within our
person structure. We defined a date of birth (dob) structure inside our
Person structure. So, to get to the individual data members of the inner
structure, we just use the dot operator twice. There is no special
trick, because once we use the dot operator the first time, we treat it
just like any normal variable.
// free the memory used by the input buffer
free(input);
Up until now, we have always freed our memory at the very end of our
_main() method. While there is nothing wrong with this approach, the
idea is to share the memory between the other programs and other parts
of your own program, so to be a good memory manager, you should really
free up memory as soon as you are no longer using it. But, so long as
you free it before you exit the program, you will probably be okay. It's
just good programming practice.
// print the structure information
printPerson(person);
The last part of our program calls the printPerson() function which we
defined to print the data members of the structure. We could have
embedded this in our _main() method, but it's really something we should
abstract into a function. This is just one of the concepts in functional
programming. If you have a task that will be done more than once, it
should probably be a function. If we were writing a real program to deal
with people, a printPerson() function would probably be used more than
once in the course of the program. So let's take a look at the function
real quick.
void printPerson(struct Person person) {
// clear the screen
clrscr();
// print the structure information
printf("Name: %s\n", person.name);
printf("Height: %d inches\n", person.height);
printf("Birthday: %d-%d-%d\n", person.birthday.month,
person.birthday.day, person.birthday.year);
// wait for user to press a key to see the results
ngetchx();
}
This part of the program is very straight forward, we just print the
data members using printf() functions.
I think you can see where structures would be very useful when creating
larger programs. The capability to combine several smaller data members
into larger constructs is very important in C.
Continue the Lesson in Part II
|