Chapter 5
Introducing Structure into Your Programs

  • How to declare and write your own C++ functions
  • How function arguments are defined and used
  • How to pass arrays to and from a function
  • What pass-by-value means
  • How to pass pointers to functions
  • How to use references as function arguments, and what pass-by-reference means
  • How the const modifier affects function arguments
  • How to return values from a function
  • How to use recursion

You can find the wrox.com code downloads for this chapter on the Download Code tab at www.wrox.com/go/beginningvisualc. The code is in the Chapter 5 download and individually named according to the names throughout the chapter.

UNDERSTANDING FUNCTIONS

Up to now, you haven’t really been able to structure your program code in a modular fashion, because you only know how to construct a program as a single function, main(); but you have been using library functions of various kinds as well as functions belonging to objects. Whenever you write a C++ program, you should have a modular structure in mind from the outset, and as you’ll see, a good understanding of how to implement functions is essential to object-oriented programming.

There’s quite a lot to structuring your C++ programs, so to avoid indigestion, you won’t try to swallow the whole thing in one gulp. After you have chewed over and gotten the full flavor of these morsels, you move on to the next chapter, where you get further into the meat of the topic.

First, I’ll explain the broad principles of how a function works. A function is a self-contained block of code with a specific purpose. A function has a name that both identifies it and is used to call it for execution in a program. The name of a function is global if it is not defined within a namespace, otherwise the name is qualified by the namespace name. The name of a function is not necessarily unique, as you’ll see in the next chapter; however, functions that perform different actions should generally have different names.

The name of a function is governed by the same rules as those for a variable. A function name is, therefore, a sequence of letters and digits, the first of which is a letter, where an underscore (_) counts as a letter. The name of a function should generally reflect what it does, so, for example, you might call a function that counts beans count_beans().

You pass information to a function by means of arguments specified when you invoke it. These arguments must correspond with parameters that appear in the definition of the function. The arguments that you specify replace the parameters used in the definition of the function when the function executes. The code in the function then executes as though it were written using your argument values. Figure 5-1 illustrates the relationship between arguments in the function call and the parameters you specify in the definition of the function.

image

FIGURE 5-1

In Figure 5-1, the add_ints() function returns the sum of the two arguments passed to it. In general, a function returns either a single value to the point in the program where it was called, or nothing at all, depending on how you define the function. You might think that returning a single value from a function is a constraint, but the single value returned can be a pointer that could contain the address of an array, for example. You will learn more about how data is returned from a function a little later in this chapter.

Why Do You Need Functions?

One major advantage that a function offers is that it can be executed as many times as necessary from different points in a program. Without the ability to package a block of code into a function, programs would end up being much larger, because you would typically need to replicate the same code at various points in them. You also use functions to break up a program into easily manageable chunks for development and testing; a program of significant size and complexity that consists of several small blocks of code is much easier to understand and test than if it were written as one large chunk.

Imagine a really big program — let’s say a million lines of code. A program of this size would be virtually impossible to write and debug without functions. Functions enable you to segment the program so that you can write the code piecemeal. You can test each piece independently before bringing it together with the other pieces. This approach also allows the development work to be divided among members of a programming team, with each team member taking responsibility for a tightly specified piece of the program that has a well-defined, functional interface to the rest of the code.

Structure of a Function

As you have seen when writing the function main(), a function consists of a function header that identifies the function, followed by the body of the function between braces that makes up the executable code for the function. Let’s look at an example. You could write a function to raise a value to a given power; that is, to compute the result of multiplying the value x by itself n times, which is xn:

// Function to calculate x to the power n, with n greater than or
// equal to 0
double power(double x, int n)          // Function header
{                                      // Function body starts here...
  double result {1.0};                 // Result stored here
  for(int i {1}; i <= n; i++)
    result *= x;
        
  return result;
}                                      // ...and ends here

The Function Header

Let’s first examine the function header in this example. The following is the first line of the function:

double power(double x, int n)          // Function header

It consists of three parts:

  • The type of the return value (double, in this case)
  • The name of the function (power, in this case)
  • The parameters of the function enclosed between parentheses (x and n, in this case, of types double and int, respectively)

The return value is returned to the calling function when the function is executed, so when the function is called, it results in a value of type double in the expression in which it appears.

