Go deep into C ++'s new

Source: Internet
Author: User
Tags traits

 

Transferred from:

Http://www.builder.com.cn/2008/0104/696370.shtml

 

"New" is a key word of C ++ and also an operator. There are many topics about new, because it is indeed complicated and mysterious. Next I will summarize what I have learned about new.

In the new process, when we use the keyword new to dynamically create an object on the stack, it actually does three things: getting a memory space, calling constructors, and returning the correct pointer. Of course, if we create a simple type variable, the second step will be omitted. Suppose we have defined the following Class A: Class
{
Int I;
Public:
A (INT _ I): I (_ I * _ I ){}
Void say () {printf ("I = % DN", I );}
};
// Call New:
A * pA = new A (3); then the above process of dynamically creating an object is roughly equivalent to the following three statements (just roughly): A * pA = (*) malloc (sizeof ());
Pa-> A: A (3 );
Return Pa; although the results show that these three sentences also obtain a valid pointer PA pointing to the object on the stack, the difference is that when malloc fails, it does not call the allocation memory failure handler new_handler, but uses new. Therefore, we should try to use new unless there are some special requirements. The three forms of new so far, the new mentioned in this Article refer to "new operator" or "new expression", but in fact we mentioned new in C ++, it may at least represent three meanings: new operator, operator new, and placement new. New operator is the new method we usually use. Its behavior is the three steps mentioned above, and we cannot change it. However, if the behavior in a specific step does not meet our specific requirements, we may change it. The last step in the three steps is simply to convert the pointer type. There is nothing to say, and this conversion is not required in the compiled code. It's just a human understanding. But there is something in the first two steps. The first step of new operator to allocate memory is actually done by calling operator new. Here, new is actually the same operator as addition, subtraction, multiplication, division, and so can be reloaded. Operator new calls the code for memory allocation by default to get a heap space. If it succeeds, it returns. If it fails, it calls new_hander instead and repeats the previous process. If we are not satisfied with this process, we can reload operator new to set the desired behavior. Example: Class
{
Public:
Void * operator new (size_t size)
{
Printf ("operator new calledn ");
Return: Operator new (size );
}
};

A * A = new ();

Here, operator new calls the original global new to output a sentence before memory allocation. The global operator new can also be reloaded, but in this way, you can no longer recursively use new to allocate memory, but you can only use malloc: void * operator new (size_t size)
{
Printf ("Global newn ");
Return malloc (size );
} Correspondingly, delete can also be divided into Delete operator and operator delete, and the latter can also be reloaded. In addition, if operator new is reloaded, operator Delete should be reloaded accordingly, which is a good programming habit. The third form of new -- Placement new is used to locate the structure, so we can implement the second step in the three-step new operator operation, that is, after obtaining a memory that can hold the specified type of objects, construct an object in the memory, which is somewhat similar to the "p-> :: A (3); ", but this is not a standard syntax. The correct syntax is placement New: # include <New. h>

Void main ()
{
Char s [sizeof (a)];
A * P = (A *) S;
New (p) A (3); // P-> A: A (3 );
P-> say ();
} It is required to reference the <New> or <new. h> file to use placement new. Here, the strange Syntax of "New (p) A (3)" is placement new, which implements the function of constructing an object with the specified type of constructor at the specified memory address, A (3) is an explicit call to the constructor. It is not difficult to find that the specified address can be either a stack or a heap. placement does not distinguish this. However, unless necessary, do not directly use placement new. After all, this is not a formal method for constructing objects, but a step of new operator. The new operator compiler automatically generates the code for calling placement new. Therefore, it also generates the code for calling the Destructor when using Delete. If placement new is used on the stack as above, you must manually call the destructor, which is the only situation for explicitly calling the Destructor: p-> ~ A (); when we think that the default new operator memory management cannot meet our needs, and we want to manually manage the memory, placement new will be useful. Allocator in STL uses this method, and uses placement new to implement more flexible and effective memory management. Handle memory allocation exceptions. As mentioned above, operator new's default behavior is to request memory allocation. If it succeeds, this memory address is returned. If it fails, a new_handler is called and then this process is repeated. Therefore, to return from the execution process of operator new, one of the following conditions must be met: l memory allocation is successful l new_handler throws bad_alloc exception l new_handler calls exit () or similar function, so that the program ends. We can assume that operator new acts like this by default: void * operator new (size_t size)
{
Void * P = NULL
While (! (P = malloc (size )))
{
If (null = new_handler)
Throw bad_alloc ();
Try
{
New_handler ();
}
Catch (bad_alloc E)
{
Throw E;
}
Catch (...)
{}
}
Return P;
} By default, new_handler throws a bad_alloc exception. Therefore, the preceding loop is executed only once. If you do not want to use the default behavior, you can define a new_handler and use the STD: set_new_handler function to make it take effect. In the custom new_handler, we can throw an exception, end the program, or run some code to make it possible that memory is idle, so that the next allocation may succeed, you can also use set_new_handler to install another possibly more effective new_handler. Example: void mynewhandler ()
{
Printf ("new handler called! N ");
Throw STD: bad_alloc ();
}

