7.7. Arrays And Functions

Time: 00:05:03 | Download: Large, Large (CC), Small | Streaming, Streaming (CC) | Slides (PDF)
Prerequisite Concepts

Array Function Arguments And Parameters

Chapter 6 stated that pass-by-value is the default argument passing technique for C++ functions, which is true for most kinds of data. But arrays are the exception to this rule: programs always pass them to functions by pointer. The difference between arrays and the pass-by-pointer examples presented in Chapter 6 is how we obtain the address. Previous examples required us to use the address-of-operator to get the address of the passed data. But the array's name is its address, specifically, the address of the first element in the array.

void function(int size, int* data_ptr)
{
	for (int i = 0; i < size; i++)
		... data_ptr[i] ...
}

int main()
{
	int data[5];

	function(5, data);
}
Arrays are always passed as pointers. The picture illustrates the code appearing in part (a). The variable data_ptr points to the array named data.
(a)(b)
An array as a function argument. Programs pass arrays to functions by pointer, meaning that array arguments can move data into and out of functions.
  1. Although the program passes data into the function as an address and stores it in a pointer variable, it is still possible to use the index operator, [], to access the individual elements of the array. Unlike Java, which includes the array's size or length as an attribute, C++ programs must pass the size of the array as an explicit argument.
  2. Passing data by pointer involves two variables: the original data and a pointer. While the function is running, the data has two names defined in two different scopes: the original name, defined in the caller's scope, and the function parameter name, a local variable, defined in the function's scope. Only one array exists, so any changes made through the function parameter change the original array.
Array Definition Function Calls and Arguments Function Parameter Alternatives
int test[10]; func1(test); void func1(int* student)
void func1(int student[])
void func1(int student[10])
float test_score[10][4]; func2(test_score); void func2(float scores[][4])
void func2(float scores[10][4])
double class_score[5][5][5]; func3(class_score); void func3(double scores[][5][5])
void func3(double scores[5][5][5])
Passing arrays as function arguments. C++ passes array arguments to function parameters by the array's address, generally represented by the array's name. The table demonstrates:
  1. The syntax for defining arrays of one, two, and three dimensions.
  2. Array function arguments consist of the array's address, independent of the number of array dimensions.
  3. When defining a one-dimensional array parameter, programmers can choose between pointer or array notation, an asterisk, or empty square brackets. When defining multi-dimensional array parameters, programmers may omit the size of the first dimension, but the sizes of subsequent dimensions are required. The next section explains the reason for this requirement.
Although programs can define function parameters as pointers or can skip the size of the first dimension, function statements typically access array elements with the normal array index notation:

Returning One-Dimensional Arrays From Functions

Review

Please review the following as needed:

Just as it is the default to pass most arguments to functions by value, it is also the default to return values (via the return operator) by value. But again, arrays are the exception and are returned by-pointer. Returning an array by pointer has some very interesting ramifications. First, keep in mind that by default, arrays, like any other kind of data, are automatic variables unless specifically defined differently. When an array goes out of scope when a function ends, the memory occupied by the array is deallocated and made available for other use by the program. So, when returning the address of an array, you must ensure that the program doesn't deallocate the array while a pointer to it might still be in use elsewhere in the program. The following function highlights this problem. Can you see the error?

int* get_scores()
{
	int	scores[100];
		. . .
	return scores;
}
A function returning an array: A frequent error. The figure illustrates a frequently overlooked error - so often overlooked that I faced it twice during job interviews. The ellipses denote arbitrary and, regarding the problem, irrelevant statements. The function is syntactically correct and compiles without error. We begin with a few hints:

