The use of memory allocator and new&delete of memory management in C++

Memory management in C++

Clarify the concept of "memory actual use" and "memory allocation"

Memory structure in Vector container

 

Size: The size of the memory that has actually been initialized and used in all the memory of the vector container;

Resize member function: adjust the size of the memory area actually used in the total memory of the vector (the area that is not explicitly initialized uses the default initialization method);

Capacity: The capacity of the vector container refers to the total memory space we reserve for the vector container. Of course, the total memory space includes "unused (not accessible before initialization)" and "used" (This part of the memory has been initialized)";

Reverse member function: This member function is used to adjust the total memory space of the vector, that is, the memory capacity.

Why does the memory capacity of vector improve program efficiency?

We know that the bottom layer of the vector container is to use the new operator to continuously apply for space and delete to continuously release space, but frequent applications and releases will inevitably lead to low efficiency. At this time, these IT giants came up with a way to effectively avoid this inefficient working method:

① According to the memory space I need to use, I first go to the memory area to find an area larger than this memory space. At this time, the memory capacity of the vector appears, so much memory space is enough for us to load our target data;

② If you want to use the memory, you can take it from the already opened memory space;

③ When the memory capacity, that is, the total capacity of the vector container is insufficient, we dynamically apply for a larger memory space to use, which repeats our first step and repeats these steps.

Note: Although the vector container is the least efficient container in the STL, the use of the vector container and the structure in the memory are very similar to our array. I can also say that the vector container is "the final evolution mode of C-style arrays". Although the "super C-style array" of the vector container has been optimized to the extreme, the vector container still retains the essential properties of the traditional C-style array: it requires continuous storage space, so in actual use, the efficiency is still low.

The use of Allocator memory allocator

The structure of Allocator memory allocator

pointer allocate (size_type n, allocator<void>::const_pointer hint=0)

Structure memory area

void deallocate (pointer p, size_type n)

Destroy the n bytes of memory space pointed to by the p pointer

template <class U, class... Args>

  void construct (U* p, Args&&... args);

Call p to point to the constructor of the class type;

The Arg parameter is a value that initializes the p pointer to the object

template <class U>

  void destroy (U* p);

Call the destructor pointed to by the p pointer to destruct the class object in the memory area

We should pay attention to the following points:

① Calling the destructor does not release the corresponding memory area, but erases the class object from the corresponding memory area, which means that when we call the destructor, the memory area is still intact but the data on the memory area Has disappeared

② All the data on the "pure" memory space (memory space that has not been initialized) is meaningless, which means that the memory area we applied for does not belong to any data type, so we use void* Typeless pointer to point to it.

Please take a closer look at the following procedure:

#include <iostream>  
using namespace std;  
#include <string>  
#include <memory>  
  
class Person  
{  
private:  
    string name;  
    int age;  
public:  
    Person(const Person& obj)  
    {  
        cout << "调用构造函数" << endl;  
        this->name = obj.name;  
        this->age = obj.age;  
    }  
    Person(const string& name, const int& age)  
    {  
        this->name = name;  
        this->age = age;  
    }  
    ~Person()  
    {  
        cout << "调用析构函数" << endl;  
    }  
};  
  
int main()  
{  
    allocator<Person> ALLOC;  
    void* ptr = ALLOC.allocate(sizeof(Person));  // 分配内存,但没调用构造函数
    ALLOC.construct((Person*)ptr, Person("张三", 19));  // 在已分配内存空间上,调用析构函数
    ALLOC.destroy((Person*)ptr);  // 针对于ptr指针指向的一个Person类对象,去调用析构函数销毁这块内存空间上的Person类对象
    ALLOC.deallocate((Person*)ptr, sizeof(Person));  // 销毁ptr指针所指向的sizeof(Person)个内存区域
}  

 

operation result:

 

The destructor in the middle is because the anonymous variable (temporary variable) "Person("Zhang San", 19)" on line 33 was destructed on line 34, which appeared in the second line of the command line window. This "call destructor" sign.

① The first "calling the constructor" of the command line should be caused by "calling the constructor in the existing memory area, that is, calling the constructor function to initialize the class type area pointed to by the ptr pointer";

