[New features of C++ memory management overloading new/delete operator] In-depth exploration of the principle and application of new/delete elision (new/delete elision) in C++14


1 Introduction

In C++, you can overload operator delete. Overloading operator deleteallows you to customize how objects are reclaimed when they are deleted. This is useful in situations where you need fine-grained control over memory management, such as when implementing a custom memory allocator or when dealing with special hardware resources.

The problem, however, is that operator deleteis not a virtual function, nor is it stored in the vtable. This means that when you delete a base class pointer pointing to a derived class object, the compiler will default to calling the base class's operator deleteinstead of the derived class's operator delete. This can lead to incorrect memory reclamation behavior.

To solve this problem, C++ introduces the concept of "deleting destructor". When you delete a pointer, the compiler actually calls a special destructor called the "deleting destructor". This destructor, in addition to performing all the operations of a full object destructor, also calls the appropriate operator deleteto reclaim memory. In this way, even if operator deleteis not a virtual function, the compiler can correctly call the derived class operator delete.

This is the rationale for "new/delete elision" or "deleting destructor". This is a fairly in-depth topic and requires a solid understanding of C++'s memory management and polymorphism.

1.1 Custom operator delete

In most cases, C++ programmers do not need to customize operator delete. The default operator deleteis usually good enough that it frees operator newmemory allocated via .

However, in some cases customization operator deletemay be useful. Here are some possible causes:

  1. Performance optimization : If your program runs under certain memory patterns (for example, frequently allocating and freeing small blocks of memory), then a custom memory management strategy may be more efficient operator newthan the default one. operator deleteFor example, you can implement a custom memory pool, which can reduce memory fragmentation and improve the speed of memory allocation and deallocation.

  2. Debugging and Error Checkingoperator delete : You can add additional error checking and debugging information in custom . For example, you can check if you are trying to free unallocated memory, or if you have a memory leak.

  3. Special memory requirements : If your program has special memory requirements, for example, it needs to store data at a specific physical memory address, or needs to use a specific memory alignment, then you may need to customize the operator newsum operator delete.

Note that customizations operator deleteneed to be handled with care, as incorrect memory management can lead to various problems such as memory leaks, memory fragmentation, or more difficult to debug problems. Before customizing operator delete, you should make sure you understand C++'s memory management model, and really need to customize memory management.

1.2 Overview of new features of C++14

C++14 is a version of the C++ standard with many improvements and extensions over C++11. C++14 introduces some new language features, such as generic lambda expressions, return type deduction, compile-time integer sequences, etc. These features make C++ programming more flexible and powerful.

However, some new features in C++14 are not so obvious, but very important. One of them is the topic we are going to discuss today - new/delete elision.

2. Memory management and polymorphism in C++

Before in-depth understanding of new/delete elision (new/delete elision), we first need to understand the basic concepts of memory management and polymorphism in C++.

2.1 Basic concepts of memory management

In C++, memory management is a core topic that deals with how memory is allocated, used, and freed. The memory in C++ is mainly divided into three types: heap memory (Heap), stack memory (Stack) and static memory (Static).

2.1.1 Heap memory

Heap memory is memory dynamically allocated when the program is running. Its size is not determined at compile time, but allocated as needed at runtime. In C++, we use newoperators to allocate memory on the heap and deleteoperators to free memory.

2.1.2 Stack memory

Stack memory is used to store local variables and parameters for function calls. When the function is called, a new stack frame (Stack Frame) will be created and pushed onto the stack, and when the function returns, the stack frame will be popped. The allocation and release of stack memory is very fast, but the size is limited.

2.1.3 Static memory

Static memory is used to store global and static variables. These variables exist throughout the life of the program, they are created at the beginning of the program and destroyed at the end of the program.

The following figure shows the basic concepts of these three types of memory:

Basic concepts of memory management

2.2 Basic concepts of polymorphism

Polymorphism is an important feature of object-oriented programming, which allows us to manipulate objects of derived classes through pointers or references of base classes. There are two main forms of polymorphism in C++: static polymorphism (Static Polymorphism) and dynamic polymorphism (Dynamic Polymorphism).

2.2.1 Static polymorphism

Static polymorphism is implemented at compile time, mainly through templates and function overloading. The advantage of static polymorphism is high efficiency, because all decisions are made at compile time, there is no runtime overhead. However, the disadvantage of static polymorphism is that it cannot handle situations that cannot be determined at runtime.

2.2.2 Dynamic polymorphism

Dynamic polymorphism is implemented at runtime, mainly through virtual functions and inheritance. The advantage of dynamic polymorphism is that it can handle situations that can only be determined at runtime, providing greater flexibility. However, the disadvantage of dynamic polymorphism is that it has a certain runtime overhead, because it needs to look up the virtual function table at runtime to determine the function to call.

