Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Professional C++ [eng].pdf
Скачиваний:
284
Добавлен:
16.08.2013
Размер:
11.09 Mб
Скачать

Effective Memory Management

Another point to note is that the operators work the way you would want them to You might be concerned that using = will somehow result in two variables that point to the same memory, but that is not the case. The = operator copies the strings, which is most likely what you wanted. Similarly, the == operator really compares the actual contents of two strings, not the memory locations of the strings. If you are used to working with array-based strings, this will either be refreshingly liberating for you or somewhat confusing. Don’t worry – once you learn to trust the string class to do the right thing, life gets so much easier.

For compatibility, you can convert a C++ string into a C-style string by using the c_str() method. You should call the method just before using the result so that it accurately reflects the current contents of the string.

The on-line reference lists all the operations you can perform on string objects.

Low-Level Memor y Operations

One of the great advantages of C++ over C is that you don’t need to worry quite as much about memory. If you code using objects, you just need to make sure that each individual class properly manages its own memory. Through construction and destruction, the compiler helps you manage memory by telling you when to do it. As you saw in the string class, hiding the management of memory within classes makes a huge difference in usability.

With some applications, however, you may encounter the need to work with memory at a lower level. Whether for efficiency, debugging, or a sick curiosity, knowing some techniques for working with raw bytes can be helpful.

Pointer Arithmetic

The C++ compiler uses the declared types of pointers to allow you to perform pointer arithmetic. If you declare a pointer to an int and increase it by 1, the pointer moves ahead in memory by the size of an int, not by a single byte. This type of operation is most useful with arrays, since they contain homogeneous data that is sequential in memory. For example, assume you declare an array of ints on the heap:

int* myArray = new int[8];

You are already familiar with the following syntax for setting the value in position 2:

myArray[2] = 33;

With pointer arithmetic, you can equivalently use the following syntax, which obtains a pointer to the memory that is “2 ints ahead” of myArray, then dereferences it to set the value.

*(myArray + 2) = 33;

369

Chapter 13

As an alternative syntax for accessing individual elements, pointer arithmetic doesn’t seem too appealing. Its real power lies in the fact that an expression like myArray + 2 is still a pointer to an int, and thus can represent a smaller int array. Suppose you had a C-style string, as shown below:

const char* myString = “Hello, World!”;

Suppose you also had a function that took in a string and returned a new string that contains a capitalized version of the input:

char* toCaps(const char* inString);

You could capitalize myString by passing it into this function. However, if you only wanted to capitalize part of myString, you could use pointer arithmetic to refer to only a latter part of the string. The following code calls toCaps() on the World part of the string:

toCaps(myString + 7);

Another useful application of pointer arithmetic involves subtraction. Subtracting one pointer from another of the same type gives you the number of elements of the pointed-to type between the two pointers, not the absolute number of bytes between them.

Custom Memory Management

For 99 percent of the cases you will encounter (some might say 100 percent of the cases), the built-in memory allocation facilities in C++ are adequate. Behind the scenes, new and delete do all the work of handing out memory in properly sized chunks, maintaining a list of available areas of memory, and releasing chunks of memory back to that list upon deletion.

When resource constraints are extremely tight, managing memory on your own may be a viable option. Don’t worry — it’s not as scary as it sounds. Basically, managing memory yourself generally means that classes allocate a large chunk of memory and dole out that memory in pieces as it is needed.

How is this approach any better? Managing your own memory can potentially reduce overhead. When you use new to allocate memory, the program also needs to set aside a small amount of space to record how much memory was allocated. That way, when you call delete, the proper amount of memory can be released. For most objects, the overhead is so much smaller than the memory allocated that it makes little difference. However, for small objects or programs with enormous numbers of objects, the overhead can have an impact.

When you manage memory yourself, you know the size of each object a priori, so you can avoid the overhead for each object. The difference can be enormous for large numbers of small objects. The syntax for performing custom memory management is described in Chapter 16.

Garbage Collection

At the other end of the memory hygiene spectrum lies garbage collection. With environments that support garbage collection, the programmer rarely, if ever, explicitly frees memory associated with an object. Instead, a low-priority background task keeps an eye on the state of memory and cleans up portions that it decides are no longer needed.

370

Effective Memory Management

Garbage collection is not built into the C++ language as it is in Java. Most C++ programs manage memory at the object level through new and delete. It is possible to implement garbage collection in C++, but freeing yourself from the task of releasing memory would probably introduce new headaches.

One approach to garbage collection is called mark and sweep. With this approach, the garbage collector periodically examines every single pointer in your program and annotates the fact that the referenced memory is still in use. At the end of the cycle, any memory that hasn’t been marked is deemed to be not in use and is freed.

A mark and sweep algorithm could be implemented in C++ if you were willing to do the following:

1.Register all pointers with the garbage collector so that it can easily walk through the list of all pointers

2.Subclass all objects from a mix-in class, perhaps GarbageCollectible, that allows the garbage collector to mark an object as in-use

3.Protect concurrent access to objects by making sure that no changes to pointers can occur while the garbage collector is running

As you can see, this simple approach to garbage collection requires quite a bit of diligence on the part of the programmer. It may even be more error-prone than using delete! Attempts at a safe and easy mechanism for garbage collection have been made in C++, but even if a perfect implementation of garbage collection in C++ came along, it wouldn’t necessarily be appropriate to use for all applications. Among the downsides of garbage collection:

When the garbage collector is actively running, it will likely slow the program down.

If the program is aggressively allocating memory, the garbage collector may be unable to keep up.

If the garbage collector is buggy or thinks an abandoned object is still in use, it can create unrecoverable memory leaks.

Object Pools

Custom memory management, as described above, is the coding equivalent to shopping for a picnic at a warehouse superstore. You fill your SUV with more paper plates than you need right now so that you can avoid the overhead of going back to the store for subsequent picnics. Garbage collection is like leaving any used plates out in the yard where the wind will conveniently blow them into the neighbor’s yard. Surely, there must be a more ecological approach to memory management.

Object pools are the analog of recycling. You buy a reasonable number of plates, but you hang onto them after use so that later on you can clean and reuse them. Object pools are ideal for situations where you need to use many objects of the same type over time, and creating each one incurs overhead.

Chapter 17 contains further details about using object pools for performance efficiency.

371

Chapter 13

Function Pointers

You don’t normally think about the location of functions in memory, but each function actually lives at a particular address. In C++, you can use functions as data. In other words, you can take the address of a function and use it like you use a variable.

Function pointers are typed according to the parameter types and return type of compatible functions. The easiest way to work with function pointers is to use the typedef mechanism to assign a type name to the family of functions that have the given characteristics. For example, the following line declares a type called YesNoFcn that represents a pointer to any function that has two int parameters and returns a bool.

typedef bool(*YesNoFcn)(int, int);

Now that this new type exists, you could write a function that takes a YesNoFcn as a parameter. For example, the following function accepts two int arrays and their size, as well as a YesNoFcn. It iterates through the arrays in parallel and calls the YesNoFcn on corresponding elements of both arrays, printing a message if the YesNoFcn function returns true. Notice that even though the YesNoFcn is passed in as a variable, it can be called just like a regular function.

void findMatches(int values1[], int values2[], int numValues, YesNoFcn inFunction)

{

for (int i = 0; i < numValues; i++) {

if (inFunction(values1[i], values2[i])) { cout << “Match found at position “ << i <<

“ (“ << values1[i] << “, “ << values2[i] << “)” << endl;

}

}

}

To call the findMatches() function, all you need is any function that adheres to the defined YesNoFcn type — that is, any type that takes in two ints and returns a bool. For example, consider the following function, which returns true if the two parameters are equal:

bool intEqual(int inItem1, int inItem2)

{

return (inItem1 == inItem2);

}

Because the intEqual() function matches the YesNoFcn type, it can be passed as the final argument to findMatches(), as in the following program:

int main(int argc, char** argv)

{

int arr1[7] = {2, 5, 6, 9, 10, 1, 1}; int arr2[7] = {4, 4, 2, 9, 0, 3, 4};

cout << “Calling findMatches() using intEqual():” << endl; findMatches(arr1, arr2, 7, &intEqual);

return 0;

}

372

Effective Memory Management

Notice that the intEqual() function is passed into the findMatches() function by taking its address. Technically, the & character is optional — if you simply put the function name, the compiler will know that you mean to take its address. The output of this program will be:

Calling findMatches() using intEqual():

Match found at position 3 (9, 9)

The magic of function pointers lies in the fact that findMatches() is a generic function that compares parallel values in two arrays. As it is used above, it compares based on equality. However, since it takes a function pointer, it could compare based on other criteria. For example, the following function also adheres to the definition of a YesNoFcn:

bool bothOdd(int inItem1, int inItem2)

{

return (inItem1 % 2 == 1 && inItem2 % 2 == 1);

}

The following program calls findMatches() using both YesNoFcns:

int main(int argc, char** argv)

{

int arr1[7] = {2, 5, 6, 9, 10, 1, 1}; int arr2[7] = {4, 4, 2, 9, 0, 3, 4};

cout << “Calling findMatches() using intEqual():” << endl; findMatches(arr1, arr2, 7, &intEqual);

cout << endl;

cout << “Calling findMatches() using bothOdd():” << endl; findMatches(arr1, arr2, 7, &bothOdd);

return 0;

}

The output of this program will be:

Calling findMatches() using intEqual():

Match found at position 3 (9, 9)

Calling findMatches() using bothOdd():

Match found at position 3 (9, 9)

Match found at position 5 (1, 3)

By using function pointers, a single function, findMatches(), was customized to different uses based on a parameter, inFunction.

373