Passing Arguments to Functions

 < Day Day Up > 



Arguments are passed to functions in two ways; by value or by reference. It is important to know the difference between the two and the effect each form of argument passing produces. But first, I want to show you some of the mechanics of a function call so you will better understand what’s happening behind the scenes when one function calls another.

Function Calling

It is helpful to understand the concepts of function activation records and function calling protocols. Figure 9-5 shows the sequence of action records of a calling function and a called function before, during, and after a function call.

click to expand
Figure 9-5: Function Activation Record Sequence

Every programming language that implements functions assigns a set of responsibilities to the function making the call and the function being called. These responsibilities are referred to as function calling conventions or function calling protocols, or simply calling conventions. The important thing to know about calling conventions is that if you’re only programming in C++ you can read this section and then safely forget about them. On the other hand, if you’re planning to do mixed language programming, you will have to be aware that some programming languages have different calling conventions than others.

Responsibilities of the Calling Function

A calling function is usually responsible for providing a called function access to any arguments with which it was called. As you will see below, a calling function will usually make a called function’s arguments available in processor registers.

Responsibilities of the Called Function

A called function must save the contents of any processor register it intends to use, and restore the contents of those registers before returning to the called function. As figure 9-5 illustrates, a link between the called function and calling function is established in each activation record. The control link facilities a called function’s return to its caller.

Passing Arguments by Value

You saw an argument passed to a function by value in the testFunctionThree() function shown in examples 9.9 and 9.10. When an argument is passed to a function by value, the value of the argument is copied to the function parameter for use inside the function. Figure 9-6 shows a partial disassembly of the main.cpp file from the testFunctionThree program. The listing is given in PowerPC assembly.

click to expand
Figure 9-6: Partial Disassembly of main.cpp

The first thing main() does on lines 1 and 2 is to preserve the contents of the link register. This value will be used to return control to the function that called this program. Next, on line 3, the stack pointer is reset. Remember, the stack grows down and the heap grows up. That’s why a negative value is added to the stack pointer.

On line 4, the integer value 5 is loaded into r3. This is followed by a branch to testFunctionThree(). Upon testFunctionThree()’s return, the r3 is set to zero; this is the return value. Next, the saved link register value is retrieved from the stack and placed in r0. The stack is reset to its previous position and the return is made.

What has main() done? Essentially, it placed the value of testFunctionThree()’s argument in a register so it would be accessible to it when called. After the call to testFunctionThree(), main() placed a zero in the same register so it would be available to main()’s caller.

What do you suppose testFunctionThree() will do? Before looking at figure 9-7 go back and look at the C++ listing for testFunctionThree(). It has access to two values: a global i which equals 35, and a local parameter named i which has been set by main(). Remember, every function will go through the same house keeping chores. These include preserving the return link and preserving and restoring any registers used by the function.

click to expand
Figure 9-7: Partial Disassembly of testFunctionThree.cpp

The assembly code for testFunctionThree() is more complicated than main() because it is making several iostream function calls itself. I have to remind you, this is not the complete assembly listing. Due to the iostream functions the complete listing is just too long and has a lot of stuff not related to the discussion, but it is interesting to look at!

The function starts on lines 11 and 12 where the contents of the link register is saved for the return trip. Line 13 allocates some stack space for the function. The instruction on line 14 stores the value of r3 in memory for future use. Remember, r3 is where main() put the value 5. Line 15 loads the location of the cout object code in r3 in preparation for the iostream calls. (RTOC stands for Table of Contents Register) Line 16 loads the location of the string “Parameter i = “, followed by line 17’s branch to the code that prints it out.

Next, line 20 loads the value 5 from memory to r4, followed by a call to the code to print it out. Line 23 loads the endl code in r4, followed by a call to the code that prints it.

Line 29 loads the string, “Global i = “, followed by the call to the code to print the string. Next, on line 33, the global variable i’s address is loaded into r4, and then, on line 34, the value pointed to by the address is loaded into r4. Where is it getting i from? Look at lines 1 and 2. The symbolic name for that hunk of data is “i” and its value is hexadecimal 19, which equals 25 in decimal. Once its value is loaded in r4 it is printed to the screen. The rest of the function prints endl, does housekeeping and returns to main().