In the next chapters, we'll dig into the concept of new/delete elision and how it works on top of C++'s memory management and polymorphism.

3. The concept of new/delete elision

3.1 Definitions and concepts

In C++, when we delete a pointer, two things happen:

  1. Calls the destructor of the object pointed to by the pointer.
  2. Call operator delete to reclaim heap memory.

For the first part, if the pointer's static type has a virtual destructor, then the compiler looks in the object's virtual function table for the actual destructor to call. If the dynamic type of the pointer is a derived class, then the destructor found will be that of the derived class, which is correct.

However, for operator delete, the situation is different. operator delete is not a virtual function, nor is it stored in the virtual function table. In fact, operator delete is a static member. So, how does the compiler know which operator delete to call?

这就是"deleting destructor"(删除析构函数)的作用。当我们删除一个指针时,编译器实际上会调用一个特殊的析构函数,称为"deleting destructor"。这个析构函数除了执行完整对象析构函数的所有操作外,还会调用适当的删除函数来回收内存。这样,即使operator delete不是虚函数,编译器也能正确地调用派生类的operator delete。

3.2 与C++11的对比

在C++11中,我们通常需要显式地调用正确的operator delete。
在C++14之前,如果你通过一个基类指针删除一个派生类对象,那么会调用基类的operator delete,而不是派生类的。这可能会导致问题,因为派生类可能有自己的内存管理策略,例如,可能使用了自定义的内存池。

C++14引入了"deleting destructor",这是一个特殊的析构函数,它不仅会调用对象的析构函数,还会调用正确的operator delete。这样,即使你通过基类指针删除派生类对象,也会调用派生类的operator delete,而不是基类的。

这就是C++14中新的/删除的省略(new/delete elision)的主要改进。这个特性使得C++的内存管理更加灵活和强大,特别是在处理多态删除时。

在C++14中,通过使用"deleting destructor",编译器可以自动地为我们做这件事。

下面是一个代码示例,展示了如何在C++14中使用"deleting destructor":

class Base {
    
    
public:
    virtual ~Base() {
    
     }
    void operator delete(void* p) {
    
     /* custom delete for Base */ }
};

class Derived : public Base {
    
    
public:
    ~Derived() override {
    
     }
    void operator delete(void* p) {
    
     /* custom delete for Derived */ }
};

int main() {
    
    
    Base* p = new Derived;
    delete p;  // Calls Derived::operator delete
}

在这个示例中,当我们删除一个Base类型的指针p时,编译器实际上会调用Derived类的operator delete,而不是Base类的operator delete。这是因为编译器调用了"deleting destructor",而不是直接调用operator delete。

下面的图表展示了"deleting destructor"的工作流程:

deleting destructor workflow

这是一个相当深入的主题,需要对C++的内存管理和多态性有深入的理解。在接下来的章节中,我们将深入探讨这个主题,包括"deleting destructor"的内部工作机制,以及如何在实际代码中使用它。

4. 新的/删除的省略(new/delete elision)的原理

在这一章节中,我们将深入探讨新的/删除的省略(new/delete elision)的原理,特别是"删除析构函数"(“deleting destructor”)的工作机制。

4.1 编译器如何处理新的/删除的省略

在C++中,当我们删除一个指针时,会发生两件事:

  1. 调用指针所指对象的析构函数(Destructor)。
  2. 调用operator delete来回收堆内存。

对于第一部分,如果指针的静态类型有虚析构函数(Virtual Destructor),那么编译器会在对象的虚函数表(Virtual Function Table)中查找实际要调用的析构函数。如果指针的动态类型是派生类,那么找到的析构函数将是派生类的析构函数,这是正确的。

然而,对于operator delete,情况就不同了。operator delete并不是虚函数,也不存储在虚函数表中。实际上,operator delete是一个静态成员。那么,编译器如何知道调用哪个operator delete呢?

这就是"删除析构函数"(“deleting destructor”)的作用。当我们删除一个指针时,编译器实际上会调用一个特殊的析构函数,称为"删除析构函数"。这个析构函数除了执行完整对象析构函数的所有操作外,还会调用适当的删除函数来回收内存。这样,即使operator delete不是虚函数,编译器也能正确地调用派生类的operator delete。

下面的图表展示了这个过程:

Deleting Destructor Process

4.2 "删除析构函数"的内部工作机制

在这一部分,我们将通过一个综合的代码示例来展示这个过程。

class Base {
    
    
public:
    virtual ~Base() {
    
     std::cout << "Base Destructor\n"; }
    void operator delete(void* ptr) {
    
     std::cout << "Base operator delete\n"; ::operator delete(ptr); }
};

