9.3 Functions

I l @ ve RuBoard

Functions allow you to group commonly used code into a compact unit that can be used repeatedly. You have already encountered one function, main . It is a special function called at the beginning of the program after all static and global variables have been initialized .

Suppose you want to write a program to compute the area of three triangles . You could write out the formula three times, or you could create a function to do the work and then use that function three times. Each function should begin with a comment block containing the following:

Name

Name of the function

Description

Description of what the function does

Parameters

Description of each parameter to the function

Returns

Description of the return value of the function

Additional sections may be added, such as file formats, references, or notes. Refer to Chapter 3 for other suggestions.

The function to compute the area of a triangle could begin with the following comment block:

 /*******************************************   * Triangle -- compute area of a triangle  *   *                                         *   * Parameters                              *   *  width -- width of the triangle         *   *  height -- height of the triangle       *   *                                         *   * Returns                                 *   *  area of the triangle                   *   *******************************************/ 

The function proper begins with the line:

 float triangle(float width, float height) 

float is the function type. This defines the type of data returned by the function. width and height are the parameters to the function. Parameters are variables local to the function that are used to pass information into the function.

We first check the parameters from the caller. Everybody knows that a triangle can't have a negative width or height. But programming is a world of its own, and you can trust nothing. So let's verify the input with a couple of assert statements:

 assert(width >= 0.0); assert(height >= 0.0); 

This sort of paranoia is extremely useful when debugging large programs. assert statements like these can be a tremendous help when tracking down bad code. They serve to stop the program at the earliest possible time, thus saving you lots of time tracing bad data back to its source. Remember: just because you're paranoid , it doesn't mean they aren't out to get you.

The function computes the area with the statement:

 area = width * height / 2.0; 

What's left is to give the result to the caller. This is done with the return statement:

 return (area) 

The full triangle function can be seen in Example 9-2.

