Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Beginning Visual C++ 2005 (2006) [eng]

.pdf
Скачиваний:
125
Добавлен:
16.08.2013
Размер:
18.66 Mб
Скачать

Introducing Structure into Your Programs

return 0;

}

 

double& lowest(double a[], int len)

 

{

 

int j = 0;

// Index of lowest element

for(int i = 1; i < len; i++)

 

if(a[j] > a[i])

// Test for a lower value...

j = i;

// ...if so update j

return a[j];

// Return reference to lowest

 

// element

}

 

The output from this example is:

3

10

1.5

15

2.7

23

4.5

12

6.8

13.5

2.1

14

3

10

6.9

15

2.7

23

4.5

12

6.8

13.5

7.9

14

How It Works

Let’s first take a look at how the function is implemented. The prototype for the function lowest() uses double& as the specification of the return type, which is therefore of type ‘reference to double’. You write a reference type return value in exactly the same way as you have already seen for variable declarations, appending the & to the data type. The function has two parameters specified — a one-dimen- sional array of type double and a parameter of type int that specifies the length of the array.

The body of the function has a straightforward for loop to determine which element of the array passed contains the lowest value. The index, j, of the array element with the lowest value is arbitrarily set to 0 at the outset, and then modified within the loop if the current element, a[i], is less than a[j]. Thus, on exit from the loop, j contains the index value corresponding to the array element with the lowest value. The return statement is as follows:

return a[j];

// Return reference to lowest element

In spite of the fact that this looks identical to the statement that would return a value, because the return type was declared as a reference, this returns a reference to the array element a[j] rather than the value that the element contains. The address of a[j] is used to initialize the reference to be returned. This reference is created by the compiler because the return type was declared as a reference.

Don’t confuse returning &a[j] with returning a reference. If you write &a[j] as the return value, you are specifying the address of a[j], which is a pointer. If you do this after having specified the return type as a reference, you get an error message from the compiler. Specifically, you get this:

error C2440: ‘return’ : cannot convert from ‘double *__w64 ‘ to ‘double &’

The function main(), which exercises the lowest()function, is very simple. An array of type double is declared and initialized with 12 arbitrary values, and an int variable len is initialized to the length of the array. The initial values in the array are output for comparison purposes.

Again, the program uses the stream manipulator setw() to space the values uniformly, requiring the

#include directive for <iomanip>.

259

Chapter 5

The function main() then calls the function lowest() on the left side of an assignment to change the lowest value in the array. This is done twice to show that it does actually work and is not an accident. The contents of the array are then output to the display again, with the same field width as before, so corresponding values line up.

As you can see from the output with the first call to lowest(), the third element of the array, array[2], contained the lowest value, so the function returned a reference to it and its value was changed to 6.9. Similarly, on the second call, array[10] was changed to 7.9. This demonstrates quite clearly that returning a reference allows the use of the function on the left side of an assignment statement. The effect is as if the variable specified in the return statement appeared on the left of the assignment.

Of course, if you want to, you can also use it on the right side of an assignment, or in any other suitable expression. If you had two arrays, X and Y, with the number of array elements specified by lenx and leny respectively, you could set the lowest element in the array x to twice the lowest element in the array y with this statement:

lowest(x, lenx) = 2.0*lowest(y, leny);

This statement would call your lowest()function twice — once with arguments y and leny in the expression on the right side of the assignment and once with arguments x and lenx to obtain the address where the result of the right-hand expression is to be stored.

A Teflon-Coated 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 if you need to. 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, as they can be accessed from anywhere in a program, which makes it 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 the 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.

260

Introducing Structure into Your Programs

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 = 0;

This also initializes the variable to zero.

Initialization of a static variable within a function only occurs the first time that the function is called. In fact, on the first call of a function, the static variable is created and initialized. It then continues to exist for the duration of program execution, and whatever value it contains when the function is exited is available when the function is next called.

Try It Out

Using Static Variables in Functions

You can demonstrate how a static variable behaves in a function with the following simple example:

//Ex5_13.cpp

//Using a static variable within a function #include <iostream>

using std::cout; using std::endl;

void record(void);

// Function prototype, no arguments or return value

int main(void)

{

record();

for(int i = 0; i <= 3; i++) record();

cout << endl; return 0;

}

// A function that records how often it is called void record(void)

{

static int count = 0;

 

cout << endl

 

<< “This is the “ << ++count;

 

if((count > 3) && (count < 21))

// All this....

cout <<”th”;

 

else

 

switch(count%10)

// is just to get...

{

 

case 1: cout << “st”;

 

break;

 

case 2: cout << “nd”;

 

break;

 

case 3: cout << “rd”;

 

261

Chapter 5

break;

 

default: cout << “th”;

// the right ending for...

}

// 1st, 2nd, 3rd, 4th, etc.

cout << “ time I have been called”;

 

return;

 

}

 

 

 

