Do you really understand "new in C++"?

Do you really understand "new in C++"?

Memory allocation

Memory release

Types of

Can it be overloaded

::new

::delete

expression

no

new

delete

expression

no

::new[]

::delete[]

expression

no

new[]

delete[]

expression

no

:: operator new

::operator delete

function

Yes

:: operator new[]

::operator new[]

function

Yes

operator new

operator delete

function

Yes

operator new[]

operator new[]

function

Yes

placement new

placement delete

function

Yes

note:

① ::operator new refers to the global new operator, that is, the global new function, and operator new refers to "the new operator that we generally overload in the class type, that is, the new function belonging to the class type";

② In fact, placement new is a kind of global new operator, which can initialize data objects on the built-in memory space, that is, place data on the built-in memory space;

③ The new operator, that is, the new function is only used to apply for memory space without any other operations, and the object is a pointer of the void* data type;

④ We know the function of new function, the function of new expression, and the function of placement new, we can easily know operator new + placement new = new expression;

⑤ Generally speaking, "new function" can be summarized into the following categories:

Operator new is a function, divided into three forms (the first two do not call the constructor, which is different from the new operator): 

⑴ void* operator new (std::size_t size) throw (std::bad_alloc); 

⑵ void* operator new (std::size_t size, const std::nothrow_t& nothrow_constant) throw(); 

⑶ void* operator new (std::size_t size, void* ptr) throw(); 

The first type allocates size bytes of storage space and aligns the object types in memory. If successful, return a non-null pointer to the first address. Failure to throw bad_alloc exception. 

The second type does not throw an exception when the allocation fails, it returns a NULL pointer. 

The third is the placement new version, which is essentially an overload of operator new, defined in #include <new>. It does not allocate memory, calls the appropriate constructor to construct an object at the place pointed to by ptr, and then returns the actual parameter pointer ptr. 

All three new functions can be overloaded, please see below for specific overloading forms.

Different types of new and delete expressions will cause exceptions

We sometimes say that new and delete are a pair, and array new and array delete are a pair. If we use it incorrectly, it will cause errors, such as:

① Use new and delete[] together:

#include <iostream>  
using namespace std;  
  
class Base  
{  
private:  
    int obj;  
public:  
    Base()  
    {  
        cout << "调用默认构造函数" << endl;  
    }  
    ~Base()  
    {  
        cout << "调用析构函数" << endl;  
    }  
};  
  
int main()  
{  
    Base* ptr = new Base;  // new expression
    delete[] ptr;  // array delete expression
}  

 

Output result:

 

② Use new[] and delete together

#include <iostream>  
using namespace std;  
  
class Base  
{  
private:  
    int obj;  
public:  
    Base()  
    {  
        cout << "调用默认构造函数" << endl;  
    }  
    ~Base()  
    {  
        cout << "调用析构函数" << endl;  
    }  
};  
  
int main()  
{  
    Base* ptr = new Base[2];  // array new expression
    delete ptr;  // delete expression
}  

 

Output result:

 

Under what circumstances improper use of delete will cause memory leaks?

First of all, memory leak refers to "I cannot reclaim the used memory space in an effective way".

① When the destructor of the class type is meaningless, delete or not will not cause a memory leak:

#include <iostream>  
using namespace std;  
  
class Base  
{  
private:  
    int obj;  
public:  
    Base()  
    {  
        cout << "调用默认构造函数" << endl;  
    }  
    ~Base()  
    {  
        cout << "调用析构函数" << endl;  
    }  
};  
  
int main()  
{  
    Base* ptr = new Base[2];  
} 

 

At this time, the destructor is meaningless, because the implementation of the destructor does not include the recovery of the heap memory. When the program finishes executing the main function, the system will release all the memory of the entire project in the Stack stack, so delete Whether the execution of the main function is completed but the memory is still left in the heap area will not occur.

② When the destructor is meaningful, not deleting class objects will cause memory leaks

#include <iostream>  
using namespace std;  
  
class Base  
{  
private:  
    char* ptr;  
public:  
    Base()  
    {  
        ptr = new char[2]; // 在堆区内开辟一块空间  
        cout << "调用默认构造函数" << endl;  
    }  
    ~Base()  
    {  
        if (ptr != nullptr)  
        {  
            delete[] ptr;  
        }  
        ptr = nullptr;  
        cout << "调用析构函数" << endl;  
    }  
};  
  
int main()  
{  
    Base* ptr = new Base[2];  
    delete[] ptr;  
} 

 

If you do not call delete[] to release the memory space applied for in the heap area pointed to by ptr, then the operating system will not complete it for you. The operating system will only release the stack memory space occupied by the project file after the main function is executed. When the operating system releases the memory in the stack area, we cannot find the address corresponding to the memory we apply for in the heap area.

note:

The reason for the memory leak is that after the operating system releases the stack area memory, the local variables in the main function of ptr will also be released. We know that ptr is equivalent to finding the "GPS navigation" in the village in the heap area. When we lose With this GPS navigation, we can no longer find a way to reclaim the memory in the heap area, thus causing a memory leak.

③ Freeing memory for calling delete before the exception is triggered will also cause memory leaks:

why would you said this? I have an "exception handling article" which explains in detail the processing flow when an exception is triggered, but this is not important here, we just need to be clear: when an exception is triggered, the system will automatically recover the part of the remodel function that has been executed. Stack area memory, but the "heap area memory" applied for inside this function will not be recycled, so memory leaks will occur at this time.

When the compiler releases the memory of the stack area where ps is located, we can no longer find the memory space dynamically applied for by the heap area through a certain address. We cannot find this memory space, let alone release it, which is equivalent to " You want to go home for dinner, but you can't even find the door to talk about eating." The following diagram is what I want to illustrate:

 

To prevent this from happening, we can use smart pointers. In "C++ Primer", the emergence of smart pointers is derived from the above example, why use smart pointers:

With smart pointers, automatic memory recovery will become a reality:

Low-level implementation of ordinary global functions operator new and operator delete

 

We see that in the implementation source code of ordinary global function ::operator new:

① First, use the underlying function malloc to continuously apply for size bytes of memory;

② If the application fails, the returned pointer value = NULL (NULL=0). At this time, the while function is continuously polling and waiting, and the _callnewh(size_t) function is continuously called. This function is used to call the new function handle. The trigger timing of the function handle is "there is no memory available in the computer or there is little memory left but not enough size bytes". C++ is very smart. The new function handle is giving us a chance-"When there is little memory left/remaining memory For a few moments, what actions should we take to restore these situations? This is giving us a chance to save";

③ If we do not make full use of this opportunity provided by C++, that is, we do not define the new function handle, then the low pass will call the _CATCH (const exception& exp) macro to throw the exception bad_alloc.

Example of a program containing a handle to the new function:

#include <iostream>  
using namespace std;  
  
void InsuffientMemory()  
{  
    cout << "内存已不足,等待处理......" << endl;  
}  
  
int main()  
{  
    new_handler InsuffientMemory_Handler = set_new_handler(InsuffientMemory);  
  
    void* ptr = ::operator new(1000000000); // 内存不足会调用new函数句柄  
}  

 

The relationship between new expression and operator new

The execution process of new expression consists of three parts:

① Allocate memory to void* type pointers;

② Cast the void* pointer to a pointer of the specified type;

③ Call the default constructor of the specified type;

According to our above process, a complete new expression implementation code is as follows:

#include <iostream>  
using namespace std;  
  
class Base  
{  
private:  
    char* ptr;  
public:  
    Base()  
    {  
        ptr = new char[2]; // 在堆区内开辟一块空间  
        cout << "调用默认构造函数" << endl;  
    }  
    ~Base()  
    {  
        if (ptr != nullptr)  
        {  
            delete[] ptr;  
        }  
        ptr = nullptr;  
        cout << "调用析构函数" << endl;  
    }  
};  
  
int main()  
{  
    void* temp = ::operator new(sizeof(Base));  
    Base* ptr = static_cast<Base*>(temp);  
    ptr->~Base();  
}  

 

However, things went counterproductive and the results were not what we expected:

 

The reason for the above exception is "we called the default constructor in violation of regulations"! We can’t call the default constructor, so we thought of “placement new”. This new operator is quite awesome. Although we can’t directly call the default constructor for initialization, we can use placement new’s “in The characteristics of placing data on the existing memory space are used to implement the two operations of "application for memory area and initialization of data on the memory area" respectively:

#include <iostream>  
using namespace std;  
  
class Base  
{  
private:  
    char* ptr;  
public:  
    Base()  
    {  
        ptr = new char[2]; // 在堆区内开辟一块空间  
        cout << "调用默认构造函数" << endl;  
    }  
    ~Base()  
    {  
        if (ptr != nullptr)  
        {  
            delete[] ptr;  
        }  
        ptr = nullptr;  
        cout << "调用析构函数" << endl;  
    }  
};  
  
