Introduction to Programming and C++

Contents Previous Topic Next Topic

Okay, some of you have probably been wondering how we've gotten so far without looking at functions... Well, here they are.

One of the best tools engineers have to solve problems is the divide-and-conquer approach; to take a large problem and to solve it by breaking it up into smaller problems, solving those smaller problems and recombining the answers to solve the larger problem.

A program is a solution to some form of problem, be it a small problem such as calculating a value, or a large problem, such as creating a general word processor which is everything to everyone.

To demonstrate this concept, suppose we are writing a number of mathematical functions, such as sine, cosine, and the exponential function. Mathematically, these functions are expressed by sin(x), cos(x), and ex. Suppose now we wanted to calculate the exponential function for a complex variable z = a + jb, where a and b are real. In this case, we could go back to to the Taylor series and repeat for complex numbers what we did for real numbers, however, we would simply be implementing more complex versions of what we have already implemented.

Instead, we can rely on Euler's formula to simplify our lives:

ez = ea + jb = ea(cos(b) + j sin(b))

In the previous topic on resizing arrays, we go through a number of steps to either double or halve the size of the array. You may also remember that these are always the exact same steps which are followed, no-matter-what. In this case, why must we cut-and-paste this code each time we want to change the size of an array? Instead, could we not write some code somewhere which could do this for all arrays? Not only do we have to write it only in one place, but if we make a mistake, then we have to fix the mistake in only one place.

For example, in the above example of calculating ez, any error in calculating the exponential of a real number would be duplicated in the calculation of the exponential of a complex number.

Functions

The tool for solving these problems is to introduce functions, code which, like cos(x), takes one argument (a real number), performs an operation (such as a Taylor series approximation), and then returns a value (another real number).

When we define the function cos(x), we state that x is a formal parameter: that is, it is a place holder. We use that place holder in our definition of cos(x):

If we actually try to calculate, for example, cos(1), the value 1 is the actual parameter. We would substitute x = 1 into the formula for cos(x) and then return the result. In this case, we might get an approximation which looks like 0.5403023058681397174. This is the return value of the function cos(1).

In C++, we can similarly define a function, however, just as we defined variables to have a given type, we must define the types of the formal parameters and the return type, together with a name and code which must be evaluated.

Example 1

We have already seen such a function: main. Program 1 recalls the first program we saw: Hello World!

Program 1. Printing the value of a variable and its address.

#include <iostream>

using namespace std;

int main() {
	cout << "Hello World!" << endl;

	return 0;
}

We will now examine each of the features:

int
The return type of the function: this function returns an int.
main
The name of the function (how it will be called).
()
The list of formal parameters. In this example, there are no formal parameters, so the set of parentheses is left empty.

{ ... }
This is the body of the function, the instructions which take the parameters and convert them into appropriate return values.
return 0;
The return keyword indicates that the following object should be returned from the function. The type of the object being returned must match the return type, in this case, int.

When you compile a file with a int main() function, the generated executable begins by executing that function. The returned value (in most cases 0) is returned to the operating system.

Example 2

Let us now write a function which calculates the absolute value of an int. We will call this function int abs( int x ) and it is shown in Program 2. The function void main() asks the user for a int and then prints the absolute value of the given int.

Program 2. Requesting a value and printing its absolute value.

#include <iostream>

using namespace std;

int abs( int n ) {
	if ( n >= 0.0 ) {
		return n;
	} else {
		return -n;
	}
}

int main() {
	int input;
	cout << "Enter a number: ";
	cin >> input;

	cout << "|" << input << "| = " << abs( input ) << endl;

	return 0;
}

The compiler uses the type of the parameter n to ensure that the actual parameter input is of the same type (or, at least, can be cast into the stated type). The compiler also uses the return type (int) to ensure that the result is printed correctly (as an int).

Example 3

In this third example, we will see a poor implementation of the cosine function which uses a Taylor series approximation. This is demonstrated in Program 3.

