|
INPRISE Online And ZD Journals Present:
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
Out with the old…
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:
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
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
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
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:
You can then reference a particular string in the list by its index number:
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
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 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:
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:
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:
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?
A typical cleanup operation for a list might look like this:
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…
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:
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…
Conclusion
|