Unit 5: Functions
(Credit to Chris Brown for the original version of these notes.)
This unit will teach us how to write our very own functions in C. This is a very powerful concept that is central both to understanding how C works, but also to discovering how to write larger and more easily maintainable programs.
1 Writing our own functions
1.1 Motivating Example
Sometimes there is a particular chunk of code that appears over and over again in a program. For example, if we’re writing a program to compute gcd’s, we’d ask the user twice to enter in a positive number. This program illustrates how we’d have to do it. Now, there are plenty of nice functions in standard libraries, like sqrt
and cos
that do all sorts of nice things for us. If only there were a function getposint
that would get a positive int from the user and return it to our program, we could rewrite our program as:
int main() {
int a = getposint();
int b = getposint();
// Compute gcd
while(b != 0) {
int r = a % b;
a = b;
b = r;
}
// Write out gcd
printf("The gcd is %i\n", a);
return 0;
}
This is a tremendous improvement! The code is not only less repetitive, but it’s also clearer what’s going on. Unfortunately, such a function is not a part of any of the standard libraries. Therefore, it’s up to us to make it!
1.2 Prototypes
You should have noticed when looking at documentation for the math.h library that they give a description of the function like:
double cos(double x);
This is called a prototype. It tells you (and it tells the compiler) that cos
takes an object of type double
(that’s the x
) and returns or evaluates to an object of type double
. It doesn’t tell you how or what it’s doing, but it does tell you that something like
cos("Brown")
is not going to make sense, since "Brown"
has type cstring
, not type double
. Similarly, something like
if (cos(3*z) && k < 0)
You need to understand prototypes to understand how expressions that involve function calls are evaluated. For example: cos(45)/2
What happens here? Either we’ll do int division and the answer will be zero, or we’ll do double division and we’ll get some answer in the range [-0.5,0.5]. So which is it? Well, evaluating cos(45)
results in a double, so as in any operation in which int’s and double’s are mixed, the int is promoted to a double and we get double division and a double result.
isn’t going to make sense, since the &&
operator expects to see two true/false boolean values (which will really be int
s), but the cos(3*z)
evaluates to an object of type double
.
When you define functions of your own, you need to define a prototype as well. It must be defined outside of the main
block, and it must appear before you ever use the function. Now, in the getposint
function we’d envisioned earlier, there’s nothing that the function takes as input from the program, and it should evaluate to or return the positive integer it’s read in from the user, so the right prototype would be:
int getposint();
1.3 Function Definitions
In addition to giving the function prototype, you have to provide a function definition, which is what tells the computer what the function is supposed to do. The function’s definition can appear anywhere after the prototype. You repeat the prototype (without the ‘;’) and then give a block of code that comprises the function. Just as the return
statement in main leaves the program, a return
statement in your function body leaves the function. Instead of return
ing 0 however, we’ll return whatever value the function’s supposed to give. So our getposint
function might have the following definition:
int getposint() {
int k;
printf("Enter a positive integer: ");
fflush(stdout);
int check = scanf(" %i", &k);
while(check < 1 || k <= 0) {
printf("I said *positive integer*; try again: ");
fflush(stdout);
check = scanf(" %i", &k);
}
return k;
}
The function definition also has to appear outside of the main
block. This program gives a complete picture of how to rewrite our GCD calculator to make use of a getposint
function.
1.4 Functions with an argument
More interesting are functions which take an argument, i.e. some kind of input object. For example, we may often be in the situation of having to compute something like the factorial of a number. Especially if this crops up more than once in a program, it’d be really nice to have a function like the pow or sqrt functions from math.h to do the factorial for us. First, we need a prototype that specifies that our function (we’ll call it factorial
) takes an integer value and returns an integer value:
int factorial(int x);
From this point on in our program we can use the factorial
function. However, somewhere along the way we’ll actually have to define the function — something like this:
int factorial(int x) {
int f = 1;
while (x > 0) {
f = f*x;
x--;
}
return f;
}
Notice that here we give a name to the int
value that gets passed into the function, so that we can reference it within the body of the function definition. Values that get passed in to a function in C are called function arguments. It’s important to note that the arguments are passed by value, meaning that you get a copy of the value of the variable function
is called with, not the variable itself. So, for example, if our main
function looked like
main() {
int y = 4;
printf("%i\n", factorial(y));
printf("%i\n", y);
return 0;
}
The result would be 24 followed by a 4. Although the argument x
in the function definition does get modified, the variable y
does not, because y
’s value (i.e. 4) got passed to factorial
, not the variable y
itself. So pass by value means that a copy of the object appearing in the function call is what gets passed along to the function, not the object itself.
Remember also that scoping rules show us that any variable named x
or f
in main
have no relation to the x
and f
in the factorial
function. To reiterate, pass-by-value means that you get a copy of the argument. By analogy, calling the “haircut function” with argument “MIDN Jones” would 1) cause a clone to be made of MIDN Jones, 2) cause the clone’s hair to get cut, and 3) cause the clone to get destroyed after the haircut. Thus, when MIDN Jones showed up in class the next day, his hair would still be shaggy.
1.5 Functions that return nothing
Sometimes we’re interested in functions that return nothing at all. These functions are called for their side effects such as printing something to the screen. This is an issue for our prototype, however, since we need to specify something for return type. In this case, we use void
in place of a return type. Here’s a small example:
#include <stdio.h>
void greetings();
int main() {
greetings();
// probably you would do something else here,
// but this is just a small, simple example.
return 0;
}
void greetings() {
printf("Well hello there.\n");
printf("Welcome to my exciting program.\n");
printf("Notice this function has no return statement!\n");
}
This function is only used for the side effect of writing something on the screen, not for any value it would return.
1.6 Multi-Parameter Functions
Functions get infinitely more interesting when they have more than one argument or parameter. We’ve been using many examples of multi-parameter functions already with things like readstring
, fputs
, fopen
, and many more.
Specifying multiple parameters for a function is just like specifying several single parameters in a comma-separated list. For example, suppose you wanted to define a function max
that looked at two int
s and returned the larger of the two. Its prototype would be
int max(int a, int b);
The definition would look like this:
int max(int a, int b) {
if (a < b) {
return b;
}
else {
return a;
}
}
Note that this example shows you that you can return from anywhere within a function, just like you can return from anywhere within main
.
It’s important to note that the order of arguments is matters. For example, suppose you had a function
void rep(int times, char c);
that printed the char
argument to the screen the number of times given by the int
. If I want to print a #
symbol 42 times, I need to be sure to say rep(42,'#')
, because the function expects the int
object first. If I said rep('#',42)
instead, do you know what’d happen? I’d print 35 *
s! Why? Because the same kind of implicit type conversions that go on inside expressions go on with function arguments! The rep
function expects an int
as its first argument, and when it gets '#'
instead, it simply converts it to an int
… and the ASCII value of '#'
is 35, so that’s the int
you get. Likewise, a char
is expected as the second argument, and when rep
gets the number 42 instead, it converts it to a character, and ASCII value 42 gives you the char
'*'
.
When implicit conversions aren’t possible, the compiler gives you an error message. For example, if you tried to call rep("#",42)
the compiler would give you an error saying that the first argument of rep
was supposed to be an int
, but you gave it a string, and there’s no way to do that conversion. You’ll likely see lots of these messages in your life!
Armed with the ability to make multi-parameter functions, we can now fully function-ize our gcd program from earlier: this program uses a getposint
function to read each input integer, as well as a gcd
function to do the actual computation.
1.7 Vocabulary
- function prototype - The prototype tells us what we need to know to use the function … everything except what the function actually does! If you are presented with only a prototype there is usually some documentation that describes what task the function accomplishes. These should show up at the beginning of your program, before
main
.
- function definition - This is where we provide the code that determines how the function operates, i.e. how it does whatever it does. These will usually be at the end of your program, after
main
.
- argument to a function - when we use a function (“call” a function) and we provide an expression whose value will be passed into the function, that expression is called an argument to the function.
- function parameter - a function gives a name (and a type) for the value that is going to be passed into the function. That name is called a parameter. It’s what is used inside the function definition to refer to the value that was passed into the function when the function was called.
- pass-by-value - describes the basic function calling mechanism for C, in which the function receives a copy of whatever argument object appears where the function is called, not the actual argument object itself.
- function call - also called application - the point in the execution of a program at which the function expression is evaluated and, as a result, the function body executed.
- function call site - the location in the source code of the expression that uses the function.
- function’s return value - also called result - the object that results from evaluating the function call expression.
2 Designing Functions
2.1 Functions and Top-Down design
main
.main
.The essence of programming is breaking down a single, large, complex problem into many simple pieces that can be attacked independently. Functions help us to do this, and that is one of their most important features. They help us write programs in a top down manner, essentially be letting us write our program using a “wish-list” of functions that we can actually go back and implement later. An example should make this clearer.
Problem: You have a bank account whose annual interest rate depends on the amount of money you have in your account at the beginning of each year. Your annual rate starts at 3%, and grows by an additional half a percent for each thousand dollars in your account up to, but not exceeding 8%. So, for example, if you have $3,358.02 in your account at the beginning of the year, your rate is 4.5%. Interest in this account is compounded monthly at the annual rate (i.e. the monthly compounding rate is the annual rate divided by 12). Each year you also make a transaction (deposit or withdrawal) before the bank figures out what your rate is (fortunately!). Write a program that simulates 5 years under this system, interactively querying the user for transactions at the beginning of each year, and returning the balance at the end of the 5th year.
There’s a lot to this problem. However, imagine how much easier it would be to solve if the following functions were available:
double transaction(double bal);
- which would do the interactive part for you - taking the initial balancebal
and returning the balance after the transaction.double rate(double bal);
- which would return the interest rate based on the account balance.double compound(double rate);
- which would give you the value of a single dollar after a year’s monthly compounding with annual interest rater
. If your blance isbal
and your rate isr
at the beginning of the year, the new balance isbal*compound(r)
.
With this “wish list” of functions, we could write the program quite easily:
double bal = 0.0;
// Simulate 5 years
for(int i = 0; i < 5; i++) {
bal = transaction(bal);
double rate = intrate(bal);
bal = bal*compound(rate);
}
// Print out final balance
printf("Balance = $%.2f\n", bal);
This is a pretty easy program! Of course, the 3 functions in our wish list do not exist, so we’ll have to implement them for ourselves.
double transaction(double bal) {
// Get type of transaction
printf("Enter w:withdrawl or d:deposit ");
fflush(stdout);
char act;
scanf(" %c", &act);
// Get amount of transaction
printf("Enter amount: ");
fflush(stdout);
double amount;
scanf(" %lg", &amount);
// Get new Balance figure
if (act == 'w') {
bal = bal - amount;
}
else {
bal = bal + amount;
}
return bal;
}
double intrate(double bal) {
// Get # of thousands
int thous = bal/1000;
// Calc rate
double rate = 3 + thous*0.5;
if (rate > 8) {
rate = 8;
}
return rate;
}
double compound(double rate) {
// Simulate year with monthly compounding
double scalerate = rate/100;
double total = 1.0;
for(int i = 0; i < 12; i++) {
total = total*(1 + scalerate/12);
}
return total;
}
The key here is that each of these functions can be implemented independently. When I implement transaction
, I don’t need to worry about any other aspect of the program - it’s like transaction
is its own little (easy!) program to write. Take a look at the complete program.
2.2 Scope and Functions
There’s some room for confusion with functions when the same name pops up in different places. For example, consider this program:
int f();
int main() {
int a = 0;
int b = f();
printf("a = %i\n", a);
return 0;
}
int f() {
int a = 2;
return -1;
}
What gets printed out? On the one hand, I’d say “0”, since a just got assigned that value. On the other hand, the function f
is called in between, and there I see a being given the value 2. So which is it?
The answer is that “0” gets printed out. It all goes back to scope. The a
in main does not exist outside of main, and likewise the a
in the function f
does not exist outside of f
. These variables are two different objects that happen to have the same name. Since they are in different scopes, however, there is no confusion or conflict. We say that variables like this are local to the functions in which they are defined, i.e. they don’t exist outside of the functions in which they are defined.
The way that you want to think of this is that each function call is like a piece of paper with boxes for each of the function’s local variables. When a function is called a new piece of paper is stacked on the others. The computer only actually works on the function call represented by the top paper on the stack. This image helps you think about how variable scope works with function calls.
2.3 Predicates
Functions which return the true/false values (1 or 0 in C) are traditionally referred to as predicates.
For example, it’s considered a “bad idea” to compare two double
s directly using ==
, because round-off errors could mean that the numbers are very, very close but not exactly identical to each other.
So let’s make a function to do what we want:
// predicate function to test whether two numbers are
// equal up to the specified number of digits
int approxequal(double x, double y, int digits) {
// get the positive difference between them
double diff;
if (x > y) {
diff = x - y;
} else {
diff = y - x;
}
// compute the smallest allowable difference
double precision = 1.0;
for (int i=0; i<digits; ++i) {
precision = precision / 10.0;
}
if (diff <= precision) {
return 1; // the two numbers are close
} else {
return 0; // the numbers are far apart
}
}
In fact, we could be even more clever and write that last if/else statement as just
return (diff <= precision);
since the expression diff <= precision
evaluates to the int
that we want to return!
Really there’s no need to bring predicates up as a special subject, since functions that return true/false are not any more special than functions that return anything else. However, you might not have thought much about the use of having such functions.
2.4 Composing Functions
Let’s suppose that I had the function max
defined as
int max(int a, int b) {
if (b > a) {
return b;
}
else {
return a;
}
}
but that my program had three int
s, x
, y
and z
, amongst which I need the largest. Were I to write
max(x,y,z)
the compiler would complain … the only max
function it knows about only takes two arguments! However, I could say the following:
max(max(x,y),z)
This is our first example of composition of functions. When the function max
gets called, its two argument expressions are evaluated. The first is max(x,y)
, which evaluates to the larger of the two values, and the second is simply z
. So, what we get out of this is the maximum of all three values.
The most important thing I can tell you about composing functions, is that there is really nothing to talk about. Function arguments are given in your code by expressions, right? And those expressions are evaluated before calling the function to produce the argument objects that are passed to the function. So, whether or not the argument expressions themselves contain function calls is immaterial — the system works the same.
3 Passing pointers to functions
Recall that arguments to our functions have been passed by value, meaning that inside the function we get a copy of the argument object given in the function call. Sometimes, however, we’d like to get the actual object from the function call rather than a copy. There are three basic reasons for this:
- We may want to modify the object.
- A copy may not make sense for some objects.
- If an object is really big, copying may be expensive in terms of time or memory space.
3.1 Using pointers to modify the original value
As an example of the first reason: Suppose we have two variables hour
and minute
for a 24-hour clock, and we want to update the time in fifteen minute increments. We’d like a function that adds 15 minutes to the total time. Here’s a first attempt (that doesn’t work!):
void tick(int h, int m);
int main() {
int hour = 9;
int minute = 55;
for (int i=0; i<10; ++i) {
tick(hour, minute); // doesn't work!
printf("current time: %02i;%02i\n", hour, minute);
}
return 0;
}
void tick(int h, int m) {
if (m < 45) {
m += 15;
} else {
++h;
m -= 45;
}
}
But remember, the normal mode of passing arguments to functions is called pass by value, and it means the function gets a copy of the original value. So the arguments, such as hour
and minute
in this example, can’t possibly be changed by the function tick
. The program above just prints the same time over and over again.
Instead, to give the function the ability to change the original values, we have to pass pointers to the function, like so:
void tick(int* hptr, int* mptr);
int main() {
int hour = 9;
int minute = 55;
for (int i=0; i<10; ++i) {
tick(&hour, &minute);
printf("current time: %02i:%02i\n", hour, minute);
}
return 0;
}
void tick(int* hptr, int* mptr) {
if (*mptr < 45) {
*mptr += 15;
} else {
++(*hptr);
*mptr -= 45;
}
}
Notice what changed: The parameters in the function changed from type int
to type int*
, a pointer to an integer. This means that the arguments in the function call changed from the integer variables hour
and minute
to the addresses of those variables, &hour
and &minute
.
Note that the parameter passing method in C is still pass-by-value; the function gets a copy of the arguments passed into it. But since those values are pointers, they still point to the same original variables (in this case, the hour
and minute
variables in main()
). So the function tick
actually has the ability to change them!
3.2 The Famous swap!
With multi-parameter functions, we really start to see some interesting reasons to use pass-by-reference. For example, one of the most common operations in computing is the swap
. For example, suppose we read two numbers from the user and want to print out all the integers between them, comma-separated. Here’s how that might look:
int a;
int b;
scanf(" %i %i", &a, &b);
if (a < b) {
for (int i = a+1; i < b; ++i) {
printf("%i\n", i);
}
} else {
for (int i = b+1; i < a; ++i) {
printf("%i\n", i);
}
}
Hmm, that if/else is pretty ugly looking. If we had a swap
function that too two pointers to integers and swapped their values, we could do the following instead:
int a;
int b;
scanf(" %i %i", &a, &b);
if (b < a) {
swap(&a, &b);
}
for (int i = a+1; i < b; ++i) {
printf("%i\n", i);
}
Notice, our swap
function has to take addresses as its arguments, because otherwise it would have no hope of modifying the original values! Here’s a definition of the swap
function that would complete the program above:
void swap(int* aptr, int* bptr) {
int temp = *aptr;
*aptr = *bptr;
*bptr = temp;
}
3.3 Using pointers to return multiple values
Sometimes there are several things we’d like to return with a function. For example, if you have a program that works with vectors, you might want to convert back and forth from the ((r,)) and ((x,y)) representation. It’d be nice to have a function that would do this, so I’d give it r
and theta
and it’d give back the appropriate x
and y
.
Unfortunately, there are two values we’d need to return from our function, and a function in C always returns just one thing (or nothing, if the return type is void
). What we can do is pass pointers to the variables we want the function to change, and then the function can dereference those pointers and assign the values as needed.
void polar2rect(double* x, double* y, double r, double theta);
...
void polar2rect(double* x, double* y, double r, double theta) {
*x = r * cos(theta);
*y = r * sin(theta);
}
With this definition, if I had a vector represented by double
s radius
and angle
, I could convert it to rectangular coordinates stored in variables xcor
and ycor
by writing:
polar2rect(&xcor, &ycor, radius, angle);
3.4 Using pointers when copying an object doesn’t make sense
Reason 2 from my list of reasons to pass by value is that copying doesn’t make sense for some type of objects. The best example of this is something you already know about - file streams.
Recall that the old stream
type from si204.h is actually type FILE*
, a pointer to a FILE
object. Now we don’t know exactly what a FILE
object entails (it’s built in to the C standard library), but you can imagine it stores things like the operating system “handle” to connect to that open file as well as the current position of reading or writing in the file.
So why is it pointers, FILE*
, rather than just a FILE
object directly, that get passed to all the reading and writing functions like fputs
or fscanf
? The key is that it doesn’t make sense to copy a stream object. If you read some values from the copy of the stream, would the original one stay in the same place? What if you closed the copy - would the original stream still be open?
With this in mind, we can of course write functions that take streams as arguments - which means they will have FILE*
parameter types. For example, here’s a function that reads a time in a format like “8:30pm” and returns a 24-hour time as a single integer such as 1335
:
int readtime(FILE* instream) {
int hrs;
int mins;
char ap;
fscanf(instream, " %d:%d%cm", &hrs, &mins, &ap);
int res = hrs * 100 + mins;
if (hrs == 12) {
res -= 1200
}
if (ap == 'p') {
res += 1200
}
return res;
}
Now we could call readtime
after opening a file, like:
cstring file = "time.txt";
FILE* fin = fopen(file);
if (readtime(fin) >= 1530) {
printf("Time to relax.\n");
}
fclose(fin);
And we could also use the same function to read from the terminal, by just passing stdin
to the function instead of a FILE*
variable that we created:
printf("Enter time like 8:30pm: ");
fflush(stdout);
int time = readtime(stdin);
printf("Current time is %04i\n", time);
4 Recursion
4.1 Introduction: Printing a starry triangle
Let’s consider the following function, which is a variation a now-familiar example: A function void line(int k)
that prints a line of k
asterisks. And just to be on the safe side, we’ll print out a helpful error message if a negative value of k
is passed in, since that doesn’t make sense.
void line(int k) {
// Check for invalid argument
if (k < 0) {
printf("ERROR: negative value for k\n");
return;
}
// Now take care of the usual case
for(int i=0; i < k; i++) {
printf("*");
}
printf("\n");
}
If you call this function with an argument like 35, you get:
***********************************
Now, suppose I want to modify this so that it prints a second line below of length k-1
, so that calling this function with an argument like 35 would give me
***********************************
**********************************
Well, I might decide to try the following idea. Since line
already does all the work of printing out a line for me, why don’t I just add a line(k-1)
at the end of my function definition? In other words, how about:
void line(int k) {
// Check for invalid argument
if (k < 0) {
printf("ERROR: negative value for k\n");
return;
}
// Now take care of the usual case
for(int i=0; i < k; i++) {
printf("*");
}
printf("\n");
// Print next line?
line(k-1);
}
Well, if I compile and run this with something like line(10)
, here’s what I get:
**********
*********
********
*******
******
*****
****
***
**
*
ERROR: negative value for k
So what happened? A function that “calls itself” as this one does is called recursive, and the best way to understand what’s going on here is to step through it showing what happens step by step. See if you can write out the “stack” of recursive calls that go on and follow the execution step by step.
Of course, we’d like to avoid that error message at the end. What’s really needed is called a base case for the recursion — what condition should stop any further recursive calls? In the case of the starry triangle, it should probably stop when it reaches zero. Actually, all that’s needed to fix the program is to just take out the printing of the error message and have it simply return;
in that situation!
Oh, and just for fun, let’s also have the program print out a line of +
s after the recursive call, with the same length as the line of *
s. Here’s the complete function:
void line(int k) {
// Base case
if (k <= 0) {
return;
}
// Now take care of the usual case
// Step 1: print stars
for(int i=0; i < k; i++) {
printf("*");
}
printf("\n");
// Step 2: recursive call
line(k-1);
// Step 3: print plusses
for(int i=0; i < k; i++) {
printf("+");
}
printf("\n");
}
And here’s what happens when we run line(5)
now:
*****
****
***
**
*
+
++
+++
++++
+++++
Yes, that’s really what you’ll get! Notice that the first and last lines come from the “top-level” call to line(5)
, then the second and second-to-last lines come from the next recursive call line(4)
, and so on. If you don’t believe me, download the complete program and try for yourself!
4.2 How recursion works
The line
function above was our first “sneak peek” at recursion. A function is recursive if the function, in order to compute its result, ends up “calling itself” in some cases.
This idea can seem paradoxical when you’re started, but like most things it will make more sense as time goes on. We will talk about this and look in more detail during class, but the upshot is that we have the same function, yes, but it is one call of the function that in turn makes a separate call to the same function, with different arguments. So each of the recursive calls has its own scope and own variables, all executing the same lines of code but on different values.
Here’s another (classic) example, computing the factorial of n defined by
\[n! = n \cdot (n-1) \cdot (n-2) \cdot (n-3) \cdots 3 \cdot 2 \cdot 1\]
int factorial(int n);
int main() {
int f = factorial(4);
cout << f << endl;
return 0;
}
int factorial(int n) {
if (n <= 1) {
return 1;
} else {
return n * factorial(n-1);
}
}
The key take-away from this is that there is not contradiction in having multiple active calls to a function (like factorial
) because each function call is essentially its own copy of the function variables. So we end up with a “stack” of function calls that includes many copies of the factorial function, but they’re all called with different argument values.
4.3 Base cases
Now that we’ve seen a few examples of recursive functions, it’s time to think about how you might design such a function yourself. But before we start looking at how one devises a recursive function that accomplishes a given task, let’s look at a recursive function that doesn’t work. Consider this function:
int f(int k) {
int a = k*k;
int b = f(k + 1);
return a + b;
}
Now, let us suppose that we start off by calling f(3)
. The following table shows what happens as time evolves a few steps:
call to… | awaiting return… |
---|---|
main() |
|
f(3) |
main() |
f(4) |
f(3) main() |
f(5) |
f(4) f(3) main() |
f(6) |
f(5) f(4) f(3) main() |
You can see that the stack of functions awaiting the return of the most recently called function simply keeps growing and growing, with nothing but the limits of time and space to stop it from growing forever!
This function has an infinite recursion, meaning that recursive calls are made over and over again, without any mechanism to stop the process. A proper recursive function must always have a base case, meaning a way to return without making a recursive call, which is the mechanism that stops this process of ever more recursive calls and an ever growing stack of function calls waiting on the return of other function calls. Moreover, this base case, i.e. this way of returning without making a recursive call, must be something which we’ll eventually hit. Once we hit it, we can start taking function calls off the top of the stack and returning from them.
Whether or not we are in the base case is determined by the parameters we’re passed. Consider this function
// adds up the numbers from 0 up to k
int sum(int k) {
int part = 0;
int ans = 0;
// Base case
if (k == 0) {
return 0;
}
// Recursive case
int part = sum(k-1);
int ans = part + k;
return ans;
}
The base case here is reached when k
is zero. Moreover, no matter how big the number in our initial call to sum
, we must eventually reach our base case, because the argument to our recursive calls keep getting smaller and smaller, until they finally reach zero. If we call sum(3)
from the main
function, the following table shows how the computation evolves over time:
call to/return value | awaiting return… |
---|---|
main() |
|
sum(3) |
main() |
sum(2) |
sum(3) main() |
sum(1) |
sum(2) sum(3) main() |
sum(0) |
sum(1) sum(2) sum(3) main() |
returns 0 |
sum(1) sum(2) sum(3) main() |
returns 1 |
sum(2) sum(3) main() |
returns 3 |
sum(3) main() |
returns 6 |
main() |
So, to be a properly defined recursive function you must have a base case, i.e. a way for the function to return without making a recursive call, and your recursive calls must work towards the base case.
4.4 Solving problems recursively
Let’s develop a methodology for writing recursive functions. We’ll illustrate it as we go along with the following problem: Write a function that takes a positive integer n
as input and returns the value of \[1^2 + 2^2 + 3^2 + \cdots + n^2.\] Not a very interesting problem, but so simple that we can concentrate on problem solving using recursion.
When you’re setting out to write a recursive function, you at least have a prototype in mind. In this case that’ll be
int sumsquare(int k);
That part is easy, and no recursion is involved. If we’re going to solve this recursively, we have two big questions:
What is the base case? In other words, for what inputs can we automatically just spit out the answer without having to do any real work? In particular, without needing a recursive call?
For this problem, I’m asking what values of
k
are particularly easy to give the answer to? Of course, since we’re assuming thatk
is positive, the easiest value fork
is 1. Ifk
is one we can just immediately return 1. So that gives us our base case.// Base case if (k == 1) { return 1; }
Sometimes it’s helpful to think about recursion this way: “if I had a function that solved my problem, but only for arguments of value less than n, how could I solve the problem for n? For example, suppose I had a function factorialSmall
that computed the factorial of its argument, but only for numbers less than 10. How could I use it to solve factorial 10? Easy:
10*factorialSmall(9)
Now: suppose factorialSmall
worked for any number less than some value n, but I’m not telling you exactly what n is. How could I compute factorial(n)? Easy:
n*factorialSmall(n-1)
Great! Now I’ve got my recursive case!
What is the recursive case? How would the answer to a “smaller” problem of the same kind help get the answer to the original problem. In other words, if I just assume that the function (which I already have the prototype for) just “works” for smaller inputs, how will that help me get the answer?
For this problem, what I’m asking is this: How would the answer to
sumsquare(k-1)
help me figure out the answer tosumsquare(k)
? Well, all I’d need to add to the result ofsumsquare(k-1)
is ak
2, and I’d have the answer tosumsquare(k)
. This then gives us our recursive case:// Recursive case int part = sumsquare(k - 1); int ans = part + k*k; return ans;
If you put the answer to these questions together, you’ll have a recursive function that solves the problem. Really the two keys are to find a base case, and to assume your function already works as you go about using recursive calls to define it. In the above we just assumed that sumsquare
would work right when we called sumsquare(k - 1)
. So, here’s the complete function:
int sumsquare(int k) {
// Base case
if (k == 1) {
return 1;
}
// Recursive case
int part = sumsquare(k - 1);
int ans = part + k*k;
return ans;
}
5 The structure of multi-file programs
As our programs get more complicated with more and more functions, it can be useful to put some of those functions in their own files that are separate from our main program.
Going back to our familiar example, let’s say we have a program to draw some ASCII shapes. Granted this isn’t the most life-changing program, but it will be useful to understand what’s going on:
#include <stdio.h>
void rep(int times, char c);
void triangle(int k);
void rectangle(int len, int wid);
int main() {
int k = 10;
// print an interesting shape
rectangle(k/2, k);
triangle(k);
return 0;
}
void rep(int times, char c) {
for (int i=0; i<times; ++i) {
printf("%c", c);
}
printf("\n");
}
void triangle(int k) {
for (int wid=k; wid >= 1; --wid) {
rep(wid, 'T');
}
}
void rectangle(int len, int wid) {
for (int row=0; row < len; ++row) {
rep(wid, 'R');
}
}
Now suppose this is just too “crowded” with three functions, and we’d like to split this into multiple files. Specifically, we’ll make a separate header file for the function prototypes, and a definitions file that contains the actual function definitions.
5.1 Header files
First, the header file, which we might call draw.h
:
// prototypes for drawing functions
// note: these lines "protect" us against someone #including this file twice.
#ifndef DRAW_H
#define DRAW_H
void rep(int times, char c);
void triangle(int k);
void rectangle(int len, int wid);
#endif // DRAW_H
Something to notice there is the lines with #ifndef
. This is code that you see in just about any header file, and it tells the compiler to skip re-including this code if it’s already been included some other way! That protects you from accidentally having duplicate definitions, which would be a bad thing.
Specifically, the #ifndef
is a sort of if statement that says “only include the rest of this if DRAW_H
hasn’t been defined yet”. Then of course the next line goes ahead and defines DRAW_H
so that if this comes back again, it won’t get included again. And you can’t forget the #endif
at the end!
Note that DRAW_H
really could be any name you want, but it’s a useful convention to use the name of the header file itself, in all caps, with _H
at the end. That’s mostly to make sure you don’t accidentally use the same in two different header files (what would happen?) or - worse - as a variable in your program.
In this example we have only one header file, but of course you could also use multiple header files if you like, each with their own function prototypes. You always want to end your header files with a .h
filename extension, and remember that header files don’t get compiled directly. Instead, they will be #include
d into other .c
programs that do get compiled!
5.2 Definitions file
Since the header file only has the function prototypes, we put the definitions in their own file, which for this example we might call draw.c
:
// Definitions for drawing functions
#include <stdio.h>
#include "draw.h"
void rep(int times, char c) {
for (int i=0; i<times; ++i) {
printf("%c", c);
}
printf("\n");
}
void triangle(int k) {
for (int wid=k; wid >= 1; --wid) {
rep(wid, 'T');
}
}
void rectangle(int len, int wid) {
for (int row=0; row < len; ++row) {
rep(wid, 'R');
}
}
Notice that this looks very similar to any other program we’re written, except that there’s no main
method! Also see that we #included
the header file draw.h
, using double-quotes since that’s not a standard header file; it’s one that we wrote ourselves!
Now this file is not going to be included anywhere else. It’s not a header file! So we don’t have to do the #ifndef
thing to protect from multiple includes, but we do have to compile this piece. If you try to compile like normal, you’ll get an error message like
/usr/lib/gcc/x86_64-linux-gnu/4.9/../../../x86_64-linux-gnu/crt1.o: In function '_start':
/build/glibc-Wb9zo9/glibc-2.19/csu/../sysdeps/x86_64/start.S:118: undefined reference to 'main'
collect2: error: ld returned 1 exit status
The important part of that is this: undefined reference to 'main'
. It’s saying, I don’t know how to make a program out of this, since there’s no main
function!
Instead, you have to give the -c
flag to the compiler to tell it that you just want to create object code. Think of that as part of a program; it’s machine code without a defined starting point (because there’s no main
). Object code files should always have the same name as the corresponding .c
file, but they end in .o
instead.
So you could compile with:
gcc -c draw.c -o draw.o
(And actually the -o draw.o
part is unnecessary, since gcc
will automatically give the object file that name by default.)
5.3 Main program and linking together
What’s left is just our main
function, which could go in its own file that we might call shapes.c
:
#include <stdio.h>
#include "draw.h"
int main() {
int k = 10;
// print an interesting shape
rectangle(k/2, k);
triangle(k);
return 0;
}
You should notice that there is a #include
for the header file "draw.h"
, but no include for the definitions file! That’s because, again, header files get included (at the beginning, hence the name “header”), and definition files get compiled separately. We could also do a separate compilation of our main program shapes.c
into object code with the following command:
gcc -c shapes.c -o shapes.o
Now after doing the two separate compilations of draw.c
and shapes.c
, using the -c
flag so gcc produces object code, you will have two object code files in your directory, draw.o
and shapes.o
. Those are not programs; they’re just compiled bits of machine code.
The process of stitching together our object code into an actual complete program is called linking, and gcc
can do that for us too! Here’s the command you would use to link together draw.o
and shapes.o
and create an executable program shapes
:
gcc draw.o shapes.o -o shapes
Putting this all together, the compilation and linking steps for this program are as follows:
roche@ubuntu$
ls
draw.c shapes.c
roche@ubuntu$
# COMPILATION STEPS
roche@ubuntu$
gcc -c draw.c -o draw.o
roche@ubuntu$
gcc -c shapes.c -o shapes.o
roche@ubuntu$
ls
draw.c draw.o shapes.c shapes.o
roche@ubuntu$
# LINKING STEP
roche@ubuntu$
gcc draw.o shapes.o -o shapes
roche@ubuntu$
ls
draw.c draw.o shapes shapes.c shapes.o
It’s very important to understand those steps! I can’t stress that enough — if you’re fuzzy on the difference between compilation and linking, you’ll be mightily confused by the errors that you see and how to fix them.
But… it’s not so important to follow all the steps above, because gcc is kind enough to do it all for us in one step if we just give it all of the source code .c
files (but not the header files!!!) in a single command like
gcc draw.c shapes.c -o shapes
This actually does the separate compilation and linking steps we saw above, and roughly in that same order, but you can get it all to happen with that single command.
6 Problems
- Marathon Times
- Date Calculator
- Approximating e
- Surveying Problem This gives you another simple example of top-down design.
- Distance between points This is a small multi-parameter function problem.
- What percentage of numbers are prime? This is an example using a predicate.