C++ added the necessary language constructs to the memory model of C to support object semantics. In addition, it fixed some loopholes in the original model and enhanced it with higher levels of abstraction and automation. This chapter delves into the memory model of C++, starting with the three types of data storage. Next, the various versions of operators new and delete are discussed; finally, some techniques and guidelines for effective and bug-free usage of the memory management constructs are presented.
C++ has three fundamental types of data storage: automatic storage, static storage, and free store. Each of these memory types has different semantics of object initialization and lifetime.
Local objects that are not explicitly declared static or extern, local objects that are declared auto or register, and function arguments have automatic storage. This type of storage is also called stack memory. Automatic objects are created automatically upon entering a function or a block. They are destroyed when the function or block exits. Thus, on each entry into a function or a block, a new copy of its automatic objects is created. The default value of automatic variables and nonclass objects is indeterminate.
Global objects, static data members of a class, namespace variables, and static variables in functions reside in static memory.
The address of a static object remains the same throughout the program's execution.
Every static object is constructed only once during the lifetime of the program. By default, static data are initialized to binary zeros. Static objects with a nontrivial constructor (see Chapter 4, "Special Member Functions: Default Constructor, Copy Constructor, Destructor, And Assignment Operator") are subsequently initialized by their constructors. Objects with static storage are included in the following examples:
int num; //global variables have static storage int func() { static int calls; //initialized to 0 by default return ++calls; } class C { private: static bool b; }; namespace NS { std::string str; //str has static storage }
Free store memory, also called heap memory or dynamic memory, contains objects and variables that are created by operator new. Objects and variables that are allocated on the free store persist until they are explicitly released by a subsequent call to operator delete. The memory that is allocated from the free store is not returned to the operating system automatically after the program's termination.
Therefore, failing to release memory that was allocated using new generally yields memory leaks. The address of an object that is allocated on the free store is determined at runtime. The initial value of raw storage that is allocated by new is unspecified.
A POD (plain old data) object has one of the following data types: a fundamental type, pointer, union, struct, array, or class with a trivial constructor. Conversely, a non-POD object is one for which a nontrivial constructor exists. The properties of an object are in effect only during its lifetime.
A POD object begins its lifetime when it obtains storage with the proper alignment and size for its type, and its lifetime ends when the storage for the object is either reused or deallocated.
A non-POD object begins its lifetime after the constructor call has completed; its lifetime ends when its destructor has started.
C++ defines the global allocation functions new and new[] as well as the corresponding global deallocation functions delete and delete[]. These functions are accessible from each translation unit of a program without including the header <new>. Their implicit declarations are as follows:
void* operator new(std::size_t) throw(std::bad_alloc); // new void* operator new[](std::size_t) throw(std::bad_alloc); // new [] void operator delete(void*) throw(); // delete void operator delete[](void*) throw(); // delete[]
The implicit declarations introduce only the function names operator new, operator new[], operator delete, and operator delete[]. However, they do not introduce the names std, std::bad_alloc, and std::size_t. An explicit reference to any of these names requires that the appropriate header file be included. For example
#include <new> // declarations of std and size_t using namespace std; char * allocate (size_t bytes); int main { char * buff = allocate(sizeof (char) ); return 0; }
The return type of an allocation function is void *, and its first parameter is of type size_t. The value of the first parameter is interpreted as the requested memory size. The allocation function attempts to allocate the requested size of memory from the free store. If the allocation request is successful, it returns the address of the start of a block of storage whose size, in bytes, is at least as large as the requested size.
The return type of a deallocation function is void; its first parameter is of type void *. A deallocation function can have more than one parameter. The value of the first argument that is supplied to a deallocation function can be NULL (in this case, the deallocation function call has no effect). Otherwise, the value supplied to a deallocation function must be one of the values returned by a previous invocation of a corresponding allocation function.Allocation and deallocation functions perform the basic operations of allocating memory from the free store and releasing it. Note however, that in general, you do not invoke these functions directly. Rather, you use a new expression and a delete expression. A new expression implicitly invokes an allocation function and then constructs an object on the allocated memory; likewise, a delete expression destroys an object, and then it invokes a deallocation function to release the storage of the destroyed object.
NOTE: In the following sections, new and delete refer to a new expression and a delete expression, respectively, unless stated otherwise.
C++ still supports the standard C library functions malloc() and free(). The backward compatibility with C is useful in three cases: for combining legacy code that was originally written in C in C++ programs, for writing C++ code that is meant to be supported in C environment (more on this in Chapter 13, "C Language Compatibility Issues"), and for making new and delete implementable by calling malloc() and free().
Otherwise, malloc() and free() are not to be used in C++ code because -- unlike new and delete -- they do not support object semantics. new and delete are also significantly safer and more extensible.
new and delete automatically construct and destroy objects. malloc() and free(), on the other hand, merely allocate and deallocate raw memory from the heap. In particular, using malloc() to create a non-POD object yields undefined behavior. For example
#include <cstdlib> #include <string> using namespace std; string* func() //very bad { string *pstr = static_cast<string*> (malloc (sizeof(string))); //disaster! return pstr; //any attempt to use pstr as a pointer to a string is undefined }
Operator new automatically calculates the size of the object that it constructs. Conversely, with malloc(), the programmer has to specify explicitly the number of bytes that have to be allocated. In addition, malloc() returns a pointer to void, which has to be explicitly cast to the desired type. This is both tedious and dangerous. Operator new returns a pointer to the desired type, so no explicit type cast is required. For example
#include <cstdlib> using namespace std; void func() { int * p = static_cast<int *> malloc(sizeof(int)); int * p2 = new int; }
Operator new can be overloaded by a class. This feature enables specific classes to use different memory management policies, as you will see next. On the other hand, malloc() cannot be overloaded for a specific class.
The results of calling free() to release a pointer that was allocated by new, or of using delete to release memory that was allocated by malloc(), are undefined. The Standard does not guarantee that the underlying implementation of operator new uses malloc(); furthermore, on some implementations malloc() and new use different heaps.
new[] allocates an array of objects of the specified type. The value that is returned by new[] is the address of the first element in the allocated array. For example
int main() { int *p = new int[10]; bool equal = (p == &p[0]); //true delete[] p; return 0; }
Objects that are allocated using new[] must be released by a call to delete[]. Using plain delete instead of delete[] in this case results in undefined behavior. This is because when new[] is executed, the runtime system stores the number of elements in the allocated array in an implementation-defined way. The corresponding delete[] expression retrieves the number of allocated elements to invoke the same number of destructors. How does new[] store the number of elements in the allocated array? The most widely used technique is to allocate an extra sizeof(std::size_t) bytes; that is, for a class C, the expression
C * p = new C[n];
allocates a memory buffer that contains sizeof(std::size_t) + n * sizeof bytes. The value n is written to the allocated buffer just before the first C object. When delete[] is invoked, it looks for the value n in a fixed offset before p (which must point to the first element in the array). delete[] then invokes C's destructor n times and, finally, releases the memory block. Plain delete, on the other hand, does not perform such offset adjustments -- it simply invokes the destructor of the object to which p points.
An alternative technique is to store n in an associative array in which p serves as the key and n is its associated value. When the statement
delete[] p;
is executed, delete[] can lookup p in an associative array such as
std::map<void *, std::size_t>
and retrieve its associated value n. Other techniques for storing the number of array elements can be used as well, but in any one of them, using plain delete instead of delete[] to release an array of objects allocated by new[] results in undefined behavior and should never happen. Similarly, using delete[] to release a single object that was allocated by plain new is also disastrous: It might cause memory leaks, heap corruption, or a program crash.
Contrary to popular belief, the same rules apply to arrays of fundamental types -- not just to arrays of objects. Although delete[] does not invoke destructors in the case of fundamental types, it still has to retrieve the number of elements in the array to calculate the complete size of the memory block. For example
#include<string> void f() { char *pc = new char[100]; string *ps = new std::string[100]; //...use pc and ps delete[] pc; //no destructors invoked, still delete[] is required // for arrays allocated by new[] delete[] ps //ensures each member's destructor is called }
In pre-Standard C++, new returned a NULL pointer when it failed to allocate the requested amount of memory. In this respect, new behaved like malloc() in C. Programmers had to check the value that was returned from new before they used it to make sure that it was not NULL. For example
void f(int size) //anachronistic usage of new { char *p = new char [size]; if (p == 0) //this was fine until 1994 { //...use p safely delete [] p; } return; } const int BUF_SIZE = 1048576L; int main() { f(BUF_SIZE); return 0; }
Returning a NULL pointer upon failure, however, was problematic. (Note that the NULL pointer policy was applicable to both plain new and new[]. Similarly, the modified behavior applies to new as well as new[].) It forced programmers to test the value that was returned by every invocation of operator new, which is a tedious and error-prone process. In addition, the recurrent testing of the returned pointer can increase the size of the programs and add a runtime performance overhead (you might recall that these are the drawbacks associated with the return value policy, discussed in Chapter 6, "Exception Handling"). Failures in dynamic memory allocation are rather rare and generally indicate an unstable system state. This is exactly the kind of runtime errors that exception handling was designed to cope with. For these reasons, the C++ standardization committee changed the specification of new a few years ago. The Standard now states that operator new throws an exception of type std::bad_alloc when it fails, rather than returning a NULL pointer.
CAUTION: Although compiler vendors have been sluggish in adopting this change, most C++ compilers now conform to the standard in this respect, and throw an exception of type std::bad_alloc when new fails. Please consult your compiler's documentation for more details.
A program that calls new either directly or indirectly (for example, if it uses STL containers, which allocate memory from the free store) must contain an appropriate handler that catches a std::bad_alloc exception. Otherwise, whenever new fails, the program terminates due to an uncaught exception. The exception-throwing policy also implies that testing the pointer that is returned from new is completely useless. If new is successful, the redundant test wastes system resources. On the other hand, in the case of an allocation failure, the thrown exception aborts the current thread of execution from where it was thrown -- so the test is not executed anyway. The revised, standard-conforming form of the previously presented program looks similar to the following:
void f(int size) //standard-conforming usage of new { char *p = new char [size]; //...use p safely delete [] p; return; } #include <stdexcept> #include <iostream> using namespace std; const int BUF_SIZE = 1048576L; int main() { try { f(BUF_SIZE); } catch(bad_alloc& ex) //handle exception thrown from f() { cout<<ex.what()<<endl; //...other diagnostics and remedies } return -1; }
Still, under some circumstances, throwing an exception is undesirable. For example, exception handling might have been turned off to enhance performance; on some platforms, it might not be supported at all.
The Standardization committee was aware of this and added an exception-free version of new to the Standard. The exception-free version of new returns a NULL pointer in the event of a failure, rather than throwing a std::bad_alloc exception. This version of new takes an additional argument of type const std::nothrow_t& (defined in the header <new>). It comes in two flavors, one for plain new and another for new[].
//exception-free versions of new and new[] defined in the header <new> void* operator new(std::size_t size, const std::nothrow_t&) throw(); void* operator new[](std::size_t size, const std::nothrow_t&) throw();
The exception-free new is also called nothrow new. It is used as follows:
#include <new> #include <string> using namespace std; void f(int size) // demonstrating nothrow new { char *p = new (nothrow) char [size]; //array nothrow new if (p == 0) { //...use p delete [] p; } string *pstr = new (nothrow) string; //plain nothrow new if (pstr == 0) { //...use pstr delete [] pstr; } return; } const int BUF_SIZE = 1048576L; int main() { f(BUF_SIZE); return 0; }
The argument nothrow is defined and created in header <new> as follows:
extern const nothrow_t nothrow;
Class nothrow_t is defined as follows:
struct nothrow_t {}; //an empty class
In other words, the type nothrow_t is an empty class (the empty class idiom is discussed in Chapter 5, "Object-Oriented Program and Design") whose sole purpose is to overload global new.
An additional version of operator new enables you to construct an object (or an array of objects) at a predetermined memory position. This version is called placement new and has many useful applications, including building a custom-made memory pool or a garbage collector. Additionally, it can be used in mission-critical applications because there is no danger of allocation failure (the memory that is used by placement new has already been allocated). Placement new is also faster because the construction of an object on a preallocated buffer takes less time. Following is an example of using placement new:
#include <new> #include <iostream> using namespace std; void placement() { int *pi = new int; //plain new float *pf = new float[2]; //new [] int *p = new (pi) int (5); //placement new float *p2 = new (pf) float; //placement new[] p2[0] = 0.33f; cout<< *p << p2[0] << endl; //... delete pi; delete [] pf; }
Destructors of objects that were constructed using placement new have to be invoked explicitly. To see why, consider the following example:
#include <new> #include <iostream> using namespace std; class C { public: C() { cout<< "constructed" <<endl; } ~C(){ cout<< "destroyed" <<endl; } }; int main() { char * p = new char [sizeof ]; // pre-allocate a buffer C *pc = new (p) C; // placement new //... used pc pc->C::~C(); // 1:explicit destructor invocation is required delete [] p; //2 return 0; }
Without an explicit destructor invocation in (1), the object that is pointed to by p will never be destroyed, but the memory block on which it was created will be released by the delete[] statement in (2).
As was previously noted, new performs two operations: It allocates memory from the free store by calling an allocation function, and it constructs an object on the allocated memory. The question is, does the allocated memory leak when an exception is thrown during the construction process? The answer is no, it doesn't. The allocated memory is returned to the free store by the system before the exception propagates to the program. Thus, an invocation of operator new can be construed as two consecutive operations. The first operation merely allocates a sufficient memory block from the free store with the appropriate alignment requirements. In the event of failure, the system throws an exception of type std::bad_alloc. If the first operation was successful, the second one begins. The second operation consists of invoking the object's constructor with the pointer that is retained from the previous step. Put differently, the statement
C* p = new C;
is transformed by the compiler into something similar to the following:
#include <new> using namespace std; class C{/*...*/}; void __new() throw (bad_alloc) { C * p = reinterpret_cast<C*> (new char [sizeof ]); //step 1: allocate // raw memory try { new (p) C; //step 2: construct the objects on previously allocated buffer } catch(...) //catch any exception thrown from C's constructor { delete[] p; //free the allocated buffer throw; //re-throw the exception of C's constructor } }
The pointer that is returned by new has the suitable alignment properties so that it can be converted to a pointer of any object type and then used to access that object or array. Consequently, you are permitted to allocate character arrays into which objects of other types will later be placed. For example
#include <new> #include <iostream> #include <string> using namespace std; class Employee { private: string name; int age; public: Employee(); ~Employee(); }; void func() //use a pre allocated char array to construct //an object of a different type { char * pc = new char[sizeof(Employee)]; Employee *pemp = new (pc) Employee; //construct on char array //...use pemp pemp->Employee::~Employee(); //explicit destruction delete [] pc; }
It might be tempting to use a buffer that is allocated on the stack to avoid the hassle of deleting it later:
char pbuff [sizeof(Employee)]; Employee *p = new (pbuff ) Employee; //undefined behavior
However, char arrays of automatic storage type are not guaranteed to meet the necessary alignment requirements of objects of other types. Therefore, constructing an object of a preallocated buffer of automatic storage type can result in undefined behavior. Furthermore, creating a new object at a storage location that was previously occupied by a const object with static or automatic storage type also results in undefined behavior. For example
const Employee emp; void bad_placement() //attempting to construct a new object //at the storage location of a const object { emp.Employee::~Employee(); new (&emp) const Employee; // undefined behavior }
The size of a class or a struct might be larger than the result of adding the size of each data member in it. This is because the compiler is allowed to add additional padding bytes between members whose size does not fit exactly into a machine word (see also Chapter 13). For example
#include <cstring> using namespace std; struct Person { char firstName[5]; int age; // int occupies 4 bytes char lastName[8]; }; //the actual size of Person is most likely larger than 17 bytes void func() { Person person = {{"john"}, 30, {"lippman"}}; memset(&person, 0, 5+4+8 ); //may not erase the contents of //person properly }
On a 32-bit architecture, three additional bytes can be inserted between the first and the second members of Person, increasing the size of Person from 17 bytes to 20.
On some implementations, the memset() call does not clear the last three bytes of the member lastName. Therefore, use the sizeof operator to calculate the correct size:
memset(&p, 0, sizeof(Person));
An empty class doesn't have any data members or member functions. Therefore, the size of an instance is seemingly zero. However, C++ guarantees that the size of a complete object is never zero. Consider the following example:
class Empty {}; Empty e; // e occupies at least 1 byte of memory
If an object is allowed to occupy zero bytes of storage, its address can overlap with the address of a different object. The most obvious case is an array of empty objects whose elements all have an identical address. To guarantee that a complete object always has a distinct memory address, a complete object occupies at least one byte of memory. Non-complete objects -- for example, base class subobjects in a derived class -- can occupy zero bytes of memory.
User-defined versions of new and delete can be declared in a class scope. However, it is illegal to declare them in a namespace. To see why, consider the following example:
char *pc; namespace A { void* operator new ( size_t ); void operator delete ( void * ); void func () { pc = new char ( 'a'); } } void f() { delete pc; } // A::delete or ::delete?
Declaring new and delete in namespace A is confusing for both compilers and human readers. Some programmers might expect the operator A::delete to be selected in the function f() because it matches the operator new that was used to allocate the storage. In contrast, others might expect delete to be called because A::delete is not visible in f(). For this reason, the Standardization committee decided to disallow declarations of new and delete in a namespace.
It is possible to override new and delete and define a specialized form for them for a given class. Thus, for a class C that defines these operators, the following statements
C* p = new C; delete p;
invoke the class's versions of new and delete, respectively. Defining class-specific versions of new and delete is useful when the default memory management scheme is unsuitable. This technique is also used in applications that have a custom memory pool. In the following example, operator new for class C is redefined to alter the default behavior in case of an allocation failure; instead of throwing std::bad_alloc, this specific version throws a const char *. A matching operator delete is redefined accordingly:
#include <cstdlib> // malloc() and free() #include <iostream> using namespace std; class C { private: int j; public: C() : j(0) { cout<< "constructed"<<endl; } ~C() { cout<<"destroyed";} void* operator new (size_t size); //implicitly declared static void operator delete (void *p); //implicitly declared static }; void* C::operator new (size_t size) throw (const char *) { void * p = malloc(size); if (p == 0) throw "allocation failure"; //instead of std::bad_alloc return p; } void C::operator delete (void *p) { free(p); } int main() { try { C *p = new C; delete p; } catch (const char * err) { cout<<err<<endl; } return 0; }
Remember that overloaded new and delete are implicitly declared as static members of their class if they are not explicitly declared static. Note also that a user-defined new implicitly invokes the objects's constructor; likewise, a user-defined delete implicitly invokes the object's destructor.
Choosing the correct type of storage for an object is a critical implementation decision because each type of storage has different implications for the program's performance, reliability, and maintenance. This section tells you how to choose the correct type of storage for an object and thus avoid common pitfalls and performance penalties. This section also discusses general topics that are associated with the memory model of C++, and it compares C++ to other languages.
Creating objects on the free store, when compared to automatic storage, is more expensive in terms of performance for several reasons:
Runtime overhead Allocating memory from the free store involves negotiations with the operating system. When the free store is fragmented, finding a contiguous block of memory can take even longer. In addition, the exception handling support in the case of allocation failures adds additional runtime overhead.
Maintenance Dynamic allocation might fail; additional code is required to handle such exceptions.
Safety An object might be accidentally deleted more than once, or it might not be deleted at all. Both of these are a fertile source of bugs and runtime crashes in many applications.
The following code sample demonstrates two common bugs that are associated with allocating objects on the free store:
#include <string> using namespace std; void f() { string *p = new string; //...use p if (p->empty()!= false) { //...do something return; //OOPS! memory leak: p was not deleted } else //string is empty { delete p; //..do other stuff } delete p; //OOPS! p is deleted twice if isEmpty == false }
Such bugs are quite common in large programs that frequently allocate objects on the free store. Often, it is possible to create objects on the stack, thereby simplifying the structure of the program and eliminating the potential for such bugs. Consider how the use of a local string object simplifies the preceding code sample:
#include <string> using namespace std; void f() { string s; //...use s if (s.empty()!= false) { //...do something return; } else { //..do other stuff } }
As a rule, automatic and static storage types are always preferable to free store.
The correct syntax for instantiating a local object by invoking its default constructor is
string str; //no parentheses
Although empty parentheses can be used after the class name, as in
string str(); //entirely different meaning
the statement has an entirely different meaning. It is parsed as a declaration of a function named str, which takes no arguments and returns a string by value.
The literal 0 is an int. However, it can be used as a universal initializer for every fundamental data type. Zero is a special case in this respect because the compiler examines its context to determine its type. For example:
void *p = 0; //zero is implicitly converted to void * float salary = 0; // 0 is cast to a float char name[10] = {0}; // 0 cast to a '\0' bool b = 0; // 0 cast to false void (*pf)(int) = 0; // pointer to a function int (C::*pm) () = 0; //pointer to a class member
An uninitialized pointer has an indeterminate value. Such a pointer is often called a wild pointer. It is almost impossible to test whether a wild pointer is valid, especially if it is passed as an argument to a function (which in turn can only verify that it is not NULL). For example
void func(char *p ); int main() { char * p; //dangerous: uninitialized //...many lines of code; p left uninitialized by mistake if (p)//erroneously assuming that a non-null value indicates a valid address { func(p); // func has no way of knowing whether p has a valid address } return 0; }
Even if your compiler does initialize pointers automatically, it is best to initialize them explicitly to ensure code readability and portability.
As was previously noted, POD objects with automatic storage have an indeterminate value by default in order to avoid the performance penalty incurred by initialization. However, you can initialize automatic POD objects explicitly when necessary. The following sections explain how this is done.
One way to initialize automatic POD objects is by calling memset() or a similar initialization function. However, there is a much simpler way to do it -- without calling a function, as you can see in the following example:
struct Person { long ID; int bankAccount; bool retired; }; int main() { Person person ={0}; //ensures that all members of //person are initialized to binary zeros return 0; }
This technique is applicable to every POD struct. It relies on the fact that the first member is a fundamental data type. The initializer zero is automatically cast to the appropriate fundamental type. It is guaranteed that whenever the initialization list contains fewer initializers than the number of members, the rest of the members are initialized to binary zeros as well. Note that even if the definition of Person changes -- additional members are added to it or the members' ordering is swapped -- all its members are still initialized. The same initialization technique is also applicable to local automatic arrays of fundamental types as well as to arrays of POD objects :
void f() { char name[100] = {0}; //all array elements are initialized to '\0' float farr[100] = {0}; //all array elements are initialized to 0.0 int iarr[100] = {0}; //all array elements are initialized to 0 void *pvarr[100] = {0};//array of void * all elements are initialized to NULL //...use the arrays }
This technique works for any combination of structs and arrays:
struct A { char name[20]; int age; long ID; }; void f() { A a[100] = {0}; }
You can initialize a union. However, unlike struct initialization, the initialization list of a union must contain only a single initializer, which must refer to the first member in the union. For example
union Key { int num_key; void *ptr_key; char name_key[10]; }; void func() { Key key = {5}; // first member of Key is of type int // any additional bytes initialized to binary zeros }
The term endian refers to the way in which a computer architecture stores the bytes of a multibyte number in memory. When bytes at lower addresses have lower significance (as is the case with Intel microprocessors, for instance), it is called little endian ordering. Conversely, big endian ordering describes a computer architecture in which the most significant byte has the lowest memory address. The following program detects the endian of the machine on which it is executed:
int main() { union probe { unsigned int num; unsigned char bytes[sizeof(unsigned int)]; }; probe p = { 1U }; //initialize first member of p with unsigned 1 bool little_endian = (p.bytes[0] == 1U); //in a big endian architecture, //p.bytes[0] equals 0 return 0; }
You can safely bind a reference to a temporary object. The temporary object to which the reference is bound persists for the lifetime of the reference. For example
class C { private: int j; public: C(int i) : j(i) {} int getVal() const {return j;} }; int main() { const C& cr = C(2); //bind a reference to a temp; temp's destruction //deferred to the end of the program C c2 = cr; //use the bound reference safely int val = cr.getVal(); return 0; }//temporary destroyed here along with its bound reference
The result of applying delete to the same pointer after it has been deleted is undefined. Clearly, this bug should never happen. However, it can be prevented by assigning a NULL value to a pointer right after it has been deleted. It is guaranteed that a NULL pointer deletion is harmless. For example
#include <string> using namespace std; void func { string * ps = new string; //...use ps if ( ps->empty() ) { delete ps; ps = NULL; //safety-guard: further deletions of ps will be harmless } //...many lines of code delete ps; // ps is deleted for the second time. Harmless however }
Both C and C++ make a clear-cut distinction between two types of pointers -- data pointers and function pointers. A function pointer embodies several constituents, such as the function's signature and return value. A data pointer, on the other hand, merely holds the address of the first memory byte of a variable. The substantial difference between the two led the C standardization committee to prohibit the use of void* to represent function pointers, and vice versa. In C++, this restriction was relaxed, but the results of coercing a function pointer to a void* are implementation-defined. The opposite -- that is, converting data pointers to function pointers -- is illegal.
Pointers to objects or functions of the same type are considered equal in three cases:
If both pointers are NULL. For example
int *p1 = NULL, p2 = NULL; bool equal = (p1==p2); //true
If they point to the same object. For example
char c; char * pc1 = &c; char * pc2 = &c; bool equal = (pc1 == pc2); // true
If they point one position past the end of the same array. For example
int num[2]; int * p1 = num+2, *p2 = num+2; bool equal = ( p1 == p2); //true
In addition to malloc() and free(), C also provides the function realloc() for changing the size of an existing buffer. C++ does not have a corresponding reallocation operator. Adding operator renew to C++ was one of the suggestions for language extension that was most frequently sent to the standardization committee. Instead, there are two ways to readjust the size of memory that is allocated on the free store. The first is very inelegant and error prone. It consists of allocating a new buffer with an appropriate size, copying the contents of the original buffer to it and, finally, deleting the original buffer. For example
void reallocate { char * p new char [100]; //...fill p char p2 = new char [200]; //allocate a larger buffer for (int i = 0; i<100; i++) p2[i] = p[i]; //copy delete [] p; //release original buffer }
Obviously, this technique is inefficient and tedious. For objects that change their size frequently, this is unacceptable. The preferable method is to use the container classes of the Standard Template Library (STL). STL containers are discussed in Chapter 10, "STL and Generic Programming."
By default, local static variables (not to be confused with static class members) are initialized to binary zeros. Conceptually, they are created before the program's outset and destroyed after the program's termination. However, like local variables, they are accessible only from within the scope in which they are declared. These properties make static variables useful for storing a function's state on recurrent invocations because they retain their values from the previous call. For example
void MoveTo(int OffsetFromCurrentX, int OffsetFromCurrentY) { static int currX, currY; //zero initialized currX += OffsetFromCurrentX; currY += OffsetFromCurrentY; PutPixel(currX, currY); } void DrawLine(int x, int y, int length) { for (int i=0; i<length; i++) MoveTo(x++, y--); }
However, when the need arises for storing a function's state, a better design choice is to use a class. Class data members replace the static variables and a member function replaces the global function. Local static variables in a member function are of special concern: Every derived object that inherits such a member function also refers to the same instance of the local static variables of its base class. For example
class Base { public: int countCalls() { static int cnt = 0; return ++cnt; } }; class Derived1 : public Base { /*..*/}; class Derived2 : public Base { /*..*/}; // Base::countCalls(), Derived1::countCalls() and Derived2::countCalls // hold a shared copy of cnt int main() { Derived1 d1; int d1Calls = d1.countCalls(); //d1Calls = 1 Derived2 d2; int d2Calls = d2.countCalls(); //d2Calls = 2, not 1 return 0; }
Static local variables in the member function countCalls can be used to measure load balancing by counting the total number of invocations of that member function, regardless of the actual object from which it was called. However, it is obvious that the programmer's intention was to count the number of invocations through Derived2 exclusively. In order to achieve that, a static class member can be used instead:
class Base { private: static int i; public: virtual int countCalls() { return ++i; } }; int Base::i; class Derived1 : public Base { private: static int i; //hides Base::i public: int countCalls() { return ++i; } //overrides Base:: countCalls() }; int Derived1::i; class Derived2 : public Base { private: static int i; //hides Base::i and distinct from Derived1::i public: virtual int countCalls() { return ++i; } }; int Derived2::i; int main() { Derived1 d1; Derived2 d2; int d1Calls = d1.countCalls(); //d1Calls = 1 int d2Calls = d2.countCalls(); //d2Calls also = 1 return 0; }
Static variables are problematic in a multithreaded environment because they are shared and have to be accessed by means of a lock.
An anonymous union (anonymous unions are discussed in Chapter 12, "Optimizing Your Code") that is declared in a named namespace or in the global namespace has to be explicitly declared static. For example
static union //anonymous union in global namespace { int num; char *pc; }; namespace NS { static union { double d; bool b;}; //anonymous union in a named namespace } int main() { NS::d = 0.0; num = 5; pc = "str"; return 0; }
There are several phases that comprise the construction of an object, including the construction of its base and embedded objects, the assignment of a this pointer, the creation of the virtual table, and the invocation of the constructor's body. The construction of a cv-qualified (const or volatile) object has an additional phase, which turns it into a const/volatile object. The cv qualities are effected after the object has been fully constructed.
The complex memory model of C++ enables maximal flexibility. The three types of data storage -- automatic, static, and free store -- offer a level of control that normally exist only in assembly languages.
The fundamental constructs of dynamic memory allocation are operators new and delete. Each of these has no fewer than six different versions; there are plain and array variants, each of which comes in three flavors: exception throwing, exception free, and placement.
Many object-oriented programming languages have a built-in garbage collector, which is an automatic memory manager that detects unreferenced objects and reclaims their storage (see also Chapter 14, "Concluding Remarks and Future Directions," for a discussion on garbage collection). The reclaimed storage can then be used to create new objects, thereby freeing the programmer from having to explicitly release dynamically-allocated memory. Having an automatic garbage collector is handy because it eliminates a large source of bugs, runtime crashes, and memory leaks. However, garbage collection is not a panacea. It incurs additional runtime overhead due to repeated compaction, reference counting, and memory initialization operations, which are unacceptable in time-critical applications. Furthermore, when garbage collection is used, destructors are not necessarily invoked immediately when the lifetime of an object ends, but at an indeterminate time afterward (when the garbage collector is sporadically invoked). For these reasons, C++ does not provide a garbage collector. Nonetheless, there are techniques to minimize -- and even eliminate -- the perils and drudgery of manual memory management without the associated disadvantages of garbage collection. The easiest way to ensure automatic memory allocation and deallocation is to use automatic storage. For objects that have to grow and shrink dynamically, you can use STL containers that automatically and optimally adjust their size. Finally, in order to create an object that exists throughout the execution of a program, you can declare it static. Nonetheless, dynamic memory allocation is sometimes unavoidable. In such cases, auto_ptr(discussed in Chapters 6 and 11, "Memory Management") simplifies the usage of dynamic memory.
Effective and bug-free usage of the diversity of C++ memory handling constructs and concepts requires a high level of expertise and experience. It isn't an exaggeration to say that most of the bugs in C/C++ programs are related to memory management. However, this diversity also renders C++ a multipurpose, no compromise programming language.
© Copyright 1999, Macmillan Computer Publishing. All rights reserved.