You are on page 1of 1

December 1997

The right list for the right job


by Kent Reisdorph

Storing a list of objects for later reference is a common programming technique. For example, a paint program might store a list of drawing objects so it can reconstruct the drawing on demand. A
game program might store a list of player moves so the game state can be restored or replayed. Or, perhaps you just want to store a list of strings to be used for error messages. Whatever your
needs, VCL has classes you can use to handle your lists. In last month's article "Implementing a Recent Files List," we put the TStringList class to work. This month, we'll look at the TStringList
and TList classes in depth. We'll show where you'd use each type of list and how to add items to and retrieve them from the list.

Container classes

Classes that store objects (instances of a class) are often called container classes. These classes do a lot of work for you behind the scenes. The primary task a container class performs is
managing memory. Let's first look at the traditional C++ array; then we can compare it to container classes.

Out with the old...

Consider a regular C++ array of objects. For example, say you wanted to maintain a list of 20 strings. The code would look like this:

String* MyStrings[20];
MyStrings[0] = new String("Hello There");
// etc.

Here you have an array of AnsiString pointers. You must assign a valid pointer to each item in the array and you must be sure to delete the memory associated with each element of the array when
you no longer need the array. These requirements lead to a common programming error: Since you may not use the entire array, you may accidentally try to delete an object that doesn't exist. In
order to prevent such an occurrence, you'd first have to set each element of the array to 0, then check for a valid value before deleting the object. The code looks something like this:

String* MyStrings[20];
for (int i=0;i<20;i++)
MyStrings[i] = 0;
MyStrings[0] = new String("Hello There");
// Add more strings
// later when we're done with the array...
for (int i=0;i<20;i++)
if (MyStrings[i]) delete MyStrings[i];

It's a bit cumbersome, but certainly not a major problem. A bigger issue lies in the static nature of our example array. What if you've misjudged the number of strings you need? You may suddenly
find yourself needing 25 strings, or 30, or 100! However, the array size is limited to 20 strings--so you're stuck. You'll have to rewrite your code to allow for more strings.

That problem brings up another issue: If you allocate space for 100 strings but you only use 20, then you're wasting memory. Clearly, using regular arrays is not the best method when the size of
the array may vary.

...in with the new


Thanks to the advent of object-oriented programming, there's a better way. Earlier, we said that one of the major roles of a container class is memory management. The VCL container classes
dynamically allocate and free memory for the array as needed. As a result, the container is able to grow and shrink as items are added and removed. You can add items without fear of a memory
overrun, and the burden of memory management is largely removed from your shoulders. Here's how the process works. VCL container classes have an initial capacity. No memory is allocated
when the container is initially created. When you add the first item, the container allocates enough space for four objects (actually, since the containers store only pointers to objects, the space
allocated is for four pointers, not four actual objects). When you add the fifth object , the container allocates space for another four objects. The amount by which the container grows is called the
delta.

Now events get more complicated. When you add the ninth object, the container again allocates more memory--but this time it allocates enough memory for an additional eight objects. Finally,
when you add the seventeenth object, the container allocates enough memory for an additional 16 objects. From this point on, the delta remains at 16 and the container will grow by that amount
each time the current capacity is exceeded.

This rather peculiar architecture is designed to save both time and memory. It saves memory initially because only small chunks of memory are allocated--an efficient mechanism in terms of
memory, but costly in terms of processor time. Each reallocation of memory requires a certain amount of overhead; so, if you're adding hundreds of items to a list, the number of allocations
required is time consuming. Once the array size grows to 17 elements, then additional memory is allocated in 64-byte chunks (16 pointers times 4 bytes per pointer). The actual process of
inserting elements speeds up, since the reallocation of memory only happens every sixteenth add operation rather than every fourth. If you know beforehand exactly how many elements your list
will contain, then it's best to set the initial size before adding elements. We'll get back to that subject in just a bit.

Personally, I'd prefer a container in which I can set the delta and initial capacity myself. It's easy enough to derive a class from TList and add that functionality, so I won't complain too loudly.