Our function has two parameters: x, the value to be raised to a given power, which is of type double, and the value of the power, n, which is of type int. The computation that the function performs is written using these parameter variables together with another variable, result, declared in the body of the function. The parameter names and any variables defined in the body of the function are local to the function.

The General Form of a Function Header

The general form of a function header can be written as follows:

return_type function_name(parameter_list)

The return_type can be any legal type. If the function does not return a value, the return type is specified by the keyword void. The keyword void is also used to indicate the absence of parameters, so a function that has no parameters and doesn’t return a value would have the following function header.

void my_function(void)

An empty parameter list also indicates that a function takes no arguments, so you could omit the keyword void between the parentheses like:

void my_function()

The Function Body

The desired computation in a function is performed by the statements in the function body that follow the function header. The first of these in our power() example declares a variable result that is initialized with the value 1.0. The variable result is local to the function, as are all automatic variables that you declare within the function body. This means that the variable result ceases to exist after the function has completed execution. What might immediately strike you is that if result ceases to exist on completing execution of the function, how is it returned? The answer is that a copy of the value to be returned is made automatically, and this copy is made available to the return point in the program.

The calculation in power() is performed in the for loop. A loop control variable i is declared in the for loop, which assumes successive values from 1 to n. The variable result is multiplied by x once for each loop iteration, so this occurs n times to generate the required value. If n is 0, the statement in the loop won’t be executed at all because the loop continuation condition immediately fails, and so result is left as 1.0.

As I’ve said, the parameters and all the variables declared within the body of a function are local to the function. There is nothing to prevent you from using the same names for variables in other functions for quite different purposes. Indeed, it’s just as well this is so because it would be extremely difficult to ensure variables’ names were always unique within a program containing a large number of functions, particularly if the functions were not all written by the same person.

The scope of variables declared within a function is determined in the same way that I have already discussed. A variable is created at the point at which it is defined and ceases to exist at the end of the block containing it. There is one type of variable that is an exception to this — variables declared as static. I’ll discuss static variables a little later in this chapter.

The return Statement

The return statement returns the value of result to the point where the function was called. The general form of the return statement is

return expression;

where expression must evaluate to a value of the type specified in the function header for the return value. The expression can be any expression you want, as long as you end up with a value of the required type. It can include function calls — even a call of the same function in which it appears, as you’ll see later in this chapter.

If the type of return value has been specified as void, there must be no expression appearing in the return statement. It must be written simply as:

return;

You can also omit the return statement when it is the last statement in the function body and there is no return value.

Alternative Function Syntax

There is an alternative syntax for writing the function header. Here’s an example of the power() function that you saw earlier defined using it:

auto power(double x, int n)-> double   // Function header
{                                      // Function body starts here...
  double result {1.0};                 // Result stored here
  for(int i {1}; i <= n; i++)
    result *= x;
        
  return result;
}                                      // ...and ends here

This will work in exactly the same way as the previous version of the function. The return type of the function appears following the -> in the header. This is referred to as a trailing return type. The auto keyword at the beginning indicates to the compiler that the return type is determined later.

So why was it necessary to introduce the alternative syntax? Isn’t the old syntax good enough? The answer is no. In the next chapter you’ll learn about function templates, where situations can arise when you need to allow for the return type from a function to vary depending on the result of executing the body of the function. You can’t specify that with the old syntax. The alternative function syntax does allow you to do that, as you’ll see in Chapter 6.

Using a Function

At the point at which you use a function in a program, the compiler must know something about it to compile the function call. It needs enough information to be able to identify the function, and to verify that you are using it correctly. If the definition of the function that you intend to use does not appear earlier in the same source file, you must declare the function using a statement called a function prototype.

Function Prototypes

The prototype of a function provides the basic information that the compiler needs to check that you are using the function correctly. It specifies the parameters to be passed to the function, the function name, and the type of the return value — basically, it contains the same information as appears in the function header, with the addition of a semicolon. Clearly, the number of parameters and their types must be the same in the function prototype as they are in the function header in the definition of the function.

A prototype or a definition for each function that you call from within another function must appear before the statements doing the calling. Prototypes are usually placed at the beginning of the program source file. The header files that you’ve been including for standard library functions contain the prototypes of the functions provided by the library, amongst other things.