It is a good idea to disassemble simple functions to see how they work. Although high-level languages are meant to provide a certain level of abstraction, it is helpful to know what is happening at the assembly level. And although PowerPC assembly is used here, the same principles apply to other processors as well.

Another Example

Let us look at another example of passing arguments by value that better shows the side effects of using this method. The header file, implementation file, and main file are shown in examples 9.11 through 9.13 below.

Listing 9.11: testfunctionfour.h

start example
1 #ifndef TEST_FUNCTION_FOUR  2 #define TEST_FUNCTION_FOUR  3  4 void testFunctionFour(int input);  5  6 #endif
end example

Listing 9.12: testfunctionfour.cpp

start example
1 #include <iostream>    9.  2 #include "testfunctionfour.h"  3 using namespace std;  4  5 void testFunctionFour(int input){  6    input++;  7     cout<<"Function argument input = "<<input<<endl;  8 }
end example

Listing 9.13: main.cpp

start example
1 #include <iostream>  2 #include "testfunctionfour.h"  3 using namespace std;   4  5 int main(){  6    int i = 0;  7    cout<<"Before function call i = "<<i<<endl;  8    testFunctionFour(i);  9    cout<<"After function call i = "<<i<<endl; 10    return 0; 11 }
end example

In this example, an integer variable named i is declared in the main() function and initialized to zero. The value of i is printed to the screen before i is passed to the function testFunctionFour() as an argument. testFunctionFour() is declared to take an integer argument with the parameter name input. Inside the body of testFunctionFour(), input is incremented by one and its value is then printed to the screen. testFunctionFour() then returns control to the main() function and the value of i is again printed to the screen. What do you suppose the value of i will be when it is printed the second time? Figure 9-8 shows the results of running this program.

click to expand
Figure 9-8: Results of Running testFunctionFour Program

As you can see, the manipulation of testFunctionFour()’s input parameter had no effect on the value of the variable i. That’s because the value of i was copied to testFunctionFour()’s input parameter, and it was this copy that was incremented and printed to the screen inside the function. When you need to manipulate directly the values of the arguments passed to functions you should pass those arguments by reference. This is discussed in the next section.

Passing Arguments by Reference

When you need to directly manipulate a function argument or intend for a function to work on large objects, you should pass the argument to the function by reference, meaning, you need to supply the argument’s memory address to the function so it has direct access to the argument. The argument is then accessed via its memory address. In the case of large objects, passing arguments by reference can significantly increase processing efficiency.

It is important to note what is really happening when you pass an argument by reference. The memory address is being copied to the function parameter (pass by copy), but the object can then be manipulated directly via this address. The efficiency comes from only copying word sized objects (memory addresses) no matter the size of the object itself. The following test program demonstrates how addresses are passed by copy.

Listing 9.14: passing addresses by copy

start example
1  #include <iostream>  2  using namespace std;  3  4  /******************************************************************  5      Function Declaration  6  ******************************************************************/  7  8  void addressCopyTest(int* ipA);  9 10  /**************************************************************** 11      Function Definition 12  *****************************************************************/ 13 14  void addressCopyTest(int* ipA){ 15       cout<<"Address of ipA = "<<ipA<<"  Value at ipA = "<<*ipA<<endl; 16       ipA++; 17       cout<<"Address of ipA = "<<ipA<<"  Value at ipA = "<<*ipA<<endl; 18  } 19 20  /**************************************************************** 21      main() Function 22  ****************************************************************/ 23 24  int main(){ 25       int i = 3; 26       cout<<"Address of i = "<<&i<<"  value of i = "<<i<<endl; 27       addressCopyTest(&i); 28       cout<<"Address of i = "<<&i<<"  value of i = "<<i<<endl; 29 30       return 0; 31  }
end example

