CS 111 Assignment 7
One-dimensional arrays (including C-strings) as parameters to functions


  1. Operations on arrays.   Except for initializations, we cannot change or otherwise access the contents of an array all at once. We can only access the elements of an array one at a time. For example, suppose we've created an array of long integers a as follows:

       long a[] = { 123456789, 987654321, 112233445566778899 };
       const int LENGTH = 3;
    

    Suppose we now want to add 2 to every element in a. We cannot simply write:

       a += 2;
    

    Instead, we must individually change all the elemnents in a, one at a time:

       for ( int i = 0; i < LENGTH; i++ )
          a[i] += 2;
    

    Likewise, suppose we've created another array b, having the same length as a:

       long b[LENGTH];
    

    Suppose we now want to copy the entire contents of array a into array b. We CANNOT simply write:

       b = a;
    

    which would be a syntax error. Instead, we must copy the elements one at a time, as follows:

       for ( int i = 0; i < LENGTH; i++ )
          b[i] = a[i];
    

    However, one thing we CAN do with an entire array, all at once, is pass it as an argument to a function. For example, suppose we have a function with the following heading:

    myFunction(long myArray[], int length)
    

    To call this function, passing our previously-defined array a as an argument, we would write:

       myFunction(a, LENGTH)
    

    Note the need for a separate parameter for the length, because the array cannot, by itself, remember its own length (unless the array contains an extra location for a sentinel value, as a C-string does).


  2. Arrays and pointers, and arrays as parameters to functions.   We will soon consider what happens when a one-dimensional array is passed as an argument to a function. But first, let us consider in more detail what happens when you declare an array as a non-parameter variable (e.g. as a local variable). For example:

       double numbers[10];
    

    Such a declaration creates not only the array itself, but also a pointer to the array. We will now explain what a pointer is.

    A computer's entire random access memory (RAM) can be thought of as one big array of generic memory locations, not specific as to data type. You have probably heard of computers described as "16-bit machines," "32-bit machines," "64-bit machines," or "128-bit machines." This terminology refers to the size of memory locations in the one big array. Each memory location has an address, which is the index of a location in the one big array.

    The memory is described as "random access" because the computer can access any memory location immediately, simply by specifying its address. (In contrast, a disk has "sequential access", meaning that the information on the disk must be accessed in a particular order; a disk drive can't go just anywhere on the disk immediately.)

    A pointer is a memory location which contains the address of another memory location. An array declaration creates not only the array itself but also a pointer to location 0 in the array. In other words, in addition to the array itself, another separate memory location is created, containing the address of location 0.

    When an array is passed as an argument to a function, the name of the array itself is treated as the name of the memory location holding the pointer.

    It is also possible to declare a pointer separately from an array. For example, the following statements declare a pointer ptr and then assign it to contain the same address as the pointer for array numbers:

       double* ptr;
       ptr = numbers;
    

    The pointer ptr then give us another way to access the elements in array numbers. For example:

       for ( int i = 0; i < 10; i++ )
          cout << ptr[i] << endl;
    

    is now equivalent to:

       for ( int i = 0; i < 10; i++ )
          cout << numbers[i] << endl;
    

    Note that ptr and numbers are pointers to the SAME ARRAY, not two separate arrays. Note also that it is acceptable to put the name of an array on the right-hand side of an assignment statement, when the variable on the left is a pointer:

       double* ptr;
       ptr = numbers;
    

    where numbers was previously declared as an array. However, it is NOT acceptable to put the name of an array on the right-hand side of an assignment statement whose left-hand side is another array:

       double anotherArray[10];
       anotherArray = numbers;     // wrong! syntax error!
    

    When you declare an array as a non-parameter variable, e.g. as a local variable, the array's name is treated as a pointer constant. It will always point to location 0 of that particular array and cannot be reassigned to point to a different array. On the other hand, a pointer variable can point anywhere, and can be reassigned, which is why it IS okay to put an array on the right-hand side of an assignment statement whose left-hand side is a pointer variable.

    Declaring an array as a formal parameter to a function is quite different from declaring an array as a non-parameter variable. When an array is declared as a formal parameter, i.e. inside the parentheses of the function heading, then that parameter is treated as a pointer variable, NOT as the declaration of a new array. In a parameter declaration, array notation is exactly equivalent to pointer notation, though such is not the case when declaring non-parameter variables.

    When the function is called, the array parameter (really just a pointer variable) is assigned the address of location 0 of the array that has been passed as an argument by the caller.


  3. Examples of arrays as parameters to functions.   Compile arrayFunctionDemo1A.cpp and run it. Then look at the source code. In the main function, thie program first declares two arrays with the same length, as follows:

       int array1[] = {0, 2, 4, 6, 8};
       int array2[] = {1, 3, 5, 7, 9};
       const int LENGTH = 5;
    

    In then prints out the contents of both arrays, using a call to the printArray function defined at the bottom of the file:

       cout << "main:        array1:  ";
       printArray(array1, LENGTH, cout);
       cout << "             array2:  ";
       printArray(array2, LENGTH, cout);
       cout << endl;
    

    The above statements result in the following output:

    main:        array1:  0 2 4 6 8
                 array2:  1 3 5 7 9
    

    Then, the main function then calls the swap function, defined below the main function. The swap function has the following heading:

    void swap(int a[], int b[], int length, ostream& out)
    

    and the main function's call to the swap function looks like this:

       swap(array1, array2, LENGTH, cout);
    

    Note that the length of the two arrays is passed as a separate parameter. (Both arrays have the same length.)

    Inside the swap function, the contents of the arrays are printed out again, with the following output:

    Begin swap:       a:  0 2 4 6 8
                      b:  1 3 5 7 9
    

    Note that parameters a and b are pointer variables which point to the same arrays that are referred to in the main function by the names array1 and array2.

    After printing out the contents of the two arrays, the swap functions swaps the contents of the two arrays, one element at a time, as follows:

       for ( int i = 0; i < length; i++ )  {
          int temp = a[i];
          a[i] = b[i];
          b[i] = temp;
       }  // for i
    

    The swap function then prints out the contents of the arrays again. Note that they have been successfully swapped:

    End swap:         a:  1 3 5 7 9
                      b:  0 2 4 6 8
    

    Then the swap function finishes executing, and control returns to the main function. After the main function's call to swap, the main function then prints out the contents of the arrays again:

    main:        array1:  1 3 5 7 9
                 array2:  0 2 4 6 8
    

    Note that they have been successfully swapped, as seen from the main function, as well as from the swap function. That is because a is a pointer to the same array that is referred to by the name array1 and, likewise, b is a pointer to the same array that is referred to by the name array2. Thus, any change to the contents of these arrays, as seen via the pointers a and b in the swap function, is necessarily also a change to the contents of these same arrays as seen via the names array1 and array2 in the main function.

    Now compile arrayFunctionDemo2A.cpp and run it. This program behaves exactly like arrayFunctionDemo1A.cpp. Compare the source code of the two programs and observe that they are exactly identical, except that the swap function in arrayFunctionDemo2A.cpp has the following heading:

    void swap(int* a, int* b, int length, ostream& out)
    

    whereas the swap function in arrayFunctionDemo1A.cpp has the following heading:

    void swap(int a[], int b[], int length, ostream& out)
    

    As explained earlier, an array parameter is a pointer variable. Thus, in a function paremeter list, array notation and pointer notation are interchangeable -- even though they are NOT interchangeable for non-parameter variables.


  4. Arrays as parameters: Are they value parameters or reference paramters?   On page 676 of the textbook, Dale says that arrays are always passed by reference. This is an oversimplification. Arrays are passed by pointer value. As we have seen, as far as the elements in an array are concerned, passing arrays by pointer value is equivalent to passing them by reference. However, the pointers themselves are passed by value.

    To see what this means, compile arrayFunctionDemo2B.cpp and run it. This program is identical to arrayFunctionDemo2A.cpp except that, in arrayFunctionDemo2B.cpp, the swap function swaps the pointers to the arrays:

       int* temp = a;
       a = b;
       b = temp;
    

    instead of swapping the contents of the arrays, as the swap function does in arrayFunctionDemo1A.cpp and arrayFunctionDemo2A.cpp.

    At the beginning of the swap function in arrayFunctionDemo2B.cpp, pointer a points to the array known in the main function as array1, and pointer b points to the array known in the main function as array2. After the pointers are swapped, a points to the same array that b formerly pointed to, and vice versa. Thus, at the end of the swap function, a points to array2 and b points to array1, but the contents of array1 and array2 themselves have NOT been changed.

    Following is the output of arrayFunctionDemo2B.cpp:

    main:        array1:  0 2 4 6 8
                 array2:  1 3 5 7 9
    
    Begin swap:       a:  0 2 4 6 8
                      b:  1 3 5 7 9
    
    End swap:         a:  1 3 5 7 9
                      b:  0 2 4 6 8
    
    main:        array1:  0 2 4 6 8
                 array2:  1 3 5 7 9
    

    Observe that the swap appears to be successful as seen from the swap function, but NOT as seen from the main function. This is because the pointers were swapped, but the contents of the arrays themselves were NOT swapped; and the pointers are passed by value, not by reference. In the main function, array variables array1 and array2 still refer to the same arrays they referred to originally, even though the pointers to them were swapped inside the swap function.

    Now compile arrayFunctionDemo2C.cpp and run it. This program behaves exactly like arrayFunctionDemo2B.cpp. Compare the source code of arrayFunctionDemo2C.cpp and arrayFunctionDemo2B.cpp, and observe that they are identical except for the heading of the swap function, for which arrayFunctionDemo2B.cpp uses pointer notation, whereas arrayFunctionDemo2C.cpp uses array notation. As explained earlier, the two notations are equivalent when used in declarations of formal parameters, though they are NOT equivalent when used in declarations of variables other than parameters.

    Now try to compile arrayFunctionDemo1B.cpp. This program is identical to arrayFunctionDemo2B.cpp, except that, in the swap function, it uses a local array variable instead of a local pointer variable:

       int temp[] = a;   //  does not compile:
                         //  "invalid initializer"
       a = b;
       b = temp;
    

    This does not compile because an array cannot be initialized to anything except an initializer list or (in the case of an array of characters) a string literal.

    At the bottom of all five of the example programs you've looked at recently is a function printArray, which has the following heading:

    void printArray(const int array[], int length, ostream& out)
    

    The keyword const, in the parameter declaration const int array[], prevents us from modifying any of the elements of the array inside the function. Any attempt to modify them will be a syntax error. A const declaration should be used whenever we want to use an array as an in-parameter only.

    Another of the printArray function's parameters is a reference to an object of class ostream. Remember that cout is an object of class ostream and also that class ofstream (for file output streams) is a subclass of class ostream, i.e. any object of class ofstream is also an object class ostream. Thus, the printArray function can be used print an array EITHER to the terminal window or to a text file. To print the array to the terminal window, we call the function using cout as an argument:

       printArray(array1, LENGTH, cout);
    

    To print the array to a text file, we could call this function using an ofstream object as an argument, instead of cout.

    In our present programs, it so happens that we've used the print function only with cout, and not with a file output stream, but we've written the function so that we could use it more flexibly if we wanted to.


  5. Still more about arrays as parameters.   Compile arrayFunctionDemo3.cpp by typing:

    g++ arrayFunctionDemo3.cpp textUtility2.cpp
    

    and run it. This program has the following output:

    After initializing array:    0.00   2.00   4.00   6.00   8.00
    After call to function_A:    0.00   2.00   4.00   6.00   8.00
    After call to function_B:    0.00  20.00   4.00   6.00   8.00
    After call to function_C:    0.00   1.00   2.00   3.00   4.00
    After call to function_D:    0.00   1.00   2.00   3.00   4.00
    After call to function_E:    1.00   0.50   0.33   0.25   0.20
    After call to function_F:    1.00   0.50 -10.00 -11.00 -12.00
    

    This program's main function initialized an array of five floating-point numbers and then prints out the contents of the array, using a call to function print, defined at the bottom of the file. The main function then calls each of a sequence of six other functions, function_A through function_F. After calling each of these six functions, the main function again prints out the contents of the array, using calls to print each time.

    In this program, print is called only from the main function, not from any of the other functions. So, the printed array is always the array as seen from the main function, NOT as seen from any of functions function_A through function_F.

    Function function_A has the following heading:

    void function_A(float x)
    

    and is called in the main function as follows:

       function_A(array[0]);
    

    Note that this function does NOT take the entire array as an argument. The formal parameter is of type float, a simple data type, and the argument that is paased to it, when the function is called, is an element of an array -- not the entire array. Recall that an array is a collection of variables of the same type that are next to each other in memory. Thus, an element of an array of float is, itself, a variable of type float,

    When function function_A is called, tts argument can be ANY expression with a value of type float. It could be a numeric literal, a named constant, a variable, or a more complex expression of type float, such as an arithmetic expression or a call to a function which returns a float value. In this particular program, the argument happens to be an element of an array, but this fact is irrelevant to what goes on inside the function. What gets passed to the function, via the parameter, is simply a float value. And, because the parameter is passed by value, any change to its value within the function does not affect the value of any variable outside the function. Again, the fact that the argument variable happens to be an element of an array is irrelevant. Thus, calling function function_A does not have any effect on the contents of any element of the array.

    Function function_B has the following heading:

    void function_B(float& x)
    

    and is called in the main function as follows:

       function_B(array[1]);
    

    Funciton function_B takes a reference parameter of type float. It can take, as an argument, any variable of type float. In this program, the argument variable happens to be an element of an array. Again, the fact that it's an element in an array is irrelevant; it is treated as an individual variable of type float. Because the parameter is a reference parameter, it is another name for the SAME variable that has been passed as an argument; it is not a separate copy. Thus, any change to the value of the parameter inside the function is also a change to the value of the argument variable. Thus, when function_B is called in this program, it does change the value of the one array element that has been passed to it as an argument, without affecting the rest of the array.

    Functions function_C, function_D, function_E, and function_F each pass an entire array as a parameter, plus a separate int parameter to keep track of the array's length. For example, function_C has the following heading:

    void function_C(float a[], int length)
    

    and is called in the main function as follows:

       function_C(array, LENGTH);
    

    As explained earlier, arrays are passed by pointer value, which, as far as the elements of the array are concerned, is equivalent to passing by reference. In function function_C, the elements in the array are changed. Since the array which is pointed to by parameter a is the same array which was passed to the function as an argument in the main function, the call to function_C effectively does change the contents of array, as seen from the main function.

    Function function_D, likewise, has the following heading:

    void function_D(float a[], int length)
    

    and is called in the main function as follows:

       function_D(array, LENGTH);
    

    However, in function_D, a second array b is created, and pointer a is then reassigned to point to array b instead of to the argument array:

       float b[length];
       a = b;
    

    Subsequently, changes are made to the contents of the array pointed to by pointer a:

       for (int i = 0; i < length; i++)
         a[i] = i + 3;
    

    But because the array pointed to by a is now the local array b rather than the argument array, these changes to the array pointed to by a have no effect on the argument argument array. They affect only array b, a local array which is inaccessible outside function_D and will die as soon as function_D is finished executing. Thus, any changes to the contents of the array pointed to a will NOT be noticed after function_D is finished executing, and the contents of array are unchanged.

    Function function_E, likewise, has the following heading:

    void function_E(float a[], int length)
    

    and is called in the main function as follows:

       function_E(array, LENGTH);
    

    Inside function function_E, a local pointer b is declared and assigned to point to the same address in memory that pointer a points to:

       float* b = a;
    

    Because a points to the argument array, so too does pointer b. So, b is now yet another way of accessing the argument array. Thus, the following changes to the contents of the array pointed to by b:

       for (int i = 0; i < length; i++)
         b[i] = 1.0 / (1 + i);
    

    do affect the contents of the argument array, because array is the array pointed to by b.

    Function function_F, likewise, has the following heading:

    void function_F(float a[], int length)
    

    and is called in the main function as follows:

       function_F(array, LENGTH);
    

    Inside function function_F, a local pointer b is assigned to point to location 2 of the array pointed to by a:

       float* b = a + 2;
    

    whereas, in all our previous examples, pointers were assigned to point to location 0 of an array. The above statement is an example of pointer arithmetic, one of the most powerful -- and dangerous -- features of C and C++.

    The subsequent for loop:

       for (int i = 0; i < length - 2; i++)
         b[i] = 0 - (10 + i);
    

    does the same thing we could have done using the following for involving pointer a:

       for (int i = 2; i < length; i++)
         a[i] = 0 - (10 + i);
    

    Locations 0 to length minus 3 of the array pointed to by b are the same memory locations that are also known as locations locations 2 to length minus 1 of the array pointed to by a, which are, in turn, the same memory locations also known as locations 2 to length minus 1 of the argument array, since array is the array pointed to by a. Thus, when function_F is called in the main function, it affects the contents of memory locations 2 to length minus 1 of the argument array.


  6. C-strings as parameters to functions.   Look now at source code file TextUtility2.cpp, which is similar to the file TextUtility.cpp that you worked with in Assignment 6, except that TextUtility.cpp does not use objects of class string. Instead, it uses C-strings only.

    TextUtility2.cpp includes the header file TextUtility2.h, as must all programs which use functions defined in TextUtility2.cpp. Note that TextUtility2.cpp, like TextUtility.cpp, does not contain a main function. That's because it is not a program. Rather, it is a programmer-defined library of functions intended to be used by programs in separate files.

    Below is a C-string version of the function containsAsciiControl. (In the Assignment 6 tutorial material, we discussed a version which handles string class objects.) This function returns true if the string contains at least one ASCII control character, and false if it doesn't contain any.

    bool containsAsciiControl(const char text[])
    {
       // Search for an ASCII control character,
       // and return true when one is found:
       int index = 0;
       while ( text[index] != '\0' )
          if ( isAsciiControl(text[index++]) )
              return true;
    
       // Assertion:  If we have reached this point,
       // text contains no ASCII control characters.
    
       return false;
    }  // function containsAsciiControl
    

    When a function takes a C-string as a parameter, it is usually assumed that the length of the string is unknown. Unlike a string class object, a C-string doesn't have a length function. So, to access all the characters in a C-string, we cannot use a count-controlled for loop to count from index zero up to one less than the length of the string. Instead, we use a sentinel-controlled while loop, incrementing the index until the null character ('\0') is reached.

    In the homework, you will be asked to write a function isNaturalNumber which will test whether a C-string represents a natural number (non-negative integer). A C-string represents a non-negative integer if, and only if, (1) it contains at least one character (besides the null character) and (2) it does not contain any non-digit characters. Checking whether it contains any digit characters will be similar to checking whether it contains any control characters, except that you'll need to use a negated (!) call to the isAsciiDigit function, and you'll need to be very careful about when your isNaturalNumber function returns true vs. when it returns false. And remember to treat the empty string as a special case.

    A C-string is considered empty if the character at location 0 is the null character.

    Once you have successfully written the isNaturalNumber function, the following isInteger function should work too:

    /*
     * bool isInteger(const char text[])
     *
     * Tests whether the specified string represents
     * an integer.  A string represents an integer if,
     * and only if, it consists of a string representing
     * a natural number preceded by an optional leading
     * minus sign. 
     *
     * Parameter:
     *    text -  the string to be tested.
     *
     * Returns:
     *    true if text represents an integer,
     *    false otherwise.
     */
    bool isInteger(const char text[])
    {
       if ( text[0] == '\0' )
          return false;
    
       if ( text[0] == '-' )
          text = text + 1;
       return isNaturalNumber(text);   // stub -- doesn't work yet
    }  // function isInteger
    

    If the string does NOT begin with a minus sign, we can simply test whether the original string text represents a natural number. On the other hand, if the string does begin with a minus sign, then we want to test whether the substring beginning at location 1 represents a natural number. To obtain the substring beginning at location 1, we can simply move the pointer text so that it points to location 1 instead of location 0 of the original string. Above, we have accomplished this via pointer arithmetic.

    The const keyword, in front of the data type in the parameter declaration, prevents us from changing elements in the string but does NOT prevent us from moving the pointer. Yet it does prevent us from using another, separate pointer to point to any part of the string, e.g. as follows:

          char* substring = text + 1;
    

    So, we have no choice but to move the text pointer itself:

          text = text + 1;
    

    You will need to use this technique in another homework problem, in which you will be asked to write a C-string version of the isForbinInt function (of which you wrote a string class version in Assignment 6).

    When writing isForbinInt, you will also need to compare two strings lexicographically. When at least one of the two strings you are comparing is a C++ string object, you can use the relational operators (<, >, <=, >=, ==, and !=) to do lexicographical comparisons. But, when both strings are C-strings, you cannot use the relational operators to do lexicographical comparisons. (If you use the relationsl operators to compare C-strings, the compiler will allow it, but the results will be unpredictable.) So, we must use other means to do a lexicographical comparison of C-strings.

    For this purpose, we have defined a compare function in textUtility2.cpp:

    /*
     * int compare(const char text1[], const char text2[])
     *
     * Determines the lexicographical ordering of
     * two C-strings.
     *
     * Parameters:
     *    text1 - a C-string
     *    text2 - another C-string
     *
     * Returns:
     *    An integer < 0, if text1 precedes
     *      text2 in a lexicographical ordering;
     *    an integer > 0, if text2 precedes
     *      text1 in a lexicographical ordering; or
     *    0, if text1 equals text2.
     */
    int compare(const char text1[], const char text2[])
    {
       int index = 0;
       while ( (text1[index] == text2[index])
               && (text1[index] != '\0') )
          index++;
       return (text1[index] - text2[index]);
    }  // function compare
    

    Note that the comments do not specify the exact value of the returned value. What matters is whether it is less than zero, greater than zero, or equal to zero.


Back to: