Home  >  Article  >  Backend Development  >  Detailed explanation of C++ memory management

Detailed explanation of C++ memory management

黄舟
黄舟Original
2016-12-16 09:50:571052browse

1. The corresponding new and delete should be in the same form. What is wrong with the following statement?
string *stringarray = new string[100];
...
delete stringarray;

Everything seems to be in order - a new corresponds to a delete - but there is a big mistake hidden: the running of the program The situation will be unpredictable. At least 99 out of 100 string objects pointed to by stringarray will not be properly destroyed because their destructors are never called.

Two things will happen when using new. First, memory is allocated (via the Operator new function, see Item 7-10 and Item m8 for details), and then one or more constructors are called for the allocated memory. When you use delete, two things also happen: first, one or more destructors are called for the memory to be freed, and then, the memory is freed (via the operator delete function, see Items 8 and m8 for details). For delete, there is such an important question: How many objects in the memory need to be deleted? The answer determines how many destructors will be called.

This question is simply: Does the pointer to be deleted point to a single object, or an array of objects? Only you can tell delete. If you use delete without parentheses, delete will think it points to a single object. Otherwise, it will think it points to an array:

string *stringptr1 = new string;
string *stringptr2 = new string[100 ];
...

delete stringptr1;//Delete an object
delete [] stringptr2;//Delete an array of objects

What happens if you add "[]" before stringptr1? The answer is: That would be unguessable; what would happen if you did not add "[]" before stringptr2? The answer is also: unguessable. And for fixed types like int, the results are unpredictable, even if such types don't have a destructor. Therefore, the rule for solving this type of problem is simple: if you use [] when calling new, you should also use [] when calling delete. If you don't use [] when calling new, don't use [] when calling delete.

It is especially important to keep this rule in mind when writing a class that contains pointer data members and provides multiple constructors. Because in this case, you must use the same new form in all constructors that initialize pointer members. Otherwise, what form of delete will be used in the destructor? For further elaboration on this topic, see Item 11.

This rule is also very important for people who like to use typedef, because programmers who write typedef must tell others what form of delete should be used to delete an object of the type defined by typedef using new. For example:

typedef string addresslines[4]; //A person's address, 4 lines in total, one string per line
//Because addresslines is an array, use new:
string *pal = new addresslines; // Pay attention to " new addresslines" returns string*, which is the same as
// "new string[4]" returns. When deleting, it must correspond to it in the form of an array:
delete pal;// Error!
delete [] pal;// Correct

To avoid confusion, it is best to avoid using typedefs for array types. This is actually easy, because the standard C++ library (see Item 49) includes string and vector templates, and using them will reduce the need for arrays to almost zero. For example, addresslines can be defined as a vector of strings, that is, addresslines can be defined as a vector type.

2. Call delete on pointer members in the destructor

In most cases, classes that perform dynamic memory allocation use new in the constructor to allocate memory, and then use delete in the destructor to release the memory. It's certainly not difficult to do when you first write this class, and you'll remember to use delete at the end for all members that have memory allocated in all constructors.

However, after this class is maintained and upgraded, the situation will become difficult, because the programmer who modified the code of the class is not necessarily the first person to write this class. Adding a pointer member means almost the following work:
·Initialize the pointer in each constructor. For some constructors, if no memory is to be allocated to the pointer, the pointer is initialized to 0 (i.e. a null pointer).
·Delete the existing memory and allocate new memory to the pointer through the assignment operator.
·Delete the pointer in the destructor.

If you forget to initialize a pointer in the constructor, or forget to handle it during the assignment operation, the problem will appear quickly and obviously, so in practice these two problems will not torture you so much. However, if the pointer is not deleted in the destructor, it will not show obvious external symptoms. Instead, it may just appear as a tiny memory leak that keeps growing until it eats up your address space and causes the program to die. Because this situation is often not noticeable, you must remember it clearly every time you add a pointer member to the class.

Also, deleting the null pointer is safe (since it does nothing). Therefore, when writing constructors, assignment operators, or other member functions, each pointer member of the class either points to valid memory or points to null. Then in your destructor you can simply delete Drop them without worrying about whether they have been new.

Of course, the use of these terms should not be absolute. For example, you certainly wouldn't use delete to delete a pointer that was not initialized with new, and just like using a smart pointer object without having to delete it, you would never delete a pointer that was passed to you. In other words, unless the class member originally used new, there is no need to use delete in the destructor.

Speaking of smart pointers, here is a way to avoid having to delete pointer members, which is to replace these members with smart pointer objects, such as auto_ptr in the c++ standard library. To know how it works, take a look at clauses m9 and m10.

