CTEC1335/2008F Pointers, Part 2 ================ 1. Using pointers to traverse and manipulate arrays. ------------------------------------------------ Given an array: double arr [] = { 1.2, 5.6, 8.2, 3.4, 2.7, 1.2, 1.3, 4.0, 5.0 } ; We could use a for loop to calculate the mean (average): // calculate the number of elements in the array const int n = sizeof(arr) / sizeof(arr[ 0 ]) ; double sum = 0.0 ; for ( int i = 0 ; i < n ; i++ ) { sum += arr[ i ] ; } double avg = sum / double( n ) ; Or we could use a pointer: const double * p = arr ; for ( int i = 0 ; i < n ; i++, p++ ) { sum += *p ; } This works, but clearly, using just an int to access each array element is better, in this case. Using a pointer to access an array is better when we don't know how many elements are in the array. This requires that a "sentinel" value be placed as the last element of the array. The code will look for the sentinel and stop when it finds it. C style strings work this way: each C string is terminated with a zero byte (called a "terminating null character"; sometimes C style strings are called "zero terminated strings.") For example, the C string "hello" is actually stored as an array of six (6) characters: { 'h', 'e', 'l', 'l', 'o', '\0' } Let's look at a summing loop, which adds an array of positive integers. The sentinel value is -1, which we can make a constant. const int STOP = -1 ; const int arr [] = { 1, 5, 2, 7, 8, 23, 23, 12, 5, 2, 42, 632, 324, STOP } ; Using a for loop with an int, we can do this: int sum, i ; for ( sum = i = 0 ; arr[ i ] != STOP ; i++ ) { sum += arr[ i ] ; } Using a for loop with a pointer, we can do this: int sum = 0 ; for ( const int * p = arr ; *p != STOP ; p++ ) { sum += *p ; } 2. Using pointers for passing by reference. --------------------------------------- You can pass by reference using a pointer. In fact, this is how reference variables in C++ actually work. Sometimes, you need to use code that is designed to work with C and C++. C doesn't have reference variables, only pointers. For example, Microsoft's Win32 API, which is used to develop Windows programs in both C and C++ has a structure called a RECT and a function called GetClientRect( ) which gets the coordinates of a window's client area, so that it can be painted. struct RECT { int left ; int top ; int right ; int bottom ; } ; void GetClientRect( RECT * lprc ) ; To call this function correctly, you need to first declare a RECT variable, so that you have memory to store the left, top, right, and bottom members (16 bytes in total). Then you take the address of your RECT variable and pass that address to GetClientRect( ) as a pointer, so that the function can fill in the values for left, top, right, and bottom. RECT rcClient ; GetClientRect( & rcClient ) ; By passing the pointer, we are only copying four bytes on the stack (the address), instead of having to copy 16 bytes (the data). (Microsoft uses a lot of these kinds of functions that accept pointers to structures.) You can write a similar function of your own to calculate the area of a rectangle: int rectArea( const RECT * lprc ) { // the -> operator is used to "dereference" the pointer, // i.e., get the data that it points to int width = lprc->right - lprc->left ; int height = lprc->bottom - lprc->top ; return width * height ; } RECT rc = { 10, 20, 30, 40 } ; cout << "Area of rectangle is " << rectArea( & rc ) << endl ; If you are using C++ only, then you can use a reference variable to do the same thing: int rectArea( const RECT & rc ) { int width = rc.right - rc.left ; int height = rc.bottom - rc.top ; return width * height ; } RECT rc = { 10, 20, 30, 40 } ; cout << "Area of rectangle is " << rectArea( rc ) << endl ; 3. Using assert( ) to check for null pointer assignment ---------------------------------------------------- #include The assert( ) function can be used to test any Boolean expression (the same type of expression that you use in an if, while, or for loop.) If the expression evaluates to true, assert( ) does nothing. However, if the expression evaluates to false, assert( ) will stop the program and display the filename and line number where the "assertion failure" occurred. Using assert( ) can be used in a function to enforce a contract: it can check parameter values at the beginning of the function to verify preconditions; it can be used to check return values at the end of function to verify postconditions. The use of assert( ) is typically only necessary during the software development process, when you "release" software, you would typically disable the asserts( ), because presumably you have sufficiently tested the software to eliminate bugs like broken contracts. For example, in the pointer version of rectArea( ) above, you can say that the precondition is that lprc must be a valid pointer. You can use an assert( ) to partially enforce this contract: int rectArea( const RECT * lprc ) { assert( lprc != 0 ) ; int width = lprc->right - lprc->left ; int height = lprc->bottom - lprc->top ; return width * height ; } The assert( ) call is very similar to writing: if ( lprc == 0 ) { cerr << "Assertion failed: lprc != 0, file " << __FILE__ << ", line " << __LINE__ << endl ; exit( 255 ) ; } Note: There is no way of telling whether lprc points to an actual valid block of 16 bytes where a RECT variable is stored. The best we can do is check for a zero or "NULL" pointer. It is perfectly legal to do this, although you may get a compiler warning about lprc not being initialized: const RECT * lprc ; int area = rectArea( lprc ) ; This code is WRONG, because declaring a pointer variable only allocates four bytes for the address stored in the pointer variable. It DOES NOT allocate any memory for the data bytes stored at that address! The correct code would be: // initialize pointer and data memory RECT * prc = new RECT ; // not const, so we can set the data values // initialize data values prc->left = 10 ; prc->top = 20 ; prc->right = 30 ; prc->bottom = 40 ; int area = rectArea( prc ) ; // . . . do more stuff with the RECT at prc delete prc ; // prc RECT no longer required; free the memory prc = 0 ; // a good idea to reset the pointer, just in case // it is used below. (This is called a "dangling // pointer" problem; setting the pointer to zero // after the memory is released is one way to // address this problem; using contracts and assertions // is another way.) 4. Using default pointer parameters for "optional return values" ------------------------------------------------------------ We can use pointers for "optional" parameters in functions. This works because you can always pass a zero as a pointer (e.g, a "NULL" pointer). For example, for the above rectArea( ) function, if the user wants the width and height returned, we can do it with reference parameters: int rectArea( const RECT & rc, int & width, int & height ) ; But this forces the programmer to _always_ pass three arguments when calling rectArea( ). If the user does not care about the width and height, this function becomes cumbersome to use. It is possible to "overload" the function to remove this requirement, but then you have to write two versions: int rectArea( const RECT & rc ) ; int rectArea( const RECT & rc, int & width, int & height ) ; Using pointers and C++'s "default arguments", you can have one function that can be called in three or four different ways. // function declaration: the last two arguments' default values are zero int rectArea( const RECT *, int * = 0 , int * = 0 ) ; // . . . // function definition: default arguments specified in the declaration only int rectArea( const RECT * lprc, int * lpWidth, int * lpHeight ) { assert( lprc != 0 ) ; // required argument int width = lprc->right - lprc->left ; int height = lprc->bottom - lprc->top ; if ( lpWidth != 0 ) // check if optional parameter has been // supplied with a non-NULL pointer { *lpWidth = width ; // return by dereferencing the pointer } if ( lpHeight != 0 ) { *lpHeight = height ; } return width * height ; } int w, h ; RECT rc = { 10, 20, 30, 40 } ; // choice of calls: rectArea( & rc ) ; // area only (returned) rectArea( & rc, & w ) ; // area and width only rectArea( & rc, & w, & h ) ; // area, width, and height rectArea( & rc, 0, & h ) ; // area and height only // ^^^^^^^^^^^^^^^^^^^^^ // (You have to fill in the zero for // lpWidth because you can't skip a // parameter unless its the last one // [or more] and has a default [all // remaining parameters have defaults].) You can extend the function one step further: void calcRect( const RECT * pRect, int * pWidth = 0, int * pHeight = 0, int * pArea = 0, int * pPerimeter = 0 ) { assert( pRect != 0 ) ; int width = pRect->right - pRect->left ; int height = pRect->bottom - pRect->top ; if ( pWidth != 0 ) { *pWidth = width ; } if ( pHeight != 0 ) { *pHeight = height ; } if ( pArea != 0 ) { *pArea = width * height ; } if ( pPerimeter != 0 ) { *pPerimeter = width + height + width + height ; } } ===