class Derived : public Base {
    
    
public:
    ~Derived() override {
    
     std::cout << "Derived Destructor\n"; }
    void operator delete(void* ptr) {
    
     std::cout << "Derived operator delete\n"; ::operator delete(ptr); }
};

int main() {
    
    
    Base* b = new Derived();
    delete b;
    return 0;
}

在这个示例中,我们有一个基类Base和一个派生类Derived。每个类都有自己的析构函数和重载的operator delete。在main函数中,我们创建了一个Derived对象,但是我们通过一个Base指针来删除它。

当我们运行这个程序时,输出将是:

Derived Destructor
Base Destructor
Derived operator delete

这就是"删除析构函数"的工作原理。即使我们通过基类指针删除对象,编译器也能正确地调用派生类的析构函数和operator delete。这是因为编译器生成了一个"删除析构函数",这个析构函数知道要调用哪个operator delete

这个示例展示了新的/删除的省略(new/delete elision)的原理,以及"删除析构函数"如何使得编译器能正确地处理多态性。

5. 新的/删除的省略(new/delete elision)的应用

在本章节中,我们将深入探讨新的/删除的省略(new/delete elision)在实际编程中的应用。我们将通过一个综合的代码示例来展示如何在代码中使用新的/删除的省略,以及在这个过程中需要注意的关键点。

5.1 实例分析:如何在代码中使用新的/删除的省略

让我们通过一个具体的例子来看看新的/删除的省略(new/delete elision)是如何工作的。在这个例子中,我们将创建一个基类和一个派生类,每个类都有自己的析构函数和删除函数。

class Base {
    
    
public:
    virtual ~Base() {
    
     std::cout << "Base Destructor\n"; }
    static void operator delete(void* ptr) {
    
    
        std::cout << "Base operator delete\n";
        ::operator delete(ptr);
    }
};

class Derived : public Base {
    
    
public:
    ~Derived() override {
    
     std::cout << "Derived Destructor\n"; }
    static void operator delete(void* ptr) {
    
    
        std::cout << "Derived operator delete\n";
        ::operator delete(ptr);
    }
};

在这个例子中,我们可以看到,当我们删除一个指向派生类对象的基类指针时,会发生什么。

Base* b = new Derived;
delete b;

输出将是:

Derived Destructor
Base operator delete

这是因为,虽然析构函数是虚函数,可以正确地调用派生类的析构函数,但是删除函数operator delete并不是虚函数,它是一个静态成员。所以,当我们删除一个指向派生类对象的基类指针时,会调用基类的删除函数,而不是派生类的删除函数。

这就是新的/删除的省略(new/delete elision)的重要性。在C++14中,编译器会生成一个特殊的析构函数,称为"deleting destructor"。这个析构函数除了执行完整对象析构函数的所有操作外,还会调用适当的删除函数来回收内存。这样,即使operator delete不是虚函数,编译器也能正确地调用派生类的operator delete

下面的图示展示了这个过程:

Deleting destructor process

5.2 常见的使用场景和模式

新的/删除的省略(new/delete elision)主要用于处理多态性,特别是当我们需要在基类中删除派生类对象时。这在以下几种场景中非常常见:

  1. 当我们使用工厂模式创建对象时,工厂函数通常返回一个基类指针,指向一个在堆上创建的派生类对象。当我们完成使用这个对象后,我们需要删除它。在这种情况下,新的/删除的省略(new/delete elision)可以确保我们正确地调用了派生类的删除函数。

  2. 当我们使用容器存储基类指针时,这些指针实际上可能指向派生类对象。当我们清空容器时,我们需要删除这些对象。在这种情况下,新的/删除的省略(new/delete elision)可以确保我们正确地调用了派生类的删除函数。

在这两种场景中,新的/删除的省略(new/delete elision)都可以帮助我们正确地管理内存,避免内存泄漏和未定义的行为。

结语

Comprehension is an important step towards the next level in our programming learning journey. However, mastering new skills and ideas always takes time and persistence. From a psychological point of view, learning is often accompanied by continuous trial and error and adjustment, which is like our brain gradually optimizing its "algorithm" for solving problems.

That's why when we encounter mistakes, we should see them as opportunities to learn and improve, not just obsessions. By understanding and solving these problems, we can not only fix the current code, but also improve our programming ability and prevent the same mistakes from being made in future projects.

I encourage everyone to actively participate and continuously improve their programming skills. Whether you are a beginner or an experienced developer, I hope my blog can help you in your learning journey. If you find this article useful, please click to bookmark it, or leave your comments to share your insights and experiences. You are also welcome to make suggestions and questions about the content of my blog. Every like, comment, share and follow is the greatest support for me and the motivation for me to continue to share and create.


Read my CSDN homepage to unlock more exciting content: Bubble's CSDN homepage
insert image description here

Guess you like

Origin blog.csdn.net/qq_21438461/article/details/131636445