STD: set_new_handler (mynewhandler );

Here, the new_handler program will output a sentence before throwing an exception. Note that the new_handler code should avoid nested calls to new, because if the call to New fails, it may cause calls to new_handler, this leads to infinite recursive calls. -- This is what I guess, and I have never tried it. During programming, we should note that exceptions may be thrown to new calls. Therefore, we should ensure that new code is transactional, that is to say, you cannot throw an exception to call new to cause incorrect program logic or data structure. Example: Class someclass
{
Static int count;
Someclass (){}
Public:
Static someclass * getnewinstance ()
{
Count ++;
Return new someclass ();
}
}; The static variable count is used to record the number of instances generated for this type. In the above Code, if an exception is thrown due to memory allocation failure of new, the number of instances does not increase, however, the value of the count variable has already exceeded one, and the data structure is damaged. The correct syntax is: static someclass * getnewinstance ()
{
Someclass * P = new someclass ();
Count ++;
Return P;
} In this way, if new fails, an exception is thrown directly, and the value of Count does not increase. Similarly, when processing thread synchronization, you should also pay attention to the similar problem: void somefunc ()
{
Lock (somemutex); // Add a lock
Delete P;
P = new someclass ();
Unlock (somemutex );
} In this case, if new fails, unlock will not be executed, so not only will a pointer P pointing to an incorrect address exist, but also somemutex will never be unlocked. This situation should be avoided. (Reference: C ++ proverbs: code for exceptional security) STL memory allocation and traits techniques detail the behavior of sgi stl memory distributors in STL original code analysis. Unlike using new operator directly, sgi stl does not rely on C ++'s default memory allocation method, but uses a self-implemented solution. First, sgi stl allocates the entire available memory block to make it available to the current process. When the program actually needs to allocate memory, first, try to get the memory from these large memory blocks that have been requested, and then try to allocate large memory for the entire block if it fails. This method effectively avoids the emergence of a large number of memory fragments and improves the memory management efficiency. To achieve this, STL uses placement new to construct objects by using placement new in the memory space managed by itself, so as to achieve the functions of the original new operator. Template <class T1, class T2>
Inline void construct (T1 * P, const T2 & value)
{
New (p) T1 (value );
} This function receives a constructed object and creates a new object on the given memory address P through copy construction. The last half of the Code is T1 (value) it is the way to call the constructor in the placement new syntax. If the input object value is the required Type T1, it is equivalent to calling the copy constructor. Similarly, because placement new is used, the compiler will not automatically generate code to call the destructor, and manual implementation is required: Template <class T>
Inline void destory (T * pointer)
{
Pointer-> ~ T ();
} At the same time, STL also has a destory version that receives two iterators, which can destroy all objects within a specified range on a container. A typical implementation method is to call the Destructor one by one for objects in this range through a loop. If the input object is of a non-simple type, this is necessary, but if the input object is of a simple type, or there is no need to call the custom type of The Destructor (for example, the struct that contains only several int members), so it is unnecessary to call the Destructor one by one, which also wastes time. To this end, STL uses a technique called "type Traits". In the compiler, it determines whether the input type needs to call the Destructor: Template <class forwarditerator>
Inline void destory (forwarditerator first, forwarditerator last)
{
_ Destory (first, last, value_type (first ));
} Value_type () is used to retrieve the type information of the object pointed to by the iterator, so: Template <class forwarditerator, class T>
Inline void _ destory (forwarditerator first, forwarditerator last, T *)
{
Typedef typename _ type_traits <t>: has_trivial_destructor trivial_destructor;
_ Destory_aux (first, last, trivial_destructor ());
}
// If you need to call the Destructor:
Template <class forwarditerator>
Inline void _ destory_aux (forwarditerator first, forwarditerator last, _ false_type)
{
For (; first <last; ++ first)
Destory (& * First); // because first is an iterator, * First retrieves the real content and then uses & obtains the address
}
// Do nothing if you do not need it:
Tempalte <class forwarditerator>
Inline void _ destory_aux (forwarditerator first, forwarditerator last, _ true_type)
{} Because all the above functions are inline, multi-layer function calls do not affect the performance. The final compilation result is only a for loop or nothing based on the specific type. The key here is the _ type_traits <t> template class, which defines the results of different has_trivial_destructor Based on Different T types. If T is a simple type, it is defined as _ true_type, otherwise it is defined as _ false_type. Among them, _ true_type and _ false_type are only two classes without any content. They have no significance for the execution result of the program, but in the compiler's view, it has very important guiding significance for the template to be specific, as shown in the code above. _ Type_traits <t> is also a series of special template classes: struct _ true_type {};
Struct _ false_type {};
Template <class T>
Struct _ type_traits
{
Public:
Typedef _ false _ type has_trivial_destructor;
......
};
Template <> // template Specialization
Struct _ type_traits <int> // special version of the int
{
Public:
Typedef _ true_type has_trivial_destructor;
......
};
...... // For other simple type Special versions, if you want to define a custom type myclass as not calling the destructor, you only need a specific version of _ type_traits <t>: Template <>
Struct _ type_traits <myclass>
{
Public:
Typedef _ true_type has_trivial_destructor;
......
}; Templates are relatively advanced C ++ programming skills. template-specific and template-specific are even more skillful. The type_traits in STL fully utilizes the template-specific functions, during program compilation, the compiler is used to determine the specific version used for each call. Therefore, the program running efficiency is greatly improved without increasing programming complexity. For more details, see related content in Chapter 2 and Chapter 3 of STL source code analysis. For new and delete with "[]", we often use new to dynamically create an array, for example, char * s = new char [100];
......
Delete s; strictly speaking, the above Code is incorrect, because we use new [] When allocating memory, rather than simply new, however, delete is used to release the memory. The correct statement is to use Delete []: Delete [] S. However, the Code with the preceding errors may also be compiled and executed without causing any errors. In fact, there are differences between new and new [], delete and delete [], especially when used to operate complex types. If a custom class myclass uses new []: myclass * P = new myclass [10], the above Code is allocated 10 consecutive myclass instances on the stack, the constructor has been called for them in turn, so we get 10 available objects, which is different from Java and C, in Java and C #, only 10 null values are obtained. In other words, when using this method, myclass must have constructors without parameters. Otherwise, a compilation error will be found because the compiler cannot call constructors with parameters. After the structure is successful, we can release it again. When releasing it, we use Delete []: Delete [] P. When we call Delete [] for the dynamically allocated array, the behavior varies according to the applied variable type. If P points to a simple type, such as Int or char, the result is that the memory is recycled. In this case, delete [] is used and delete, but if P points to a complex type, delete [] calls the Destructor for each dynamically allocated object, and then releases the memory. Therefore, if we use Delete to recycle the P pointer allocated above, although no error is reported during the compilation period (because the compiler cannot see how the P pointer is allocated ), however, a debug assertion failed prompt is displayed during running (in the case of DEBUG. Here, we can easily ask a question: how does Delete [] Know how many objects need to call the Destructor? To answer this question, we can first take a look at the heavy load of new. Class myclass
{
Int;
Public:
Myclass () {printf ("ctorn ");}
~ Myclass () {printf ("dtorn ");}
};