Program 3. Requesting a value and printing its cosine.

#include <iostream>

using namespace std;

double cos( double x ) {
	// use the Taylor series approximation 1 - x^2/2! + x^4/4! - ...
	double result = 1.0;
	double term = 1.0;
	double n = 0.0;

	while ( term != 0.0 ) {
		n += 2.0;
		term *= -x*x/(n - 1)/n;

		result += term;
	}

	return result;
}

int main() {
	double x;
	cout << "Enter a number: ";
	cin >> x;

	cout << "cos( " << x << " ) = " << cos( x ) << endl;

	return 0;
}

Again, the return type double is used to ensure that whatever value is returned is printed as a double.

Example 4

Okay, the last two examples have been math-oriented. Now we will see a function which is related to programming.

Suppose you are writing a large piece of software and need to initialize all the entries of arrays of int to a default value: sometimes 0, sometimes 1, and sometimes -1. You could place a for-loop in every location, however, we will see that you will be writing a lot of code to do this.

Let's consider the Code Fragments 1 and 2.

Code Fragment 3. Initializing an array to -1.

	int array_size = 1000;
	int * array = new int[array_size];

	for ( int i = 0; i < array_size; ++i ) {
		array[i] = -1;
	}

Code Fragment 4. Initializing an array to 0.

	int num_accounts = 325;
	int * account_balance = new int[num_accounts];

	for ( int i = 0; i < num_accounts; ++i ) {
		account_balance[i] = 0;
	}

While examining the for loop, we notice that some things are the same: there is a variable i and entries of an array are being assigned.

Code Fragment 5. Extracting the common features of Code Fragments 3 and 4.

	for ( int i = 0; i < array_limit; ++i ) {
		array_name[i] = initial_value;
	}

We can now create a function as follows:

  1. Pick a name: initialize_array
  2. Determine the return type: in this case, there is nothing to return, so we use void to indicate that there is no return type, and
  3. We have three formal parameters. We list them in order of relevance together with their types: int * array_name, int array_limit, int initial_value.

We now take these pieces and define the function shown in Code Fragment 6.

Code Fragment 6. A function which initializes an array.

#include <iostream>

using namespace std;

void initialize_array( int * array_name, int array_limit, int initial_value ) {
	for ( int i = 0; i < array_limit; ++i ) {
		array_name[i] = initial_value;
	}

	// nothing to return 
}

Feature Creep

Now that we have written a function which initializes an array, you may consider adding another feature: instead of starting at i = 0, perhaps we could let the user specify both the lower and upper limits. In fact, we could add many other features to this function, however, this leads to the next heading:

Purpose of a Function

It should be possible to summarize the purpose of a function in a sentence or two. If a function does too much, chances are that there are smaller functions within the code dying to be separated out. This is part of a process called refactoring, where one step is to recognize common code and to separate it out in one function, as opposed to having multiple copies of the same, or similar code in numerous locations. This performs two tasks: it improves

  1. Understanding: seeing initialize_array( account_balance, num_accounts 0 ); is much easier to understand than trying to read Code Fragment 4.
  2. Maintainability: if there is a bug in your code, this means that it needs to be fixed in only one place, as opposed to possibly requiring numerous fixes in numerous locations.

Here are some examples of re-factorization using code which was submitted during various projects from previous semesters:

You really should read these...

Signature and Prototypes

You will notice that a lot of information about a function can be provided in the first line. From the three examples, we have:

  • int main();
  • int abs( int x );
  • double cos( double x );

These are referred to as the signatures of the functions. This is enough information for the compiler to know:

  1. Is the function being called correctly (with arguments of the correct types), and
  2. Is the return value being correctly dealt with.

In some cases, it is not even necessary to provide the variable of the argument. You may run across a prototype such as:

  • int abs( int );
  • double cos( double );

This indicates that the function is defined elsewhere.

Questions

1. Did you go through the examples of code re-factoring?


Contents Previous Topic Top Next Topic