For the power() function example, you could write the prototype as:

double power(double value, int index);

Note that I have specified names for the parameters in the function prototype that are different from those I used in the function header when I defined the function. This is just to indicate that it’s possible. Most often, the same names are used in the prototype and in the function header in the definition of the function, but this doesn’t have to be so. You can use longer, more expressive parameter names in the function prototype to aid understanding of the significance of the parameters, and then use shorter parameter names in the function definition where the longer names would make the code in the body of the function less readable.

If you like, you can even omit the names altogether in the prototype, and just write:

double power(double, int);

This provides enough information for the compiler to do its job; however, it’s better practice to use some meaningful name in a prototype because it aids readability and, in some cases, makes all the difference between clear code and confusing code. If you have a function with two parameters of the same type (suppose our index was also of type double in the function power(), for example), the use of suitable names indicates clearly which parameter appears first and which second. Without parameter names it would be impossible to tell.

PASSING ARGUMENTS TO A FUNCTION

It’s very important to understand how arguments are passed to a function, because it affects how you write functions and how they ultimately operate. There are also a number of pitfalls to be avoided, so we’ll look at the mechanism for this quite closely.

The arguments you specify when a function is called should usually correspond in type and sequence to the parameters that appear in the definition of the function. As you saw in the last example, if the type of an argument you specify in a function call doesn’t correspond with the type of the parameter in the function definition, the compiler arranges for the argument to be converted to the required type, obeying the same rules as those for converting operands that I discussed in Chapter 2. If the conversion is not possible, you get an error message from the compiler. However, even if the conversion is possible and the code compiles, it could result in the loss of data (for example, a conversion from type long to type short) and should therefore be avoided.

There are two mechanisms used to pass arguments to functions. The first mechanism applies when you specify the parameters in the function definition as ordinary variables (not references). This is called the pass-by-value method of transferring data to a function, so let’s look into that first.

The Pass-by-Value Mechanism

With this mechanism, the variables, constants, or expression values that you specify as arguments are not passed to a function at all. Instead, copies of the argument values are created, and these copies are used as the values to be transferred to the function. Figure 5-3 shows this using the example of our power() function.

image

FIGURE 5-3

In Figure 5-3, the value returned by power() is used to initialize result. Each time you call the power() function, the compiler arranges for copies of the arguments to be stored in temporary location plural in memory. During execution of the function, all references to the function parameters are mapped to these temporary copies of the arguments.

Pointers as Arguments to a Function

When you use a pointer as an argument, the pass-by-value mechanism still operates as before; however, a pointer is an address of another variable, and if you take a copy of this address, the copy still points to the same variable. This is how specifying a pointer as a parameter enables your function to get at a caller argument.

Passing Arrays to a Function

You can pass an array to a function, but in this case, the array is not copied, even though a pass-by-value method of passing arguments still applies. The array name is converted to a pointer, and a copy of the pointer to the beginning of the array is passed by value to the function. This is quite advantageous because copying large arrays is very time-consuming. As you may have worked out, elements of the array may be changed within a function, and thus, an array is the only type that cannot be passed by value.

Passing Multidimensional Arrays to a Function

Passing a multidimensional array to a function is quite straightforward. The following statement declares a two-dimensional array, beans:

double beans[2][4];

You could then write the prototype of a hypothetical function, yield(), like this:

double yield(double beans[2][4]);

When you are defining a multidimensional array as a parameter, you can also omit the first dimension value. Of course, the function needs some way of knowing the extent of the first dimension. For example, you could write this:

double yield(double beans[][4], int index);

Here, the second parameter provides the necessary information about the first dimension. The function can operate with a two-dimensional array, with the value for the first dimension specified by the second argument and with the second dimension fixed at 4.

References as Arguments to a Function

We now come to the second of the two mechanisms for passing arguments to a function. Specifying a parameter to a function as a reference changes the method of passing data for that parameter. The method used is not pass-by-value, where an argument is copied before being transferred to the function, but pass-by-reference, where the parameter acts as an alias for the argument passed. This eliminates any copying of the argument supplied and allows the function to access the caller argument directly. It also means that the de-referencing, which is required when passing and using a pointer to a value, is also unnecessary.