② The "call destructor" flag on the last line of the command line should be caused by "the execution of the 34th line of code, that is, the execution of the destroy function makes the destructor of the ~Person() class object pointed to by the ptr pointer be executed";

③ The operation of destroying the memory area, that is, the execution of the deallocate function does not call the destructor of the class object.

Image metaphor of memory release process

It is equivalent to "a mountain (a mountain is a memory area) with many trees (these trees are class objects), when people hold an axe (the axe is equivalent to a destructor, the destructor will affect the class object but not this memory Area) After the trees are felled, the mountain will still exist (the memory area will still exist after the class object is destroyed, unless the memory area is manually destroyed by deallocate), but "when we hold the shovel (deallocate command to destroy the memory area) When I went to remove this mountain, the trees on the mountain were not felled, which means I uprooted the memory area on the bottom layer, and it made no difference whether everything on the building above was destroyed or not.

The relationship between Allocator memory allocator and vector container

Code example:

Vector.hpp

#include <iostream>  
#include <math.h>  
#include "MyAlloc.hpp"  
using namespace std;  
  
template <typename T, template<typename T> class ALLOC = MyAlloc>  
class Vector  
{  
private:  
    MyAlloc<T> MemoryManager;  
    T* Element;  
    int ElementCapacity;  
    int ElementSize;  
public:  
    Vector();  
    Vector(const Vector& obj);  
  
    int CalCapacity(double ArraySize);  
  
    int Size();  
    void Resize(int NewSize);  
    int Capacity();  
    void Reserve(int NewCapacity);  
    T& At(int Order);  
    T* Begin();  
    T* End();  
  
    void Push_back(const T& obj);  
    void Pop_back();  
  
    ~Vector();  
};  
  
template <typename T, template<typename T> class ALLOC /*= MyAlloc*/>  
void Vector<T, ALLOC>::Reserve(int NewCapacity)  
{  
    T* TempElement = nullptr;  
  
    if (NewCapacity > this->ElementCapacity)  
    {  
        TempElement = this->MemoryManager.Allocate(NewCapacity);  
        for (int i = 0; i < this->ElementSize; i++)  
        {  
            this->MemoryManager.Constructor(TempElement + i, *(this->Element + i));  
            this->MemoryManager.Destroy(this->Element + i);  
        }  
        this->MemoryManager.Deallocate(this->Element, this->ElementCapacity);  
        this->Element = TempElement;  
        this->ElementCapacity = NewCapacity;  
    }  
    else if (NewCapacity < this->ElementCapacity && NewCapacity > 0)  
    {  
        this->MemoryManager.Deallocate(this->Element + NewCapacity, this->ElementCapacity - NewCapacity);  
        this->ElementCapacity = NewCapacity;  
    }  
    else  
    {  
        throw out_of_range("NewCapacity is over valid range!");  
    }  
}  
  
template <typename T, template<typename T> class ALLOC /*= MyAlloc*/>  
void Vector<T, ALLOC>::Resize(int NewSize)  
{  
    T* TempElement = nullptr;  
  
    if (NewSize > this->ElementCapacity)  
    {  
        TempElement = MemoryManager.Allocate(CalCapacity(NewSize));  
        for (int i = 0; i < this->ElementSize; i++)  
        {  
            MemoryManager.Constructor(TempElement + i, *(this->Element + i));  
            this->MemoryManager.Destroy(this->Element + i);  
        }  
        this->MemoryManager.Deallocate(this->Element, this->ElementCapacity);  
  
        this->Element = TempElement;  
        this->ElementSize = NewSize;  
        this->ElementCapacity = CalCapacity(NewSize);  
    }  
    else if (NewSize > this->ElementSize && NewSize < this->ElementCapacity)  
    {  
        for (int i = this->ElementSize; i < NewSize; i++)  
        {  
            this->MemoryManager.Constructor(this->Element + i, T());  
        }  
        this->ElementSize = NewSize;  
    }  
    else if (NewSize < this->ElementSize)  
    {  
        for (int i = NewSize; i < this->ElementSize; i++)  
        {  
            this->MemoryManager.Destroy(this->Element + i);  
        }  
        this->ElementSize = NewSize;  
    }  
    else  
    {  
        throw out_of_range("NewSize is over valid range!");  
    }  
}  
  
