cs1ch7sec4.htm
 

CHAPTER 7

ARRAYS: A BETTER WAY OF HANDLING MULTIPLE INSTANCES
 


Section IV: Arrays as Parameters Section I : Introducing Arrays Section II: Accessing Array Elements Section III: Arrays and For Loops

  


Table of Contents

Learning C++:
An Index of Entry Points


2. The

of C++

A reference document on the basic elements of C++.



3. The Patterns





A. Typedef's and Arrays
We now have most of the elements for a simple program to handle inventory. Such a program would allow the user to initialize the inventory to empty, enter the inventory amounts for all products, update the amounts as desired, and output the amounts as desired. However, each of the pieces we have looked at so far - initializing the array, reading values into the array, and outputting those values - would be best written as separate functions.

The names for these three functions might be 'InitializeInventory', 'ReadInInventory', and OutputInventory'. We will explore 'OutputInventory' first. As usual when working with a new function, we start with the design:

OutputInventory
Purpose: To output the inventory amounts for all the items in the inventory
Goal: The inventory amounts are all output

What does this function need, if anything, from the calling function and what does it return? As with all output functions, this one returns nothing, but it needs the data that it is to output. We could explicitly pass each individual element and have 10,000 parameters! Or, we could pass the array. Here we see another advantage to giving a group name for all 10,000 items. We can pass the group name and the system knows to pass all 10,000 elements. Our 'receives' and 'returns' analysis then looks as follows:

Receives: The inventory of items
Returns: NONE

As we move on to declare this function, we need to ask what is the type of this array? Careful! The type of the elements of the array is integer, but the array itself is NOT an integer. To make this clear, consider this analogy. Each of the students on a class roster is a person (we presume!), but the roster itself is not a person.

We need to indicate to the function that 10,000 integers are to be passed. Given what you know at this point, there are two ways to accomplish this. First, we could write

OutputInventory(int items[]);

This is legal and 'kind of' does what we want but only kind of. What this actually means is that the function will accept arrays of ANY number of integers. Interestingly enough, if one adds 'MAX_ITEMS' and writes

OutputInventory(int items[MAX_ITEMS]);

the compiler ignores the 'MAX_ITEMS' and still accepts any array of integers. (What is actually going on here will become clearer when we study pointers and their relationship to arrays in Section III of Chapter 9.)

To make our code safer, it would be nice if we had a 'type' that represented an array of 10,000 integers. It is time to use the 'typedef' statement we learned in the previous chapter. At that time we declared a new type 'Boolean' with the declaration going into a separate file because we anticipated using that type in many programs and wanted to be able to repeatedly take advantage of the type declaration without re-writing it. In this case, the specific array type we are working with is unlikely to be found in too many programs, so it should not be coded in a separate file. Instead, we put it at the top of the file containing 'main':

// Program to handle inventory for a small company
// File: ch7inv1.cpp

#include <iostream.h>

typedef int InventoryArray[10000];

void main()
{

InventoryArray itemInventory;
.
.
}

What the 'typedef' line does here is declare the existence of a new type called 'InventoryArray' and say that any variable of this type is an array of 10,000 integers. Remember that a type is not a variable, so what the typedef is doing is describing a new type of 'thing' and giving that type a name. It is not setting aside any specific memory. Inside 'main', the line

InventoryArray itemInventory;

declares the existence of the variable 'itemInventory' of the type 'InventoryArray'. Since 'InventoryArray' is the name for an array of 10,000 integers, 'itemInventory' is the symbolic name for enough memory for an array of 10,000 integers.

With this, we now also have a type name for the type of the data to be received by 'OutputInventory'. Here is the finished declaration:

void OutputInventory(InventoryArray items);

We again use the type name 'InventoryArray'. This indicates that the array will receive an array of 10,000 integers - which is just what we want. The local name for this array was arbitrarily chosen to be 'items'. We learned earlier that the name does not matter. We could have used the same name, 'itemInventory', that we used in 'main', but that sometimes is confusing. When we write the function definition, however, we need to use whatever name we placed in the parameter list. Therefore, if the function definition parameter is 'items', the rest of the code better use the variable 'items':

    void OutputInventory(InventoryArray items)
    {
    	for (int itemCode = 0; itemCode < MAX_ITEMS;  itemCode++)
    	{	
    		cout << "The inventory amount for item " << itemCode << " is: " 
    	        	        <<  items[itemCode] << endl;
    	}
    }
    