Using reference parameters to a function has particular significance when you are working with objects of a class type. Objects can be large and complex, in which case, the copying process can be very time-consuming. Using reference parameters in these situations can make your code execute considerably faster.

The security you get by using an lvalue reference parameter is all very well, but if the function didn’t modify the value, you wouldn’t want the compiler to create all these error messages every time you passed a reference argument that was a constant. Surely, there ought to be some way to accommodate this? As Ollie would have said, “There most certainly is, Stanley!”

Use of the const Modifier

You can apply the const modifier to a function parameter to tell the compiler that you don’t intend to modify it in any way. This causes the compiler to check that your code indeed does not modify the argument, and there are no error messages when you use a constant argument.

Rvalue Reference Parameters

I’ll now illustrate briefly how parameters that are rvalue reference types differ from parameters that are lvalue reference types. Keep in mind that this won’t be how rvalue references are intended to be used. You’ll learn about that later in the book. Let’s look at an example that is similar to Ex5_07.cpp.

Arguments to main()

You can define main() with no parameters or you can specify a parameter list that allows the main() function to obtain values from the command line from the execute command for the program. Values passed from the command line as arguments to main() are always interpreted as strings. If you want to get data into main() from the command line, you must define it like this:

int main(int argc, char* argv[])
{
  // Code for main()...
}

The first parameter is the count of the number of strings found on the command line, including the program name, and the second parameter is an array that contains pointers to these strings plus an additional element that is null. Thus, argc is always at least 1, because you at least must enter the name of the program. The number of arguments received depends on what you enter on the command line to execute the program. For example, suppose that you execute the DoThat program with the command:

DoThat.exe

There is just the name of the .exe file for the program, so argc is 1 and the argv array contains two elements — argv[0] pointing to the string "DoThat.exe", and argv[1] that contains nullptr.

Suppose you enter this on the command line:

DoThat or else "my friend" 999.9

Now argc is 5 and argv contains six elements, the last element being nullptr and the first five pointing to the strings:

"DoThat" "or" "else" "my friend" "999.9"

You can see from this that if you want to have a string that includes spaces received as a single string, you must enclose it between double quotes. You can also see that numerical values are read as strings, so if you want conversion to the numerical value, that is up to you.

Let’s see it working.

Accepting a Variable Number of Function Arguments

You can define a function so that it allows any number of arguments to be passed to it. You indicate that a variable number of arguments can be supplied by placing an ellipsis (which is three periods, ...) at the end of the parameter list in the function definition. For example:

int sumValues(int first,...)
{
  //Code for the function
}

There must be at least one ordinary parameter, but you can have more. The ellipsis must always be placed at the end of the parameter list.

Obviously, there is no information about the type or number of arguments in the variable list, so your code must figure out what is passed to the function when it is called. The C++ library defines va_start, va_arg, and va_end macros in the cstdarg header to help you do this. It’s easiest to show how these are used with an example.

RETURNING VALUES FROM A FUNCTION

All the example functions that you have created have returned a single value. Is it possible to return anything other than a single value? Well, not directly, but as I said earlier, the single value returned need not be a numeric value; it could also be an address, which provides the key to returning any amount of data. You simply use a pointer. Unfortunately, this also is where the pitfalls start, so you need to keep your wits about you for the adventure ahead.

Returning a Pointer

Returning a pointer value is easy. A pointer value is just an address, so if you want to return the address of some variable value, you can just write the following:

return &value;                     // Returning an address

As long as the function header and function prototype indicate the return type appropriately, you have no problem — or at least, no apparent problem. Assuming that the variable value is of type double, the prototype of a function called treble, which might contain the preceding return statement, could be as follows:

double* treble(double data);

I have defined the parameter list arbitrarily here.

So let’s look at a function that returns a pointer. It’s only fair that I warn you in advance — this function doesn’t work, but it is educational. Let’s assume that you need a function that returns a pointer to a memory location containing three times its argument value. Our first attempt to implement such a function might look like this:

// Function to treble a value - mark 1
double* treble(double data)
{
  double result {};
  result = 3.0*data;
  return &result;
}

