SI 204 Spring 2017 / Notes


This is the archived website of SI 204 from the Spring 2017 semester. Feel free to browse around; you may also find more recent offerings at my teaching page.

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 ints), 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 returning 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 ints 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

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 balance bal 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 rate r. If your blance is bal and your rate is r at the beginning of the year, the new balance is bal*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 doubles 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 ints, 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:

  1. We may want to modify the object.
  2. A copy may not make sense for some objects.
  3. 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 doubles 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:

  1. 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 that k is positive, the easiest value for k is 1. If k 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!

  1. 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 to sumsquare(k)? Well, all I’d need to add to the result of sumsquare(k-1) is a k2, and I’d have the answer to sumsquare(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 #included 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

  1. Marathon Times
  2. Date Calculator
  3. Approximating e
  4. Surveying Problem This gives you another simple example of top-down design.
  5. Distance between points This is a small multi-parameter function problem.
  6. What percentage of numbers are prime? This is an example using a predicate.
  1. Incrementing a Military Clock (You might prefer this solution.) This is a simple pass-by-reference example that modifies its argument.
  2. Reading Binary Numbers Here’s a simple example in which we use pass-by-reference to avoid making a copy of an istream object.

  3. Look at this program and explain what happens, i.e. answer the question posed up in the comment block.
  4. Look at this program, similar to the one above, and explain what happens, i.e. answer the question posed up in the comment block. Hint: recall that char + int produces an int value!
  5. Step through this program with the debugger and figure out what useful work (if any) the mystery function accoplishes.

  6. Revisiting the GCD. The way Euclid would’ve phrased his GCD algorithm would be more like this: If A and B (okay, he was greek, so it probably would’ve been α and β …) are two integers with A >= B >= 0 then the GCD of A and B is given by:
    • if B = 0 then the GCD is A
    • if B > 0 then the GCD is the same as the GCD of B and A%B.
    Using this phrasing of the algorithm, implement the GCD as a recursive function. Here’s a solution. (Think, what’s the base case? What’s the recursive case? Does the recursive case work towards the base case?)
  7. Making Change My solution prints out the coins in decreasing value. Any thoughts on how you could modify it to print out coins in increasing value?
  8. Factorization … Again. Using the function int firstfactor(int); that you guys defined for me in Class 15 homework, write a recursive function void factor(int n, ostream& OUT); that prints out the factorization of n to the output stream OUT. What’s the base case? How could a recursive call call to factor with a smaller n-value help solve the problem? Take a look at my solution.