Basic functions are great, but sometimes you need more.
So far, you’ve focused on the basics, but what if you need even more power and flexibility to achieve what you want? In this chapter, you’ll see how to up your code’s IQ by passing functions as parameters. You’ll find out how to get things sorted with comparator functions. And finally, you’ll discover how to make your code super stretchy with variadic functions.
You’ve used a lot of C functions in the book so far, but the truth is that there are still some ways to make your C functions a lot more powerful. If you know how to use them correctly, C functions can make your code do more things but without writing a lot more code.
To see how this works, let’s look at an example. Imagine you have an array of strings that you want to filter down, displaying some strings and not displaying others:
int NUM_ADS = 7; char *ADS[] = { "William: SBM GSOH likes sports, TV, dining", "Matt: SWM NS likes art, movies, theater", "Luis: SLM ND likes books, theater, art", "Mike: DWM DS likes trucks, sports and bieber", "Peter: SAM likes chess, working out and art", "Josh: SJM likes sports, movies and theater", "Jed: DBM likes theater, books and dining" };
Let’s write some code that uses string functions to filter this array down.
What you need is some way of passing the code for the test to the find()
function. If you had some
way of wrapping up a piece of code and handing that code to the
function, it would be like passing the find()
function a testing
machine that it could apply to each piece of data.
This means the bulk of the find()
function would stay exactly the same. It would still contain the
code to check each element in an array and display the same kind of
output. But the test it applies against each element in the array would
be done by the code that you pass to it.
Imagine you take our original search condition and rewrite it as a function:
int sports_no_bieber(char *s) { return strstr(s, "sports") && !strstr(s, "bieber"); }
Now, if you had some way of passing the
name of the function to find()
as a parameter,
you’d have a way of injecting the
test:
If you could find a way of passing a function name to find()
, there would be no limit to the kinds
of tests that you could make in the future. As long as you can write a
function that will return true or
false to a string, you can reuse the same find()
function.
find(sports_no_bieber); find(sports_or_workout); find(ns_theater); find(arts_theater_or_dining);
But how do you say that a parameter stores the name of a function? And if you have a function name, how do you use it to call the function?
You probably guessed that pointers would come into this somewhere, right? Think about what the name of a function really is. It’s a way of referring to the piece of code. And that’s just what a pointer is: a way of referring to something in memory.
That’s why, in C, function names are also pointer variables. When
you create a function called go_to_warp_speed(int speed)
, you are also
creating a pointer variable called go_to_warp_speed
that contains the address of
the function. So, if you give find()
a parameter that has a function pointer type, you
should be able to use the parameter to call the function it points
to.
Let’s look at the C syntax you’ll need to work with function pointers.
Usually, it’s pretty easy to declare pointers in C. If you
have a data type like int
, you just
need to add an asterisk to the end of the data type name, and you
declare a pointer with int *
.
Unfortunately, C doesn’t have a function
data type, so you can’t declare a
function pointer with anything like function
*
.
C doesn’t have a function
data type because there’s not just one type of
function. When you create a function, you can vary a lot of things,
such as the return type or the list of parameters it takes. That
combination of things is what defines the type of
the function.
So, for function pointers, you’ll need to use slightly more complex notation...
Say you want to create a pointer variable that can store the address of each of the functions on the previous page. You’d have to do it like this:
That looks pretty complex, doesn’t it?
Unfortunately, it has to be, because you need to tell C the return type and the parameter types the function will take. But once you’ve declared a function pointer variable, you can use it like any other variable. You can assign values to it, you can add it to arrays, and you can also pass it to functions...
...which brings us back to your find()
code...
Lots of programs need to sort data. And if the data’s something simple like a set of numbers, then sorting is pretty easy. Numbers have their own natural order. But it’s not so easy with other types of data.
Imagine you have a set of people. How would you put them in order? By height? By intelligence? By hotness?
When the people who wrote the C Standard Library wanted to create a sort function, they had a problem:
How could a sort function sort any type of data at all?
You probably guessed the solution: the C Standard Library has a sort function that accepts a pointer to a comparator function, which will be used to decide if one piece of data is the same as, less than, or greater than another piece of data.
This is what the qsort(
)
function looks like:
The qsort()
function compares
pairs of values over and over again, and if they are in the wrong order,
the computer will switch them.
And that’s what the comparator function is for. It will tell
qsort()
which order a pair of
elements should be in. It does this by returning three different
values:
To see how this works in practice, let’s look at an example.
Don’t worry if this exercise caused you a few problems.
It involved pointers, function pointers, and even a little math. If you found it tough, take a break, drink a little water, and then try it again in an hour or two.
Do this!
Great, it works.
Now try writing your own example code. The sorting functions can be incredibly useful, but the comparator functions they need can be tricky to write. But the more practice you get, the easier they become.
Imagine you’re writing a mail-merge program to send out
different types of messages to different people. One way of creating the
data for each response is with a struct
like this:
The enum
gives you the names
for each of the three types of response you’ll be sending out, and that
response type can be recorded against each response. Then you’ll be able
to use your new response
data type by
calling one of these three functions for each type of response:
void dump(response r) { printf("Dear %s, ", r.name); puts("Unfortunately your last date contacted us to"); puts("say that they will not be seeing you again"); } void second_chance(response r) { printf("Dear %s, ", r.name); puts("Good news: your last date has asked us to"); puts("arrange another meeting. Please call ASAP."); } void marriage(response r) { printf("Dear %s, ", r.name); puts("Congratulations! Your last date has contacted"); puts("us with a proposal of marriage."); }
So, now that you know what the data looks like, and you have the functions to generate the responses, let’s see how complex the code is to generate a set of responses from an array of data.
The trick is to create an array of function pointers that match the different response types. Before seeing how that works, let’s look at how to create an array of function pointers. If you had an array variable that could store a whole bunch of function names, you could use it like this:
replies[] = {dump, second_chance, marriage};
But that syntax doesn’t quite work in C. You have to tell the compiler exactly what the functions will look like that you’re going to store in the array: what their return types will be and what parameters they’ll accept. That means you have to use this much more complex syntax:
Look at that array. It contains a set of function names that are
in exactly the same order as the types in the
enum
:
enum response_type {
DUMP, SECOND_CHANCE, MARRIAGE};
This is really important, because when C
creates an enum
, it gives each of
the symbols a number starting at 0. So DUMP
== 0
, SECOND_CHANCE == 1
,
and MARRIAGE == 2
. And that’s
really neat, because it means you can get a pointer to one of your
sets of functions using a response_type
:
Let’s see if you can use the function array to replace your old main() function.
Sometimes, you want to write C functions that are really
powerful, like your find()
function that could search using
function pointers. But other times, you just want to write functions
that are easy to use. Take the printf()
function. The printf()
function has one really cool feature
that you’ve used: it can take a variable number
of arguments:
And you’ve got just the problem that needs it. Down in the Head
First Lounge, they’re finding it a little difficult to keep track of
the drink totals. One of the guys has tried to make life easier by
creating an enum
with the list of
cocktails available and a function that returns the prices for each
one:
enum drink { MUDSLIDE, FUZZY_NAVEL, MONKEY_GLAND, ZOMBIE }; double price(enum drink d) { switch(d) { case MUDSLIDE: return 6.79; case FUZZY_NAVEL: return 5.31; case MONKEY_GLAND: return 4.82; case ZOMBIE: return 5.89; } return 0; }
And that’s pretty cool, if the Head First Lounge crew just wants the price of a drink. But what they want to do is get the price of a total drinks order:
They want a function called total()
that will accept a count of the
drinks and then a list of drink names.
A macro is used to rewrite
your code before it’s compiled. The macros you’re using here (
va_start
, va_arg
, and va_end
) might look like functions, but
they actually hide secret instructions that tell the
preprocessor how to generate lots of extra
smart code inside your program, just before compiling it.
You’ve got Chapter 7 under your belt, and now you’ve added advanced functions to your toolbox. For a complete list of tooltips in the book, see Appendix B.