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 storageint 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.
C++ defines the global allocation functionsnew and new[]
as well as the corresponding global deallocation functionsdelete
and delete[]. These functions are accessible from each translation
unit of a program without including the header <new>. Their implicit
declarations are as follows:
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
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;}
Explicit Destructor Invocation Is Required for an Object Created by Placement
new
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:
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 paddingbytes 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 bytesvoid 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:
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.
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 cntint 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::ipublic: 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::ipublic: 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.