3. Prepare in advance for insufficient memory
Operator new will throw an exception when it cannot complete the memory allocation request (the previous method was to return 0, and some older compilers still do this. You can also use Your compiler is set up like this. I will defer discussion of this topic to the end of this article). Everyone knows that handling exceptions caused by insufficient memory can really be regarded as a moral act, but in practice it will be as painful as a knife on the neck. So, you sometimes leave it alone, maybe you never care about it. But you must still have a deep sense of guilt hidden in your heart: What if something really goes wrong with new?
You will naturally think of a way to deal with this situation, which is to go back to the old way and use preprocessing. For example, a common practice in C is to define a type-independent macro to allocate memory and check whether the allocation is successful. For c++, this macro might look like this:


#define new(ptr, type)
try { (ptr) = new type; }
catch (std::bad_alloc&) { assert(0); }

("Slow! What does std::bad_alloc do?" you ask. bad_alloc is the exception type thrown when operator new cannot satisfy the memory allocation request, std is the name of the namespace (see Item 28) where bad_alloc is located "Okay!" you continue to ask, "what's the use of assert?" If you look at the standard C header file (or its equivalent using namespaces, see Item 49), you will find that assert is Macro. This macro checks whether the expression passed to it is non-zero. If it is not non-zero, it will issue an error message and call abort. assert only does this when the standard macro ndebug is not defined, that is, in debugging mode. . In the product release state, that is, when ndebug is defined, assert does nothing and is equivalent to an empty statement, so you can only check the assertion (assertion) during debugging.

The new macro not only has the common problem mentioned above, that is, using assert to check the status that may occur in the published program (however, insufficient memory may occur at any time), at the same time, it also has another function in c++ One flaw: it doesn't take into account the various ways new can be used. For example, if you want to create an object of type t, there are generally three common syntax forms. You must handle the exceptions that may occur in each form:


new t;
new t(constrUCtor arguments);
new t[size ];

The problem is greatly simplified here, because some people will also customize (overload) operator new, so the program will contain any syntax form using new.

So, what to do? If you want to use a very simple error handling method, you can do this: when the memory allocation request cannot be satisfied, call an error handling function you specified in advance. This method is based on a convention, that is, when operator new cannot satisfy the request, it will call an error handling function specified by the customer before throwing an exception - generally called the new-handler function. (The actual work of operator new is more complicated, see Clause 8 for details)

The set_new_handler function is used when specifying the error handling function. It is roughly defined as follows in the header file:


typedef void (*new_handler) ();
new_handler set_new_handler(new_handler p) throw();

As you can see, new_handler is a custom function pointer type, which points to a function with no input parameters and no return value. set_new_handler is a function that inputs and returns new_handler type.

The input parameter of set_new_handler is the pointer of the error handling function to be called when operator new fails to allocate memory, and the return value is the pointer of the old error handling function that was already in effect before set_new_handler was called.

You can use set_new_handler as follows:


// function to call if operator new can't allocate enough memory
void nomorememory()
{
cerr << "unable to satisfy request for memory
";
abort();
}

int main()
{
set_new_handler(nomorememory);
int *pbigdataarray = new int[100000000];

...

}

If operator new cannot allocate space for 100,000,000 integers, nomorememory will be called and the program will terminate with an error message. This is better than simply letting the system kernel generate an error message and terminate the program. (By the way, think about it, what will happen if cerr needs to dynamically allocate memory during the process of writing error information...)

When operator new cannot satisfy the memory allocation request, the new-handler function is not only called once, but continuously Repeat until enough memory is found. The code to implement repeated calls can be seen in Item 8, where I use descriptive language to explain: A well-designed new-handler function must implement one of the following functions.
·Generate more available memory. This will make operator new's next attempt to allocate memory more likely to succeed. One way to implement this strategy is to allocate a large block of memory when the program starts and then free it the first time new-handler is called. The release is accompanied by some warning messages to the user. For example, if the amount of memory is too small, the next request may fail unless there is more free space.
·Install a different new-handler function. If the current new-handler function cannot produce more available memory, maybe it will know that another new-handler function can provide more resources. In this case, the current new-handler can install another new-handler to replace it (by calling set_new_handler). The next time operator new calls new-handler, the most recently installed one will be used. (Another variation of this strategy is to allow new-handler to change its own running behavior, so that the next time it is called, it will do something different. This is done by allowing new-handler to modify static variables that affect its own behavior. or global data. )
·Remove new-handler. That is, pass a null pointer to set_new_handler. If new-handler is not installed, operator new will throw a standard std::bad_alloc type exception when it fails to allocate memory.
·Throw std::bad_alloc or other types of exceptions continuing from std::bad_alloc. Such exceptions are not caught by operator new, so they are sent to the place where the memory request was originally made. (Throwing exceptions of different types will violate the operator new exception specification. The default behavior in the specification is to call abort, so when new-handler wants to throw an exception, it must be sure that it continues from std::bad_alloc . For more information on exception specifications, see Item m14. )
·No return. The typical approach is to call abort or exit. abort/exit can be found in the standard C library (and the standard C++ library, see Item 49).

The above options give you great flexibility in implementing the new-handler function.

The method to take when handling memory allocation failure depends on the class of the object to be allocated:


class x {
public:
static void

outofmemory();

...

};

class y {
public:
static void outofmemory();

...

};

x* p1 = new x; // If the allocation is successful, call x::outofmemory
y* p2 = new y ; // If the allocation is unsuccessful, call y::outofmemory

c++ does not support the new-handler function specifically for classes, and it is not needed. You can implement it yourself by providing your own version of set_new_handler and operator new in each class. A class's set_new_handler can specify a new-handler for the class (just like the standard set_new_handler specifies a global new-handler). Operator new of a class ensures that the new-handler of the class is used instead of the global new-handler when allocating memory for objects of the class.

Suppose handling the case of class x memory allocation failure. Because operator new fails to allocate memory for an object of type x, the error handling function must be called every time, so a static member of the new_handler type must be declared in the class. Then the class

Static members of a class must be defined outside the class. Because I want to borrow the default initialization value 0 of the static object, I did not initialize it when defining x::currenthandler.


new_handler x::currenthandler; //The default currenthandler is set to 0 (i.e. null)
The set_new_handler function in class x will save any pointer passed to it and return any pointer saved before calling it. This is exactly what the standard version of set_new_handler does:


new_handler x::set_new_handler(new_handler p)
{
new_handler oldhandler = currenthandler;
currenthandler = p;
return oldhandler;
}

Finally look at the operator of x What new does:
1. Call the standard set_new_handler function, and the input parameter is the error handling function of x. This makes x's new-handler function a global new-handler function. Note that in the following code, the "::" symbol is used to explicitly reference the std space (the standard set_new_handler function exists in the std space).

2. Call global operator new to allocate memory. If the first allocation fails, global operator new will call x's new-handler because it has just been installed as a global new-handler (see 1.). If the global operator new finally fails to allocate memory, it throws a std::bad_alloc exception, and x's operator new will catch it. x's operator new then restores the originally replaced global new-handler function, and finally returns by throwing an exception.

3. Assuming that the global operator new successfully allocates memory for the object of type x, the operator new of x will call the standard set_new_handler again to restore the original global error handling function. Finally, a pointer to the successfully allocated memory is returned.
C++ does this:


void * x::operator new(size_t size)
{
new_handler globalhandler = // Install x’s new_handler
std::set_new_handler(currenthandler);

void *memory;

try { // Try to allocate memory
memory = ::operator new(size);
}

catch (std::bad_alloc&) { // Restore the old new_handler
std::set_new_handler(globalhandler);
throw; // Throws an exception
}
std::set_new_handler(globalhandler); //Restore the old new_handler
return memory;
}

If you don't like the repeated calls to std::set_new_handler above, you can see Item m9 to remove them.

The memory allocation processing function of class x is roughly as follows:


void nomorememory();//Declaration of the new_handler function called when the object of Set nomorememory to the
// new-handling function of Call the global new-handling function

x::set_new_handler(0);
// Set the new-handling function of x to be empty

x *px2 = new x;
// If memory allocation fails, an exception will be thrown immediately
// (There is no new-handling function in class As Item 41 explains, continuations and templates can be used to design reusable code. Here, we combine the two methods to meet your requirements.

You only need to create a "mixin-style" base class, which allows subclasses to continue a specific function of it - here refers to the function of creating a new-handler of a class. The reason why a base class is designed is to allow all subclasses to continue the set_new_handler and operator new functions, and the template is designed so that each subclass has different currenthandler data members. This sounds complicated, but you'll see that the code is actually very familiar. The only difference is that it can now be reused by any class.


template // Provides support for class set_new_handler
class newhandlersupport { // Base class for "mixed style"
public:
static new_handler set_new_handler(new_handler p);

static void * operator new(size_t size);

private :
static new_handler currenthandler;
};

template
new_handler newhandlersupport::set_new_handler(new_handler p)
{
new_handler oldhandler = currenthandler;
currenthandler = p;
return oldhandler;
}

template
void * newhandler support:: operator new(size_t size)
{
new_handler globalhandler =
std::set_new_handler(currenthandler);
void *memory;
try {
memory = ::operator new(size);
}
catch (std::bad_alloc&) {
std::set_new_handler(globalhandler);
throw;
}

std::set_new_handler(globalhandler);
return memory;
}
// this sets each currenthandler to 0

template
new_handler newhandlersupport::currenthandler;
With this template class, adding the set_new_handler function to class why
// private inheritance might be preferable here.)
class x: public newhandlersupport {

... // as before, but no declarations for
}; // set_new_handler or operator new


still works when using x Don't worry about what it's doing behind the scenes; the old code still works. This is good! The things you often ignore are often the most trustworthy.

Using set_new_handler is a convenient and simple way to deal with insufficient memory. This is certainly much better than wrapping each new in a try module. Furthermore, templates like newhandlersupport make it easier to add a specific new-handler to any class. The "mixed style" continuation inevitably leads the topic to multiple continuations. You must read Item 43 before moving to this topic.

Before 1993, C++ always required operator new to return 0 when memory allocation failed. Now it requires operator new to throw std::bad_alloc exception. Many C++ programs were written before compilers started supporting the new specification. The C++ standards committee didn't want to abandon existing code that followed the return-0 specification, so they provided alternative forms of operator new (and operator new[]—see Item 8) that continue to provide return-0 functionality. These forms are called "throwless" because they do not use a throw, but use a nothrow object at the entry point using new:


class widget { ... };

widget *pw1 = new widget; // Allocation failure throws std::bad_alloc if

if (pw1 == 0) ... // This check must fail
widget *pw2 = new (nothrow) widget; // If allocation fails, return 0

if (pw2 == 0) ... // This check may succeed

Whether you use the "regular" (that is, throw an exception) form of new or the "no-throwing" form of new, the important thing is that you must Prepare for memory allocation failure. The easiest way is to use set_new_handler as it works for both forms.

4. Follow the conventions when writing operator new and operator delete

When rewriting operator new yourself (Item 10 explains why you sometimes need to override it), it is important that the behavior provided by the function is consistent with the system default operator new is consistent. The actual implementation is: have a correct return value; call the error handling function when the available memory is not enough (see Item 7); handle the 0-byte memory request situation. Also, avoid accidentally hiding the standard form of new, but that's the topic of Item 9.

The part about the return value is simple. If the memory allocation request succeeds, a pointer to the memory is returned; if it fails, an exception of type std::bad_alloc is thrown as specified in Item 7.

But things are not that simple. Because operator new will actually try to allocate memory more than once, it needs to call the error handling function after each failure, and also expects the error handling function to find a way to release memory elsewhere. Operator new throws an exception only if the pointer to the error handling function is null.

In addition, the C++ standard requires that operator new must return a legal pointer even when requesting to allocate 0 bytes of memory. (In fact, this strange-sounding requirement does bring simplicity to other parts of the C++ language)

In this way, the pseudocode of operator new in the form of non-class members will look like the following:
void * operator new(size_t size) // operator new may also have other parameters
{

if (size == 0) { // When processing a 0-byte request,
size = 1; // Treat it as a 1-byte request
}
while (1) {
Allocate size bytes of memory;

if (allocation successful)
return (pointer to memory);

//If allocation is unsuccessful, find out the current error handling function
new_handler globalhandler = set_new_handler (0);
set_new_handler(globalhandler);

if (globalhandler) (*globalhandler)();
else throw std::bad_alloc();
}
}

The trick to handling zero-byte requests is to treat it as Requests a byte to process. This may seem weird, but it's simple, legal, and effective. And, how often do you encounter a zero-byte request?

You may wonder why the error handling function is set to 0 and then restored immediately in the above pseudocode. This is because there is no way to directly get the pointer to the error handling function, so it must be found by calling set_new_handler. The method is stupid but effective.


Item 7 mentioned that operator new contains an infinite loop internally, and the above code clearly illustrates this - while (1) will cause an infinite loop. The only way to break out of the loop is that the memory allocation is successful or the error handler completes one of the events described in Item 7: more available memory is obtained; a new new -handler (error handler) is installed; new-handler; threw an exception of std::bad_alloc or its derived type; or returned failure. Now we understand why new-handler must do one of these tasks. If you don't do this, the loop in operator new will not end.

One thing that many people don’t realize is that operator new is often inherited by subclasses. This leads to some complications. In the pseudocode above, the function will allocate size bytes of memory (unless size is 0). size is important because it is the parameter passed to the function. But most operator new written for classes (including the one in Item 10) are designed only for a specific class, not for all classes, nor for all its subclasses. This means that for an operator new of class But due to existence, operator new in the base class may be called to allocate memory for a subclass object:
class base {
public:
static void * operator new(size_t size);
...
};

class derived: public base // The derived class does not declare operator new
{ ... }; //

derived *p = new derived; // Call base::operator new

If the operator new of the base class does not want to take the trouble to specifically handle this situation - which is unlikely to occur - then the easiest way is to transfer this "wrong" number of memory allocation requests to the standard operator new. Process, like this:
void * base::operator new(size_t size)
{
if (size != sizeof(base)) // If the quantity is "wrong", let the standard operator new
return ::operator new( size); // Go to handle this request
//

... // Otherwise handle this request
}

"Stop!" I heard you shouting, "You forgot to check a method that is unreasonable but possible A situation that occurs - size may be zero!" Yes, I didn't check it, but please don't be so formal next time you scream. :) But in fact the check is still done, but it is integrated into the size != sizeof(base) statement. The C++ standard is weird, one of which is that all freestanding classes have a non-zero size. So sizeof(base) can never be zero (even if the base class has no members). If size is zero, the request will go to ::operator new, which will handle the request in a reasonable way. (Interestingly, if base is not an independent class, sizeof(base) may be zero, see "my article on counting objects" for details).