int main()  
{  
    void* temp = ::operator new(sizeof(Base));  
    Base* ptr = static_cast<Base*>(temp);  
    new(ptr)Base();  
} 

 

In this case, it is completely OK, so I said at the beginning:

Functionally, new expression = operator new + placement new.

What is the charm of the placement new function?

Operator new applies for "pure memory", which has not been initialized by any constructor. This memory allows us to build various types of data on it. Not only can we build data of basic data types, but also objects of class types. But "we must use the placement new function to operate". Based on "memory application and the use of memory space are separated from each other", we built the concept of "memory pool", which is why people often compare "placement new" and "operator new" to "two major pieces of memory pool construction".

Examples of functions of the placement new function:

If there is such a scenario, we need to apply a large amount of similar memory space and then release it. For example, in a server request for a client, we need to apply for a memory for each upstream data of each client. When we finish processing the request and give the client a downstream reply, the memory is released. On the surface, it seems that it meets the memory management requirements of C++ and there is no error, but it is very unreasonable to think about it carefully. Why do we have to reapply for a piece of memory for each request Well, it’s important to know that for every internal application, the system must find a suitable size of continuous memory space in the memory. This process is very slow (relatively speaking). In extreme cases, if there are a large number of The memory is fragmented, and the space we applied for is large, and it may even fail. Why can't we share a piece of memory we prepared in advance? Yes, we can use placement new to construct the object, then the object will be constructed in the memory space we specify. 

The relationship between delete expression and operator delete

The process of delete expression execution:

① Use pointers to call the destructor of the class type; (the basic data type has no destructor, so there is no need to call)

② Release the memory space of this block.

#include <iostream>  
using namespace std;  
  
class Base  
{  
private:  
    char* ptr;  
public:  
    Base()  
    {  
        ptr = new char[2]; // 在堆区内开辟一块空间  
        cout << "调用默认构造函数" << endl;  
    }  
    ~Base()  
    {  
        if (ptr != nullptr)  
        {  
            delete[] ptr;  
        }  
        ptr = nullptr;  
        cout << "调用析构函数" << endl;  
    }  
};  
  
int main()  
{  
    void* temp = operator new(sizeof(Base));  
    Base* ptr = static_cast<Base*>(temp);  
    new(ptr)Base();  
  
    ptr->~Base();  
    operator delete(ptr, sizeof(Base));  
}  

 

We see: here I am using operator new. In fact, before we overload operator new, operator new is ::operator new, but it is different after overloading:

 

The principle of operator delete is the same as that of operator new. The following is the code for overloading "operator new/delete function":

#include <iostream>  
using namespace std;  
  
class Base  
{  
private:  
    char* ptr;  
public:  
    Base()  
    {  
        ptr = new char[2]; // 在堆区内开辟一块空间  
        cout << "调用默认构造函数" << endl;  
    }  
    static void* operator new(size_t Size)  
    {  
        cout << "进行更加人性化的操作......" << endl;  
        return malloc(Size);  
    }  
    static void* operator new(size_t Size, void* ptr)  
    {  
        Base* temp = static_cast<Base*>(ptr); // 强制类型转换时已调用默认构造函数  
        return temp;  
    }  
    static void operator delete(void* ptr, size_t Size)  
    {  
        cout << "调用自定义delete函数" << endl;  
        free(ptr);  
    }  
    ~Base()  
    {  
        if (ptr != nullptr)  
        {  
            delete[] ptr;  
        }  
        ptr = nullptr;  
        cout << "调用析构函数" << endl;  
    }  
};  
  
int main()  
{  
    void* temp = Base::operator new(sizeof(Base)); // 切记要使用Base::去访问Base的静态类型成员函数  
    Base* ptr = static_cast<Base*>(temp);  
    new(ptr)Base();  
  
    ptr->~Base();  
    operator delete(ptr, sizeof(Base));  
}  

 

The results are as follows:

 

However, we noticed that the compiler does not call our custom delete function. The placement delete function will be called in the following two ways:

① The call timing of our overloaded delete function will only be called when we explicitly specify the domain name for delete:

#include <iostream>  
using namespace std;  
  