The problem is a logical rather than a syntax error - the function compiles and runs but exhibits unpredictable behavior. The function correctly returns the address of the local array scores, but the program deallocates it when the function ends. However, the code calling get_scores likely needs the data stored there. But once the program deallocates that memory, it may be reallocated and overwritten very quickly, destroying whatever information get_scores saved there. The function will likely not perform as expected (and if it does work, it's good luck and not being correct). Programmers can choose one of three ways to solve the problem.

The static Modifier

The text first introduced the auto and static keywords Chapter 1 as modifiers that change how programs allocate variable memory. If a variable definition doesn't use either keyword, auto is assumed.

int* get_scores()
{
	static	int	scores[100];
		. . .
	return scores;
}
int* exam_scores = get_scores();

for (int i = 0; i < 100; i++)
	... exam_scores[i] ...
(a)(b)
Make the array static. The memory allocated for static variables is not deallocated when the variable goes out of scope (i.e., when the using function returns). Just as C++ passes arrays as pointers, it also returns them as pointers. The elements in an array returned by-pointer are accessed just as the elements of any array are: by indexing into the array.
  1. Although the variable name scores goes out of scope when the get_scores function returns, the program doesn't deallocate the memory and can access the data through the returned address.
  2. A code fragment showing the syntax for saving and using the address returned by the get_scores function. The square brackets that form array indexes are just an operator that operates on an address.
Advantages Disadvantages
  • Easy to understand
  • Easy to implement
  • Function and caller must agree on the fixed size of the array at compile time
  • Subsequent function calls overwrite previous data
  • Increases minimum program memory requirements

The suitability of one solution over another ultimately depends on the underlying problem the program solves. You must be aware of one potential problem when adopting the "static" solution: the function reuses the same memory each time it's called. If the code calling the function copies or fully processes (i.e., finishes with) the data get_scores returns, then this situation is not a problem.

int* array[5];

for (int i = 0; i < 5; i++)
	array[i] = get_scores();
The picture illustrates the behavior of the code appearing in part (a). All the elements of the array point to the scores array.
(a)(b)
A potential problem with the static solution. Each call to get_scores (Figure 4) returns the same value: the address of the scores array. So, the only way to save multiple return values simultaneously is to copy the data between function calls. Note that this is only a problem in the rare situation when a program processes return values concurrently rather than sequentially.
  1. A code fragment illustrating a situation in which the static solution fails
  2. An abstract representation of the potential problem with the static solution

Array Argument/Parameter

int* get_scores(int* scores)
{
		. . .
	return scores;
}
int exam_scores[100];

get_scores(exam_scores);
for (int i = 0; i < 100; i++)
	... exam_scores[i] ...
Function definition with a pointer parameterFunction call with an array argument
(a)(b)
Define the array in the function caller's scope. Passing the address of structured data (such as arrays or objects - instances of structures or classes) to a pointer parameter in a function is a common technique used to gather large amounts of data from a function. In this example, returning (with the return operator) the address of the data isn't required and isn't always done. Returning the array is a convenience, permitting the function call to act as an expression.
  1. The parameter scores is a local variable, and so goes out of scope and is deallocated when the function returns, but the program defines the array to which it points in a different scope and its memory is therefore not deallocated when the function returns (see Figure 1(b) above).
  2. The code fragment illustrates how a program can define an array in the function caller's scope, pass it as an argument to the function, and access the array's elements following the function return. The example ignores the value get_scores returns.
Advantages Disadvantages
  • The function can operate on arrays of any size
  • Moves memory management to the caller, making the function more general
  • May require a second argument to communicate the array size
  • Potential for misunderstanding the argument in documentation

The argument/parameter solution is more flexible than making the array a static local variable with a fixed size. Defining the array in the client code allows the client to determine the array's size and makes the function more general. The trade-off is that the function requires one more parameter: the size of the array. Furthermore, returning the address of the array with the return operator allows programmers to use the function as an expression. While this is not especially useful in many situations, it is in some special cases (e.g., C-strings, introduced in the next chapter).

int a[4];
int b[4];
int c[4];

get_scores(a);
get_scores(b);
get_scores(c);
int array[3][4];

get_scores(array[0]);
get_scores(array[1]);
get_scores(array[2]);
(a)(b)
One function but multiple array arguments. With the argument/parameter solution, simultaneously saving multiple return values is easy. Instead of copying the data between function calls, the caller passes a different array as an argument during each function call.
  1. A code fragment illustrating how three distinct arrays are passed to and filled by the function.
  2. A code fragment illustrating the definition of a two-dimensional array that the program passes into the function one row at a time; the calls treat each row as a separate array. In this case, programmers must define the array as rows x columns.

Dynamic Array With new

int* get_scores()
{
	int*	scores = new int[100];
		. . .
	return scores;
}
int* exam_scores = get_scores();

for (int i = 0; i < 100; i++)
	... exam_scores[i] ...
. . .
delete exam_scores;
(a)(b)
Allocate the array with the new operator. Memory allocated with the new operator is allocated in a different part of memory (the heap) automatic variables (the stack), remaining available until the program explicitly deallocates it. So, the program deallocates the variable scores but not the memory to which it points while the function runs, making that memory available to the function calling get_scores.
  1. Dynamically allocating memory on the heap to hold an array with 100 integer elements.
  2. Defining an integer pointer to store the address of the array returned by get_scores. Although the program defines the variable exam_scores as a pointer, it accesses the individual elements with the array index operator: [].
Advantages Disadvantages
  • Easy to understand
  • Easy to implement
  • Easy to save multiple return values at the same time
  • C++ programs manage allocatable memory in two locations, the stack and heap, using different algorithms
  • Neglecting to deallocate the returned heap memory will cause a memory leak
int* array[4];

array[0] = get_scores();
array[1] = get_scores();
array[2] = get_scores();
array[3] = get_scores();
The picture shows an array of pointers. Each pointer element points to a different array.
(a)(b)
The dynamic array solution is flexible.
  1. A code fragment demonstrating that each call to get_scores returns a new array that a program may individually save and access
  2. An abstract representation of the arrays created in (a)

Counting Array Elements

As described previously in the chapter, it is a common practice to partially fill an array. Programs use the elements at the beginning of a partially filled array but not the elements at the end of the array. Whenever a function partially fills an array, it must communicate to the client or caller how many array elements it fills. The following figure illustrates some of the ways that a function satisfies this responsibility.

int get_scores(int *scores)
{
	int count;
		. . .
	count++;
		. . .
	return count;
}
int* get_scores(int* count)
{
	static int scores[100];
		. . .
	(*count)++;
		. . .
	return scores;
}
void get_scores(int* count, int *scores)
{
		. . .
	(*count)++;
		. . .
}
(a)(b)(c)
Returning the number of used or filled elements in an array. The number of used or filled elements in an array is frequently less than the size or capacity of the array. The examples demonstrate the get_scores function filling the scores array. Without prior knowledge about the number of filled elements, the function must return that information to the caller. The figure demonstrates three function call variations.
  1. The example passes the array in as a function argument, defines count as a local variable, and returns it as the function return value.
  2. This example defines the array as a local variable but passes in the counter by pointer, which implements an in/out argument. The function must dereference count when incrementing it. The parentheses are necessary because the dereference operator has a lower precedence than the auto-increment operator.
  3. The example passes both the array and count as pointer arguments and the function does not have a return value.

Returning Two-Dimensional Arrays

The general syntax for returning a multi-dimensional array with the return operator is a little daunting. So, programmers typically avoid that syntax by using the argument/parameter technique, which, with arrays, is implemented as pass-by-pointer. Nevertheless, they can return multi-dimensional arrays, but the notation requires one asterisk for each dimension.

C++ programmers can create multi-dimensional arrays the way that Java programs always do: by creating arrays of arrays with the new operator. The following example illustrates this technique by creating a two-dimensional array. The first array is an array of pointers, and the second array stores the data. Once we get past the strange double-pointer notation and the extra work involved, the actual behavior of the array is relatively straightforward.

#include <iostream>
#include <iomanip>
using namespace std;

int** function(int rows, int cols)
{
	int** array = new int*[rows];
	for (int i = 0; i < rows; i++)
		array[i] = new int[cols];

	for (int i = 0; i < rows; i++)
		for (int j = 0; j < cols; j++)
			array[i][j] = i * j;

	return array;
}

int main()
{
	int	rows = 10;
	int	cols = 5;
	int**	a = function(rows, cols);

	for (int i = 0; i < rows; i++)
	{
		for (int j = 0; j < cols; j++)
			cout << setw(3) << a[i][j];
		cout << endl;
	}

	return 0;
}
A complex picture illustrating part (a). The pointer variable array points to an array of pointers. Each element of the pointer array points to a data array.
(a)(b)
Array of arrays: Creating and returning a two-dimensional array. Programmers can extend this technique to higher dimensions, but this example is sufficient to demonstrate the basic idea.
  1. The C++ code for creating and returning a two-dimensional array
  2. An abstract representation of how an array of arrays "appears" in memory
Dynamic memory allocated on the heap by the new operator is always reclaimed by the operating system when a program terminates, as is the case with (a) above. Realistically, programs that use heap memory must deallocate that memory to avoid creating a memory leak. When a program creates an array of arrays, deallocating heap memory is a little messy:
for (int i = 0; i < rows; i++)
	delete[] a[i];
delete[] a;