TIGCC Programming Lessons
Lesson 9: Advanced Pointers
Step 4 - Type Casting
A very powerful feature of C, which we have largely glossed over, is
very useful when considering advanced pointers. Type casting is the
ability to convert between different type variables. So, we could
convert a long int into a short int (with some loss of data, naturally),
or we could convert a float to an integer (with a loss of the decimal
place of course), and to avoid warnings about signed and unsigned
comparisons, we can cast unsigned values to signed values (which will
place a different interpretation on the value).
So, how do we cast a variable to another type? Simple, simply put the
new type in parentheses before the variable, and it will be cast to the
new type. So, if we had a short int sInt = 780, but we needed a long
int, we simply cast it. (long int)sInt; The value represented with sInt
is now a long integer. This is mainly useful when we are expecting a
variable of a certain type, but we don't have that type. We can convert
pretty much anything to anything else, but some casts are more useful
than others. You will quickly learn that type casts are a C programmer's
best friend.
There is a lot of implicit type casting present in C as well. Smaller
integral types such as char can be implicitly promoted to an int or a
long int. For example, it is perfectly valid to say char x = '0'; int i
= x; We didn't need a cast because there is no damage done here. If we
were to go the other way however, int i = 257; char x = i; The compiler
will give us a warning about loss of precision. This is because a char
type cannot hold the value 257. We can ignore this loss with an explicit
type cast, as in int i = 257; char x = (char)i; However, x will have the
value 1 since we have lost the upper byte of the integer.
Now, the reason this is important for pointers is because of a special
kind of pointer. There are pointers of type int, and type char, and type
float, etc. However, there is one kind of pointer that does not have a
type, and can be used to represent pointers of any type. This is known
as a void pointer. Any type of pointer can be cast to a void pointer,
and conversely, any void pointer can be cast to a pointer of a type.
However, void pointers are useful for when we need a block of data, but
don't know how that data will be interpreted. Remember from lesson 6
that malloc() returns a void pointer, though we didn't go much into
depth about void pointers at that time. Since malloc() (and calloc() and
realloc()) did not know how you would use the memory they allocated for
you, they returned a void pointer to the memory block. This was done so
we could cast the pointer to the type we actually needed. Another good
use for void pointers is when it's not important what type of data we
have. If we only need a block of data, and have no need of knowing what
kind of data it is, we can simply use a void pointer. This helps us
avoid using separate functions to handle separate types of data. The
fread() and fwrite() functions are two examples of functions that take
blocks of data without knowing what kind of data they are. This is
because the only purpose is to read or write that data from/to a file.
It does not matter what the data is, because they don't have to
interpret it, they just put it somewhere. This will be nice in the next
segment we will cover, which depends upon void pointers heavily.
To cast a void pointer to a pointer of a type, simply use the standard
cast notation. char *ptr; void *vptr; ptr = (char *)vptr; One note on
void pointers though. They implicitly cast to whatever type is provided.
So, we could have just done ptr = vptr, and vice versa, if we wanted to
assign the character pointer to the void pointer, vptr = ptr.
Step 5 - Pointers to Functions
A seldom used but very interesting topic in pointers is pointers to
functions. You may think that pointers are only for variables, but
that's not actually so. A pointer is any memory address, so a pointer to
a function is a pointer to the address at the start of the function.
This is actually how functions are called, by having the program jump to
the address where the function starts, then return when the function
ends, or processes a return call.
So, how do we create a pointer to a function? Simple, we just need a
type, a name, and an argument list. So, assume we have a function short
int mod(short int numerator, short int divisor). To create pointer to
this function, we simply create it like so:
type (*name)(args); // replace type, name, and args
short int (*funcptr)(short int numerator, short int divisor);
However, this is only the declaration, not the implementation. If you
tried to call the function funcptr(), the program would crash. So, we
need to store the address of the function into our pointer. To do that,
just use the assignment operator:
funcptr = mod; // sans ()'s, functions are pointers inherently...
Now, we could call funcptr() just like a real function, and it would
have the same effect as calling the mod() function.
This may not seem particularly useful, but it can be helpful in
specialized ways, like passing a function as an argument to another
function. Consider a sorting algorithm. To sort a list of elements, we
need some way of comparing the elements relative to each other. However,
there are many ways to compare two items. So, how can a general purpose
sorting algorithm be defined? Using pointers to functions. We don't need
to know how to compare two values, if the user can give us a function
that gives us a value for comparing two items. So, if an item is
analytically 'higher' than another item, it might return a positive
value, and conversely, an item with is analytically 'lower' than another
item might return a negative value.
Another use for pointers to functions is in event-driven programming. In
event-driven programming, a function gets called when some event
happens. Often this is the result of a timer, but this is not the only
possible event. We may cover event-driven programming in more detail in
a future lesson.
Step 6 - Using the qsort() General Purpose Sorting Algorithm
Start TIGCC and begin a new project. Create a new C file named qsort.
Modify the file so that it looks like this:
qsort.c
#include <tigcclib.h>
long int compare(const void *value1, const void *value2) {
return (*(short int *)value1 - *(short int *)value2);
}
void printList(short int *list) {
short int loop, loop2;
// display the separator
printf("-----------------------\n");
// print the integer list
for (loop = 0; loop < 5; loop++) {
// indent the list
printf("| ");
for (loop2 = 0; loop2 < 5; loop2++) {
// display the integers
printf("%hd ",*(list + (5 * loop) + loop2));
}
// skip to the next line
printf("|\n");
}
// put the final line separator
printf("-----------------------\n\n");
}
void fillList(short int *list) {
short int loop;
for (loop = 0; loop < 25; loop++) {
*(list + loop) = (rand() % 900) + 100; // find numbers between 100 and 999
}
}
void _main(void) {
short int list[25];
// seed the random number generator
randomize();
// clear the screen
clrscr();
// get numbers for the list
fillList(list);
// display the title
printf("Random List of Integers\n");
// display the list
printList(list);
// give time for user to see the list
printf("Press any key to sort...\n");
ngetchx();
// sort the list according to compare
qsort(list,25,sizeof(short int),(compare_t)compare);
// screen cleanup
printf("Sorted List of Integers\n\n");
// display the sorted list
printList(list);
// wait for user to press a key before exiting
ngetchx();
}
Step 6a - Compile and Run the Program
Save and build the project, then send it to TiEmu. It will look
something like this:
Step 6b - Program Analysis
Again is it time for the ever-essential program analysis. The program
has a lot of display code to make the output look nice, which is where
most of the code is. Skipping over that, let's start the analysis. We'll
begin with the fillList() function.
void fillList(short int *list) {
short int loop;
for (loop = 0; loop < 25; loop++) {
*(list + loop) = (rand() % 900) + 100; // find numbers between 100 and 999
}
}
Okay, the fillList() function's job is surely obvious. We need a list of
25 integers to sort, and rather than give a static list, I decided to
make the computer generate them, if only to prove to you that the qsort
function will work on any set of numbers.
Since we just covered pointer arithmetic, I decided to use it here in
our list fill, instead of list[loop], we used *(list + loop), which we
know is the same thing.
Now, the generated random number. Well, it works best if the numbers are
all the same length (i.e. 2 digits or 3 digits, etc). It makes the
output look good, and since 5 sets of 3-digit numbers nearly fills the
screen on the TI-89, I decided to use that. Now, to make sure we have
three digit numbers, we need to get numbers from 100-999. So, rand()
generates a really really big number, and by performing modulus
with 900, we get a number between 0 and 899, so just add 100 to that and
we have the numbers from 100-999. So, now we have a nice list of 3-digit
numbers.
Continuing in the analysis, the printList() function is next.
void printList(short int *list) {
short int loop, loop2;
// display the separator
printf("-----------------------\n");
// print the integer list
for (loop = 0; loop < 5; loop++) {
// indent the list
printf("| ");
for (loop2 = 0; loop2 < 5; loop2++) {
// display the integers
printf("%hd ",*(list + (5 * loop) + loop2));
}
// skip to the next line
printf("|\n");
}
// put the final line separator
printf("-----------------------\n\n");
}
This function is fairly straightforward, but I again chose to use
pointer arithmetic, since it just seems like a good way to reinforce the
concept, though you would probably use array notation here.
Basically, we have two loops here. The outer loop runs through 5 sets of
5 integers, so we can display the list in 5 rows instead of 25, which is
too many to fit on the screen. We also use some characters to box in the
list, so it looks nice, but let's look at the pointer arithmetic, as
it's the most relevant. I think everything else should be familiar to
you.
So, to find a value, we use *(list + (position - 1)). In this case, we
have two loops governing the position, the first loop is one of our sets
of 5. The second loop is which member of the set we are working on. So,
if we want position 18, that's set 4, member 3. So, (5 * 3) + 2 = 17, or
position 18 (remember, start counting at 0. Set 4 = 3, Member 3 = 2.
array[17] = 18th element). This should be getting old hat by now. Just
carry out the arithmetic in your head. list + (5 * 0) + 0 is list, the
first element. So, the first time through the loops, we get the first
element, then it's list + (5 * 0) + 1, or the 2nd element, and so on.
Anyway, this should not be hard for you by now. So, let's take a look at
the compare() function.
long int compare(const void *value1, const void *value2) {
return (*(short int *)value1 - *(short int *)value2);
}
The compare function returns a long integer value that tells the qsort()
function how to treat the values. If value 1 is less than value 2, a
negative value is returned. If value 1 is greater than value 2, a
positive value is returned. If they are equal, 0 is returned. So, let's
talk about how the compare function works. First, the qsort() wants a
pointer to a function whose arguments are constant void pointers. This
is done so any type of value can be used (void pointers can be anything,
remember). So, to get a short integer from the pointer, we first need to
cast the value to a short int pointer (we can't cast a void pointer to a
value directly, because you cannot dereference a void pointer, since the
type is unknown). So, after converting the void pointer into a short
integer pointer, we can dereference that pointer to get the value. So,
this is like performing (x - y), where x and y are short integers. Now,
qsort() puts values that return negative towards the front, and positive
values towards the back. Very simple, no?
Now, the final segment of the program is to use the qsort() function:
// sort the list according to compare
qsort(list,25,sizeof(short int),(compare_t)compare);
qsort() takes an array, the number of elements in the array, the size of
one element, and the comparison function. It is defined like this:
void qsort(void *array, unsigned long int elements, unsigned long int size,
long int (*compare)(void *value1, void *value2));
The important part is how the pointer to the function is defined. A
comparison function that takes two void pointer values (which is up to
the compare function to interpret) and returns a long integer value
based on the two values.
After we reprint the list, you'll see that the list is in perfect
ascending order. This is because values return negative when they are
less than the compare value. If we wanted to perform a descending sort,
we would simply need to reverse the value 1 and 2 in the subtraction.
Step 7 - Conclusions
Advanced pointers are nothing you haven't already dealt with in the
course of these lessons, and probably your independent programming
projects as well. People need multi-dimensional arrays, and now that you
know pointer arithmetic, you will use it. Trust me. Type casting, though
we have used it extensively in the past, will become an even more
important part of your C programming toolkit. This lesson may have
seemed short or pointless, but it addresses the cornerstone of C
programming, and it's not something to be taken lightly. These are the
most used concepts in C programming, I guarantee it, with the exception
of pointers to functions which aren't used that often. Pointers and type
casting are among the most important concepts in C, if not the most
important. Be prepared, you are now ready to take the step beyond simple
programming skills, and advance to a degree of real programming skill.
Lesson 9: Advanced Pointers
Questions or Comments? Feel free to
contact us.
|