Our function here serves only to record the fact that it was called. If you build and execute it, you get this output:

This is the 1st time I have been called

This is the 2nd time I have been called

This is the 3rd time I have been called

This is the 4th time I have been called

This is the 5th time I have been called

How It Works

You initialize the static variable count with 0 and increment it in the first output statement in the function. Because the increment operation is prefixed, the incremented value is displayed by the output statement. It will be 1 on the first call, 2 on the second, and so on. Because the variable count is static, it continues to exist and retain its value from one call of the function to the next.

The remainder of the function is concerned with working out when ‘st’, ‘nd’, ‘rd’, or ‘th’ should be appended to the value of count that is displayed. It’s surprisingly irregular. (I guess 101 should be 101st rather than 101th, shouldn’t it?)

Note the return statement. Because the return type of the function is void, to include a value would cause a compiler error. You don’t actually need to put a return statement in this particular case as running off the closing brace for the body of the function is equivalent to the return statement without a value. The program would compile and run without error even if you didn’t include the return.

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 infinite loop, and if you aren’t careful it certainly can be. An infinite 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 infinite 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 1x2x3...xN. 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, we will look at something even simpler.

262

Introducing Structure into Your Programs

Try It Out

A Recursive Function

At the start of this chapter (see Ex5_01.cpp), you produced a function to compute the integral power of a value, that is, to compute xn. This is equivalent to x multiplied by itself n times. You can implement this as a recursive function as an elementary illustration of recursion in action. You can also improve the implementation of the function to deal with negative index values, where x-n is equivalent to 1/xn.

//Ex5_14.cpp (based on Ex5_01.cpp)

//A recursive version of x to the power n #include <iostream>

using std::cout; using std::endl;

double power(double x, int n);

// Function prototype

int main(void)

 

{

 

double x = 2.0;

// Different x from that in function power

double result = 0.0;

 

// Calculate x raised to powers -3 to +3 inclusive for(int index = -3 ; index<=3 ; index++)

cout << x << “ to the power “ << index << “ is “ << power(x, index)<< endl;

return 0;

}

//Recursive function to compute integral powers of a double value

//First argument is value, second argument is power index

double power(double x, int n)

{

if(n < 0)

{

x = 1.0/x; n = -n;

}

if(n > 0)

return x*power(x, n-1); else

return 1.0;

}

The output from this program is:

2 to the power -3 is 0.125

2 to the power -2 is 0.25

2 to the power -1 is 0.5

2 to the power 0 is 1

2 to the power 1 is 2

2 to the power 2 is 4

2 to the power 3 is 8

263

Chapter 5

How It Works

The function now supports positive and negative powers of x, so the first action is to check whether the value for the power that x is to be raised to, n, is negative:

if(n < 0)

{

x = 1.0/x; n = -n;

}

Supporting negative powers is easy; it just uses the fact that x-n can be evaluated as (1/x)n. Thus if n is negative, you set x to be 1.0/x and changed the sign of n so it’s positive.

The next if statement decides whether or not the power() function should call itself once more:

if(n > 0)

return x*power(x, n-1); else

return 1.0;

The if statement provides for the value 1.0 being returned if n is zero, and in all other cases it returns the result of the expression, x*power(x, n-1). This causes a further call to the function power() with the index value reduced by 1. Thus the else clause in the if statement provides the essential mechanism necessary to avoid an indefinite sequence of recursive function calls.

Clearly, within the function power(), if the value of n is other than zero, a further call to the function power()occurs. In fact, for any given value of n other than 0, the function calls itself n times, ignoring the sign of n. The mechanism is illustrated in Figure 5-4, where the value 3 for the index argument is assumed.

As you see, the power() function is called a total of four times to generate x3, three of the calls being recursive where the function is calling itself.

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. This is much more efficient than using recursive function calls. Think about what happens with our last example to evaluate a simple product, x*x*...x, 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 the function 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 can 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.

264

Introducing Structure into Your Programs

Result: x3

 

power( x , 3 )

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

x*x*x

double power( double x, int n )

 

 

 

 

 

 

 

 

 

 

{

 

 

 

 

 

 

 

 

 

 

 

...

 

 

 

 

 

 

 

 

 

 

 

return x*power( x , n - 1 );

 

 

 

 

 

 

 

 

 

 

 

...

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

}

 

 

 

 

 

 

2

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

x*x

Figure 5-4

double power( double x, int n )

{

 

...

 

return x*power( x , n - 1 );

 

...

}

1

xdouble power( double x, int n )

{

...

 

return x*power( x , n - 1 );

 

...

}

0

 

1.0 double power( double x, int n )

{

...

return 1.0;

...

}

C++/CLI Programming

For the most part, functions in a C++/CLI program work in exactly the same way as in a native program. Of course, you deal in handles and tracking references when programming for the CLR not native pointers and references and that introduces some differences. There are a few other things that are a little different, so let’s itemize them.