template <typename T, template<typename T> class ALLOC /*= MyAlloc*/>  
T* Vector<T, ALLOC>::End()  
{  
    if (this->ElementSize == 0)  
    {  
        throw out_of_range("Empty!");  
    }  
    return this->Element + this->ElementSize;  
}  
  
template <typename T, template<typename T> class ALLOC /*= MyAlloc*/>  
T* Vector<T, ALLOC>::Begin()  
{  
    if (this->ElementSize == 0)  
    {  
        throw out_of_range("Empty!");  
    }  
    return this->Element;  
}  
  
template <typename T, template<typename T> class ALLOC /*= MyAlloc*/>  
void Vector<T, ALLOC>::Pop_back()  
{  
    if (this->ElementSize == 0)  
    {  
        throw out_of_range("Empty!");  
    }  
    MemoryManager.Destroy(this->Element + this->ElementSize - 1);  
    this->ElementSize--;  
}  
  
template <typename T, template<typename T> class ALLOC /*= MyAlloc*/>  
void Vector<T, ALLOC>::Push_back(const T& obj)  
{  
    T* TempElement = nullptr;  
  
    if (this->ElementCapacity == this->ElementSize)  
    {  
        TempElement = MemoryManager.Allocate(this->CalCapacity(this->ElementSize + 1));  
        for (int i = 0; i < this->ElementSize; i++)  
        {  
            MemoryManager.Constructor(TempElement + i, *(this->Element + i));  
            MemoryManager.Destroy(this->Element + i);  
        }  
        MemoryManager.Constructor(TempElement + this->ElementSize, obj);  
        MemoryManager.Deallocate(this->Element, this->ElementCapacity);  
  
        this->ElementSize++;  
        this->ElementCapacity = this->CalCapacity(this->ElementSize);  
        this->Element = TempElement;  
    }  
    else  
    {  
        MemoryManager.Constructor(this->Element + this->ElementSize, obj);  
        this->ElementSize++;  
    }  
}  
  
template <typename T, template<typename T> class ALLOC /*= MyAlloc*/>  
T& Vector<T, ALLOC>::At(int Order)  
{  
    if (this->ElementSize == 0)  
    {  
        throw invalid_argument("Order is an invalid argument!");  
    }  
    return *(this->Element + Order);  
}  
  
template <typename T, template<typename T> class ALLOC /*= MyAlloc*/>  
Vector<T, ALLOC>::~Vector()  
{  
    for (int i = 0; i < this->ElementSize; i++)  
    {  
        MemoryManager.Destroy(this->Element + i);  
    }  
    this->ElementSize = 0;  
    MemoryManager.Deallocate(this->Element, this->ElementCapacity);  
    this->ElementCapacity = 0;  
  
    this->Element = nullptr;  
}  
  
template <typename T, template<typename T> class ALLOC /*= MyAlloc*/>  
int Vector<T, ALLOC>::Capacity()  
{  
    return this->ElementCapacity;  
}  
  
template <typename T, template<typename T> class ALLOC /*= MyAlloc*/>  
int Vector<T, ALLOC>::Size()  
{  
    return this->ElementSize;  
}  
  
template <typename T, template<typename T> class ALLOC /*= MyAlloc*/>  
int Vector<T, ALLOC>::CalCapacity(double ArraySize)  
{  
    return (int)exp2(ceil(log2(ArraySize)));  
}  
  
template <typename T, template<typename T> class ALLOC /*= MyAlloc*/>  
Vector<T, ALLOC>::Vector(const Vector& obj)  
{  
    this->ElementCapacity = obj.Size();  
    this->ElementSize = obj.Size();  
  
    this->Element = MemoryManager.Allocate(this->ElementCapacity);  
    for (int i = 0; i < this->ElementSize; i++)  
    {  
        MemoryManager.Constructor(this->Element + i, obj.At(i));  
    }  
}  
  