Void * operator new [] (size_t size)
{
Void * P = Operator new (size );
Printf ("calling New [] with size = % d address = % PN", size, P );
Return P;
}

// Main Function
Myclass * MC = new myclass [3];
Printf ("Address of MC = % PN", MC );
Delete [] Mc; run this code and the result is: (vc2005) calling New [] with size =16Address =003a5a58Ctorctorctoraddress of MC =003a5a5cAlthough dtordtordtor is expected to call constructor and destructor, the size of the applied memory space and the value of the address are faulty. The size of our class myclass is obviously 4 bytes and the requested array contains 3 elements. Therefore, we should apply for 12 bytes in total, but in fact, the system applied for 16 bytes for us, and after operator new [] is returned, the memory address we get is the result of the actually applied memory address value plus 4. That is to say, when an array is dynamically allocated for a complex type, the system automatically removes four bytes before the final memory address, we have reason to believe that the content of these four bytes is related to the length of the dynamically allocated array. Through one-step tracking, it is easy to find that the int value corresponding to these four bytes is 0x00000003, that is, the number of objects we allocate is recorded. Changing the number of assignments once again proves my idea. Therefore, we also have reason to think that the behavior of new [] operator is equivalent to the following pseudocode: Template <class T>
T * New [] (INT count)
{
Int size = sizeof (t) * count + 4;
Void * P = T: Operator new [] (size );
* (Int *) P = count;
T * PT = (T *) (INT) P + 4 );
For (INT I = 0; I <count; I ++)
New (& PT [I]) T ();
Return pt;
} The above schematic Code omitted the exception handling part, only to show what the actual behavior is when we use new [] for a complex type to dynamically allocate an array, it can be seen that it allocates 4 more bytes of memory than expected and uses it to save the number of objects. Then, it uses placement New for each of the following spaces to call the no-argument constructor, this explains why in this case, the class must have no constructor, and then return the first address. Similarly, we can easily write the implementation code of the corresponding Delete []: Template <class T>
Void Delete [] (T * PT)
{
Int COUNT = (int *) pt) [-1];
For (INT I = 0; I <count; I ++)
PT [I]. ~ T ();
Void * P = (void *) (INT) Pt-4 );
T: Operator Delete [] (P );
} It can be seen that by default, operator new [] and operator new have the same behavior, and operator Delete [] and operator Delete are the same, the difference is that new operator and new [] Operator, delete operator and delete [] operator. Of course, we can choose to reload operator new and delete with and without "[]" based on different needs to meet different specific needs. Slightly modify the code of myclass in the front class -- comment out the destructor, and then look at the output of the program: calling new [] with size = 12 address = 003a5a58ctorctorctoraddress of MC = 003a5a58 this time, new [] Honestly applied for 12 bytes of memory, and the application result is the same as the result returned by new [] Operator. It seems that whether or not to add the first four bytes depends on whether the class has a destructor. Of course, the correct statement is whether the class needs to call the constructor, because although the class does not declare the destructor in the following two cases, however, four more bytes are applied. One is that this class has members who need to call the destructor, and the other is that this class inherits the class that needs to call the destructor. Therefore, we can recursively define the class for calling destructor as one of the following three situations: 1 explicitly declares that the Destructor 2 has a member of the class that needs to call the Destructor 3 inherits the class that needs to call the destructor, when you dynamically apply for an array of simple types, no more than four bytes will be applied. Therefore, in both cases, delete or delete [] can be used to release the memory. However, in order to develop a good habit, we should pay attention to the fact that an array is dynamically allocated, delete [] is used for release. How to know the length when releasing the memory but this also brings about a new problem. Since the number of records is not recorded when applying for an array of classes or simple types without calling the destructor, operator Delete, or, more directly, how does free () recycle the memory? This requires studying the structure of memory returned by malloc. Similar to new [], in fact, when malloc () applies for memory, it also applies for several bytes of content, but this has nothing to do with the type of the applied variable, we can also understand this from the parameter passed in when calling malloc-it only receives the length of the memory to be applied for, and does not matter what type of memory this memory is used to save. Run the following code to make an experiment: char * p = 0;
For (INT I = 0; I <40; I + = 4)
{
Char * s = new char [I];
Printf ("alloc % 2D bytes, address = % P Distance = % DN", I, S, S-p );
P = s;
} Let's look at the running results of the release version under vc2005. The debug version does not analyze alloc 0 bytes because it contains a lot of debugging information, address = 003a36f0 distance = 3815152 alloc 4 bytes, address = 003a3700 distance = 16 alloc 8 bytes, address = 003a3710 distance = 16 alloc 12 bytes, address = 003a3720 distance = 16 alloc 16 bytes, address = 003a3738 distance = 24 alloc 20 bytes, address = 003a84c0 distance = 19848 alloc 24 bytes, address = 003a84e0 distance = 32 alloc 28 bytes, address = 003a8500 Distance = 32 alloc 32 bytes, address = 003a8528 distance = 40 alloc 36 bytes, address = 003a8550 distance = 40 each time the number of allocated bytes is 4 more than the previous time, the distance value records the difference with the previous allocation. The first difference has no practical significance. There is a large difference in the middle. It may be because the memory has been allocated, so it is ignored. The minimum difference in the result is 16 bytes until we apply for 16 bytes, the difference is changed to 24, and there is a similar law in the future, we can think that the memory structure obtained from the application is as follows: it is not difficult to see that when we want to allocate a memory segment, the obtained memory address must be at least 8 bytes apart from the last address (more in the debug version), so we can guess, the eight bytes should record information related to the memory allocated in this section. Observe the content in these eight sections. The result is as follows: the right side of the figure shows the hexadecimal representation of the content of the first eight bytes of the address allocated each time. The red line shows the result, multiply the first byte of the eight bytes by 8 to get the distance between the two allocation times. After the test, the larger length is allocated at one time. We can see that the second byte also means this, in addition, it indicates that the first two bytes of the eight bytes that are left blank record the length of the memory allocated at a time, the next six bytes may be related to the idle memory linked list information, which is used to provide necessary information during memory translation. This answers the previous question. In the past, C/C ++ recorded sufficient information for memory allocation, but we usually don't care about it.

Related Article

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.