Function parameters and return values in a CLR program can be value class types, tracking handles, tracking references, and interior pointers.

When a parameter is an array there no need to have a separate parameter for the size of the array because C++/CLI arrays have the size built into the Length property.

You cannot do address arithmetic with array parameters in a C++/CLI program as you can in a native C++ program so you must always use array indexing.

265

Chapter 5

Returning a handle to memory you have allocated on the CLR heap is not a problem because the garbage collector takes care of releasing the memory when it is no longer in use.

The mechanism for accepting a variable number of arguments in C++/CLI is different from the native C++ mechanism.

Accessing command line arguments in main() in a C++/CLI program is also different from the native C++ mechanism.

Let’s look at the last two differences in more detail.

Functions Accepting a Variable Number of Arguments

The C++/CLI language provides for a variable number of arguments by allowing you to specify the parameter list as an array with the array specification preceded by an ellipsis. Here’s an example of a function with this parameter list:

int sum(... array<int>^ args)

{

// Code for sum

}

The sum() function here accepts any number of arguments of type int. To process the arguments you just access the elements of the array args. Because it is a CLR array, the number of elements is recorded as its Length property, so you have no problem determining the number of arguments in the body of the function. This mechanism is also an improvement over the native C++ mechanism you saw earlier because it is type-safe. The arguments clearly have to be of type int to be accepted by this function. Let’s try a variation on Ex5_10 to see the CLR mechanism working.

Try It Out

A Variable Number of Function Arguments

Here’s the code for this CLR project.

//Ex5_15.cpp : main project file.

//Passing a variable number of arguments to a function

#include “stdafx.h”

using namespace System;

double sum(... array<double>^ args)

{

double sum = 0.0;

for each(double arg in args) sum += arg;

return sum;

}

int main(array<System::String ^> ^args)

{

Console::WriteLine(sum(2.0, 4.0, 6.0, 8.0, 10.0, 12.0));

Console::WriteLine(sum(1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9)); return 0;

}

266

Introducing Structure into Your Programs

This example produces the following output:

42

49.5

Press any key to continue . . .

How It Works

The sum() function here has been implemented to accept arguments of type double. The ellipsis preceding the array parameter tells the compiler to expect an arbitrary number of arguments and the argument values should be stored in an array of elements of type double. Of course, without the ellipsis, the function would expect just one argument when it was called that was a tracking handle for an array.

Compared to the native version in Ex5_10 the definition of the sum() function is remarkably simple. All the problems associated with the type and the number of arguments have disappeared in the C++/CLI version. The sum is accumulated in a simple for each loop that iterates over all the elements in the array.

Arguments to main()

You can see from the previous example that there is only one parameter to the main() function in a C++/CLI program and it is an array of elements of type String^. Accessing and processing commandline arguments in a C++/CLI program boils down to just accessing the elements in the array parameter. You can try it out.

Try It Out

Accessing Command Line Arguments

Here’s a C++/CLI version of Ex5_09.

//Ex5_16.cpp : main project file.

//Receiving multiple command liner arguments.

#include “stdafx.h”

using namespace System;

int main(array<System::String ^> ^args)

{

Console::WriteLine(L”There were {0} command line arguments.”, args->Length);

Console::WriteLine(L”Command line arguments received are:”); int i = 1;

for each(String^ str in args) Console::WriteLine(L”Argument {0}: {1}”, i++, str);

return 0;

}

You can enter the command-line arguments in the command window or through the project properties window as described earlier in the chapter. This example produces the following output:

C:\Visual C++ 2005\Examples\>Ex5_16 trying multiple “argument values” 4.5 0.0 There were 5 command line arguments.

Command line arguments received are:

267

Chapter 5

Argument 1: trying

Argument 2: multiple

Argument 3: argument values

Argument 4: 4.5

Argument 5: 0.0

How It Works

From the output, you can see that one difference between this and the native C++ version is that you don’t get the program name passed to main() not really a great disadvantage, really a positive feature in most circumstances. Accessing the command line arguments is now a trivial exercise involving just iterating through the elements in the args array.

Summar y

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 key points that you should keep in mind about writing your own functions are these:

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.

Always provide a function prototype for each function defined in your program, positioned before you call that function.

Passing values to a function using a reference can avoid the copying implicit in the call-by-value transfer of arguments. Parameters that are not modified in a function should be specified as const.

When returning a reference or a pointer from a native C++ 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 native C++ function.

In a C++/CLI program there is no problem with returning a handle to memory that has been allocated dynamically because the garbage collector takes care of deleting it when it is no longer required.

When you pass a C++/CLI array to a function, there is no need for another parameter for the length of the array, as the number of elements is available in the function body as the Length property for the array.

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 we look into objectoriented programming.

268