class Base  
{  
private:  
    char* ptr;  
public:  
    Base()  
    {  
        ptr = new char[2]; // 在堆区内开辟一块空间  
        cout << "调用默认构造函数" << endl;  
    }  
    static void* operator new(size_t Size)  
    {  
        cout << "进行更加人性化的操作......" << endl;  
        return malloc(Size);  
    }  
    static void* operator new(size_t Size, void* ptr)  
    {  
        cout << "调用自定义placement new函数" << endl;  
        Base* temp = static_cast<Base*>(ptr); // 强制类型转换时已调用默认构造函数  
        return temp;  
    }  
    static void operator delete(void* ptr, size_t Size)  
    {  
        cout << "调用自定义delete函数" << endl;  
        free(ptr);  
    }  
    static void operator delete(void* ptr)  
    {  
        cout << "调用自定义delete函数" << endl;  
        free(ptr);  
    }  
    ~Base()  
    {  
        if (ptr != nullptr)  
        {  
            delete[] ptr;  
        }  
        ptr = nullptr;  
        cout << "调用析构函数" << endl;  
    }  
};  
  
int main()  
{  
    void* temp = Base::operator new(sizeof(Base)); // 切记要使用Base::去访问Base的静态类型成员函数  
    Base* ptr = static_cast<Base*>(temp);  
    new(ptr)Base();  
  
    Base::operator delete(ptr, sizeof(Base)); // 显式的指出Base::作用域名  
}  

 

operation result:

Note: When we overload the operator new/operator delete function, (only for the overloaded local operator new/operator delete function) inside the class type, the overloaded operator new/operator delete function is regarded as static by the compiler by default The member function of the attribute, that is, operator new/operator delete exists as a static member variable of the class type, so when we use the overloaded version, we need to use class_name:: to display the call.

② When an exception occurs in the placement new function, that is, when an exception occurs during memory allocation, the corresponding form of placement delete function will be called:

Is there a placement delete? Placement delete expression is absolutely nothing. But there is a placement delete function, not a placement delete expression. And this placement delete function is something you cannot call directly. When placement new expression calls placement new function, if an exception occurs during the construction of the constructor function, this time to prevent memory leaks, then to clean up the allocated memory, this placement delete function is required. The definition of this thing can be seen by opening the <new> header file.

 

However, only two delete expressions are exposed:

 

That is, delete for non-array and array.

Next, give an example to illustrate the situation of calling placement delete function when an exception occurs in this constructor:

#include <cstdlib>  
#include <iostream>  
  
struct A {};  
struct E {};  
  
class T  
{  
public:  
    T() { throw E(); }  
};  
  
void * operator new (std::size_t, const A &)  
{  
    void* nothing = 0;  
    std::cout << "Placement new called." << std::endl;  
    return nothing;  
}  
void operator delete (void *, const A &)  
{  
    std::cout << "Placement delete called." << std::endl;  
}  
  
int main()  
{  
    A a;  
    try {  
        T * p = new (a) T;  
    }  
    catch (E exp) { std::cout << "Exception caught." << std::endl; }  
    return 0;  
}  

 

operation result:

 

The above is the explanation of a friend of mine who knows about the copy, but I did not call this result using this example. The results of my operation in VS2017 are as follows:

 

I tried many times, and it turns out: when an exception occurs. The C++ compiler under VS2017 will not call the custom placement delete function to release the memory when the placement new function triggers an exception. It will only call the global placement delete function to clean up this "semi-finished product", and then throw an exception to trigger the exception mechanism.

But to be safe, we still define the corresponding operator delete for each operator new, so that when an exception is triggered during memory allocation (when the object is constructed), the corresponding delete is called for memory recovery. I think the result of the call varies with different compilers.

The way of memory allocation constructed using new and delete

 

note:

① Since the bottom layer of operator new/operator delete is implemented using malloc/free in CRT (C Runtime library), operator new/operator delete is an "evolutionary version" of malloc/free, and malloc/free is compared to operator In terms of new/operator delete, the significant difference between malloc/free is: "malloc/free cannot be overloaded"!

② Because when we use new expression and delete expression, no matter whether operator new/operator delete is overloaded or not, there is a high probability that ::operator new/::operator delete will be called, so overloading "global function ::operator new/::operator delete "Not the most sensible option!

③ In fact, when we overload operator new/operator delete, we generally use "global function::operator new/delete" as the underlying implementation function, or we may directly use malloc/free as the underlying implementation of the function:

 

④ The essence of overloading the operator new/delete function is to "control the memory application in your own hands" so that you can perform more user-friendly operations during subsequent memory processing.

 

Guess you like

Origin blog.csdn.net/weixin_45590473/article/details/112970002