Prev Next

More on Functions

Introduction to functions

A function performs a specific task. It encapsulates the knowledge of how that task is performed, freeing the rest of the program from the need to know such details.

  • Sometimes, a function encapsulates knowledge of an algorithm that is complicated.

  • Sometimes, the function encapsulates a very simple algorithm, but it encapsulates information that might change later, when the program is modified. When that information changes, you only need to change one function.

Functions are important program design tools because

  1. They allow you to break a large program up into manageable pieces, and to work on and test with each piece separately.

  2. They aid in modification because they act as encapsulations, or boundaries, beyond which some modifications do not need to go.

Kinds of functions

Functions in C/C++ are typically of three kinds.

Pure functions. A pure function does not change anything. It only computes a value. For example, strlen is a pure function. You pass it a string, and it gives you the length of the string.

You think of a pure function the way you do a mathematical function such as sqrt.

Procedures. A procedure is a function that makes changes to things, and that returns a void value. You think of it as performing a command. Sometimes, a procedure only changes the values of reference parameters. Sometimes, it changes other things, such as things pointed to by parameters.

Mixed functions. A mixed function returns a value, and also has side effects. For example, scanf is a mixed function. It puts values in its parameters, but it also returns a value. (The value returned by scanf is the number of items that it successfully read.) Mixed functions are useful, but should be used with care. If you do more than one in a single expression, the different mixed functions might interfere with one another. Also, the order in which they are performed might not be defined.

 

Application dependence and independence

The functions of a particular application can usually be broken into two kinds.

First are functions that are particular to the application, and would make little sense if found in another application. Such functions might, for example, print messages that would almost certainly not be appropriate to other applications. I will call these functions application-specific.

The second kind of functions are those that might easily be picked up and put into another application, without change, because the perform generally useful tasks. I will call these functions application-independent.

A good design principle is to use application-independent functions wherever possible. Doing so will help to keep your program organized and easy to understand and to modify.

Example. Suppose you need to compute the average of two members of array Horse. A highly application-dependent function would be

  double average_horse
    (int i, int j)
  {
    return (Horse[i] + Horse[j])/2.0;
  }
which uses Horse as a global array. A slightly less application-dependent function, but still one that is too specific, is
  double array_average
    (int A[], int i, double j)
  {
    return (A[i] + A[j])/2.0;
  }
But why write a function that can only compute the average of two values in an array? Why not write function
  double average
    (double x, double y)
  {
    return (x + y) / 2.0;
  }
Now to compute the average of Horse[i] and Horse[j], write
  avg = average(Horse[i], Horse[j]);

Before writing a function, step back and think about what you are doing.

Function interfaces

The interface of a function tells the function's user everything that he or she needs to know, and no more. There are two parts of the interface.

Prototype. The prototype tells the types of the parameters and the return type. It tells the compiler how the function can be used in a program.

Meaning. The meaning of a function tells what the function does. It is usually a comment, and is called a contract for the function. The contract should tell about all of the parameters, about the return value, and about any changes that the function makes. It should not tell details that are supposed to be encapsulated inside the function, such as how the function does its job. If you put details of how the function works into the contract, then you are not encapsulating them as you should.

You can tell whether a function is application-specific or application-independent by examining its contract. If the description of the function makes reference to where this function fits into the current application (for example, by saying that this function is called by function jam, which handles part three of the application) then the function is application-specific. If the description only tells what the function does, without reference to who calls it or exactly how it is used in the application, then the function is often application-independent.

Function implementations

The implementation of a function tells how the function works. It is written in C/C++, and gives all of the detail. See the examples below.

The term implementation is not used to refer to using a function. When you use a function, you are said to call it, or to apply it. Implementing a function is the act of writing its body.

Example:
Primality checking

This is a pure function. We provide the interface, consisting of the contract above the function definition; the heading of the function; and the implementation, consisting of the body of the function.

Notice that the comment describing the algorithm is in the body, since it describes the implementation, not the interface.

This is an application-independent function. Its description tells what it does, not for what purpose some other part of a program uses it. Notice that the interface says nothing about how the algorithm works, or about what other tools the body uses.

///////////////////////////////////////
// prime(n) returns TRUE if n is prime,
// and FALSE if n is not prime.
///////////////////////////////////////

bool prime(long int n)
{
  //--------------------------------------
  // The algorithm is to divide n by 
  // 2,3,...,k until a factor is found 
  // or n < k*k.  Any time a factor
  // is found, we know that n is not prime.
  //---------------------------------------

  long int k = 2;
  while(n >= k*k) {
    if(0 == n % k) return FALSE;
  }

  //-------------------------------------
  // If we get out of the loop, then 
  // no factor was found, and n must be 
  // prime.
  //-------------------------------------

  return TRUE;
}

Example:
Character replacement in a string
(destructive)

Again, we provide an interface (two parts: contract and prototype) and an implementation. This is an application-independent procedure.

////////////////////////////////////////
// replace_slash_by_colon(s) replaces 
// each '/' in null-terminated string s 
// by ':'.  The change is made in-place.
////////////////////////////////////////

void replace_slash_by_colon(char* s)
{
  char* p = s;
  while(*p != '\0') {
    if('/' == *p) *p = ':';
    p++;
  }
}

Example:
Character replacement in a string
(nondestructive)

This example is similar to the previous example, but the function is pure. The method of allocation is relevant to the interface since the user might need to free the allocated memory, and needs to know how it was allocated in order to free it.

//////////////////////////////////////////
// Parameter s must be a null-terminated 
// string.
//
// Return a pointer to a newly allocated 
// string (allocated using new), where that 
// string is a copy of s, but with each 
// '/' replaced by ':'.
//////////////////////////////////////////

char* slash_replaced_by_colon(const char* s)
{
  //--------------------------------------
  // Pointer p runs through the input 
  // string, s.
  //
  // Pointer q runs throught the copy, n, 
  // where we copy characters from s, or 
  // put ':' in place of a '/'.
  //--------------------------------------

  const char* p = s;
  char* n = new char[strlen(s) + 1];
  char* q = n;

  while(*p != 0) {
   if('/' == *p) *q = ':';
   else *q = *p;
   p++;
   q++;
  }

  *q = '\0';// copy the null. 
  return n;
}

Example:
Character replacement in a string:
(space given)

This function is similar to the preceding two, but instead of allocating memory or making changes in an existing string, this function asks the caller to provide the memory into which the new string will be placed.

///////////////////////////////////////////
// Parameter src must be a null-terminated
// string.
//
// Put, into string dest, a copy of string 
// src, but with each '/' replaced by a ':'.
///////////////////////////////////////////

void replace_slash_by_colon(char* dest, 
                            const char* src)
{
  //----------------------------------------
  // Pointer p runs through the input string,
  // src.
  //
  // Pointer q runs throught the copy, dest, 
  // where we copy characters from src, or 
  // put ':' in place of a '/'.
  //----------------------------------------

  const char* p = src;
  char* q = dest;
  while(*p != 0) {
    if('/' == *p) *q = ':';
    else *q = *p;
    p++;
    q++;
  }
  *q = '\0';  // copy the null. 
}

Pragmatics

A large program must be broken into small functions to make it manageable. How do you decide what those functions should do? Here are some guidelines.

  1. Choose functions so that their interfaces are simple and natural. A function that has ten parameters and does so many different things that its description is pages long is almost useless. A function with a few parameters and a short, simple description is generally much more useful. Do not use this as an excuse, however, to write inadequate contracts. Say everything that needs to be said.

  2. When writing a function F, ask yourself what natural subtasks the function breaks into. Ask yourself what library functions you wish that you had, functions that would make writing F easy. Then write interfaces for those functions. Now write function F, using the functions that you wish you had. Now write the functions that you wish you had, so that you have them. This is called top-down design, and is a very useful design method.

  3. Wherever possible, choose application-independent functions. You will find that they are more useful to you, even in the current application. If you choose your functions wisely, then you will find that a function that you thought would only be useful in one place turns out to be useful elsewhere as well.


Prev Next