The code inside this function is exactly the same as the code we saw above for outputting the elements of an array.

Here is how the whole program looks so far:

    // Program to handle inventory for a small company
    //  File: ch7inv1.cpp
    
    #include 
    
    typedef int InventoryArray[10000];
    
    void OutputInventory(InventoryArray items);
    
    void main()
    {
    	InventoryArray itemInventory;
    	OutputInventory(itemInventory);
    }
    
    void OutputInventory(InventoryArray items)
    {
    	for (int itemCode = 0; itemCode < MAX_ITEMS;  itemCode++)
    	{	
    		cout << "The inventory amount for item " << itemCode << " is: " 
    	        	        <<  items[itemCode] << endl;
    	}
    }
    
The typedef comes before the function declaration so that the compiler knows about the new type, 'InventoryArray', before seeing it used. Later, in 'main', the function 'OutputInventory' is given the array of inventory items which it needs. Of course, in a complete program there had better be some way of getting information into the array before printing out the information. Otherwise garbage will be printed. But, we will get there.

B. Globals
You may not have noticed, but there is a problem here with MAX_ITEMS - it has not been declared. OOPS! Now, where should the declaration go? We haven't really explained why yet, but if you look at all earlier programs, you will discover that constants have been declared outside of any specific function such as 'main'. In fact, they have been declared at the top of the file, before any variable or function declarations.

In contrast, all the variables we have used in our programs have been declared either as parameters or inside a function and we have called them local variables. By local, we mean that they are known only inside the function(s) they are declared in. Thus, we could declare MAX_ITEMS as a constant inside 'main' and add it as a parameter to 'OutputInventory'. After all, it is true that 'OutputInventory' does need it to accomplish its work.

This approach is sometimes used but, more often, a different approach is taken. It is possible to declare a constant (or a variable) as global. This means that it is known in ALL functions that occur in the same file, after the declaration. More precisely, a global constant (or variable) is declared outside all functions and classes and is known from the point of declaration to the end of the file. So, if we declare MAX_ITEMS at the top of the file containing 'main', it will be known to all functions in the file, including 'OutputInventory'. In our case, we will put it just before the typedef statement and change the typedef to take advantage of it.:

    // Program to handle inventory for a small company
    //  File: ch7inv1.cpp
    
    #include 
    
    const  int MAX_ITEMS = 10000;
    
    typedef int InventoryArray[MAX_ITEMS];
    
    void OutputInventory(InventoryArray items);
    
    void main()
    {
    	InventoryArray itemInventory;
    	OutputInventory(itemInventory);
    }
    
    
    void OutputInventory(InventoryArray items)
    {
    	for (int itemCode = 0; itemCode < MAX_ITEMS;  itemCode++)
    	{	
    		cout << "The inventory amount for item " << itemCode << " is: " 
    	        	        <<  items[itemCode] << endl;
    	}
    }
    
    

If you were reading these last paragraphs carefully, you may be asking yourself, "If variables can be declared as globals and, therefore, be known throughout the whole program, why have parameters? After all, parameters can be a real hassle!" It is true that you can do this, but remember all we said about encapsulation. Using global variables is an invitation to buggy code. Don't do it! In fact, your instructor is likely to deduct points from your assignments if he or she finds you using global variables. There are certain controlled circumstances when they can and should be used, but you have not seen any yet.

By the way, the typedef statement also creates a global type - 'InventoryArray'. We could have declared this new type inside 'main', but then it would have been unavailable outside of 'main'. In fact, it is very rare to use local typedef's.

C. Array Elements as Parameters
Our inventory program could also have a function that output the amount of inventory for one item. Such a function would be useful if one option available to users was to get the inventory information on one product. The function could receive the whole array, but if only one element is needed, why pass all 10,000? It would be better if the program first asked the user for the item code of the product he or she wanted the inventory information on and then passed the array element corresponding to that item to the function. Since the inventory amounts are integers, the output function would clearly receive an integer representing that amount. If we wanted to also output the item code as part of the display, this information would also need to be passed. Still, the function declaration would be similar to many we have already seen:

void OutputOneInventoryAmount(int amount, int itemCode);

The call to this function would be a bit different in that it would be passing an element of the array instead of the whole array. Here are the relevant lines of code for 'main':

int itemCode;
itemCode = GetItemCode(); // declared and defined elsewhere
OutputOneInventoryAmount(itemInventory[itemCode], itemCode);

Note that itemInventory[itemCode], the first actual parameter in the call to 'OutputOneInventoryAmount', is a single element of the array. In other words, one integer, an element of the array, is passed as the first parameter to 'OutputOneInventoryAmount'. Since array elements of type integer can be used wherever integer variables can be used, they can be used as integer parameters. (The same is also true for all other types including class types.)

The code for 'OutputInventory' in the previous section received the whole array and included a 'for' statement to go through the array elements one at a time. This is the most efficient way to handle the option of outputting the whole array but we could also have accomplished the same result by using the function we just wrote, 'OutputOneInventoryAmount', and rewriting the calling side as:

    for (int itemCode = 0; itemCode < MAX_ITEMS; itemCode++)
    {
      OutputOneInventoryAmount(itemInventory[itemCode], itemCode);
    }

This would cause the individual amounts to be passed to the output function. It is not the best code because every function call takes a bit of time - just as it takes slightly longer to get someone else to start a task than to it takes to start it yourself. This last bit of code makes 10,000 simple requests (function calls) as opposed to one more complex request.

D. Arrays as Function Parameters to Return Data
Now, let's consider the function to allow the user to enter the initial inventory amounts. Instead of outputting values, it inputs values and instead of receiving the inventory amounts it returns them:

InputInventory
Purpose: To allow a user to input the inventory amounts for all the items in the inventory
Goal: The inventory amounts are all input

Receives: NONE
Returns: The Inventory

Ordinarily, a function with this kind of a design analysis would have no parameters and would return one item. However, functions in C++ cannot return arrays. This means we have to use our other mechanism for returning values - the parameter list.

Earlier, we made a distinction between function parameters declared with the '&' symbol and those without. We said that formal parameters with the '&' symbol can return information because a 'two way path' has been created while formal parameters without the '&' symbol could only be used to transmit information into the function but not back. It is time to review and say more about what the '&' operator does to cause this difference.

When a formal parameter does not have the '&' symbol, the system makes a copy of the variable being passed and the function uses that copy. Consider the following code segment:

    void DemoFunction(int abc);
    .
    .
    .
    void main()
    {
      int def = 5;
      DemoFunction(def);
      ....
    }

When the system calls 'DemoFunction' from inside 'main', it already has set aside memory for a variable called 'def'. And, since the function declaration for 'DemoFunction' declares 'abc' as a formal parameter without a '&' symbol, the system, when the call actually takes place,

  • sets aside enough memory for a second integer,
  • calls that memory location 'abc', and
  • copies the contents of 'def' to 'abc'.

Thus, when the value '3' is assigned to 'abc' in the function 'DemoFunction', this operation has no effect on the contents of 'def'. When 'DemoFunction' finishes and control is returned to 'main', the memory for 'abc' is returned to the system and the variable itself no longer exists. Figure 7.2 demonstrates this.


Even if both variables had been called 'abc' the result would be the same. The system would start by setting aside the memory location for the 'abc' in 'main'. When 'DemoFunction' was called, the system would then set aside a second memory location for the 'abc' found in 'DemoFunction' and copy the value found in the first 'abc' to the second. The system would know to use the second 'abc' as long as 'DemoFunction' was executing. Again, when 'DemoFunction' completed, its 'abc' variable would disappear and so would any changes made to its contents.