Another feature that a container class adds is the ability to easily manipulate the list. List manipulation includes tasks like adding elements, inserting elements, removing elements, and clearing
the list (removing all elements at one time). The container class keeps track of indexes and adjusts them as necessary when you add, insert, or delete items.

VCL list classes

The two primary VCL container classes are TStringList and TList. The TStringList class, as its name implies, allows you to maintain a list of text strings. The TList class, on the other hand, lets
you store any type of object in a list. For example, you might want to store a list of TPoint objects, a list of TBitmap objects, or a list of objects of your own creation. These two list classes aren't
derived from a common base class, but they have several common methods, as described in Table A.

Table A: List class common methods


Method Description
Add Adds an item to the end of the list
Clear Clears all items from the list
Delete Removes an item from the list
Exchange Swaps the position of two items
Insert Inserts an item at a specified index
Move Moves an item from one location in the list to another location
Sort Sorts the list

In addition to these common methods, TList and TStringList each have a Count property. You can read the value of this property any time you need to know how many items are in the list. Now,
let's take a quick look at these classes individually.

String lists: TStringList


Programmers often need to maintain a list of strings, and VCL provides the TStringList class for this purpose. Many programmers just starting out with C++Builder commit a common
programming error: They attempt to create an instance of the TStrings class. In fact, TStrings is the base class for TStringList and is an abstract base class. You can't create an instance of an
abstract base class--you must create an instance of a class derived from the base class. In this case, you must create an instance of TStringList, not TStrings. Once you've created an instance of
TStringList, you can begin adding strings, as follows:

TStringList* List = new TStringList;


List->Add("String One");
List->Add("String Two");
// etc.

You can then reference a particular string in the list by its index number:

Label1->Caption = List->Strings[10];

The list is 0-based, so the first item in the list is at index 0, the second is at index 1, and so on. String lists can be sorted or not sorted as determined by the Sorted property. By default, string lists
aren't sorted. The Duplicates property is a Boolean property that determines whether the string list will allow duplicate entries. The Strings property allows access to the strings held in the
container. To access a particular string, you'd use the syntax from the proceeding example. The Text property, in contrast, will return all the strings in the list as a single string. The TStringList
class has one other important property--Objects--which you can use to store additional data associated with each string. We'll return to this subject after we've discussed the TList class, since
there's a correlation between them.

TStringList has quite an array (no pun intended) of methods. This article isn't meant as a reference to the list classes, so I won't cover each method. Instead, I'll refer you to Table B, which
contains some of the most useful methods.

Table B: Important TStringList methods


Method Description
AddObject Adds both a string and a pointer to additional string data
Find Searches a sorted list and returns an index to the location the string will occupy when added to the list
IndexOf Searches a list (sorted or unsorted) for the first occurrence of a particular string
LoadFromFile Loads the string list from a text file
SaveToFile Saves the string list to a text file

Of these methods, perhaps the most powerful are LoadFromFile() and SaveToFile(). These methods allow you to quickly load and save the contents of a string list using simple text files.

By now you may have figured out that the TStringList class is used throughout VCL. The ListBox, Memo, ComboBox, and RadioGroup components all use some variation of TStringList to store
their data (as do many other VCL components). Actually, these classes use some derivation of the TStrings class, but it all looks the same to you and me.

The TStringList class is handy for storing lists of strings. It suffers somewhat from poor performance if you have many strings to add, but all in all, it's very useful.

TList, the all-purpose list class

The TList class stores a list of any type of data--you can store pointers to VCL objects or pointers to your own classes The TList class maintains an array of pointers (four-byte values). It's up to
you to decide what to store in those four bytes. The Add() and Insert() methods take a void*, so you can pass a pointer to any type of object you like. Later, when you want to retrieve the values,
you'll have to cast the pointer to the type of object you stored (more on that in just a bit). The most important TList property is Capacity, which contains the current maximum capacity of the list.
This value isn't an upper limit, because the list can always be expanded if needed. The importance of the Capacity property is most obvious when you're creating large lists. Earlier we talked
about the... er ... silly (for lack of a better word) allocation algorithm used by the VCL list classes. This algorithm tends to be very slow when working with large lists because all those memory
reallocations are very costly in terms of speed. To avoid this slowdown, you can set the Capacity property to a desired size prior to adding any elements to the list. Memory for the list will be
allocated when you set the Capacity property and won't be reallocated until that capacity is exceeded. If you're going to need a large list, then you should always set the capacity immediately after
creating the list, as follows:

