C++ Week 4

Procedures and functions

Consider the following lines of code that print out a line of characters:

for (int i = 0; i < 20; i++)
   cout << "+-+";
cout << endl;

This is quite a useful procedure, and it would be nice to be able to re-use it throughout a program, rather than keep writing out the same code. This would have several advantages:

Such reusable pieces of code are known as functions. A procedure in C++ is a void function. This is how you define one:

void printline( ) // declares the printline procedure
{  for (int i = 0; i < 20; i++)
      cout << "+-+";
   cout << endl;
}

The function printline() is declared as being void (it has no return value) and it takes no parameters. This is how printline() is called:

int main( )
{  ...
   ...
   printline(); // calls the printline procedure 
   ...
   printline(); // calls it again 
   ...
}

The procedure always prints a line of twenty groups of +-+, and it would be more useful if we could pass an argument to the procedure to specify the number of groups to be printed. The definition would now look like this:

void printline (int len) // define the parameter len
{  for (int i = 0; i < len; i++)
      cout << "+-+";
   cout << endl;
}

and the call to print a line of twelve of the strings would look like this:

printline(12);

When printline() is called, we pass it the argument which the function uses as the value of len. This process is called parameter passing. (Though this is the standard term for it, it's slightly odd as the parameter is on the receiving end - it's the argument, or more strictly the value of the argument, that gets passed.) The argument is an expression which is evaluated and the value is passed to the function. The expression can be a simple integer constant (printline(12)), or a variable (printline(x)) or a more complex expression (printline(x % 3 + y / 5)).

We can now add another parameter that specifies the string of characters that we want to print:

void printline(int len, string s)
{  for (int i = 0; i < len; i++)
      cout << s;
   cout << endl;
}

The call to printline would now include a comma-separated list of arguments like:

 printline(15, "*-*");

Nested loops

Loop statements can be compound, that is, they may contain several discrete statements bracketed by { } so that they are treated as a single statement. It is possible for one of the statements inside a loop to be another loop. For example:

void printsquare( )
{  for (int i = 0; i < 5; i++)
   {  for (int j = 0; j < 5; j++) 
          cout << j << " ";       // prints the numbers: 0 1 2 3 4
      cout << endl;               // prints a line return after the 4
   }
}
The outer for loop (the i loop) executes five times. Each time round, the inner for loop (the j loop) is executed followed by a newline, so the result is five lines with five numbers on each line.

We can modify the procedure so that it takes a parameter that defines the size of the square:

void printsquare (int size)
{  for (int i = 0; i < size; i++)    // goes round the outer loop size times
  {  for (int j = 0; j < size; j++) 
          cout << j << " ";           // prints the numbers: 0 1 2 3 ... size-1 
      cout << endl;                   // prints a line return after the last digit
   }
}

Modifying it to print out a square of stars would be easy. If we also wanted to make the square hollow, we could modify the nested loop as follows:

void printsquare (int n)
{  for (int i = 0; i < n; i++)    // goes round the outer loop n times
   {  for (int j = 0; j < n; j++) 
          if (i == 0 or i == n-1 or j == 0 or j == n-1)
              cout << "*";          
          else cout " ";   
      cout << endl;               // prints a line return after the last star
   }
}

Note that we don't need curly brackets for the body of the j loop. The if .. else is a single statement. The reason we need them for the body of the i loop is that it is composed of two statements - the inner for loop and the statement that outputs the newline.

Finally let us make the procedure even more flexible by using parameters for height and width:

void printrectangle(int rows, int cols)
{  for (int i = 0; i < rows; i++)
   {  for (int j = 0; j < cols; j++)
         cout << "*";
      cout << endl;
   }
}

Generally speaking we use the term procedure to refer to a routine, like the ones above, that simply carries out some task (in C++ its definition begins with void). A function is like a procedure but it returns a value; its definition begins with a type name, e.g. int or double indicating the type of value it returns. Procedure calls are statements that get executed, whereas function calls are expressions that get evaluated.

The hailstone function H(x)

The hailstone function takes an integer parameter and returns an integer. it is defined as follows

For odd values of x, H(x) = (x*3) + 1
For even values of x, H(x) = x/2.

We can define the function in C++ as follows:

int hailstone(int n)
{  if (n % 2 == 1) return n * 3 + 1;
   else return n / 2;
}

Note that a function should always return a value for every valid argument. A call to the hailstone function might look like this:

cin >> x;
while (x > 1)
{  x = hailstone(x);
   cout << x;
}

You can see that, although the definition used n as the parameter name, we used the variable x in the function call. The value of the argument (the value of x) is passed to the function and becomes the initial value of the parameter (n). We can use different names for them (x and n, as here) or we could use the same name for both; it would make no difference. Either way, they are different entities.

Let's look at a function to calculate a percentage. It takes the dividend and divisor and returns the fraction expressed as a percentage:

double percentage (double x, double total)
{  return x / total * 100;
} 

Consider the function finalchar() that takes a string argument and returns its last character as a one-character string:

string finalchar(string s)
{   return s.substr(s.length()-1, 1);
}

Now consider the following boolean function (or predicate) that determines whether the first number is exactly divisible by the second:

bool isdivby (int i1, int i2)
{  if (i1 % i2 == 0) return true;
   else return false;
}

Or, more succinctly:

bool isdivby (int i1, int i2) 
{  return i1 % i2 == 0;
}  

Although return is normally used to specify the return value of a function, you can still use it inside a procedure to exit the procedure; you just don't specify a return value. For example:

void procedure( )
{   if (condition) return;
    else ...;
} 
The reason that you often see procedures without a return is that, in the absence of an explicit return, the procedure returns when it has finished executing, which is generally what you want anyway.

Error handling

A common problem in program design is to decide whether a function should check that the values of the parameters are acceptable or whether it should assume that any such checking has been done before the call and simply accept them. For example, isdivby will crash if the second parameter is zero. Should it check for this or simply assume that it will never be called with zero as the second argument? There is no simple answer to this question; it depends on how the program's work is divided among the separate parts, so it needs to be resolved in the context of the program as a whole.

When programming, it often happens that you can see that a certain problem might arise, but you are not expecting it to arise and you don't want to spend time writing validation routines for it, but you would like to know if it unexpectedly did arise.

C++ has a feature which can help here called assertion: to use it you should #include <cassert>. Assertions perform a test at runtime and if the test is false the program fails and issues a message stating where the failure occurred. To use it with isdivby we would insert a line as follows:

bool isdivby (int i1, int i2) 
{ assert (i2 != 0);
  return i1 % i2 == 0;
}  
If i2 is non-zero, the function proceeds as normal. If, because of some oversight somewhere else in the program, isdivby is called with i2 equal to zero, the program will terminate abruptly with a message telling you that this assertion failed. A typical use is when you are opening files, for example:

assert (not infile.fail()); // issues an error and terminates the program if the file won't open.

assert is useful to give the programmer some helpful feedback in the event that something unexpected occurs which makes it pointless for the program to continue. It is obviously not to be used in general as a method of error handling since it simply halts the program. Exception handling is covered much later in the course.


Notes on R. Mitton's lectures by S.P. Connolly, edited by R. Mitton, 2000