template <typename T, template<typename T> class ALLOC /*= MyAlloc*/>  
Vector<T, ALLOC>::Vector()  
{  
    this->Element = nullptr;  
    this->ElementCapacity = 0;  
    this->ElementSize = 0;  
}  

 

MyAlloc.hpp

#include <iostream>  
using namespace std;  
  
template <class T>  
class MyAlloc  
{  
public:  
    T* Allocate(size_t Size);  
    void Deallocate(T* ptr, size_t Size);  
    void Destroy(T* ptr);  
    void Constructor(T* ptr, const T& val);  
};  
  
template <class T>  
void MyAlloc<T>::Constructor(T* ptr, const T& val)  
{  
    ptr = new(ptr) T(val);  
}  
  
template <class T>  
void MyAlloc<T>::Destroy(T* ptr)  
{  
    ptr->~T();  
}  
  
template <class T>  
void MyAlloc<T>::Deallocate(T* ptr, size_t Size)  
{  
    ::operator delete(ptr, sizeof(T)*Size);  
}  
  
template <class T>  
T* MyAlloc<T>::Allocate(size_t Size)  
{  
    char* ptr = new char[sizeof(T)*Size];  
    return (T*)ptr;  
}  

 

Main.cpp

#include "Vector.hpp"  
#include <iostream>  
using namespace std;  
  
int main()  
{  
    Vector<int> obj;  
    obj.Push_back(10);  
    cout << obj.At(0) << endl;  
    obj.Reserve(10);      
    obj.Resize(2);  
    cout << obj.Size() << endl;  
    cout << obj.Capacity() << endl;  
}  

 

Maybe the custom Vector container code is slightly longer and difficult to read, but the principle is very simple: it is nothing more than opening up a piece of memory, and then continuously adding data to the memory area, if the memory space is not enough, apply for a larger memory area to use .

Use new and delete for management

Ordinary new

Form: int* p = new int;

At this time, you cannot judge whether the memory is successfully opened by whether p is nullptr, but you need to catch the exception through bad_alloc.

#include <iostream>  
#include <exception>  
using namespace std;  
  
int main()  
{  
    int* ptr = nullptr;  
  
    try  
    {  
        ptr = new int[0x7fffffff];  
         delete ptr;
    }  
    catch (const bad_alloc& exp)  
    {  
        cout << exp.what() << endl;  
    }  
}  

 

(nothrow) new

Form: int *p = new (nothrow) int(20);

At this point, the pointer has degenerated into a pointer obtained by opening up memory through malloc in the C language, and it is possible to verify whether the memory has been successfully opened up by nullifying.

#include <iostream>  
#include <exception>  
using namespace std;  
  
int main()  
{  
    int* ptr = nullptr;  
  
    if ((ptr = new (nothrow) int[0x7fffffff]) == nullptr)  
    {  
        printf("内存申请失败!");  
        exit(-1);  
    }  
     delete ptr;
}  

 

We can also overload the operator new operator for a certain class type or structure type (using malloc and free as the bottom layer):

#include <iostream>  
#include <exception>  
#include <string>  
using namespace std;  
  
class Person  
{  
private:  
    string name;  
    int age;  
public:  
    Person()  
    {  
        this->name = "无";  
        this->age = 0;  
    }  
    Person(const string& name, const int& age)  
    {  
        this->name = name;  
        this->age = age;  
    }  
    Person(const Person& obj)  
    {  
        this->name = obj.name;  
        this->age = obj.age;  
    }  
    void* operator new (size_t Size)  
    {  
        Person* ptr = nullptr;  
  
        ptr = (Person*)malloc(sizeof(Person));  
  
        if (ptr == nullptr)  
        {  
            printf("Bad Allocate!");  
            exit(-1);  
        }  
        return (void*)ptr;  
    }  
  
    void operator delete(void* ptr)  
    {  
        if (ptr == nullptr)  
        {  
            return;  
        }  
        free(ptr);  
    }  
};  
  
int main()  
{  
    Person* ptr = new Person;  
    delete(ptr);  
}  

 

Apply for new pointer to constant memory

Form: const int* p = new const int(20);

#include <iostream>  
#include <exception>  
using namespace std;  
  