If you want to control the memory allocation of class-based arrays, you must implement the array form of operator new - operator new[] (this function is often called "array new" because you can't think of "operator new[]"). how to pronounce). When writing operator new[], remember that you are dealing with "raw" memory and cannot perform any operations on objects that do not yet exist in the array. In fact, you don't even know how many objects are in the array because you don't know how big each object is. The operator new[] of the base class will be used to allocate memory for the array of subclass objects through continuation, and subclass objects are often larger than the base class. Therefore, it cannot be taken for granted that the size of each object in base::operator new[] is sizeof(base). In other words, the number of objects in the array is not necessarily (number of requested bytes)/sizeof(base). For a detailed introduction to operator new[], see clause m8.

That’s all the conventions to follow when overriding operator new (and operator new[]). For operator delete (and its mate operator delete[]), the situation is simpler. All you have to remember is that C++ guarantees that it is always safe to delete a null pointer, so you have to take full advantage of this guarantee. The following is the pseudo code of operator delete in the form of non-class members:
void operator delete(void *rawmemory)
{
if (rawmemory == 0) return; file://If/if the pointer is null, return
//

Release the memory pointed to by rawmemory;

return;
}

The class member version of this function is also simple, but the size of the deleted object must also be checked. Assuming that the operator new of the class transfers the allocation request of the "wrong" size to::operator new, then the deletion request of the "wrong" size must also be transferred to::operator delete:

class base { // Same as before, just It is declared here
public: // operator delete
static void * operator new(size_t size);
static void delete operator(void *rawmemory, size_t size);
...
};

void base::operator delete (void *rawmemory, size_t size)
{
if (rawmemory == 0) return; // Check the null pointer

if (size != sizeof(base)) { // If size is "wrong",
::operator delete(rawmemory); // Let the standard operator handle the request
return;
}

release the memory pointing to rawmemory;

return;
}

As you can see, there are operator new and operator delete (and their array forms) The rules are not that troublesome, the important thing is to follow them. As long as the memory allocator supports the new-handler function and handles zero memory requests correctly, that's it; if the memory deallocator handles null pointers, there's nothing else to do. As for adding continuation support to class member version functions, that will be done soon.
5. Avoid hiding the standard form of new
Because the name declared in the inner scope will hide the same name in the outer scope, so for two functions f with the same name declared inside the class and globally, the class Member functions will hide global functions:
void f(); // Global function
class x {
public:
void f(); // Member function
};

x x;

f(); // Calling f

x.f(); // Calling x::f

is not surprising or confusing, since calling global functions and member functions always use different

syntax. However, if you add an operator new function with multiple parameters to the class, the results may be surprising.

class x {
public:
void f();

// The parameter of operator new specifies a
// new-hander (new’s error handling) function
static void * operator new (size_t size, new_handler p) ;
};

void specialerrorhandler(); // Defined elsewhere

x *px1 =
new (specialerrorhandler) x; // Call x::operator new

x *px2 = new x; // Error!

After defining a function called "operator new" in a class, access to standard new will be inadvertently blocked. Item 50 explains why this is the case, but what we are more concerned with here is how to find a way to avoid this problem.

One way is to write an operator new in the class that supports the standard new calling method. It does the same thing

as the standard new. This can be implemented using an efficient inline function.

class x {
public:
void f();

static void * operator new(size_t size, new_handler p);

static void * operator new(size_t size)
{ return ::operator new(size) ; }
};

x *px1 =
new (specialerrorhandler) x; // Call x::operator
// new(size_t, new_handler)

x* px2 = new x; // Call x::operator
// new(size_t)

Another approach is to provide default values ​​for each parameter added to operator new (see Item 24):

class x {
public:
void f();

static
void * operator new(size_t size, // The default value of p is 0
new_handler p = 0); //
};

x *px1 = new (specialerrorhandler) x; // Correct

x* px2 = new x; // Also correct

No matter which method is used, if you want to customize new functions for the "standard" form of new in the future, you only need to rewrite this function.

The caller can use the new functions after recompiling and linking.

6. If you write operator new, you must also write operator delete

Let us go back and look at this basic question: Why is it necessary to write our own operator new and operator delete?

The answer is usually: for efficiency. The default operator new and operator delete are very versatile, and their flexibility also allows their performance to be further improved in certain specific situations. This is especially true in applications that require dynamic allocation of a large number of small objects.

For example, there is a class representing an airplane: class airplane only contains a pointer, which points to the actual description of the airplane object (this technique is explained in Item 34):

class airplanerep { ... }; // means An airplane object
//
class airplane {
public:
...
private:
airplanerep *rep; // Pointer to the actual description
};

An airplane object is not big, it only contains a pointer (as in clause 14 and m24 illustrate that if the aircraft class declares a virtual function, the second pointer will be implicitly included). But when you call operator new to allocate an aircraft object, you may get more memory than is needed to store the pointer (or pair of pointers). The reason why this seemingly strange behavior occurs is that operator new and operator delete need to pass information to each other.

Because the default version of operator new is a general-purpose memory allocator, it must be able to allocate memory blocks of any size. Similarly, operator delete must also be able to release memory blocks of any size. If operator delete wants to find out how much memory it wants to release, it must know how much memory was originally allocated by operator new. There is a common way for operator new to tell operator delete what the size of the memory it originally allocated is to pre-include some additional information in the memory it returns to indicate the size of the allocated memory block. That is, when you write the following statement,

airplane *pa = new airplane;

you will not get a block of memory that looks like this:

pa --> the memory of the airplane object

instead You get a memory block like this:

pa——> Memory block size data + memory of airplane object

For small objects like airplane, these additional data information will make it necessary to dynamically allocate objects. The memory size is doubled (especially when there are no virtual functions in the class).

If the software is running in an environment where memory is precious, it cannot afford such a luxurious memory allocation scheme. By writing an operator new specifically for the aircraft class, you can take advantage of the fact that the size of each aircraft is equal, without adding additional information to each allocated memory block.

Specifically, there is a way to implement your custom operator new: first let the default operator new allocate some large blocks of raw memory, each block large enough to accommodate many aircraft objects. The memory block of the airplane object is taken from these large memory blocks. Blocks of memory that are not currently being used are organized into linked lists - called free linked lists - for future use by the aircraft. It sounds like each object has to bear the overhead of a next field (to support the linked list), but no: the space in the rep field is also used to store the next pointer (because it is only needed for the memory block used as the aircraft object rep pointer; similarly, only the memory block not used as an aircraft object needs the next pointer), which can be achieved using union.


During specific implementation, it is necessary to modify the definition of aircraft to support customized memory management. You can do this:

class airplane { // Modified class - supports customized memory management
public: //

static void * operator new(size_t size);

...

private:
union {
airplanerep *rep; // For used objects
airplane *next; // For unused objects (in free linked lists)
};

// Class constants, specifying a large memory block How many
// airplane objects are placed and initialized later?
static const int block_size;

static airplane *headoffreelist;

};

The above code adds several declarations: an operator new function, a union (so that The rep and next fields occupy the same space), a constant (specifies the size of the large memory block), and a static pointer (tracks the head of the free linked list). It is important to declare the header pointer as a static member because there is only one free linked list for the entire class, not for each aircraft object.

It’s time to write the operator new function:

void * airplane::operator new(size_t size)
{
// Transfer the “wrong” size request to::operator new() for processing;
// See details Clause 8
if (size != sizeof(airplane))
return ::operator new(size);

airplane *p = // p points to the head of the free linked list
headoffreelist; //

// p if legal , then move the head of the list to its next element
//
if (p)
headoffreelist = p->next;

else {
//If the free linked list is empty, allocate a large memory block,
// Can accommodate block_size aircraft objects
airplane *newblock =
static_cast(::operator new(block_size *
sizeof(airplane)));

// Link each small memory block to form a new free linked list
// Skip the 0th element because it will be returned to the caller of operator new
//
for (int i = 1; i < block_size-1; ++i)
newblock[i].next = &newblock[i+1];

// End the linked list with a null pointer
newblock[block_size-1].next = 0;

// p is set to the head of the table, and the
// memory block pointed to by headoffreelist follows Then
p = newblock;
headoffreelist = &newblock[1];
}

return p;
}

If you read Item 8, you will know that when operator new cannot satisfy the memory allocation request, a series of Routine actions related to new-handler functions and exceptions. The above code does not have these steps. This is because the memory managed by operator new is allocated from::operator new. This means that operator new will only fail if ::operator new fails. And if ::operator new fails, it will perform the action of new-handler (may end by throwing an exception), so there is no need for aircraft operator new to also handle it. In other words, the actions of new-handler are actually still there, you just don't see it, it is hidden in ::operator new.

With operator new, the next thing to do is to define the static data members of aircraft:

airplane *airplane::headoffreelist;

const int airplane::block_size = 512;

There is no need to explicitly add headoffreelist is set to a null pointer because the initial values ​​of static members are set to 0 by default. block_size determines how large a memory block is to be obtained from ::operator new.

This version of operator new will work very well. It allocates less memory for the aircraft object than the default operator new, and runs faster, possibly twice as fast. This is not surprising. The general-purpose default operator new must deal with memory requests of various sizes and internal and external fragmentation; while your operator new only operates on a pair of pointers in a linked list. It's often easy to trade flexibility for speed.

Next we will discuss operator delete. Remember operator delete? This article is about operator delete. But until now, the aircraft class has only declared operator new, but not operator delete. Think about what would happen if you wrote the following code:

airplane *pa = new airplane; // Call
// airplane::operator new
...

delete pa; // Call::operator delete

When reading this code, if you prick up your ears, you will hear the sound of the plane crashing and burning, and the programmer crying. The problem is that operator new (the one defined in airplane) returns a pointer to memory without header information, while operator delete (the default one) assumes that the memory passed to it contains header information. This is the cause of the tragedy.

This example illustrates a general principle: operator new and operator delete must be written at the same time so that different assumptions do not occur. If you write your own memory allocation program, you must also write a release program. (For another reason why this rule should be followed, see the sidebar on placement section of the article on counting objects)

Therefore, continue to design the airplane class as follows:

class airplane { // Same as before, except Added a
public: // declaration of operator delete
...

static void operator delete(void *deadobject,
size_t size);

};

// What is passed to operator delete is a memory block, If
// its size is correct, add it to the front of the free memory block list
//
void airplane::operator delete(void *deadobject,

size_t size)
{
if (deadobject == 0) return; // See clause 8

if (size != sizeof(airplane)) { // See clause 8
::operator delete(deadobject);
return;
}

airplane *carcass =
static_cast(deadobject);

carcass->next = headoffreelist;
headoffreelist = carcass;
}

Because the "wrong" size request was transferred to the global operator new in operator new (see Item 8), then the "wrong" size object must also be handed over to the global operator here. operator delete to handle. If you don't do this, you will reproduce the problem you tried so hard to avoid before - the syntactic mismatch between new and delete.

Interestingly, if the object to be deleted is inherited from a class without a virtual destructor, the size_t value passed to operator delete may be incorrect. This is why base classes must have virtual destructors, and Item 14 lists a second, more compelling reason. Simply remember here that if the base class omits a virtual constructor, operator delete may not work correctly.

All is well and good, but from your frown I can tell you must be worried about memory leaks. If you have a lot of development experience, you will not fail to notice that aircraft's operator new calls::operator new to get a large chunk of memory, but aircraft's operator delete does not release them. Memory leak! Memory leak! I clearly hear the alarm bells ringing in your head.

But please listen to my answer carefully, there is no memory leak here!

The reason for memory leaks is that the pointer pointing to the memory is lost after memory allocation. Without garbage handling or other mechanisms outside the language, this memory would not be reclaimed. But there is no memory leak in the above design, because it will never lose the memory pointer. Each large memory block is first divided into aircraft-sized chunks, and then these chunks are placed on a free linked list. When the client calls airplane::operator new, the small block is removed from the free linked list, and the client gets a pointer to the small block. When the client calls operator delete, the small block is put back on the free list. With this design, all memory blocks are either used by the aircraft object (in which case it is the client's responsibility to avoid memory leaks), or they are on a free linked list (in which case the memory blocks have pointers). So there is no memory leak here.

However, it is true that the memory block returned by ::operator new has never been released by aircraft::operator delete. This memory block has a name, called memory pool. But there is an important difference between memory leaks and memory pools. Memory leaks can grow indefinitely, even if the client behaves well; the size of the memory pool will never exceed the maximum amount of memory requested by the client.

It is not difficult to modify aircraft’s memory management program so that the memory block returned by ::operator new is automatically released when it is not used, but we will not do this here. There are two reasons for this: the first reason and your custom memory related to the original intention of governance. You have many reasons to customize memory management, the most basic of which is that you are sure that the default operator new and operator delete are using too much memory or/and are running slowly. Every additional byte written and every additional statement written to track and free those large memory blocks will cause the software to run slower and use more memory than using a memory pool strategy. When designing a library or program with high performance requirements, if you expect the size of the memory pool to be within a reasonable range, it is best to use the memory pool method.

The second reason is related to dealing with some unreasonable program behaviors. Assuming that the aircraft's memory management program is modified, the aircraft's operator delete can release any large chunks of memory where no objects exist. Then look at the following program: int main()
{
airplane *pa = new airplane; // First allocation: Get a large block of memory,
// Generate a free linked list, etc.

delete pa; // The memory block is empty ;
// Release it

pa = new airplane; // Get a large block of memory again,
// Generate a free linked list, wait

delete pa; // The memory block is empty again,
// Release

... // You have an idea...

return 0;
}

This terrible little program will run slower than a program written with the default operator new and operator delete, take up more memory, and be more Don't compare it with programs written using memory pools.

Of course there are ways to deal with this unreasonable situation, but the more extraordinary situations you consider, the more likely you will have to reimplement the memory management function, and what will you get in the end? Memory pools cannot solve all memory management problems and are suitable in many cases.

In actual development, you will often have to implement memory pool-based functions for many different classes. You will think, "There must be some way to encapsulate this fixed-size memory allocator so that it can be used conveniently." Yes, there is a way. Although I have been nagging on this clause for so long, I still want to briefly introduce it and leave the specific implementation as an exercise for the reader.

The following is a simple minimal interface of the pool class (see Item 18). Each object of the pool class is a memory allocator for a certain type of object (the size of which is specified in the constructor of the pool).

class pool {
public:
pool(size_t n); // Create
for an object of size n// An allocator


void * alloc(size_t n); // Allocate enough memory for an object
// Follow the operator new convention of Item 8

void free( void *p, size_t n); // Will The memory pointed to by p is returned to the memory pool;
// Follow the operator delete convention in Clause 8

~pool(); // Release all memory in the memory pool

};

This class supports the creation and execution of pool objects Allocate and deallocate operations, as well as be destroyed. When a pool object is destroyed, all memory allocated by it is released. This means that there is now a way to avoid the memory leak-like behavior exhibited by airplane functions. However, this also means that if the pool's destructor is called too quickly (not all objects using the memory pool are destroyed), some objects will find that the memory they are using is suddenly gone. The results are often unpredictable.

With this pool class, even Java programmers can easily add their own memory management functions to the airplane class:

class airplane {
public:

... // Ordinary airplane functions

static void * operator new(size_t size);
static void operator delete(void *p, size_t size);


private:
airplanerep *rep; // Pointer to the actual description
static pool mempool; // Memory of airplanes Pool

};

inline void * airplane::operator new(size_t size)
{ return mempool.alloc(size); }

inline void airplane::operator delete(void *p,
size_t size)
{ mempool.free(p, size); }

// Create a memory pool for the aircraft object,
// Implement
pool airplane::mempool(sizeof(airplane));

This design is better than The previous one is much clearer and cleaner because the aircraft class is no longer mixed in with non-airplane code. The union, the free linked list head pointer, and the constants defining the size of the original memory block are all gone. They are all hidden where they should be - in the pool class. Let the programmers who write the pool worry about the details of memory management. Your job is just to make the aircraft class work properly.

You should now understand that custom memory management programs can greatly improve the performance of the program, and they can be encapsulated in classes like pool. But please don't forget the main point. Operator new and operator delete need to work at the same time. So if you write operator new, you must also write operator delete.

The above is the detailed explanation of C++ memory management. For more related articles, please pay attention to the PHP Chinese website (www.php.cn)!


Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Previous article:makefile rulesNext article:makefile rules