Example 9-2. tri/tri-sub.cpp
 /*******************************************  * triangle -- compute area of a triangle  *  *                                         *  * Parameters                              *  *  width -- width of the triangle         *  *  height -- height of the triangle       *  *                                         *  * Returns                                 *  *  area of the triangle                   *  *******************************************/ float triangle(float width, float height) {     float area; // area of the triangle      assert(width >= 0.0);     assert(height >= 0.0);     area = width * height / 2.0;     return (area); } 

The line:

 size = triangle(1.3, 8.3); 

is a call to the function triangle . When C++ sees this function call, it performs the following operations:

triangle's variable width = 1.3
triangle's variable height = 8.3
Begin execution of the first line of the function triangle .

The technical name for this type of parameter passing is "call by value." The assignment occurs only when the function is called, so data flows through the parameters only one way: in.

The return statement is how you get data out of the function. In the triangle example, the function assigns the local variable area the value 5.4 and then executes the statement return (area), so the return value of this function is 5.4 . This value is assigned to size .

figs/c++2_09p127.gif

Example 9-3 computes the area of three triangles.

Example 9-3. tri/tri.cpp
 #include <iostream> #include <assert.h> int main(  )  {     // function to compute area of triangle      float triangle(float width, float height);        std::cout << "Triangle #1 " << triangle(1.3, 8.3) << '\n';     std::cout << "Triangle #2 " << triangle(4.8, 9.8) << '\n';     std::cout << "Triangle #3 " << triangle(1.2, 2.0) << '\n';     return (0); } /*******************************************  * triangle -- compute area of a triangle  *  *                                         *  * Parameters                              *  *  width -- width of the triangle         *  *  height -- height of the triangle       *  *                                         *  * Returns                                 *  *  area of the triangle                   *  *******************************************/ float triangle(float width, float height) {     float area; // area of the triangle      assert(width >= 0.0);     assert(height >= 0.0);     area = width * height / 2.0;     return (area); } 

Functions must be declared just like variables. The declaration tells the C++ compiler about the function's return value and parameters. There are two ways of declaring a function. The first is to write the entire function before it's used. The other is to define what's called a function prototype, which gives the compiler just enough information to call the function. A function prototype looks like the first line of the function, but the prototype has no body. For example, the prototype for the triangle function is:

 float triangle(float width, float height); 

Note the semicolon at the end of the line. This is used to tell C++ that this is a prototype and not a real function.

C++ allows you to leave out the parameter names when declaring a prototype. This function prototype could just as easily have been written:

 float triangle(float, float); 

However, this technique is not commonly used, because including the parameter names gives the reader more information about what the function is doing and makes the program easier to understand. Also, it's very easy to create a prototype by simply using the editor to copy the first line of a function and putting that line where you want the prototype. (Many times this will be in a header file, as described in Chapter 23.)

Functions that have no parameters are declared with a parameter list of ( ) . For example:

 int get_value(  ); 

You can also use the parameter list (void) . This is a holdover from the old C days when an empty parameter list "( )" signaled an old K&R-style C function prototype. Actually, C++ will accept both an empty list and a void declaration.

Almost all C++ programmers prefer the empty list. The advantages of the (void) form are:

  • It provides an obvious indicator that there is no parameter list. (In other words, if the programmer puts in the void she tells the world, "This function really takes no arguments, and I didn't forget the parameter list."

  • It is compatible with the older C language.

The advantages of the empty list are:

  • The syntax ( ) is more sane and consistent with the way we declare parameters than that of (void) .

  • The void list is a historical hack put into C to solve a syntax problem that existed because the empty list was used for something else. It was ported from C to C++ for compatibility.

  • We are programming C++, not C, so why should we use relics from the past in our code?

For these reasons most people use the empty list. This author is one exception. I prefer the (void) construct, but when three reviewers and an editor tell you you're wrong, it's time to rethink your choices. The empty list is used throughout this book.

9.3.1 Returning void

The keyword void is also used to indicate a function that does not return a value (similar to the FORTRAN SUBROUTINE or PASCAL Procedure ). For example, this function just prints a result; it does not return a value:

 void print_answer(int answer)  {      if (answer < 0) {          std::cout << "Answer corrupt\n";          return;      }      std::cout << "The answer is " << answer '\n';  } 

9.3.2 Namespaces and Functions

Namespaces affect not only variables but functions as well. A function belongs to the namespace in which it is declared. For example:

 namespace math { int square(const int i) {     return (i * i); } } // End namespace namespace body { int print_value(  ) {     std::cout << "5 squared is " << math::square(5) << '\n'; } } 

All the functions in a namespace can access the variables in that namespace directly and don't need a using clause or a namespace qualification. For example:

 namespace math { const double PI = 3.14159; double area(const double radius) {     return (2.0 * PI * radius); } } 

9.3.3 const Parameters and Return Values

A parameter declared const cannot be changed inside the function. Ordinary parameters can be changed inside functions, but the changes will not be passed back to the calling program.

For example, in the triangle function, we never change width or height . These could easily be declared const . Since the return value is also something that cannot be changed, it can be declared const as well. The const declarations serve to notify the programmer that the parameters do not change inside the function. If you do attempt to change a const parameter, the compiler generates an error. The improved triangle function with the const declarations can be seen in Example 9-4.

Example 9-4. tri/tri-sub2.cpp
 const float triangle(const float width, const float height) {     float area; // area of the triangle      assert(width >= 0.0);     assert(height >= 0.0);     area = width * height / 2.0;     return (area); } 

As it stands now, the const declaration for the return value is merely a decoration. In the next section you'll see to how to return references and make the const return declaration useful.

9.3.4 Reference Parameters and Return Values

Remember that in Chapter 5 we discussed reference variables. A reference variable is a way of declaring an additional name for a variable. For global and local variables, reference variables are not very useful. However, they take on an entirely new meaning when used as parameters.

Suppose you want to write a subroutine to increment a counter. If you write it like Example 9-5, it won't work.

Example 9-5. value/value.cpp
 #include <iostream> // This function won't work void inc_counter(int counter) {     ++counter; } int main(  ) {    int a_count = 0;     // Random counter    inc_counter(a_count);    std::cout << a_count << '\n';    return (0); } 

Why doesn't it work? Because C++ defaults to call by value. This means that values go in, but they don't come out.

What happens if you convert the parameter counter to a reference? References are just another way of giving the same variable two names. When inc_counter is called, counter becomes a reference to a_count . Thus, anything done to counter results in changes to a_count . Example 9-6, using a reference parameter, works properly.

Example 9-6. value/ref.cpp
 #include <iostream> // Works void inc_counter(int& counter) {     ++counter; } int main(  ) {    int a_count = 0;     // Random counter    inc_counter(a_count);    std::cout << a_count << '\n';    return (0); } 

Reference declarations can also be used for return values. Example 9-7 finds the biggest element in an array.

Example 9-7. value/big.cpp
 int& biggest(int array[], int n_elements) {     int index;  // Current index     int biggest; // Index of the biggest element     // Assume the first is the biggest     biggest = 0;     for (index = 1; index < n_elements; ++index) {         if (array[biggest] < array[index])             biggest = index;     }     return (array[biggest]); } 

If you wanted to print the biggest element of an array, all you would have to do is this:

 int item_array[5] = {1, 2, 5000, 3, 4}; // An array std::cout << "The biggest element is " <<           biggest(item_array, 5) << '\n'; 

Let's examine this in more detail. First of all, consider what happens when you create a reference variable:

 int& big_reference = item_array[2]; // A reference to element #2 

The reference variable big_reference is another name for item_array[2] . You can now use this reference to print a value:

 std::cout << big_reference << '\n';   // Print out element #2 

But since this is a reference, you can use it on the left side of an assignment statement as well. (Expressions that can be used on the left side of the = in an assignment are called lvalues .)

 big_reference = 0;      // Zero the largest value of the array 

The function biggest returns a reference to item_array[2] . Remember that in the following code, biggest( ) is item_array[2] . The following three code sections all perform equivalent operations. The actual variable, item_array[2] , is used in all three:

 // Using the actual data std::cout << item_array[2] << '\n'; item_array[2] = 0; // Using a simple reference int big_reference = &item_array[2]; std::cout << big_reference << '\n'; big_reference = 0; // Using a function that returns a reference std::cout << biggest(  ) << '\n';   biggest(  ) = 0; 

Because biggest returns a reference, it can be used on the left side of an assignment operation ( = ). But suppose you don't want that to happen. You can accomplish this by returning a const reference:

 const int& biggest(int array[], int n_elements); 

This tells C++ that even though you return a reference, the result cannot be changed. Thus, code like the following is illegal:

 biggest(  ) = 0;              // Now it generates an error 

9.3.5 Dangling References

Be careful when using return by reference, or you could wind up with a reference to a variable that no longer exists. Example 9-8 illustrates this problem.

Example 9-8. ref/ref.cpp
 1: const int& min(const int& i1, const int& i2)  2: {  3:     if (i1 < i2)  4:         return (i1);  5:     return (i2);  6: }  7:   8: int main(  )  9: { 10:    const int& i = min(1+2, 3+4); 11:  12:    return (0); 13: } 

Line 1 starts the definition of the function min . It returns a reference to the smaller of two integers.

In line 10 we call this function. Before the function min is called, C++ creates a temporary integer variable to hold the value of the expression 1 + 2 . A reference to this temporary variable is passed to the min function as the parameter i1 . C++ creates another temporary variable for the i2 parameter.

The function min is then called and returns a reference to i1 . But what does i1 refer to? It refers to a temporary variable that C++ created in main . At the end of the statement, C++ can destroy all the temporaries.

Let's look at the call to min (line 10) in more detail. Here's a pseudo-code version of line 10, including the details that C++ normally hides from the programmer:

 create integer tmp1, assign it the value 1 + 2 create integer tmp2, assign it the value 3 + 4 bind parameter i1 so it refers to tmp1 bind parameter i2 so it refers to tmp2 call the function "min" bind main's variable i so it refers to          the return value (i1-a reference to tmp1) // At this point i is a reference to tmp1 destroy tmp1  destroy tmp2 //    At this point i still refers to tmp1 //    It doesn't exist, but i refers to it 

At the end of line 10 we have a bad situation: i refers to a temporary variable that has been destroyed . In other words, i points to something that does not exist. This is called a dangling reference and should be avoided.

9.3.6 Array Parameters

So far you've dealt only with simple parameters. C++ treats arrays a little differently. First of all, you don't have to put a size in the prototype declaration. For example:

 int sum(int array[]); 

C++ uses a parameter-passing scheme called "call by address" to pass arrays. Another way of thinking of this is that C++ automatically turns all array parameters into reference parameters. This allows any size array to be passed. The function sum we just declared may accept integer arrays of length 3, 43, 5,000, or any length.

However, you can put in a size if you want to. C++ allows this, although it ignores whatever number you put there. But by putting in the size, you alert the people reading your program that this function takes only fixed-size arrays.

 int sum(int array[3]); 

For multidimensional arrays you are required to put in the size for each dimension except the last one. That's because C++ uses these dimensions to compute the location of each element in the array.

 int sum_matrix(int matrix1[10][10]);   // Legal int sum_matrix(int matrix1[10][]);     // Legal int sum_matrix(int matrix1[][]);       // Illegal 

Question 9-1: The function in Example 9-9 should compute the length of a C-style string. [1] Instead it insists that all strings are of length zero. Why?

[1] This function (when working properly) performs the same function as the library function strlen .

Example 9-9. length/length.cpp
 /********************************************************  * length -- compute the length of a string             *  *                                                      *  * Parameters                                           *  *      string -- the string whose length we want       *  *                                                      *  * Returns                                              *  *      the length of the string                        *  ********************************************************/ int  length(char string[]) {     int index;      // index into the string      /*      * Loop until we reach the end of string character      */     for (index = 0; string[index] != ' 
 /******************************************************** * length -- compute the length of a string * * * * Parameters * * string -- the string whose length we want * * * * Returns * * the length of the string * ********************************************************/ int length(char string[]) { int index; // index into the string /* * Loop until we reach the end of string character */ for (index = 0; string[index] != '\0'; ++index) /* do nothing */ return (index); } 
'; ++index) /* do nothing */ return (index); }

9.3.7 Function Overloading

Let's define a simple function to return the square of an integer:

 int square(int value) {     return (value * value); } 

We also want to square floating-point numbers :

 float square(float value) {     return (value * value); } 

Now we have two functions with the same name. Isn't that illegal? In older languages, such as C and PASCAL, it would be. In C++ it's not. C++ allows function overloading, which means you can define multiple functions with the same names. Thus you can define a square function for all types of things: int , float , short int , double , and even char, if we could figure out what it means to square a character.

To keep your code consistent, all functions that use the same name should perform the same basic function. For example, you could define the following two square functions:

 // Square an integer int square(int value); // Draw a square on the screen void square(int top, int bottom, int left, int right); 

This is perfectly legal C++ code, but it is confusing to anyone who has to read the code.

There is one limitation to function overloading: C++ must be able to tell the functions apart. For example, the following is illegal:

 int get_number(  ); float get_number(  );  // Illegal 

The problem is that C++ uses the parameter list to tell the functions apart. But the parameter list of the two get_number routines is the same: ( ) . The result is that C++ can't tell these two routines apart and flags the second declaration as an error.

9.3.8 Default Arguments

Suppose you want to define a function to draw a rectangle on the screen. This function also needs to be able to scale the rectangle as needed. The function definition is:

 void draw(const int width, const int height, double scale) 

After using this function for a while, you discover that 90% of the time you don't use the draw 's scale ability. In other words, 90% of the time the scale factor is 1.0.

C++ allows you to specify a default value for scale . The statement:

 void draw(const int width, const int height, double scale = 1.0) 

tells C++, "If scale is not specified, make it 1.0." Thus the following are equivalent:

 draw(3, 5, 1.0);     // Explicity specify scale  draw(3, 5);          // Let it default to 1.0 

There are some style problems with default arguments. Study the following code:

 draw(3, 5); 

Can you tell whether the programmer intended for the scale to be 1.0 or just forgot to put it in? Although sometimes useful, the default argument trick should be used sparingly.

9.3.9 Unused Parameters

If you define a parameter and fail to use it, most good compilers will generate a warning. For example, consider the following code:

 void exit_button(Widget& button) {     std::cout << "Shutting down\n";     exit (0); } 

This example generates the message:

 Warning: line 1.  Unused parameter "button" 

But what about the times you really don't want to use a parameter? Is there a way to get C++ to shut up and not bother you? There is. The trick is to leave out the name of the parameter:

 // No warning, but style needs work  void exit_button(Widget&) {     std::cout << "Shutting down\n";     exit (0);  } 

This is nice for C++, but not so nice for the programmer who has to read your code. We can see that exit_button takes a Widget& parameter, but what is the name of the parameter? A solution to this problem is to reissue the parameter name as a comment:

 // Better  void exit_button(Widget& /*button*/) {      std::cout << "Shutting down\n";           exit (0);  } 

Some people consider this style ugly and confusing. They're right that it's not that easy to read. There ought to be a better way; I just wish I could think of one.

One question you might be asking by now is, "Why would I ever write code like this? Why not just leave the parameter out?"

It turns out that many programming systems make use of callback functions . For example, you can tell the X Window System, "When the EXIT button is pushed , call the function exit_button ." Your callback function may handle many buttons , so it's important to know which button is pushed. So X supplies button as an argument to the function.

What happens if you know that button can only cause X to call exit_button ? Well, X is still going to give it to you, you're just going to ignore it. That's why some functions have unused arguments.

9.3.10 Inline Functions

Looking back at the square function for integers, we see that it is a very short function, just one line. Whenever C++ calls a function, there is some overhead generated. This includes putting the parameters on the stack, entering and leaving the function, and stack fix-up after the function returns.

For example, consider the following code:

 int square(int value) {     return (value * value); } int main(  ) {     // .....     x = square(x); 

The code generates the following assembly code on a 68000 machine (paraphrased):

 label "int square(int value)"         link a6,#0              // Set up local variables         // The next two lines do the work         movel a6@(8),d1         // d1 = value         mulsl a6@(8),d1         // d1 = value * d1         movel d1,d0             // Put return value in d0         unlk a6                 // Restore stack         rts                     // Return(d0) label "main" //.... //      x = square(x) //         movel a6@(-4),sp@-      // Put the number x on the stack         jbsr "void square(int value)"                                  // Call the function         addqw #4,sp             // Restore the stack         movel d0,a6@(-4)        // Store return value in X // ... 

As you can see from this code, there are eight lines of overhead for two lines of work. C++ allows you to cut out that overhead through the use of an inline function. The inline keyword tells C++ that the function is very small. This means that it's simpler and easier for the C++ compiler to put the entire body of the function in the code stream instead of generating a call to the function. For example:

 inline int square(int value) {     return (value * value); } 

Changing the square function to an inline function generates the following, much smaller, assembly code:

 label "main" // ... //      x = square(x) //         movel d1,a6@(-4)        // d1 = x         movel a6@(-4),d0        // d0 = x         mulsl d0,d0             // d0 = (x * x)         movel d0,a6@(-4)        // Store result 

Expanding the function inline has eliminated the eight lines of overhead and results in much faster execution.

The inline specifier provides C++ a valuable hint it can use when generating code, telling the compiler that the code is extremely small and simple. Like register , the inline specifier is a hint. If the C++ compiler can't generate a function inline, it will create it as an ordinary function.

I l @ ve RuBoard


Practical C++ Programming
Practical C Programming, 3rd Edition
ISBN: 1565923065
EAN: 2147483647
Year: 2003
Pages: 364

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net