Saturday, March 10, 2007

MFC Array Classes

Type-Specific MFC Array Classes

Class Name Data Type
CByteArray 8-bit bytes (BYTEs)
CWordArray 16-bit words (WORDs)
CDWordArray 32-bit double words (DWORDs)
CUIntArray Unsigned integers (UINTs)
CStringArray CStrings
CPtrArray void pointers
CObArray CObject pointers


Once you learn to use one of these array classes, you can use the others too, because all share a common set of member functions. The following example declares an array of 10 UINTs and initializes it with the numbers 1 through 10:

CUIntArray array;
array.SetSize (10);
for (int i=0; i<10; i++)
array[i] = i + 1;




You can use the same approach to declare an array of CStrings and initialize it with textual representations of the integers 1 through 10:

CStringArray array;
array.SetSize (10);
for (int i=0; i<10; i++) {
CString string;
string.Format (_T ("%d"), i);
array[i] = string;
}




In both cases, SetSize sizes the array to hold 10 elements. In both cases, the overloaded [] operator calls the array's SetAt function, which copies a value to an element at a specified location in the array. And in both cases, the code asserts if the array's bounds are violated. The bounds check is built into the code for SetAt:

ASSERT(nIndex >= 0 && nIndex < m_nSize);




You can see this code for yourself in the MFC source code file Afxcoll.inl.

You can insert items into an array without overwriting the items that are already there by using the InsertAt function. Unlike SetAt, which simply assigns a value to an existing array element, InsertAt makes room for the new element by moving elements above the insertion point upward in the array. The following statements initialize an array with the numbers 1 through 4 and 6 through 10, and then insert a 5 between the 4 and the 6:

CUIntArray array;
array.SetSize (9);
for (int i=0; i<4; i++)
array[i] = i + 1;
for (i=4; i<9; i++)
array[i] = i + 2;
array.InsertAt (4, 5); // Insert a 5 at index 4.




You can also pass a third parameter to InsertAt specifying the number of times the item should be inserted or pass a pointer to another array object in parameter 2 to insert an entire array. Note that this example sets the array size to 9, not 10, yet no assertion occurs when InsertAt is called. That's because InsertAt is one of a handful of array functions that automatically grow the array as new items are added. Dynamically sized arrays are discussed in the next section.

Values can be retrieved from an MFC array using standard array addressing syntax. The following example reads back the UINTs written to the CUIntArray in the previous example:

for (int i=0; i<10; i++)
UINT nVal = array[i];




Used this way, the [] operator calls the array's GetAt function, which retrieves a value from a specified position in the array—with bounds checking, of course. If you'd prefer, you can call GetAt directly rather than use the [] operator.

To find out how many elements an array contains, call the array's GetSize function. You can also call GetUpperBound, which returns the 0-based index of the array's upper bound—the number of elements in the array minus 1.

MFC's array classes provide two functions for removing elements from an array: RemoveAt and RemoveAll. RemoveAt removes one or more items from the array and shifts down any items above the ones that were removed. RemoveAll empties the array. Both functions adjust the array's upper bounds to reflect the number of items that were removed, as the following example demonstrates:

// Add 10 items.
CUIntArray array;
array.SetSize (10);
for (int i=0; i<10; i++)
array[i] = i + 1;

// Remove the item at index 0.
array.RemoveAt (0);
TRACE (_T ("Count = %d\n"), array.GetSize ()); // 9 left.

// Remove items 0, 1, and 2.
array.RemoveAt (0, 3);
TRACE (_T ("Count = %d\n"), array.GetSize ()); // 6 left.

// Empty the array.
array.RemoveAll ();
TRACE (_T ("Count = %d\n"), array.GetSize ()); // 0 left.




The Remove functions delete elements, but they don't delete the objects that the elements point to if the elements are pointers. If array is a CPtrArray or a CObArray and you want to empty the array and delete the objects referenced by the deleted pointers, rather than write

array.RemoveAll ();




you should write this:

int nSize = array.GetSize ();
for (int i=0; i delete array[i];
array.RemoveAll ();




Failure to delete the objects whose addresses are stored in a pointer array will result in memory leaks. The same is true of MFC lists and maps that store pointers.

Dynamic Array Sizing
Besides being bounds-checked, the MFC array classes also support dynamic sizing. You don't have to predict ahead of time how many elements a dynamically sized array should have because the memory set aside to store array elements can be grown as elements are added and shrunk as elements are removed.

One way to dynamically grow an MFC array is to call SetSize. You can call SetSize as often as needed to allocate additional memory for storage. Suppose you initially size an array to hold 10 items but later find that it needs to hold 20. Simply call SetSize a second time to make room for the additional items:

// Add 10 items.
CUIntArray array;
array.SetSize (10);
for (int i=0; i<10; i++)
array[i] = i + 1;

// Add 10 more.
array.SetSize (20);
for (i=10; i<20; i++)
array[i] = i + 1;




When an array is resized this way, the original items retain their values. Thus, only the new items require explicit initialization following a call to SetSize.

Another way to grow an array is to use SetAtGrow instead of SetAt to add items. For example, the following code attempts to use SetAt to add 10 items to an array of UINTs:

CUIntArray array;
for (int i=0; i<10; i++)
array.SetAt (i, i + 1);




This code will assert the first time SetAt is called. Why? Because the array's size is 0 (note the absence of a call to SetSize), and SetAt doesn't automatically grow the array to accommodate new elements. Change SetAt to SetAtGrow, however, and the code works just fine:

CUIntArray array;
for (int i=0; i<10; i++)
array.SetAtGrow (i, i + 1);




Unlike SetAt, SetAtGrow automatically grows the array's memory allocation if necessary. So does Add, which adds an item to the end of the array. The next example is functionally identical to the previous one, but it uses Add instead of SetAtGrow to add elements to the array:

CUIntArray array;
for (int i=0; i<10; i++)
array.Add (i + 1);




Other functions that automatically grow an array to accommodate new items include InsertAt, Append (which appends one array to another), and Copy, which, as the name implies, copies one array to another.

MFC grows an array by allocating a new memory buffer and copying items from the old buffer to the new one. If a grow operation fails because of insufficient memory, MFC throws an exception. To trap such errors when they occur, wrap calls that grow an array in a try block accompanied by a catch handler for CMemoryExceptions:

try {
CUIntArray array;
array.SetSize (1000); // Might throw a CMemoryException.

}
catch (CMemoryException* e) {
AfxMessageBox (_T ("Error: Insufficient memory"));
e->Delete (); // Delete the exception object.
}




This catch handler displays an error message warning the user that the system is low on memory. In real life, more extensive measures might be required to recover gracefully from out-of-memory situations.

Because a new memory allocation is performed every time an array's size is increased, growing an array too frequently can adversely impact performance and can also lead to memory fragmentation. Consider the following code fragment:

CUIntArray array;
for (int i=0; i<100000; i++)
array.Add (i + 1);




These statements look innocent enough, but they're inefficient because they require thousands of separate memory allocations. That's why MFC lets you specify a grow size in SetSize's optional second parameter. The following code initializes the array more efficiently because it tells MFC to allocate space for 10,000 new UINTs whenever more memory is required:

CUIntArray array;
array.SetSize (0, 10000);
for (int i=0; i<100000; i++)
array.Add (i + 1);

No comments: