Storage Schemes and Dynamic Allocation

You’ve seen the five schemes, excluding threaded memory, C++ uses to allocate memory for variables (including arrays and structures). They don’t apply to memory allocated by using the C++ new operator (or by using the older C malloc() function). We call that kind of memory dynamic memory. As you saw in Chapter 4, dynamic memory is controlled by the new and delete operators, not by scope and linkage rules. Thus, dynamic memory can be allocated from one function and freed from another function. Unlike automatic memory, dynamic memory is not LIFO; the order of allocation and freeing depends on when and how new and delete are used. Typically, the compiler uses three separate memory chunks: one for static variables (this chunk might be subdivided), one for automatic variables, and one for dynamic storage.

Although the storage scheme concepts don’t apply to dynamic memory, they do apply to automatic and static pointer variables used to keep track of dynamic memory. For example, suppose you have the following statement inside a function:

float * p_fees = new float [20];

The 80 bytes (assuming that a float is 4 bytes) of memory allocated by new remains in memory until the delete operator frees it. But the p_fees pointer passes from existence when program execution exits the block containing this declaration. If you want to have the 80 bytes of allocated memory available to another function, you need to pass or return its address to that function. On the other hand, if you declare p_fees with external linkage, the p_fees pointer will be available to all the functions following that declaration in the file. And by using the following in a second file, you make that same pointer available in the second file:

extern float * p_fees;


Note

Memory allocated by new is typically freed when the program terminates. However, this is not always true. Under some less robust operating systems, for example, in some circumstances a request for a large block of memory can result in a block that is not deleted automatically when the program terminates. The best practice is to use delete to free memory allocated by new.


Initialization with the new Operator

What if you want to initialize a variable as part of the dynamic memory allocation? With C++98, you can do so in some instances. C++11 expands what is possible. Let’s look first at what has been possible.

If you wish to create and initialize storage for one of the scalar built-in types, such as int or double, you can do so by following the desired type with an initialization value enclosed in parentheses:

int *pi = new int (6);   // *pi set to 6
double * pd = new double (99.99);  // *pd set to 99.99

The parentheses syntax also can be used with classes having suitable constructors, but we haven’t got that far yet.

To initialize an ordinary structure or an array, however, you need C++11 and list-initialization using braces. The new standard allows the following:

struct where {double x; double y; double z;};
where * one = new where {2.5, 5.3, 7.2};  // C++11
int * ar = new int [4] {2,4,6,7};         // C++11

With C++11, you also can use the brace initialization for single-valued variables:

int *pin = new int {});   // *pi set to 6
double * pdo = new double {99.99};  // *pd set to 99.99

When new Fails

It may be that new can’t find the requested amount of memory. For its first decade, C++ handled that eventuality by having new return a null pointer. Currently, however, new throws a std::bad_alloc exception. Chapter 15, “Friends, Exceptions, and More,” provides some short examples showing how each approach works.

new: Operators, Functions, and Replacement Functions

The new and new[] operators call upon two functions:

void * operator new(std::size_t);     // used by new
void * operator new[](std::size_t);   // used by new[]

These are termed allocation functions, and they are part of the global namespace. Similarly, there are deallocation functions used by delete and delete []:

void operator delete(void  *);
void operator delete[](void *);

They use the operator-overloading syntax discussed in Chapter 11, “Working with Classes.” The std::size_t is a typedef for some suitable integer type. A basic statement such as

int * pi = new int;

gets translated into something like this:

int * pi = new(sizeof(int));

And the statement

int * pa = new int[40];

gets translated into something like this:

int * pa = new(40 * sizeof(int));

As you’ve seen, a statement with a new operator can also provide initialization values, so, in general, using the new operator may do more than just call the new() function.

Similarly,

delete pi;

invokes the following function call:

delete (pi);

Interestingly, C++ terms these functions replaceable. That means if you have sufficient expertise and desire, you can supply replacement functions for new and delete and tailor them to meet your specific requirements. One option, for instance, is to define replacement functions with class scope so that they can be tailored to fit the allocation needs of a particular class. Your code would use the new operator as usual, but the new operator would call upon the replacement new() function.

The Placement new Operator

Normally, the new operator has the responsibility of finding in the heap a block of memory that is large enough to handle the amount of memory you request. The new operator has a variation, called placement new, that allows you to specify the location to be used. A programmer might use this feature to set up his or her own memory-management procedures or to deal with hardware that is accessed via a particular address or to construct objects in a particular memory location.

To use the placement new feature, you first include the new header file, which provides a prototype for this version of new. Then you use new with an argument that provides the intended address. Aside from this argument, the syntax is the same as for regular new. In particular, you can use placement new either without or with brackets. The following code fragment shows the syntax for using these four forms of new:

#include <new>
struct chaff
{
    char dross[20];
    int slag;
};
char buffer1[50];
char buffer2[500];
int main()
{
    chaff *p1, *p2;
    int *p3, *p4;
// first, the regular forms of new
    p1 = new chaff;              // place structure in heap
    p3 = new int[20];            // place int array in heap
// now, the two forms of placement new
    p2 = new (buffer1) chaff;    // place structure in buffer1
    p4 = new (buffer2) int[20];  // place int array in buffer2
...

For simplicity, this example uses two static arrays to provide memory space for placement new. So this code allocates space for a chaff structure in buffer1 and space for an array of 20 ints in buffer2.

Now that you’ve made your acquaintance with placement new, let’s look at a sample program. Listing 9.10 uses both new and placement new to create dynamically allocated arrays. This program illustrates some important differences between new and placement new that we’ll discuss after seeing the output.

Listing 9.10. newplace.cpp


// newplace.cpp -- using placement new
#include <iostream>
#include <new>         // for placement new
const int BUF = 512;
const int N = 5;
char buffer[BUF];      // chunk of memory
int main()
{
    using namespace std;
    double *pd1, *pd2;
    int i;
    cout << "Calling new and placement new: ";
    pd1 = new double[N];           // use heap
    pd2 = new (buffer) double[N];  // use buffer array
    for (i = 0; i < N; i++)
        pd2[i] = pd1[i] = 1000 + 20.0 * i;
    cout << "Memory addresses: " << "  heap: " << pd1
        << "  static: " << (void *) buffer  <<endl;
    cout << "Memory contents: ";
    for (i = 0; i < N; i++)
    {
        cout << pd1[i] << " at " << &pd1[i] << "; ";
        cout << pd2[i] << " at " << &pd2[i] << endl;
    }

    cout << " Calling new and placement new a second time: ";
    double *pd3, *pd4;
    pd3= new double[N];            // find new address
    pd4 = new (buffer) double[N];  // overwrite old data
    for (i = 0; i < N; i++)
        pd4[i] = pd3[i] = 1000 + 40.0 * i;
    cout << "Memory contents: ";
    for (i = 0; i < N; i++)
    {
        cout << pd3[i] << " at " << &pd3[i] << "; ";
        cout << pd4[i] << " at " << &pd4[i] << endl;
    }

    cout << " Calling new and placement new a third time: ";
    delete [] pd1;
    pd1= new double[N];
    pd2 = new (buffer + N * sizeof(double)) double[N];
    for (i = 0; i < N; i++)
        pd2[i] = pd1[i] = 1000 + 60.0 * i;
    cout << "Memory contents: ";
    for (i = 0; i < N; i++)
    {
        cout << pd1[i] << " at " << &pd1[i] << "; ";
        cout << pd2[i] << " at " << &pd2[i] << endl;
    }
    delete [] pd1;
    delete [] pd3;
    return 0;
}


Here is the output from the program in Listing 9.10 on one system:

Calling new and placement new:
Memory addresses:
  heap: 006E4AB0  static: 00FD9138
Memory contents:
1000 at 006E4AB0; 1000 at 00FD9138
1020 at 006E4AB8; 1020 at 00FD9140
1040 at 006E4AC0; 1040 at 00FD9148
1060 at 006E4AC8; 1060 at 00FD9150
1080 at 006E4AD0; 1080 at 00FD9158

Calling new and placement new a second time:
Memory contents:
1000 at 006E4B68; 1000 at 00FD9138
1040 at 006E4B70; 1040 at 00FD9140
1080 at 006E4B78; 1080 at 00FD9148
1120 at 006E4B80; 1120 at 00FD9150
1160 at 006E4B88; 1160 at 00FD9158

Calling new and placement new a third time:
Memory contents:
1000 at 006E4AB0; 1000 at 00FD9160
1060 at 006E4AB8; 1060 at 00FD9168
1120 at 006E4AC0; 1120 at 00FD9170
1180 at 006E4AC8; 1180 at 00FD9178
1240 at 006E4AD0; 1240 at 00FD9180

Program Notes

The first thing to note about Listing 9.10 is that placement new does, indeed, place the p2 array in the buffer array; both p2 and buffer have the value 00FD9138. They are, however, of different types; p1 is pointer-to-double, whereas buffer is pointer-to-char. (By the way, that’s why the program uses a (void *) cast for buffer; otherwise, cout would try to display a string.) Meanwhile, regular new locates the p1 array rather far away in memory, at location 006E4AB0, which is part of the dynamically managed heap.

The second point to note is that the second call to regular new results in new finding a new block of memory—one beginning at 006E4B68. But the second call to placement new results in the same block of memory being used as before—that is, the block beginning at 00FD9138. The important fact here is that placement new simply uses the address that is passed to it; it doesn’t keep track of whether that location has already been used, and it doesn’t search the block for unused memory. This shifts some of the burden of memory management to the programmer. For example, the third call to placement new provides an offset into the buffer array so that new memory is used:

pd2 = new (buffer + N * sizeof(double)) double[N]; // offset of 40 bytes

The third point has to do with the use and nonuse of delete. For regular new, the following statement frees up the block of memory beginning at 006E4AB0, and as a result, the next call to new is able to reuse that block:

delete [] pd1;

In contrast, the program in Listing 9.10 does not use delete to free the memory used by placement new. In fact, in this case, it can’t. The memory specified by buffer is static memory, and delete can be used only with a pointer to heap memory allocated by regular new. That is, the buffer array is outside the jurisdiction of delete, and the following statement will produce a runtime error:

delete [] pd2;   // won't work

On the other hand, if you use regular new to create a buffer in the first place, you use regular delete to free that entire block.

Another way you can use placement new is to combine it with new initialization to place information at a specific hardware address.

You may wonder exactly what the placement new function does. Basically, it does nothing other than return the address passed to it, type casting it to void * so that it can be assigned to any pointer type. But that’s the default placement new. C++ allows programmers to overload placement new.

The situation becomes more involved when you use placement new with class objects. Chapter 12, “Classes and Dynamic Memory Allocation,” continues this story.

Other Forms of Placement new

Just as regular new invokes a new function with one argument, the standard placement new invokes a new function with two arguments:

int * pi = new int;             // invokes new(sizeof(int))
int * p2 = new(buffer) int;     // invokes new(sizeof(int), buffer)
int * p3 = new(buffer) int[40]; // invokes new(40*sizeof(int), buffer)

The placement new function is not replaceable, but it can be overloaded. It needs at least two parameters, the first of which always is a std::size_t parameter designating the number of bytes requested. Any such overloaded function is termed a placement new, even if the additional parameters don’t specify a location.

Namespaces

Names in C++ can refer to variables, functions, structures, enumerations, classes, and class and structure members. When programming projects grow large, the potential for name conflicts increases. When you use class libraries from more than one source, you can get name conflicts. For example, two libraries might both define classes named List, Tree, and Node, but in incompatible ways. You might want the List class from one library and the Tree from the other, and each might expect its own version of Node. Such conflicts are termed namespace problems.

The C++ Standard provides namespace facilities to provide greater control over the scope of names. It took a while for compilers to incorporate namespaces, but, by now, support has become common.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.116.60.62