A Cast-Iron Rule for Returning Addresses

There is an absolutely cast-iron rule for returning addresses:

Never, ever, return the address of a local automatic variable from a function.

You obviously can’t use a function that doesn’t work, so what can you do to rectify that? You could use a reference parameter and modify the original variable, but that’s not what you set out to do. You are trying to return a pointer to some useful data so that, ultimately, you can return more than a single item of data. One answer lies in dynamic memory allocation (you saw this in action in the previous chapter). With the operator new, you can create a new variable in the free store that continues to exist until it is eventually destroyed by delete — or until the program ends. With this approach, the function looks like this:

// Function to treble a value - mark 2
double* treble(double data)
{
  double* result {new double{}};
  *result = 3.0*data;
  return result;
}

Rather than declaring result to be type double, you now declare it to be of type double* and store in it the address returned by the operator new. Because the result is a pointer, the rest of the function is changed to reflect this, and the address contained in the result is finally returned to the calling program. You could exercise this version by replacing the function in the last working example with this version.

You need to remember that with dynamic memory allocation from within a function such as this, more memory is allocated each time the function is called. The onus is on the calling program to delete the memory when it’s no longer required. It’s easy to forget to do this in practice, with the result that the free store is gradually eaten up until, at some point, it is exhausted and the program fails. As mentioned before, this sort of problem is referred to as a memory leak.

Here you can see how the function would be used. The only necessary change to the original code is to use delete to free the memory as soon as you have finished with the pointer returned by the treble() function.

#include <iostream>
        
using std::cout;
using std::endl;
        
double* treble(double);                  // Function prototype
        
int main()
{
  double num {5.0};                      // Test value
  double* ptr {};                        // Pointer to returned value
        
  ptr = treble(num);
        
  cout << endl << "Three times num = " << 3.0*num;
        
  cout << endl << "Result = " << *ptr;   // Display 3*num
  delete ptr;                            // Don't forget to free the memory
  ptr = nullptr;
  cout << endl;
  return 0;
}
        
// Function to treble a value - mark 2
double* treble(double data)
{
  double* result {new double{}}
  *result = 3.0*data;
  return result;
}

Returning a Reference

You can also return an lvalue reference from a function. This is just as fraught with potential errors as returning a pointer, so you need to take care with this, too. Because an lvalue reference has no existence in its own right (it’s always an alias for something else), you must be sure that the object that it refers to still exists after the function completes execution. It’s very easy to forget this when you use references in a function because they appear to be just like ordinary variables.

References as return types are of primary significance in the context of object-oriented programming. As you will see later in the book, they enable you to do things that would be impossible without them. (This particularly applies to “operator overloading,” which I’ll come to in Chapter 8.) Returning an lvalue reference from a function means that you can use the result of the function on the left side of an assignment statement.

A Cast-Iron Rule: Returning References

A similar rule to the one concerning the return of a pointer from a function also applies to returning references:

Never, ever, return a reference to a local variable from a function.

I’ll leave the topic of returning a reference from a function for now, but I haven’t finished with it yet. I will come back to it again in the context of user-defined types and object-oriented programming, when you will unearth a few more magical things that you can do with references.

Static Variables in a Function

There are some things you can’t do with automatic variables within a function. You can’t count how many times a function is called, for example, because you can’t accumulate a value from one call to the next. There’s more than one way to get around this. For instance, you could use a reference parameter to update a count in the calling program, but this wouldn’t help if the function was called from lots of different places within a program. You could use a global variable that you incremented from within the function, but globals are risky things to use. Because globals can be accessed from anywhere in a program, it is very easy to change them accidentally.

Global variables are also risky in applications that have multiple threads of execution that access them, and you must take special care to manage how globals are accessed from different threads. The basic problem that has to be addressed when more than one thread can access a global variable is that one thread can change the value of a global variable while another thread is working with it. The best solution in such circumstances is to avoid the use of global variables altogether.

To create a variable whose value persists from one call of a function to the next, you can declare a variable within a function as static. You use exactly the same form of declaration for a static variable that you saw in Chapter 2. For example, to declare a variable count as static, you could use this statement:

static int count {};

This also initializes the variable to zero.

RECURSIVE FUNCTION CALLS