The function addressCopyTest() takes an integer pointer as an argument. Inside the function the address and value referenced by the address of the ipA parameter is printed to the screen. The ipA parameter is then incremented and the address and value is again printed to the screen.

In the main() function, an integer variable named i is declared and initialized to 3. i’s address and value is printed to the screen both before and after the call to the addressCopyTest() function. Figure 9-9 shows the results of running this program.

click to expand
Figure 9-9: Results of Running addressCopyTest Program

Note the address of i has not changed even though the address passed to the function was incremented during the function call. This happens because although you’re passing arguments by reference, you’re passing the address to these arguments by copy. The addresses you see if you run this program on your computer will be different, but the ultimate result will be the same.

Continuing The Story...

Passing by reference can be accomplished in two ways. You can use pointers, and do all the pointer dereferencing yourself, or you can use references, and have the pointer dereferencing done for you. There are advantages and disadvantages to each method.

The advantage of using pointers is the flexibility they provide. If you know how to manipulate pointers the C++ programming world is your oyster! (Remember chapter 7? Vaguely? I thought so!) The biggest disadvantage I can think of to using pointers for passing arguments by reference is that sloppy use can result in bugs that are difficult, if not impossible, to detect. Pointer misuse often results in memory leaks. However, experience and attention to detail should mitigate this disadvantage to a large extent.

If you use references to pass arguments to functions your code is cleaner and easier to read. However, you are restricted to what you can do with a reference, hence, you lose the programming flexibility otherwise enjoyed with the use of pointers.

Which method you use depends directly on what you need to do inside the function. For some jobs, either method will work fine; for others, your only option will be to use pointers. Let us take a closer look at each method.

Passing Pointers

To pass an argument to a function using a pointer you must declare the function to take a pointer type argument. The following statement declares a function that takes an integer pointer as an argument:

void testFunctionFive(int* ipA);

When testFunctionFive() is called, the address of an integer object must be supplied as an argument. The following example gives the complete definition of testFunctionFive():

Listing 9.15: testfunctionfive.cpp

start example
1  #include "testfunctionfive.h"  2  3  void testFunctionFive(int* ipA){  4     (*ipA)++;  5  }
end example

On line 4, the parameter ipA is dereferenced and the resulting value is incremented. The parentheses are used to explicitly show the operator association. Example 9.16 shows testFunctionFive() being called in a main() function.

Listing 9.16: main.cpp

start example
1  #include <iostream>  2  #include "testfunctionfive.h"  3  using namespace std;   4  5  int i=1, j=2;  6  7  int main(){  8      cout<<"Value of i: "<<i<<" j: "<<j<<endl;  9      testFunctionFive(&i); 10      testFunctionFive(&j); 11      cout<<"Value of i: "<<i<<" j: "<<j<<endl; 12      return 0; 13  }
end example

On line 5, two global variables i and j are declared and initialized. Their value is printed to the screen on line 8 prior to calling testFunctionFive() for the first time. Lines 9 and 10 show testFunctionFive() being called twice, once with the address of i, and the second time with the address of j. Note how the address of each variable is passed to the function by using the & operator.

The values of i and j are printed to the screen again on line 11. Figure 9-10 shows the results of running this program. Notice that after the function calls the values of the global variables have changed.


Figure 9-10: Results of Running testFunctionFive Program

This example showed you how to pass the address of a variable. You prefix the & operator to the variable name to get its address and it is this address that is passed to the function. If the variable you want to pass to the function is a pointer then you pass only the name of the variable and leave off the & operator. Example 9.17 uses testFunctionFive() again to demonstrate.

Listing 9.17: main.cpp

start example
1  #include <iostream>  2  #include "testfunctionfive.h"  3  using namespace std;   4  5  int main(){  6      int* ip1 = new int(3);  7  8      cout<<"Value of integer pointed to by ip1 = "<<*ip1<<endl;  9      testFunctionFive(ip1); 10      cout<<"Value of integer pointed to by ip1 = "<<*ip1<<endl; 11      delete ip1; 12      return 0; 13  }
end example

