TIGCC Programming Lessons
Lesson 6 - Dynamic Memory Allocation
Step 4 - More DMA Functions
Although we will primarily use malloc() and free() to allocate and free
our memory, respectively, there are other functions for allocating
memory. The other functions we will use are calloc() and realloc().
While realloc() is probably self-explanatory, we will go over it first.
The realloc() function is used to reallocate a block of memory from a
pointer we already have. But in this case reallocate does not mean to
give us the memory again, but rather to resize the block of memory we
already allocated, either larger or smaller. This is good for when you
do not know the exact size of what you need upfront. You can allocate a
large block, and then resize it down to a smaller block once you know
how much space you will actually need. For example, if you want to ask
the user to enter his or her name, you could allocate a 80 character
string to start, and if the person's name only takes 25 characters, you
can resize it down to 25 characters once the user has finished entering
their name.
It is to your advantage to use only as much memory as you need. This
will make sure you have as much memory as you need for other parts of
your program to use. If you allocate a big block of memory up front,
then there is no more memory for other purposes in the program. This is
an especially important concept in a platform that has so little
available memory.
The syntax for realloc() is very simple, void *realloc(void *ptr, long
int newSize). So, we supply the pointer to the current memory block we
have, and the size we want our new block to me. Now, because we are
resizing the block, our pointer may change. This means we have to
reassign the pointer to a new address returned by the realloc function.
So, to give a short example of how it would be used:
char *str = (char *)malloc(100 * sizeof(char)); // allocate space for a 100 character string
if (str != NULL) {
str = (char *)realloc(str, 30 * sizeof(char)); // reallocate string for 30 characters
}
Okay. We allocated a 100 character string, then we resized it down to 30
characters. So, now we have 70 bytes more memory to use in our program
elsewhere. However, do not forget that we still need to free our pointer
before we exit the program, but we do not need to free the pointer before
reallocation.
I think the use of realloc() is pretty self evident. Remember that an 80
char array embedded into a program takes 80 bytes of memory, but a
pointer to a null value which will be allocated an 80 character array
from dynamic memory takes only 4 bytes (pointers for 32-bit systems are
4 bytes). And the best part is that, if we only need 30 characters
later, we can just resize it.
Okay, we have seen the uses for the realloc() function, so what about
this calloc() function? Well, it is another useful function, and it is
useful for many reasons. First of all, calloc() allocates memory in
block sizes. What I mean here is that instead of allocating a certain
number of bytes of memory like malloc() does, we allocate a certain
number of memory blocks of a certain size. So, instead of allocating
30 * sizeof(int) bytes of memory like we do with malloc(), we can
allocate the block of memory that is sizeof(int) 30 times. This is
sometimes easier than thinking about the multiplication we need to with
malloc(), especially if we allocate space for multi-dimensional arrays
(a topic we haven't covered yet, but will someday).
If you can handle the simple mathematics required by malloc(), then
calloc() probably doesn't seem like it has much, if any advantage to it,
but there is one more thing that calloc() does for us which is nice:
initialization. When malloc() allocates a memory block for us, it just
leaves whatever was in there before right there in the block. calloc()
on the other hand sets all the allocated memory to 0, which is often
very handy.
int *block = NULL; // we should really always initialize our pointers to NULL
if ((block = (int *)calloc(50, sizeof(int))) == NULL) {
DlgMessage("DMA Failure","Unable to allocate space for our pointer",BT_OK,BT_NONE);
return;
}
Okay, but what does calloc() do? Well, that's simple, calloc takes the
size of the integer (sizeof(int)), and gives us 50 blocks of memory that
are the size of an integer. Effectively, we just allocated space for a
50 integer array. Now, we also initialized all of our integers to 0.
This is part of what calloc() does for us. This is the nice thing about
calloc().
Step 5 - Using calloc() and realloc()
Start TIGCC and begin a new project. Create a new C Source File named
alloc. Modify the file so that it looks like this:
#include <tigcclib.h>
void _main(void) {
char *string = NULL; // we should always initialize our pointers to NULL
const char *error = "DMA Failure";
short int result = 0;
HANDLE dlg = H_NULL;
// allocate the string
if ((string = (char *)calloc(120,sizeof(char))) == NULL) {
DlgMessage(error,"Unable to allocate space for our string",BT_OK,BT_NONE);
return;
}
// create a 140x45 pixel dialog box
if ((dlg = DialogNewSimple(140,45)) == H_NULL) {
DlgMessage(error,"Unable to create dialog input box",BT_OK,BT_NONE);
// release memory from string before exiting the program
free(string);
return;
}
// add the title of the dialog box
DialogAddTitle(dlg,"Enter Your Name",BT_OK,BT_NONE);
// add the input box to the dialog
DialogAddRequest(dlg,5,15,"Name:",0,119,14);
// input the user's name with the dialog box
while ((result = DialogDo(dlg, CENTER, CENTER, string, NULL)) != KEY_ENTER);
// free the memory used by the dialog box
HeapFree(dlg);
// reallocate the string buffer for just enough characters
// add one character for the string terminator
if ((string = realloc(string,(strlen(string) + 1) * sizeof(char))) == NULL) {
DlgMessage(error,"Unable to resize string!",BT_OK,BT_NONE);
return;
}
// clear the screen
ClrScr();
// draw the users name
DrawStr(0,0,"User Name: ",A_NORMAL);
DrawStr(65,0,string,A_NORMAL);
// print the exit message
DrawStr(0,20,"Press any key to exit...",A_NORMAL);
// free the memory used by the string
free(string);
// wait for user input before exiting program...
ngetchx();
}
Step 5a - Compile and Run the Program
Save the project and the source file. Build the project and send it to
TiEmu. It will look something like this:
Step 5b - Program Analysis
Time for another program analysis, as usual.
char *string = NULL; // we should always initialize our pointers to NULL
const char *error = "DMA Failure";
short int result = 0;
HANDLE dlg = H_NULL;
Our variable section has some interesting items on it this time. Our
string pointer, which will be used to allocate space for our string
storage later. It never hurts to initalize your variables.
We have another character pointer, this time to a string literal "DMA
Failure". Since we are going to allocate more than one block of memory,
and since either of the allocations could fail, we should use error
messages to warn the user. But remember that when we embed a string
literal inside a function, it is stored in memory for each place we use
it. So, if we hadn't used the error string pointer to our string
literal, but instead had just embedded them inside our dialog error
messages, it would take more memory. It takes 12 bytes to store the
string DMA Failure. So, to use it twice would take 24 bytes total. But
to store the string once and use a pointer, it only takes 12 bytes +
the 4 bytes used by the pointer, or 16 bytes. These kinds of memory
savings may seem trivial in a little example program, but if you start
using them from the beginning, you will remember to use them when memory
savings become important. And as I have said before, and without much
doubt will say again, the TI-89/92+/V200 is a very limited platform with
very limited memory. It is very likely you will run out of memory some
time. This is just a fact of the platform. This is why dynamic memory
allocation is a good thing, so we can share the memory we have between
all the programs that need it, and then return it when we are done.
Cooperation is better than competition. Anyway, the basic idea here is
you want to keep track of the memory you use, so you can make sure to
use as little as possible wherever possible. This is what we have done
here.
Now we have two other variables. The first one is a short integer which
we will use to store a result. The second one is a HANDLE type variable.
A HANDLE is a special name for an unsigned short defined for use with
dialog boxes and other structures. C has a special way of creating new
datatypes from old ones, which we will probably talk about in some
future lesson. For now, it is enough to know that a HANDLE is a special
data type we use to keep track of dialog boxes we create. It is not
important to know that it is an unsigned short, and you really shouldn't
use unsigned short as a replacement for HANDLE, because HANDLE might
change its definition. This is why we have pseudo-datatypes like this.
We initialize the handle to H_NULL, which is also 0, just like NULL for
pointers. This is just a way of saying we don't have a dialog associated
with this handle right now.
This lesson isn't really about HANDLES or dialog boxes, but I thought it
would be a good idea to start using some of the AMS functions for added
capability inside your programs. Dialogs are one of the nicest things
the AMS provides for us, and we might as well take advantage of them.
// allocate the string
if ((string = (char *)calloc(120,sizeof(char))) == NULL) {
DlgMessage(error,"Unable to allocate space for our string",BT_OK,BT_NONE);
return;
}
Here is where we allocate the string, and use the calloc() function for
the first time. Remember that calloc() allocates a given number of a
certain size memory block, in opposition to malloc() which always
allocates a certain number of bytes. So, in this example, we allocate
120 characters, which is also 120 bytes, but this is just to ensure we
get the right number of variables we wanted. Remember that we need to
cast our void pointer to a character pointer using the type cast
operator (char *). Lastly, we always need to check to make sure we got
the amount of memory we requested. So, compare the result with NULL,
and if it's equal, display an error message.
// create a 140x45 pixel dialog box
if ((dlg = DialogNewSimple(140,45)) == H_NULL) {
DlgMessage(error,"Unable to create dialog input box",BT_OK,BT_NONE);
// release memory from string before exiting the program
free(string);
return;
}
Now we need to do another kind of dynamic memory allocation. All dialog
boxes are blocks of memory themselves, and they do have pointers, but
the AMS manages them through a system of handles rather than pointers.
If we want to create a dialog box, we should also check to make sure we
got the memory we needed for it. We can do that by comparing the result
of our DialogNewSimple() function (create a new simple, i.e. blank,
dialog box) with the H_NULL handle. If the resulting handle is the null
handle, then we don't have the resources required to create the dialog.
I am not sure what would happen in this case, because if we couldn't
create a simple dialog, then we probably wouldn't be able to display our
next message in a dialog box, so the program would probably not display
the error message here, but the program should exit before anything bad
happens.
The thing we need to remember here is that we already allocated space
for the string, so if we exit the program now, our string buffer memory
will be lost, so we need to make sure we free the memory here before we
exit the program. Never forget to free every memory block you have
successfully allocated before program termination.
// add the title of the dialog box
DialogAddTitle(dlg,"Enter Your Name",BT_OK,BT_NONE);
These two functions add the parts of the dialog we need before we can
display the dialog box to input the user's name. DialogAddTitle()'s
function should be pretty obvious, it sets the title of the dialog box.
It takes the handle of the dialog box, which remember we called dlg, the
title as a string literal, and also sets the buttons for the dialog. In
this case, ENTER returns an OK status, and there is no other button.
This is the same button pattern we use on DlgMessage() boxes.
// add the input box to the dialog
DialogAddRequest(dlg,5,15,"Name:",0,119,14);
The DialogAddRequest() function is a little more complicated. It takes
several parameters, and its purpose is to put an input box (or a request
box if you will) on the dialog box. It takes the handle of the dialog
box (dlg in our case), the x and y coordinates (relative to the dialog
box -- remember 0,0 is the upper left corner of the dialog box, not the
upper left corner of the screen). Remember that our title will take up
space, so we have to put the request a little bit further down than the
top. I choose 15 pixels down, and 5 pixels of indentation, because it
looked good to me. You'll probably have to play around with your own
dialog boxes to get it to look right. The next parameter is the prompt
displayed before the input field. Generally this should be what the
input field is for. I put Name:, because we want the user to input his
or her name. The last three parameters are the most complex.
The 0 is the buffer offset parameter. Basically, this tells us at what
location relative to the start of our buffer should we start putting the
input of the input box. Since we are using the entire string as our name
variable, we want to start copying the input to the first position in
our buffer, which would be 0. Take a look at the TIGCC documentation for
an example of an offset that wouldn't be 0. I think for most purposes
though, this parameter will be zero. The times when this will not be
zero will usually be when we have more than one request box, which we
will look at in a later lesson.
The 119 is the max length of our string. Remember that we always need one
character at the end for a termination character, so our max length is
never more than 1 less the size of our buffer. Since our buffer is 120
characters, 119 is the max length of a string we can store in the
buffer. I think the AMS won't allow us to enter any more than 119
characters, but it may just cut off the input at 119 characters if you
enter more than that. In any event, I didn't test either case, but rest
assured, it will be taken care of so that only 119 characters can be
used. And who has a name longer than 119 characters anyways.
The last parameter, the 14 is the width of the field. 14 in this case
means 14 characters of input can be displayed inside the input box at
any one time. If more than that are present, the dialog box will scroll
the input for us. This is one of the nicest things about dialog box
functions. It takes care of these little nuances for us. See, dialog
boxes are not that hard, right?
// input the user's name with the dialog box
while ((result = DialogDo(dlg, CENTER, CENTER, string, NULL)) != KEY_ENTER);
Now that we have created our dialog box, we need to actually do
something with it. This is where the DialogDo() function comes in. It
creates, displays, and processes the dialog box until one of the buttons
are pressed which activate closure. In this case, we are waiting for the
user to press ENTER, so we use a while loop to make sure the result
equals enter before we can continue. The DialogDo() function returns the
status of the exit, which is equivalent to what key we assigned the OK
and CANCEL functions to be. Remember that in the DialogAddTitle()
function, we set the OK return value to be the ENTER key, and did not
assign any value to the CANCEL return value. So, in this case, we can
simply check to make sure the user pressed ENTER signifying that the
input is correct.
The DialogDo() function has several parameters, but they are not
complicated. The first parameter, as in all the dialog box functions, is
the dialog handle. The next two arguments determine where on the screen
the dialog box will be placed. For simplicity, we can use the constant
CENTER to mean we want the dialog box to be placed at the CENTER of the
screen. The fourth parameter is the string buffer used to store the
results of the request boxes. The fifth and final parameter is the pull
down menu options, which we won't get into at this time. Since we have
no pull down menus, we just put NULL here to signify that we do not have
any.
So, what does this all mean? Simple. We show the dialog box and ask the
user to enter his name, and we don't stop doing that until the user
presses ENTER to finish the dialog box input. There is a small problem
here when the system is very low on memory. If there is not enough
memory to display the dialog box, the DialogDo() function can return the
value -1 meaning ERROR, so if this happened, we would be stuck in an
infinite loop here and the calculator would appear to lock up. The
correct procedure would be to get the result value, then check it
against either the correct exit value KEY_ENTER, or the ERROR value -1,
then take appropriate action based on what the value was. But this is
just a small example program, so we won't be doing that here.
// free the memory used by the dialog box
HeapFree(dlg);
Remember that we use free() to free up memory allocated by the malloc(),
calloc(), and realloc() functions? Well, to free the memory used by
dialogs, because they are tracked through handles rather than pointers,
we need to use a special function called HeapFree(), which frees memory
associated with a handle rather than a pointer. This is just a minor
distinction from how we normally free memory.
Remember that dialog boxes use dynamic memory too, so we must never
forget to call HeapFree() on all the dialog box handles we allocate.
// reallocate the string buffer for just enough characters
// add one character for the string terminator
if ((string = realloc(string,(strlen(string) + 1) * sizeof(char))) == NULL) {
DlgMessage(error,"Unable to resize string!",BT_OK,BT_NONE);
return;
}
Now we need to illustrate the use of the realloc() function, which I
believe was the point of this lesson until I introduced Dialog Boxes for
some reason. So, to resize our string buffer to the size we need, rather
than that massive 120 character buffer, we can use the realloc()
function. Since we are using strings, it is easy to find out how long
the string is using the strlen() (string length) function. The strlen()
function returns the length (in characters) of the string we give it.
Even though characters are always 1 byte, we will still still multiply
this by the size of a character for consistency. This becomes more
important as we allocate other kinds of data like integers and long
integers which take more than one character to store. But the most
important thing to remember is that the length of a string includes the
string terminator, and a string is useless without its terminator, so
we must add one to the length of the string to get the correct length.
If we did not allocate that extra character, we would cut off our
terminator and the string wouldn't end properly.
// free the memory used by the string
free(string);
The last thing in this program we need to address is to free up the
memory used by the string. To do this, we use the free() function, as we
have always done for alloc based functions. Just remember that this is
the most important thing to remember in dynamic memory allocation,
because memory leaks are very hard to trace, and require a complete
calculator reset to recover from.
Well, that takes us past our analysis.
Step 6 - Conclusions
We learned a very powerful technique in C programming in this lesson.
Dynamic Memory Allocation is one of the building blocks of good
programs. Nearly all good programs need some kind of large memory block,
and its best to allocate that dynamically so we can share it between the
other programs on the calculator and other parts of the program itself.
We have seen three types of memory allocation (4 if you count the Dialog
box memory stuff, but that's pretty limited in the broader context), and
you have seen the uses for all of them. Now it's time to use them in
your own programs. So, in closing, never forget the two most important
principles in Dynamic Memory Allocation, how to allocate it taking into
consideration the size of the variable type you need (multiplying the
number of bytes by the sizeof(var type) operator), and how to free up
the memory you allocate using the free() function. Never never never
never never forget to FREE YOUR MEMORY!
I'll give you two hints about tracking down memory leaks on the
TI-89/92+/V200. The mem screen (2nd-6) shows us all the memory used by
the system, and what its being used by. The system itself requires a lot
of memory. If you check the amount of system memory before you run the
program, run the program, and then check the system memory usage again,
you can find memory leaks. How? Simple, if the system memory used after
you run the program is more than the system memory used before you ran
the program, then you probably have a memory leak. The one other thing
you need to check is the history screen. Because it sometimes takes up
system memory having things on the history screen, so if you see
something that looks like a memory leak, try clearing the history screen
and seeing if the system memory usage drops back to normal. If it
doesn't, then you probably have a memory leak. The only way to fix the
memory leak after the memory has been reserved and the program exits is
to reset the calculator, so try hard not to release programs that leak
memory to the public. People will track you down and kill you probably,
or at least send nasty emails.
Lesson 6: Dynamic Memory Allocation
Questions or Comments? Feel free to
contact us.
|