TList* MyList = new TList;


MyList->Capacity = 1000;
MyList->Add(new TMyObject);
// etc.

Doing so will prevent all the reallocations that would be necessary if you didn't set the capacity. The previous code snippet illustrates one way of adding objects to a TList container. You can add
any type of object in this manner.

Adding objects to the list is one thing, but getting them out again is another. In order to retrieve the stored objects, you'll have to cast from a void* to the type of object you stored. For example,
let's say your list contains pointers to a class you created. The code to extract an object from the list would look like this:

TMyObject* O = static_cast<TMyObject*>(MyList->Items[0]);
// Now you can do something with `O'

Notice I used static_cast to cast the void* to a TMyObject*. I'm a firm believer in the C++ casting operators. However, static_cast is not a typesafe cast--so in this case, there's little value in using
static_cast over the old C-style cast. We could have written the above example as:

TMyObject* O = (TMyObject*)MyList->Items[0];

The net result is the same. The moral here is that you need to be sure what type of object your list contains when performing casts--performing a non-typesafe cast can have undesirable results if
you cast to the wrong type.

Do you delete or do I?

It's important to note that the TList class doesn't "own" the objects it stores. In other words, you're responsible for freeing the memory associated with each object in the list. The VCL
documentation is somewhat confusing on this issue. Depending on how you read the documentation for Tlist, you might conclude that TList will free the memory associated with each object in
the array when the list itself is deleted. This is not the case. It's your responsibility to delete all the objects in the container before deleting the container itself. The same is true if you delete a
particular item from the list--you must delete the object. In short, don't assume anything. A typical cleanup operation for a list might look like this:

for (int i=0;i<List->Count;i++) {


// Cast to the correct type
TMyObject* mo = (TMyObject*)List->Items[i];
// Delete the object and set its pointer to 0
delete mo;
List->Items[i] = 0;
}
// Call Clear() to free memory for the pointers
List->Clear();
// Delete the TList object
delete List;

This code is necessary since the list stores only void pointers. The list doesn't know what type of objects it contains, so it can't properly delete the objects.

Back up a bit...

Earlier, we mentioned the TStringList class's Object property--an indexed property that stores additional information along with a string. You could, for example, store a list of strings and a
corresponding object such as a pointer to a TBitmap instance. You can use the AddObject method to store both the string and the object at the same time:

TStringList* MyList = new TStringList;


Graphics::TBitmap* bm = new Graphics::TBitmap;
String s = "bitmap1.bmp";
bm->LoadFromFile(s);
MyList->AddObject(s, bm);

The Object property stores TObject pointers, so you'll need to cast the pointer back to a particular type when you use the object, as follows:

bm = dynamic_cast
<Graphics::TBitmap*>(MyList->Objects[0]);
Canvas->Draw(x, y, bm);

As with TList, don't make any assumptions about the objects stored in TStringList's Object property with respect to ownership. You have the responsibility of deleting the objects when you're
done with the list.

One more thing...

TList and TStringList aren't the only game in town. The Standard Template Library (STL) is considered the default C++ library for container classes, and it has many types of container classes.
The STL classes provide support for arrays, deques, queues, stacks, and more. You can easily adapt the basic STL containers to create other types of lists, such as trees and hash tables. If you plan
on porting your applications to other C++ platforms, then you should consider using STL for your container class needs. STL comes with C++Builder, so you already have this resource at your
disposal.

Conclusion
Programmers frequently use lists and arrays. Wise use of lists can save you programming time and will make your application more system-friendly. Use TStringList for lists of strings and TList
for lists of other types of objects. And, don't forget about the STL containers. If you're educated about the options available, you can make informed choices about the types of containers to use in
your applications.

You might also like