Referring to example 9.17, an integer pointer is declared and initialized to the address of an integer object allocated on the heap. The pointer is passed to testFunctionFive() on line 9. Notice that delete must be called on ip1 to release the heap memory.

Passing References

Remember references? They’re kinda like pointers except a pointer is a variable and a reference is not. You can change what a pointer points to but once you set a reference it can’t be changed to refer to anything else. To pass an argument to a function in the form of a reference requires the function prototype to declare parameters of type reference. This confuses many students because the & operator is overloaded for this purpose. The following code shows a function named testFunctionSix() that’s declared to take an integer reference as an argument:

void testFunctionSix(int& irA);

A reference to an argument is treated differently than a pointer in the body of the function. Examine the definition of testFunctionSix() given in example 9.18 below.

Listing 9.18: testfunctionsix.cpp

start example
1  #include "testfunctionsix.h" 2 3  void testFunctionSix(int& irA){ 4        irA++; 5  }
end example

When an argument is passed to a function in the form of a reference the parameter name is used as if it were that object. No pointer dereferencing is required. Notice above on line 4 how the parameter irA is incremented and compare this with how the equivalent operation is performed on a pointer in example 9.15.

Now, to call this function with an integer argument requires no special action on your part. You simply use the name of the object as shown in the following example:

Listing 9.19: main.cpp

start example
1  #include <iostream>  2  #include "testfunctionsix.h"  3  using namespace std;   4  5  int main(){  6      int i = 3;  7      cout<<"Value of i before function call = "<<i<<endl;  8      testFunctionSix(i);  9      cout<<"Value of i after function call = "<<i<<endl; 10      return 0; 11  }
end example

Figure 9.11 shows the results of running this program.

click to expand
Figure 9.11: Results of Running testFunctionSix Program

Passing Arrays to Functions

Arrays are passed to functions by reference. The name of the array is a pointer that contains the address of the first element of the array. (see chapter 8)

The first step in writing a function that takes an array as an argument is to declare the function in such a way that it knows what to expect and that readers of the function declaration can figure out what you’re trying to do! The following code declares a function named printIntArray() that prints the contents of an integer array. It takes a single- dimensional integer array as the first argument and the number of array elements as the second:

Listing 9.20: printIntArray()

start example
 1  #ifndef Print_Int_Array_H 2  #define Print_Int_Array_H 3 4  void printIntArray(int intArray[], int elements); 5 6  #endif
end example

The function definition follows:

Listing 9.21: printintarray.cpp

start example
1  #include "printintarray.h" 2  #include <iostream> 3  using namespace std; 4 5  void printIntArray(int intArray[], int elements){ 6     for(int i = 0; i < limit; i++) 7       cout<<intArray[i]<<" "; 8     cout<<endl;    }
end example

The following main() function illustrates how the function is called:

Listing 9.22: main.cpp

start example
1  #include "printintarray.h" 2 3  int main(){ 4      int my_array[] = {1,2,3,4,5,6,7,8,9,10}; 5      printIntArray(my_array, (sizeof my_array / sizeof(int))); 6      return 0; 7  }
end example

Referring to the main() function shown above, an integer array named my_array is declared and initialized on line 4. The printIntArray() function is called on line 5. Notice that only the name of the array is passed as the first argument. The sizeof operator is used to calculate the size of the array in bytes, which is then divided by the size of the int type in bytes to determine the number of elements contained in my_array. The result of this expression is passed as an argument to printIntArray()’s elements parameter.

Passing Multi-Dimensional Arrays To Functions

Multi-dimensional arrays are passed to functions in the same manner as their single-dimensional counterparts. Your primary concern is how to declare the function so it knows what arguments to expect. Examine the following function declarations:

print2DIntArray(int intArray[][5], int rows); print3DIntArray(int intArray[][3][5], int sheets);

The print2DIntArray() function declares an array parameter of two dimensions, the right-most dimension being specified as 5 and the left-most dimension unspecified. There can be only one unspecified dimension, which is the case with normal static array declarations. The print3DIntArray() function declaration specifies an array parameter of three dimensions, two of which are specified and one not.