int main()  
{  
    int var = 10;  
    const int* ptr = new const int(var);  
    cout << *ptr << endl;  
    delete ptr;  
}  

 

Position new (placement new)

Form: int data = 0;

int *p = new (&data) int(20);

effect:

Placement new can be implemented in a specified memory (this memory can be allocated in any way) to construct an object (call the object's constructor).

#include <iostream>  
#include <exception>  
using namespace std;  
  
int main()  
{  
    char* ptr[20]; // 由于char类型所占空间是一个字节,因此我们可以用char类型数组来申请n个字节的存储空间  
    int *p = new (ptr)int(20);  
    cout << *p << endl;  
    cout << *(int*)ptr << endl;  
    cout << p << " " << ptr << endl; // 我们看见p指针和ptr指针指向的内存空间一致  
}  

 

Output result:

 

In the above program, we may ask "Why convert the ptr pointer to an int* type pointer and then dereference?" This is the case, the original data type of ptr is a char* character pointer, and ptr points to a Byte storage space, but we create an int type object on the memory area pointed to by ptr, so we must read a sizeof(int) byte storage space from ptr to read this block Data on the memory area.

note:

The contiguous memory space we apply for with the char* pointer of ptr is built on the stack area, so we don't need to call the deallocate function in the allocator memory allocator to destroy this memory area. If you want to build memory in the heap area, we can do the following:

#include <iostream>  
#include <memory>  
using namespace std;  
  
int main()  
{  
    void* ptr = nullptr;  
  
    allocator<int> ALLOC;  
    ptr = ALLOC.allocate(sizeof(int)); // 申请内存空间是以字节为单位进行申请的  
    ALLOC.construct((int*)ptr, int(10)); // 在内存区域上进行初始化  
    cout << *(int*)ptr << endl; // 使用这片内存区域上的数据  
    ALLOC.destroy((int*)ptr); // 销毁构建在内存区域上的数据  
    ALLOC.deallocate((int*)ptr, sizeof(int)); // 销毁这片内存区域  
}  

 

note:

I am applying for memory here using the void* data type, because when we apply for memory space, we don’t know what type of data is created in this memory area, and void* pointers can be forced to convert to pointers of any data type. Therefore it is called the "universal pointer in C language". But when we use void* type pointer ptr, we must pay attention to "In the above program, we have to create an int type data in the memory area for storage, so the void* type pointer ptr must be converted to int* type. It is equivalent to converting a void* pointer to a pointer of the corresponding data type for that type."

Implement a custom memory allocator allocator with new and delete

MyAlloc.hpp

#include <iostream>  
using namespace std;  
  
template <class T>  
class MyAlloc  
{  
public:  
    T* Allocate(size_t Size);  
    void Deallocate(T* ptr, size_t Size);  
    void Destroy(T* ptr);  
    void Constructor(T* ptr, const T& val);  
};  
  
template <class T>  
void MyAlloc<T>::Constructor(T* ptr, const T& val)  
{  
    ptr = new(ptr) T(val);  
}  
  
template <class T>  
void MyAlloc<T>::Destroy(T* ptr)  
{  
    ptr->~T();  
}  
  
template <class T>  
void MyAlloc<T>::Deallocate(T* ptr, size_t Size)  
{  
    ::operator delete(ptr, sizeof(T)*Size);  
}  
  
template <class T>  
T* MyAlloc<T>::Allocate(size_t Size)  
{  
    char* ptr = new char[sizeof(T)*Size];  
    return (T*)ptr;  
}  

 

Main.cpp

#include <iostream>  
#include "MyAlloc.hpp"  
using namespace std;  
  
int main()  
{  
    MyAlloc<int> ALLOC;  
    int* ptr = ALLOC.Allocate(sizeof(int)); // 申请内存区域  
    ALLOC.Constructor(ptr, int(10)); // 在内存区域上放置数据  
    cout << *ptr << endl; // 使用内存区域上的数据  
    ALLOC.Destroy(ptr); // 销毁内存区域上的数据  
    ALLOC.Deallocate(ptr, sizeof(int)); // 销毁整片内存区域  
}  

 

 

Guess you like

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