When a function contains a call to itself, it’s referred to as a recursive function. A recursive function call can also be indirect, where a function fun1 calls a function fun2, which, in turn, calls fun1.

Recursion may seem to be a recipe for an indefinite loop, and if you aren’t careful, it certainly can be. An indefinite loop will lock up your machine and require Ctrl+Alt+Del to end the program, which is always a nuisance. A prerequisite for avoiding an indefinite loop is that the function contains some means of stopping the process.

Unless you have come across the technique before, the sort of things to which recursion may be applied may not be obvious. In physics and mathematics, there are many things that can be thought of as involving recursion. A simple example is the factorial of an integer, which, for a given integer N, is the product 1 × 2 × 3 . . . × N. This is very often the example given to show recursion in operation. Recursion can also be applied to the analysis of programs during the compilation process; however, you will look at something even simpler.

Using Recursion

Unless you have a problem that particularly lends itself to using recursive functions, or if you have no obvious alternative, it’s generally better to use a different approach, such as a loop, because it will be much more efficient than using recusion. Think about what happens with our last example to evaluate a simple product, x multiplied by itself n times. On each call, the compiler generates copies of the two arguments to the function, and also has to keep track of the location to return to when each return is executed. It’s also necessary to arrange to save the contents of various registers in your computer so that they can be used within power(), and, of course, these need to be restored to their original state at each return from the function. With a quite modest depth of recursive call, the overhead will be considerably greater than if you use a loop.

This is not to say you should never use recursion. Where the problem suggests the use of recursive function calls as a solution, it can be an immensely powerful technique, greatly simplifying the code. You’ll see an example where this is the case in the next chapter.

SUMMARY

In this chapter, you learned about the basics of program structure. You should have a good grasp of how functions are defined, how data can be passed to a function, and how results are returned to a calling program. Functions are fundamental to programming in C++, so everything you do from here on will involve using multiple functions in a program.

The use of references as arguments is a very important concept, so make sure you are confident about using them. You’ll see a lot more about references as arguments to functions when you look into object-oriented programming.

EXERCISES

  1. The factorial of 4 (written as 4!) is 4 × 3 × 2 × 1 = 24, and 3! is 3 × 2 × 1 = 6, so it follows that 4! is 4 × 3!, or more generally:
    fact(n) = n*fact(n - 1)
    1. The limiting case is when n is 1, in which case, 1! = 1. Because of this, 0! is defined to be 1. Write a recursive function that calculates factorials, and test it.
  2. Write a function that swaps two integers, using pointers as arguments. Write a program that uses this function and test that it works correctly.
  3. The trigonometry functions (sin(), cos(), and tan()) in the standard cmath library take arguments in radians. Write three equivalent functions, called sind(), cosd(), and tand(), which take arguments in degrees. All arguments and return values should be type double.
  4. Write a program that reads a number (an integer) and a name (less than 15 characters) from the keyboard. Design the program so that the data entry is done in one function, and the output in another. Store the data in the main() function. The program should end when zero is entered for the number. Think about how you are going to pass the data between functions — by value, by pointer, or by reference?
  5. (Advanced) Write a function that, when passed a string consisting of words separated by single spaces, returns the first word; calling it again with an argument of nullptr returns the second word, and so on, until the string has been processed completely, when nullptr is returned. This is a simplified version of the way the C run-time library routine strtok() works. So, when passed the string "one two three", the function returns "one" after the first call, then "two" after the second, and finally "three". Passing it a new string results in the current string being discarded before the function starts on the new string.

WHAT YOU LEARNED IN THIS CHAPTER

TOPIC CONCEPT
Functions Functions should be compact units of code with a well-defined purpose. A typical program will consist of a large number of small functions, rather than a small number of large functions.
Function prototypes Always provide a function prototype for each function defined in your program, positioned before you call that function.
Reference parameters Passing values to a function using a reference can avoid the copying implicit in the pass-by-value transfer of arguments. Parameters that are not modified in a function should be specified as const.
Returning references or pointers When returning a reference or a pointer from a function, ensure that the object being returned has the correct scope. Never return a pointer or a reference to an object that is local to a function.
static variables in a function A static variable that is defined within the body of a function retains its value from one function call to the next.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.116.63.191