You can specify all dimensions of the multi-dimensional array if you desire. Doing so lessens ambiguity but at the price of flexibility. The following complete example declares, defines, and uses an alternative version of the print2DIntArray() function.

Listing 9.23: print_2d_int_array.h

start example
1  #ifndef PRINT_2D_INT_ARRAY_H 2  #define PRINT_2D_INT_ARRAY_H 3 4  const int ROWS = 5; 5  const int COLS = 5; 6 7  void print2DIntArray(int intArray[ROWS][COLS]); 8 9  #endif
end example

Notice on lines 4 and 5 that two integer constants have been declared to specify the dimensions of the array. These are included in the header file and can be used by any file that includes the print_2d_int_array.h header as is shown in the following code:

Listing 9.24: print_2d_int_array.cpp

start example
1  #include "print_2d_int_array.h"  2  #include <iostream>  3  using namespace std;  4  5  void print2DIntArray(int intArray[ROWS][COLS]){  6     for(int i = 0; i < ROWS; i++){  7       for(int j = 0; j < COLS; j++){  8          cout<<intArray[i][j]<<" ";  9       } 10     cout<<endl; 11    } 12  }
end example

Notice how the constants are then used in the body of the function to manipulate the array. The following main() function shows the print2DIntArray() function in action:

Listing 9.25: main.cpp

start example
1  #include "print_2d_int_array.h"  2  3  int main(){  4     int my_2d_array[ROWS][COLS] = {{1,2,3,4,5},  5                                    {2,3,4,5,1},  6                                    {3,4,5,1,2},  7                                    {4,5,1,2,3},  8                                    {5,1,2,3,4}};  9 10     print2DIntArray(my_2d_array); 11 12     return 0; 13  }
end example

Another Example

The following example program reuses the printIntArray() function used in examples 9.20 through 9.23. Another function called sortIntArray() is declared and defined. sortIntArray() takes an integer array as an argument and sorts the contents of the array. The printIntArray() function is used to print the array to the screen. The following code declares the sortIntArray() function:

Listing 9.26: sort_int_array.h

start example
1  #ifndef SORT_INT_ARRAY_H 2  #define SORT_INT_ARRAY_H 3 4  void sortIntArray(int intArray[], int elements); 5 6  #endif
end example

Example 9.27 gives the definition of sortIntArray(). Notice how the function performs exactly what its name implies. It performs the sort on the array elements. It doesn’t print anything to the screen or otherwise do something not hinted at by its name. This is an example of a highly cohesive and loosely coupled function.

Listing 9.27: sort_int_array.cpp

start example
1  #include "sort_int_array.h"  2  3  void sortIntArray(int intArray[], int elements){  4     for(int i = 0; i<elements; i++){  5       for(int j = 1; j<elements; j++){  6         if(intArray[j-1] > intArray[j]) {  7           int temp = intArray[j-1];  8           intArray[j-1] = intArray[j];  9           intArray[j] = temp; 10         } 11       } 12     } 13  }
end example

The following main() function shows how both printIntArray() and sortIntArray() are used together in a program:

Listing 9.28: main.cpp

start example
1  #include <iostream>  2  #include "sort_int_array.h"  3  #include "printintarray.h"  4  using namespace std;   5  6  int main(){  7      int myArray[] = {10,5,9,4,3,8,2,7,6,1,0};  8  9      printIntArray(myArray, ((sizeof myArray)/sizeof(int))); 10      sortIntArray(myArray, ((sizeof myArray)/sizeof(int))); 11      printIntArray(myArray, ((sizeof myArray)/sizeof(int))); 12 13      return 0; 14  }
end example

Figure 9-12 shows the results of running this program.


Figure 9-12: Results of Running Example 9.28



 < Day Day Up > 



C++ for Artists. The Art, Philosophy, and Science of Object-Oriented Programming
C++ For Artists: The Art, Philosophy, And Science Of Object-Oriented Programming
ISBN: 1932504028
EAN: 2147483647
Year: 2003
Pages: 340
Authors: Rick Miller

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