Technically, this parameter passing process is called pass by value. The value found in the memory location for the actual parameter is passed to the copy. When the formal parameter is associated with the '&' symbol, a process called pass by reference is used. In this process, the address of the memory symbolized by the actual parameter is passed. (Memory addresses are sometimes referred to as references thus the name 'pass by reference'.) Since the function receives and uses the address of the actual parameter, it works with the actual parameter's memory and not a copy. Any changes made in the function are made in the original.

Again, this process works whether or not the actual and formal parameters have the same name. The variable name used in the formal parameter becomes a 'nickname' for the memory location named in the actual parameter. Both the actual and formal parameter names are symbols referring to the same memory location.

Consider our same example but with the '&' symbol added to the function declaration:

    void DemoFunction(int& abc);
    .
    .
    .
    void main()
    {
      int def = 5;
      DemoFunction(def);
      ....
    }
Now, when 'def' is passed as a parameter, the system does not make a copy of the contents of 'def'. Instead, 'abc' becomes a second name for the same memory location and any changes made to 'abc' are made to 'def'. When 'DemoFunction' completes, 'abc' still disappears as a variable name, but the memory it refers to is still available as 'def'. And, any changes made to 'abc' still exist because they were made to the same memory referred to as 'def'.


When we talked about this back in chapter four, we used the idea of one-way and two-way streets. The term "pass by value" is a more precise way of talking about a one-way street. The value in the actual parameter is copied to a new memory location symbolized by the formal parameter name and the system does not provide a mechanism to directly copy the value back to the memory symbolized by the actual parameter. (Remember, the term 'actual parameter' refers to the parameter on the calling side, while 'formal parameter' refers to the parameter name in the function declaration/definition.)

The term "pass by reference" is the more precise way of talking about a two-way street. Since both the actual and the formal parameters are symbolic names for the same memory location, whatever is in the memory symbolized by one of the parameter names, is in the other. There really isn't a 'street' at all!

What all this has to do with arrays as parameters is that arrays are AUTOMATICALLY passed by reference. In other words, any time an array is used as a parameter, the 'two way path' is created. There is no need to (and it is poor style to) use the '&' symbol. Thus, the function declaration for 'InputInventory' has the same form as 'OutputInventory':

void InputInventory(InventoryArray items);

The code to call this function would look the same as the code to call 'OutputInventory'. And, the code in the definition would use the same 'for' statement we wrote above for inputting all the product inventory amounts at once. It would simply change the code inside the 'for' loop:

    void InputInventory(InventoryArray items)
    { 	int itemAmount;
    	for (int itemCode = 0; itemCode < MAX_ITEMS;  itemCode++)
    	{	
    		cout << "Please enter the inventory count for item: " << itemCode;
    		cin >> itemAmount;
    		items[itemCode] = itemAmount;
    	}
    }
    
One of the reasons for having parameters that only passed information into functions was to control the changes possible. In other words, if it was impossible to change the value of the actual parameter, the code could do whatever it wanted to the formal parameter, without worrying about the effect on the rest of the program. With arrays, however, since they are automatically passed by reference, this safeguard is not available. But, if you wish to protect an array from being changed, you can add the word 'const' in front of the parameter declaration in the function declaration. For example, it would be better to write the declaration for 'OutputInventory' as:

void OutputInventory(const InventoryArray items);

Note that we would not want to do this for 'InputInventory' because we ARE changing the values in the array elements.

Click here for the complete code for this problem - with a menu added to allow the user to do as he or she wishes. To make the program easier to test, the number of inventory items has been reduced to 5. This allows the user to enter the inventory information without a lot of effort. As we have already said when talking about program testing - it is important to design one's tests so that they focus on the real issues and don't take unnecessary time or energy. And, by the way, this demonstrates the value in using constants. No change needs to be made between the test and 'full version' of this program except for the value of MAX_ITEMS.

There is one element that makes this program highly artificial. In any real program of this kind, the user would be allowed to save the inventory data to a file at the end of the day and, at the start of next day's business, read that information back into the array. In chapter 11, we will look at file handling and fix this detail. STAY TUNED!

Topics Covered in the "Essentials of C++"

Arrays
Global vs Local Variables - the Issue of Scope
Parameter Passing
The 'for' statement

Examples

Example Code Involving Arrays

Top of Section Main Menu Next Chapter