In this chapter you’ll learn about the following:
• Function basics
• Function prototypes
• How to pass function arguments by value
• How to design functions to process arrays
• How to use const
pointer parameters
• How to design functions to process text strings
• How to design functions to process structures
• How to design functions to objects of the string
class
• Functions that call themselves (recursion)
• Pointers to functions
Fun is where you find it. Look closely, and you can find it in functions. C++ comes with a large library of useful functions (the standard ANSI C library plus several C++ classes), but real programming pleasure comes with writing your own functions. This chapter and Chapter 8, “Adventures in Functions,” examine how to define functions, convey information to them, and retrieve information from them. After reviewing how functions work, this chapter concentrates on how to use functions in conjunction with arrays, strings, and structures. Finally, it touches on recursion and pointers to functions. If you’ve paid your C dues, you’ll find much of this chapter familiar. But don’t be lulled into a false sense of expertise. C++ has made several additions to what C functions can do, and Chapter 8 deals primarily with those. Meanwhile, let’s attend to the fundamentals.
Let’s review what you’ve already seen about functions. To use a C++ function, you must do the following:
• Provide a function definition.
• Provide a function prototype.
• Call the function.
If you’re using a library function, the function has already been defined and compiled for you. Also, you can use a standard library header file to provide the prototype. All that’s left to do is call the function properly. The examples so far in this book have done that several times. For example, the standard C library includes the strlen()
function for finding the length of the string. The associated standard header file cstring
contains the function prototype for strlen()
and several other string-related functions. This advance work allows you to use the strlen()
function in programs without further worries.
But when you create your own functions, you have to handle all three aspects—defining, prototyping, and calling—yourself. Listing 7.1 shows these steps in a short example.
Here’s the output of the program in Listing 7.1:
main() will call the simple() function:
I'm but a simple function.
This example places a using
directive inside each function definition because each function uses cout
. Alternatively, the program could place a single using
directive above the function definitions.
Let’s take a more detailed look at these steps now.
You can group functions into two categories: those that don’t have return values and those that do. Functions without return values are termed type void
functions and have the following general form:
void functionName(parameterList)
{
statement(s)
return; // optional
}
Here parameterList
specifies the types and number of arguments (parameters) passed to the function. This chapter more fully investigates this list later. The optional return statement marks the end of the function. Otherwise, the function terminates at the closing brace. Type void
functions correspond to Pascal procedures, FORTRAN subroutines, and modern BASIC subprogram procedures. Typically, you use a void
function to perform some sort of action. For example, a function to print Cheers! a given number (n
) of times could look like this:
void cheers(int n) // no return value
{
using namespace std;
for (int i = 0; i < n; i++)
cout << "Cheers! ";
cout << endl;
}
The int n
parameter list means that cheers()
expects to have an int
value passed to it as an argument when you call this function.
A function with a return value produces a value that it returns to the function that called it. In other words, if the function returns the square root of 9.0 (sqrt(9.0)
), the function call has the value 3.0
. Such a function is declared as having the same type as the value it returns. Here is the general form:
typeName functionName(parameterList)
{
statements
return value; // value is type cast to type typeName
}
Functions with return values require that you use a return statement so that the value is returned to the calling function. The value itself can be a constant, a variable, or a more general expression. The only requirement is that the expression reduce to a value that has, or is convertible to, the typeName
type. (If the declared return type is, say, double
, and the function returns an int
expression, the int
value is type cast to type double
.) The function then returns the final value to the function that called it. C++ does place a restriction on what types you can use for a return value: The return value cannot be an array. Everything else is possible—integers, floating-point numbers, pointers, and even structures and objects! (Interestingly, even though a C++ function can’t return an array directly, it can return an array that’s part of a structure or object.)
As a programmer, you don’t need to know how a function returns a value, but knowing the method might clarify the concept for you. (Also, it gives you something to talk about with your friends and family.) Typically, a function returns a value by copying the return value to a specified CPU register or memory location. Then, the calling program examines that location. Both the returning function and the calling function have to agree on the type of data at that location. The function prototype tells the calling program what to expect, and the function definition tells the called program what to return (see Figure 7.1). Providing the same information in the prototype as in the definition might seem like extra work, but it makes good sense. Certainly, if you want a courier to pick up something from your desk at the office, you enhance the odds of the task being done right if you provide a description of what you want both to the courier and to someone at the office.
Figure 7.1. A typical return value mechanism.
A function terminates after executing a return statement. If a function has more than one return statement—for example, as alternatives to different if else
selections—the function terminates after it executes the first return statement it reaches. For example, in the following example, the else
isn’t needed, but it does help the casual reader understand the intent:
int bigger(int a, int b)
{
if (a > b )
return a; // if a > b, function terminates here
else
return b; // otherwise, function terminates here
}
Functions with return values are much like functions in Pascal, FORTRAN, and BASIC. They return a value to the calling program, which can then assign that value to a variable, display the value, or otherwise use it. Here’s a simple example that returns the cube of a type double
value:
double cube(double x) // x times x times x
{
return x * x * x; // a type double value
}
For example, the function call cube(1.2)
returns the value 1.728
. Note that this return statement uses an expression. The function computes the value of the expression (1.728
, in this case) and returns the value.
By now you are familiar with making function calls, but you may be less comfortable with function prototyping because that’s often been hidden in the include
files. Listing 7.2 shows the cheers()
and cube()
functions used in a program; notice the function prototypes.
The program in Listing 7.2 places a using
directive in only those functions that use the members of the std
namespace. Here’s a sample run:
Cheers! Cheers! Cheers! Cheers! Cheers!
Give me a number: 5
A 5-foot cube has a volume of 125 cubic feet.
Cheers! Cheers! Cheers! Cheers! Cheers! Cheers! Cheers! Cheers!
Note that main()
calls the type void
function cheers()
by using the function name and arguments followed by a semicolon: cheers(5);
. This is an example of a function call statement. But because cube()
has a return value, main()
can use it as part of an assignment statement:
double volume = cube(side);
But I said earlier that you should concentrate on the prototypes. What should you know about prototypes? First, you should understand why C++ requires prototypes. Then, because C++ requires prototypes, you should know the proper syntax. Finally, you should appreciate what the prototype does for you. Let’s look at these points in turn, using Listing 7.2 as a basis for discussion.
A prototype describes the function interface to the compiler. That is, it tells the compiler what type of return value, if any, the function has, and it tells the compiler the number and type of function arguments. Consider, for example, how a prototype affects this function call from Listing 7.2:
double volume = cube(side);
First, the prototype tells the compiler that cube()
should have one type double
argument. If the program fails to provide the argument, prototyping allows the compiler to catch the error. Second, when the cube()
function finishes its calculation, it places its return value at some specified location—perhaps in a CPU register, perhaps in memory. Then, the calling function, main()
in this case, retrieves the value from that location. Because the prototype states that cube()
is type double
, the compiler knows how many bytes to retrieve and how to interpret them. Without that information, the compiler could only guess, and that is something compilers won’t do.
Still, you might wonder, why does the compiler need a prototype? Can’t it just look further in the file and see how the functions are defined? One problem with that approach is that it is not very efficient. The compiler would have to put compiling main()
on hold while searching the rest of the file. An even more serious problem is the fact that the function might not even be in the file. C++ allows you to spread a program over several files, which you can compile independently and then combine later. In such a case, the compiler might not have access to the function code when it’s compiling main()
. The same is true if the function is part of a library. The only way to avoid using a function prototype is to place the function definition before its first use. That is not always possible. Also, the C++ programming style is to put main()
first because it generally provides the structure for the whole program.
A function prototype is a statement, so it must have a terminating semicolon. The simplest way to get a prototype is to copy the function header from the function definition and add a semicolon. That’s what the program in Listing 7.2 does for cube()
:
double cube(double x); // add ; to header to get prototype
However, the function prototype does not require that you provide names for the variables; a list of types is enough. The program in Listing 7.2 prototypes cheers()
by using only the argument type:
void cheers(int); // okay to drop variable names in prototype
In general, you can either include or exclude variable names in the argument lists for prototypes. The variable names in the prototype just act as placeholders, so if you do use names, they don’t have to match the names in the function definition.
You’ve seen that prototypes help the compiler. But what do they do for you? They greatly reduce the chances of program errors. In particular, prototypes ensure the following:
• The compiler correctly handles the function return value.
• The compiler checks that you use the correct number of function arguments.
• The compiler checks that you use the correct type of arguments. If you don’t, it converts the arguments to the correct type, if possible.
We’ve already discussed how to correctly handle the return value. Let’s look now at what happens when you use the wrong number of arguments. For example, suppose you make the following call:
double z = cube();
Without function prototyping, the compiler lets this go by. When the function is called, it looks where the call to cube()
should have placed a number and uses whatever value happens to be there. This is how C worked before ANSI C borrowed prototyping from C++. Because prototyping is optional for ANSI C, this is how some C programs still work. But in C++ prototyping is not optional, so you are guaranteed protection from that sort of error.
Next, suppose you provide an argument but it is the wrong type. In C, this could create weird errors. For example, if a function expects a type int
value (assume that’s 16 bits) and you pass a double
(assume that’s 64 bits), the function looks at just the first 16 bits of the 64 and tries to interpret them as an int
value. However, C++ automatically converts the value you pass to the type specified in the prototype, provided that both are arithmetic types. For example, Listing 7.2 manages to get two type mismatches in one statement:
cheers(cube(2));
First, the program passes the int
value of 2
to cube()
, which expects type double
. The compiler, noting that the cube()
prototype specifies a type double
argument, converts 2
to 2.0
, a type double
value. Then, cube()
returns a type double
value (8.0
) to be used as an argument to cheers()
. Again, the compiler checks the prototypes and notes that cheers()
requires an int
. It converts the return value to the integer 8
. In general, prototyping produces automatic type casts to the expected types. (Function overloading, discussed in Chapter 8, can create ambiguous situations, however, that prevent some automatic type casts.)
Automatic type conversion doesn’t head off all possible errors. For example, if you pass a value of 8.33E27
to a function that expects an int
, such a large value cannot be converted correctly to a mere int
. Some compilers warn you of possible data loss when there is an automatic conversion from a larger type to a smaller.
Also, prototyping results in type conversion only when it makes sense. It won’t, for example, convert an integer to a structure or pointer.
Prototyping takes place during compile time and is termed static type checking. Static type checking, as you’ve just seen, catches many errors that are much more difficult to catch during runtime.
It’s time to take a closer look at function arguments. C++ normally passes arguments by value. That means the numeric value of the argument is passed to the function, where it is assigned to a new variable. For example, Listing 7.2 has this function call:
double volume = cube(side);
Here side
is a variable that, in the sample run, had the value 5
. The function header for cube()
, recall, was this:
double cube(double x)
When this function is called, it creates a new type double
variable called x
and assigns the value 5
to it. This insulates data in main()
from actions that take place in cube()
because cube()
works with a copy of side
rather than with the original data. You’ll see an example of this protection soon. A variable that’s used to receive passed values is called a formal argument or formal parameter. The value passed to the function is called the actual argument or actual parameter. To simplify matters a bit, the C++ Standard uses the word argument by itself to denote an actual argument or parameter and the word parameter by itself to denote a formal argument or parameter. Using this terminology, argument passing assigns the argument to the parameter. (See Figure 7.2.)
Variables, including parameters, declared within a function are private to the function. When a function is called, the computer allocates the memory needed for these variables. When the function terminates, the computer frees the memory that was used for those variables. (Some C++ literature refers to this allocating and freeing of memory as creating and destroying variables. That does make it sound much more exciting.) Such variables are called local variables because they are localized to the function. As mentioned previously, this helps preserve data integrity. It also means that if you declare a variable called x
in main()
and another variable called x
in some other function, these are two distinct, unrelated variables, much as the Albany in California is distinct from the Albany in New York. (See Figure 7.3.) Such variables are also termed automatic variables because they are allocated and deallocated automatically during program execution.
A function can have more than one argument. In the function call, you just separate the arguments with commas:
n_chars('R', 25);
This passes two arguments to the function n_chars()
, which will be defined shortly.
Similarly, when you define the function, you use a comma-separated list of parameter declarations in the function header:
void n_chars(char c, int n) // two arguments
This function header states that the function n_chars()
takes one type char
argument and one type int
argument. The parameters c
and n
are assigned the values passed to the function. If a function has two parameters of the same type, you have to give the type of each parameter separately. You can’t combine declarations the way you can when you declare regular variables:
void fifi(float a, float b) // declare each variable separately
void fufu(float a, b) // NOT acceptable
As with other functions, you just add a semicolon to get a prototype:
void n_chars(char c, int n); // prototype, style 1
As with single arguments, you don’t have to use the same variable names in the prototype as in the definition, and you can omit the variable names in the prototype:
void n_chars(char, int); // prototype, style 2
However, providing variable names can make the prototype more understandable, particularly if two parameters are the same type. Then, the names can remind you which argument is which:
double melon_density(double weight, double volume);
Listing 7.3 shows an example of a function with two arguments. It also illustrates how changing the value of a formal parameter in a function has no effect on the data in the calling program.
The program in Listing 7.3 illustrates placing a using
directive above the function definitions rather than within the functions. Here is a sample run:
Enter a character: W
Enter an integer: 50
WWWWWWWWWWWWWWWWW
WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW
Enter another character or press the q-key to quit: a
Enter an integer: 20
aaaaaaaaaaaaaaaaaaaa
Enter another character or press the q-key to quit: q
The value of times is 20.
Bye
The main()
function in Listing 7.3 uses a while
loop to provide repeated input (and to keep your loop skills fresh). Note that it uses cin >> ch
rather than cin.get(ch)
or ch = cin.get()
to read a character. There’s a good reason for this. Recall that the two cin.get()
functions read all input characters, including spaces and newlines, whereas cin >>
skips spaces and newlines. When you respond to the program prompt, you have to press Enter at the end of each line, thus generating a newline character. The cin >> ch
approach conveniently skips over these newlines, but the cin.get()
siblings read the newline following each number entered as the next character to display. You can program around this nuisance, but it’s simpler to use cin
as the program in Listing 7.3 does.
The n_chars()
function takes two arguments: a character c
and an integer n
. It then uses a loop to display the character the number of times the integer specifies:
while (n-- > 0) // continue until n reaches 0
cout << c;
Notice that the program keeps count by decrementing the n
variable, where n
is the formal parameter from the argument list. This variable is assigned the value of the times
variable in main()
. The while
loop then decreases n
to 0
, but, as the sample run demonstrates, changing the value of n
has no effect on times
.
Let’s create a more ambitious function—one that performs a nontrivial calculation. Also, the function will illustrate the use of local variables other than the function’s formal arguments.
Many states in the United States now sponsor a lottery with some form of Lotto game. Lotto lets you pick a certain number of choices from a card. For example, you might get to pick 6 numbers from a card having 51 numbers. Then, the Lotto managers pick 6 numbers at random. If your choice exactly matches theirs, you win a few million dollars or so. Our function will calculate the probability that you have a winning pick. (Yes, a function that successfully predicts the winning picks themselves would be more useful, but C++, although powerful, has yet to implement psychic faculties.)
First, you need a formula. If you have to pick 6 values out of 51, mathematics says that you have 1 chance in R
of winning, where the following formula gives R
:
For 6 choices, the denominator is the product of the first 6 integers, or 6 factorial. The numerator is also the product of 6 consecutive numbers, this time starting with 51 and going down. More generally, if you pick picks
values out of numbers
numbers, the denominator is picks
factorial and the numerator is the product of picks
integers, starting with the value numbers
and working down. You can use a for
loop to make that calculation:
long double result = 1.0;
for (n = numbers, p = picks; p > 0; n--, p--)
result = result * n / p ;
Rather than multiply all the numerator terms first, the loop begins by multiplying 1.0
by the first numerator term and then dividing by the first denominator term. Then, in the next cycle, the loop multiplies and divides by the second numerator and denominator terms. This keeps the running product smaller than if you did all the multiplication first. For example, compare
(10 * 9) / (2 * 1)
with
(10 / 2) * (9 / 1)
The first evaluates to 90 / 2 and then to 45, whereas the second evaluates to 5 × 9 and then to 45. Both give the same answer, but the first method produces a larger intermediate value (90) than does the second. The more factors you have, the bigger the difference gets. For large numbers, this strategy of alternating multiplication with division can keep the calculation from overflowing the maximum possible floating-point value.
Listing 7.4 incorporates this formula into a probability()
function. Because the number of picks and the total number of choices should be positive values, the program uses the unsigned int
type (unsigned
, for short) for those quantities. Multiplying several integers can produce pretty large results, so lotto.cpp
uses the long double
type for the function’s return value. Also, terms such as 49 / 6 produce a truncation error for integer types.
Some C++ implementations don’t support type long double
. If your implementation falls into that category, try ordinary double
instead.
Here’s a sample run of the program in Listing 7.4:
Notice that increasing the number of choices on the game card greatly increases the odds against winning.
The probability()
function in Listing 7.4 illustrates two kinds of local variables you can have in a function. First, there are the formal parameters (numbers
and picks
), which are declared in the function header before the opening brace. Then come the other local variables (result
, n
, and p
). They are declared in between the braces bounding the function definition. The main difference between the formal parameters and the other local variables is that the formal parameters get their values from the function that calls probability()
, whereas the other variables get values from within the function.
So far the sample functions in this book have been simple, using only the basic types for arguments and return values. But functions can be the key to handling more involved types, such as arrays and structures. Let’s take a look now at how arrays and functions get along with each other.
Suppose you use an array to keep track of how many cookies each person has eaten at a family picnic. (Each array index corresponds to a person, and the value of the element corresponds to the number of cookies that person has eaten.) Now you want the total. That’s easy to find; you just use a loop to add all the array elements. But adding array elements is such a common task that it makes sense to design a function to do the job. Then, you won’t have to write a new loop every time you have to sum an array.
Let’s consider what the function interface involves. Because the function calculates a sum, it should return the answer. If you keep your cookies intact, you can use a function with a type int
return value. So that the function knows what array to sum, you want to pass the array name as an argument. And to make the function general so that it is not restricted to an array of a particular size, you pass the size of the array. The only new ingredient here is that you have to declare that one of the formal arguments is an array name. Let’s see what that and the rest of the function header look like:
int sum_arr(int arr[], int n) // arr = array name, n = size
This looks plausible. The brackets seem to indicate that arr
is an array, and the fact that the brackets are empty seems to indicate that you can use the function with an array of any size. But things are not always as they seem: arr
is not really an array; it’s a pointer! The good news is that you can write the rest of the function just as if arr
were an array. First, let’s ensure that this approach works, and then let’s look into why it works.
Listing 7.5 illustrates using a pointer as if it were an array name. The program initializes the array to some values and uses the sum_arr()
function to calculate the sum. Note that the sum_arr()
function uses arr
as if it were an array name.
Here is the output of the program in Listing 7.5:
Total cookies eaten: 255
As you can see, the program works. Now let’s look at why it works.
The key to the program in Listing 7.5 is that C++, like C, in most contexts treats the name of an array as if it were a pointer. Recall from Chapter 4, “Compound Types,” that C++ interprets an array name as the address of its first element:
cookies == &cookies[0] // array name is address of first element
(There are two exceptions to this rule. First, the array declaration uses the array name to label the storage. Second, applying sizeof
to an array name yields the size of the whole array, in bytes.)
Listing 7.5 makes the following function call:
int sum = sum_arr(cookies, ArSize);
Here cookies
is the name of an array, hence by C++ rules cookies
is the address of the array’s first element. The function passes an address. Because the array has type int
elements, cookies
must be type pointer-to-int
, or int *
. This suggests that the correct function header should be this:
int sum_arr(int * arr, int n) // arr = array name, n = size
Here int *arr
has replaced int arr[]
. It turns out that both headers are correct because in C++ the notations int *arr
and int arr[]
have the identical meaning when (and only when) used in a function header or function prototype. Both mean that arr
is a pointer-to-int
. However, the array notation version (int arr[]
) symbolically reminds you that arr
not only points to an int
, it points to the first int
in an array of int
s. This book uses the array notation when the pointer is to the first element of an array, and it uses the pointer notation when the pointer is to an isolated value. Remember that the notations int *arr
and int arr[]
are not synonymous in any other context. For example, you can’t use the notation int tip[]
to declare a pointer in the body of a function.
Given that the variable arr
actually is a pointer, the rest of the function makes sense. As you might recall from the discussion of dynamic arrays in Chapter 4, you can use the bracket array notation equally well with array names or with pointers to access elements of an array. Whether arr
is a pointer or an array name, the expression arr[3]
means the fourth element of the array. And it probably will do no harm at this point to remind you of the following two identities:
arr[i] == *(ar + i) // values in two notations
&arr[i] == ar + i // addresses in two notations
Remember that adding one to a pointer, including an array name, actually adds a value equal to the size, in bytes, of the type to which the pointer points. Pointer addition and array subscription are two equivalent ways of counting elements from the beginning of an array.
Let’s look at the implications of Listing 7.5. The function call sum_arr(cookies, ArSize)
passes the address of the first element of the cookies
array and the number of elements of the array to the sum_arr()
function. The sum_arr()
function assigns the cookies
address to the pointer variable arr
and assigns ArSize
to the int
variable n
. This means Listing 7.5 doesn’t really pass the array contents to the function. Instead, it tells the function where the array is (the address), what kind of elements it has (the type), and how many elements it has (the n
variable). (See Figure 7.4.) Armed with this information, the function then uses the original array. If you pass an ordinary variable, the function works with a copy. But if you pass an array, the function works with the original. Actually, this difference doesn’t violate C++’s pass-by-value approach. The sum_arr()
function still passes a value that’s assigned to a new variable. But that value is a single address, not the contents of an array.
Figure 7.4. Telling a function about an array.
Is the correspondence between array names and pointers a good thing? Indeed, it is. The design decision to use array addresses as arguments saves the time and memory needed to copy an entire array. The overhead for using copies can be prohibitive if you’re working with large arrays. With copies, not only does a program need more computer memory, but it has to spend time copying large blocks of data. On the other hand, working with the original data raises the possibility of inadvertent data corruption. That’s a real problem in classic C, but ANSI C and C++’s const
modifier provides a remedy. You’ll soon see an example. But first, let’s alter Listing 7.5 to illustrate some points about how array functions operate. Listing 7.6 demonstrates that cookies
and arr
have the same value. It also shows how the pointer concept makes the sum_arr
function more versatile than it may have appeared at first. To provide a bit of variety and to show you what it looks like, the program uses the std::
qualifier instead of the using
directive to provide access to cout
and endl
.
Here’s the output of the program in Listing 7.6:
0x0065fd24 = array address, 32 = sizeof cookies
0x0065fd24 = arr, 4 = sizeof arr
Total cookies eaten: 255
0x0065fd24 = arr, 4 = sizeof arr
First three eaters ate 7 cookies.
0x0065fd34 = arr, 4 = sizeof arr
Last four eaters ate 240 cookies.
Note that the address values and the array and integer sizes will vary from system to system. Also, some implementations will display the addresses in base 10 notation instead of in hexadecimal.
Listing 7.6 illustrates some very interesting points about array functions. First, note that cookies
and arr
both evaluate to the same address, exactly as claimed. But sizeof cookies
is 16
, whereas sizeof arr
is only 4
. That’s because sizeof cookies
is the size of the whole array, whereas sizeof arr
is the size of the pointer variable. (This program execution takes place on a system that uses 4-byte addresses.) By the way, this is why you have to explicitly pass the size of the array rather than use sizeof arr
in sum_arr()
.
Because the only way sum_arr()
knows the number of elements in the array is through what you tell it with the second argument, you can lie to the function. For example, the second time the program uses the function, it makes this call:
sum = sum_arr(cookies, 3);
By telling the function that cookies
has just three elements, you get the function to calculate the sum of the first three elements.
Why stop there? You can also lie about where the array starts:
sum = sum_arr(cookies + 4, 4);
Because cookies
acts as the address of the first element, cookies + 4
acts as the address of the fifth element. This statement sums the fifth, sixth, seventh, and eighth elements of the array. Note in the output how the third call to the function assigns a different address to arr
than the first two calls did. And yes, you can use &cookies[4]
instead of cookies + 4
as the argument; they both mean the same thing.
To indicate the kind of array and the number of elements to an array-processing function, you pass the information as two separate arguments:
void fillArray(int arr[], int size); // prototype
Don’t try to pass the array size by using brackets notation:
void fillArray(int arr[size]); // NO -- bad prototype
When you choose to use an array to represent data, you are making a design decision. But design decisions should go beyond how data is stored; they should also involve how the data is used. Often, you’ll find it profitable to write specific functions to handle specific data operations. (The profits here include increased program reliability, ease of modification, and ease of debugging.) Also, when you begin integrating storage properties with operations when you think about a program, you are taking an important step toward the OOP mind-set; that, too, might prove profitable in the future.
Let’s examine a simple case. Suppose you want to use an array to keep track of the dollar values of your real estate. (If necessary, suppose you have real estate.) You have to decide what type to use. Certainly, double
is less restrictive in its range than int
or long
, and it provides enough significant digits to represent the values precisely. Next, you have to decide on the number of array elements. (With dynamic arrays created with new
, you can put off that decision, but let’s keep things simple.) Let’s say that you have no more than five properties, so you can use an array of five doubles
.
Now consider the possible operations you might want to execute with the real estate array. Two very basic ones are reading values into the array and displaying the array contents. Let’s add one more operation to the list: reassessing the value of the properties. For simplicity, assume that all your properties increase or decrease in value at the same rate. (Remember, this is a book on C++, not on real estate management.) Next, fit a function to each operation and then write the code accordingly. We’ll go through the steps of creating these pieces of a program next. Afterward, we’ll fit them into a complete example.
Because a function with an array name argument accesses the original array, not a copy, you can use a function call to assign values to array elements. One argument to the function will be the name of the array to be filled. In general, a program might manage more than one person’s investments, hence more than one array, so you don’t want to build the array size into the function. Instead, you pass the array size as a second argument, as in the previous example. Also, it’s possible that you might want to quit reading data before filling the array, so you want to build that feature in to the function. Because you might enter fewer than the maximum number of elements, it makes sense to have the function return the actual number of values entered. These considerations suggest the following function prototype:
int fill_array(double ar[], int limit);
The function takes an array name argument and an argument specifying the maximum number of items to be read, and the function returns the actual number of items read. For example, if you use this function with an array of five elements, you pass 5
as the second argument. If you then enter only three values, the function returns 3
.
You can use a loop to read successive values into the array, but how can you terminate the loop early? One way is to use a special value to indicate the end of input. Because no property should have a negative value, you can use a negative number to indicate the end of input. Also, the function should do something about bad input, such as terminating further input. Given this, you can code the function as follows:
Note that this code includes a prompt to the user. If the user enters a non-negative value, the value is assigned to the array. Otherwise, the loop terminates. If the user enters only valid values, the loop terminates after it reads limit
values. The last thing the loop does is increment i
, so after the loop terminates, i
is one greater than the last array index, hence it’s equal to the number of filled elements. The function then returns that value.
const
Building a function to display the array contents is simple. You pass the name of the array and the number of filled elements to the function, which then uses a loop to display each element. But there is another consideration—guaranteeing that the display function doesn’t alter the original array. Unless the purpose of a function is to alter data passed to it, you should safeguard it from doing so. That protection comes automatically with ordinary arguments because C++ passes them by value, and the function works with a copy. But functions that use an array work with the original. After all, that’s why the fill_array()
function is able to do its job. To keep a function from accidentally altering the contents of an array argument, you can use the keyword const
(discussed in Chapter 3, “Dealing with Data”) when you declare the formal argument:
void show_array(const double ar[], int n);
The declaration states that the pointer ar
points to constant data. This means that you can’t use ar
to change the data. That is, you can use a value such as ar[0]
, but you can’t change that value. Note that this doesn’t mean that the original array needs be constant; it just means that you can’t use ar
in the show_array()
function to change the data. Thus, show_array()
treats the array as read-only data. Suppose you accidentally violate this restriction by doing something like the following in the show_array()
function:
ar[0] += 10;
In this case, the compiler will put a stop to your wrongful ways. Borland C++, for example, gives an error message like this (edited slightly):
Cannot modify a const object in function
show_array(const double *,int)
Other compilers may choose to express their displeasure in different words.
The message reminds you that C++ interprets the declaration const double ar[]
to mean const double *ar
. Thus, the declaration really says that ar
points to a constant value. We’ll discuss this in detail when we finish with the current example. Meanwhile, here is the code for the show_array()
function:
void show_array(const double ar[], int n)
{
using namespace std;
for (int i = 0; i < n; i++)
{
cout << "Property #" << (i + 1) << ": $";
cout << ar[i] << endl;
}
}
The third operation for the array in this example is multiplying each element by the same revaluation factor. You need to pass three arguments to the function: the factor, the array, and the number of elements. No return value is needed, so the function can look like this:
void revalue(double r, double ar[], int n)
{
for (int i = 0; i < n; i++)
ar[i] *= r;
}
Because this function is supposed to alter the array values, you don’t use const
when you declare ar
.
Now that you’ve defined a data type in terms of how it’s stored (an array) and how it’s used (three functions), you can put together a program that uses the design. Because you’ve already built all the array-handling tools, you’ve greatly simplified programming main()
. Most of the remaining programming work consists of having main()
call the functions you’ve just developed. Listing 7.7 shows the result. It places a using
directive in just those functions that use the iostream
facilities.
Here are two sample runs of the program in Listing 7.7:
Enter value #1: 100000
Enter value #2: 80000
Enter value #3: 222000
Enter value #4: 240000
Enter value #5: 118000
Property #1: $100000
Property #2: $80000
Property #3: $222000
Property #4: $240000
Property #5: $118000
Enter reassessment rate: 1.10
Property #1: $110000
Property #2: $88000
Property #3: $244200
Property #4: $264000
Property #5: $129800
Done.
Enter value #1: 200000
Enter value #2: 84000
Enter value #3: 160000
Enter value #4: -2
Property #1: $200000
Property #2: $84000
Property #3: $160000
Enter reassessment rate: 1.20
Property #1: $240000
Property #2: $100800
Property #3: $192000
Done.
Recall that fill_array()
prescribes that input should quit when the user enters five properties or enters a negative number, whichever comes first. The first output example illustrates reaching the five-property limit, and the second output example illustrates that entering a negative value terminates the input phase.
We’ve already discussed the important programming details related to the real estate example, so let’s reflect on the process. You began by thinking about the data type and designed appropriate functions to handle the data. Then, you assembled these functions into a program. This is sometimes called bottom-up programming because the design process moves from the component parts to the whole. This approach is well suited to OOP, which concentrates on data representation and manipulation first. Traditional procedural programming, on the other hand, leans toward top-down programming, in which you develop a modular grand design first and then turn your attention to the details. Both methods are useful, and both lead to modular programs.
As you’ve seen, C++ functions that process arrays need to be informed about the kind of data in the array, the location of the beginning of the array, and the number of elements in the array. The traditional C/C++ approach to functions that process arrays is to pass a pointer to the start of the array as one argument and to pass the size of the array as a second argument. (The pointer tells the function both where to find the array and the kind of data in it.) That gives the function the information it needs to find all the data.
There is another approach to giving a function the information it needs: to specify a range of elements. This can be done by passing two pointers—one identifying the start of the array and one identifying the end of the array. The C++ Standard Template Library (STL; presented in Chapter 16, “The string
Class and the Standard Template Library”), for example, generalizes the range approach. The STL approach uses the concept of “one past the end” to indicate an extent. That is, in the case of an array, the argument identifying the end of the array would be a pointer to the location just after the last element. For example, suppose you have this declaration:
double elbuod[20];
Then the two pointers elboud
and elboud + 20
define the range. First, elboub
, being the name of the array, points to the first element. The expression elboud + 19
points to the last element (that is, elboud[19]
), so elboud + 20
points to one past the end of the array. Passing a range to a function tells it which elements to process. Listing 7.8 modifies Listing 7.6 to use two pointers to specify a range.
Here’s the output of the program in Listing 7.8:
Total cookies eaten: 255
First three eaters ate 7 cookies.
Last four eaters ate 240 cookies.
In Listing 7.8, notice the for
loop in the sum_array()
function:
for (pt = begin; pt != end; pt++)
total = total + *pt;
It sets pt
to point to the first element to be processed (the one pointed to by begin
) and adds *pt
(the value of the element) to total
. Then the loop updates pt
by incrementing it, causing it to point to the next element. The process continues as long as pt != end
. When pt
finally equals end
, it’s pointing to the location following the last element of the range, so the loop halts.
Second, notice how the different function calls specify different ranges within the array:
int sum = sum_arr(cookies, cookies + ArSize);
...
sum = sum_arr(cookies, cookies + 3); // first 3 elements
...
sum = sum_arr(cookies + 4, cookies + 8); // last 4 elements
The pointer value cookies + ArSize
points to the location following the last element. (The array has ArSize
elements, so cookies[ArSize - 1]
is the last element, and its address is cookies + ArSize - 1
.) So the range cookies, cookies + ArSize
specifies the entire array. Similarly, cookies, cookies + 3
specifies the first three elements, and so on.
Note, by the way, that the rules for pointer subtraction imply that, in sum_arr()
, the expression end - begin
is an integer value equal to the number of elements in the range.
const
Using const
with pointers has some subtle aspects (pointers always seem to have subtle aspects), so let’s take a closer look. You can use the const
keyword two different ways with pointers. The first way is to make a pointer point to a constant object, and that prevents you from using the pointer to change the pointed-to value. The second way is to make the pointer itself constant, and that prevents you from changing where the pointer points. Now for the details.
First, let’s declare a pointer pt
that points to a constant:
int age = 39;
const int * pt = &age;
This declaration states that pt
points to a const int
(39
, in this case). Therefore, you can’t use pt
to change that value. In other words, the value *pt
is const
and cannot be modified:
*pt += 1; // INVALID because pt points to a const int
cin >> *pt; // INVALID for the same reason
Now for a subtle point. This declaration for pt
doesn’t necessarily mean that the value it points to is really a constant; it just means the value is a constant insofar as pt
is concerned. For example, pt
points to age
, and age
is not const
. You can change the value of age
directly by using the age
variable, but you can’t change the value indirectly via the pt
pointer:
*pt = 20; // INVALID because pt points to a const int
age = 20; // VALID because age is not declared to be const
In the past, you’ve assigned the address of a regular variable to a regular pointer. Now you’ve assigned the address of a regular variable to a pointer-to-const
. That leaves two other possibilities: assigning the address of a const
variable to a pointer-to-const
and assigning the address of a const
to a regular pointer. Are they both possible? The first is, and the second isn’t:
const float g_earth = 9.80;
const float * pe = &g_earth; // VALID
const float g_moon = 1.63;
float * pm = &g_moon; // INVALID
For the first case, you can use neither g_earth
nor pe
to change the value 9.80
. C++ doesn’t allow the second case for a simple reason: If you can assign the address of g_moon
to pm
, then you can cheat and use pm
to alter the value of g_moon
. That makes a mockery of g_moon
’s const
status, so C++ prohibits you from assigning the address of a const
to a non-const
pointer. (If you are really desperate, you can use a type cast to override the restriction; see Chapter 15 for a discussion of the const_cast
operator.)
The situation becomes a bit more complex if you have pointers to pointers. As you saw earlier, assigning a non-const
pointer to a const
pointer is okay, provided that you’re dealing with just one level of indirection:
int age = 39; // age++ is a valid operation
int * pd = &age; // *pd = 41 is a valid operation
const int * pt = pd; // *pt = 42 is an invalid operation
But pointer assignments that mix const
and non-const
in this manner are no longer safe when you go to two levels of indirection. If mixing const
and non-const
were allowed, you could do something like this:
const int **pp2;
int *p1;
const int n = 13;
pp2 = &p1; // not allowed, but suppose it were
*pp2 = &n; // valid, both const, but sets p1 to point at n
*p1 = 10; // valid, but changes const n
Here the code assigns a non-const
address (&pl
) to a const
pointer (pp2
), and that allows pl
to be used to alter const
data. So the rule that you can assign a non-const
address or pointer to a const
pointer works only if there is just one level of indirection—for example, if the pointer points to a fundamental data type.
You can assign the address of either const
data or non-const
data to a pointer-to-const
, provided that the data type is not itself a pointer, but you can assign the address of non-const
data only to a non-const
pointer.
Suppose you have an array of const
data:
const int months[12] = {31,28,31,30,31,30, 31, 31,30,31,30,31};
The prohibition against assigning the address of a constant array means that you cannot pass the array name as an argument to a function by using a non-constant formal argument:
int sum(int arr[], int n); // should have been const int arr[]
...
int j = sum(months, 12); // not allowed
This function call attempts to assign a const
pointer (months
) to a non-const
pointer (arr
), and the compiler disallows the function call.
For yet another subtle point, consider the following declarations:
int age = 39;
const int * pt = &age;
The const
in the second declaration only prevents you from changing the value to which pt
points, which is 39
. It doesn’t prevent you from changing the value of pt
itself. That is, you can assign a new address to pt
:
int sage = 80;
pt = &sage; // okay to point to another location
But you still can’t use pt
to change the value to which it points (now 80
).
The second way to use const
makes it impossible to change the value of the pointer itself:
int sloth = 3;
const int * ps = &sloth; // a pointer to const int
int * const finger = &sloth; // a const pointer to int
Note that the last declaration has repositioned the keyword const
. This form of declaration constrains finger
to point only to sloth
. However, it allows you to use finger
to alter the value of sloth
. The middle declaration does not allow you to use ps
to alter the value of sloth
, but it permits you to have ps
point to another location. In short, finger
and *ps
are both const
, and *finger
and ps
are not const
. (See Figure 7.5.)
Figure 7.5. Pointers-to-const
and const
pointers.
If you like, you can declare a const
pointer to a const
object:
double trouble = 2.0E30;
const double * const stick = &trouble;
Here stick
can point only to trouble
, and stick
cannot be used to change the value of trouble
. In short, both stick
and *stick
are const
.
Typically you use the pointer-to-const
form to protect data when you pass pointers as function arguments. For example, recall the show_array()
prototype from Listing 7.5:
void show_array(const double ar[], int n);
Using const
in this declaration means that show_array()
cannot alter the values in any array that is passed to it. This technique works as long as there is just one level of indirection. Here, for example, the array elements are a fundamental type. But if they were pointers or pointers-to-pointers, you wouldn’t use const
.
To write a function that has a two-dimensional array as an argument, you need to remember that the name of an array is treated as its address, so the corresponding formal parameter is a pointer, just as for one-dimensional arrays. The tricky part is declaring the pointer correctly. Suppose, for example, that you start with this code:
int data[3][4] = {{1,2,3,4}, {9,8,7,6}, {2,4,6,8}};
int total = sum(data, 3);
What should the prototype for sum()
look like? And why does the function pass the number of rows (3
) as an argument and not also the number of columns (4
)?
Well, data
is the name of an array with three elements. The first element is, itself, an array of four int
values. Thus, the type of data
is pointer-to-array-of-four-int
, so an appropriate prototype would be this:
int sum(int (*ar2)[4], int size);
The parentheses are needed because the declaration
int *ar2[4]
would declare an array of four pointers-to-int
instead of a single pointer-to-array-of-four-int
, and a function parameter cannot be an array. There’s an alternative format that means exactly the same thing as this first prototype, but, perhaps, is easier to read:
int sum(int ar2[][4], int size);
Either prototype states that ar2
is a pointer, not an array. Also note that the pointer type specifically says it points to an array of four int
s. Thus, the pointer type specifies the number of columns, which is why the number of columns is not passed as a separate function argument.
Because the pointer type specifies the number of columns, the sum()
function only works with arrays with four columns. But the number of rows is specified by the variable size, so sum()
can work with a varying number of rows:
int a[100][4];
int b[6][4];
...
int total1 = sum(a, 100); // sum all of a
int total2 = sum(b, 6); // sum all of b
int total3 = sum(a, 10); // sum first 10 rows of a
int total4 = sum(a+10, 20); // sum next 20 rows of a
Given that the parameter ar2
is a pointer to an array, how do you use it in the function definition? The simplest way is to use ar2
as if it were the name of a two-dimensional array. Here’s a possible function definition:
int sum(int ar2[][4], int size)
{
int total = 0;
for (int r = 0; r < size; r++)
for (int c = 0; c < 4; c++)
total += ar2[r][c];
return total;
}
Again, note that the number of rows is whatever is passed to the size
parameter, but the number of columns is fixed at four, both in the parameter declaration for ar2
and in the inner for
loop.
Here’s why you can use array notation. Because ar2
points to the first element (element 0) of an array whose elements are array-of-four-int
, the expression ar2 + r
points to element number r
. Therefore ar2[r]
is element number r
. That element is itself an array-of-four-int
, so ar2[r]
is the name of that array-of-four-int
. Applying a subscript to an array name gives an array element, so ar2[r][c]
is an element of the array-of-four-int
, hence is a single int
value. The pointer ar2
has to be dereferenced twice to get to the data. The simplest way is to use brackets twice, as in ar2[r][c]
. But it is possible, if ungainly, to use the *
operator twice:
ar2[r][c] == *(*(ar2 + r) + c) // same thing
To understand this, you can work out the meaning of the subexpressions from the inside out:
ar2 // pointer to first row of an array of 4 int
ar2 + r // pointer to row r (an array of 4 int)
*(ar2 + r) // row r (an array of 4 int, hence the name of an array,
// thus a pointer to the first int in the row, i.e., ar2[r]
*(ar2 +r) + c // pointer int number c in row r, i.e., ar2[r] + c
*(*(ar2 + r) + c // value of int number c in row r, i.e. ar2[r][c]
Incidentally, the code for sum()
doesn’t use const
in declaring the parameter ar2
because that technique is for pointers to fundamental types, and ar2
is a pointer to a pointer.
Recall that a C-style string consists of a series of characters terminated by the null character. Much of what you’ve learned about designing array functions applies to string functions, too. For example, passing a string as an argument means passing an address, and you can use const
to protect a string argument from being altered. But there are a few special twists to strings that we’ll unravel now.
Suppose you want to pass a string as an argument to a function. You have three choices for representing a string:
• An array of char
• A quoted string constant (also called a string literal)
• A pointer-to-char
set to the address of a string
All three choices, however, are type pointer-to-char
(more concisely, type char *
), so you can use all three as arguments to string-processing functions:
char ghost[15] = "galloping";
char * str = "galumphing";
int n1 = strlen(ghost); // ghost is &ghost[0]
int n2 = strlen(str); // pointer to char
int n3 = strlen("gamboling"); // address of string
Informally, you can say that you’re passing a string as an argument, but you’re really passing the address of the first character in the string. This implies that a string function prototype should use type char *
as the type for the formal parameter representing a string.
One important difference between a C-style string and a regular array is that the string has a built-in terminating character. (Recall that a char
array containing characters but no null character is just an array and not a string.) That means you don’t have to pass the size of the string as an argument. Instead, the function can use a loop to examine each character in the string in turn until the loop reaches the terminating null character. Listing 7.9 illustrates that approach with a function that counts the number of times a given character appears in a string.
Here’s the output of the program in Listing 7.9:
3 m characters in minimum
2 u characters in ululate
Because the c_int_str()
function in Listing 7.9 shouldn’t alter the original string, it uses the const
modifier when it declares the formal parameter str
. Then, if you mistakenly let the function alter part of the string, the compiler catches your error. Of course, you can use array notation instead to declare str
in the function header:
int c_in_str(const char str[], char ch) // also okay
However, using pointer notation reminds you that the argument doesn’t have to be the name of an array but can be some other form of pointer.
The function itself demonstrates a standard way to process the characters in a string:
while (*str)
{
statements
str++;
}
Initially, str
points to the first character in the string, so *str
represents the first character itself. For example, immediately after the first function call, *str
has the value m
, the first character in minimum
. As long as the character is not the null character (