C++ 析构函数与内存池

Wesley13
• 阅读 989

C++ Primer 书中也提到编写 class 时要注意 copy control 成员(拷贝构造函数,赋值操作符,析构函数, C++11 又多个移动构造函数)。工作时在 C++ 和 C# 之间切换,有时就忘记了 C++ 的细节(真的好讨厌)。 C++ 析构函数与构造函数对应,构造对象时调用构造函数,析构对象时调用析构函数,于是可以在对象的析构函数中释放资源。 C++ class type 对象的生命期可以由程序完全控制,而 C# 引用类型对象是被托管的,万一处理到关键任务时 GC 造成卡顿一下,也是蛮郁闷的。这篇博客主要总结一下 C++ 析构函数,以下内容参考《 C++ Primer 4th 》 13.3 The Destructor 小节。

■ C++ 的析构函数在哪些情况下会执行呢?

  • The destructor is run only when a pointer to a dynamically allocated object is deleted or when an actual object (not a reference to the object) goes out of scope.
  • The destructor is not run when a reference or a pointer to an object goes out of scope.
  • Destructors are also run on the elements of class type in a container whether a library container or built-in array when the container is destroyed.

前两点没有什么好说的,对第三点提一下。当 STL 容器或者内部数组中存放的是 class type 对象元素,那么当容器或者数组销毁时,各元素的析构函数也会被执行(如果自己编写容器类,不要忘记这个功能点)。如下代码。

{
    Foo *p = new Foo[10];
    vector<Foo> vec(p, p + 10);
    delete p; // array is freed, destructor run on each element
    // vec goes out of scope, call destructor on each element
}

但是后面紧接着说,容器中的元素逆序被析构,也就说 size() - 1 这个元素最先被析构。我翻了一下 msys2 平台 g++ 6.2.0 版本中的 vector 源码,发现它的析构函数最终会调用下面的函数(文件 stl_construct.h ),反而是从 0 索引开始析构的。所以书上这句话看看就行了,别当真。我测试了一下,数组中的元素倒是按逆序被析构的。

template<bool>
struct _Destroy_aux
{
    template<typename _ForwardIterator>
    static void
    __destroy(_ForwardIterator __first, _ForwardIterator __last)
    {
      for (; __first != __last; ++__first)
        std::_Destroy(std::__addressof(*__first));
    }
};

■ 什么时候需要编写类的析构函数呢?

通常情况下并不需要为类编写析构函数,如果需要在对象析构时处理一些事情,比如释放资源,那么就需要编写析构函数。书中提到 Rule of Three 就是指,如果一个类需要析构函数,那么就还需要 copy control 其他成员(拷贝构造函数,赋值操作符, C++11 的移动构造函数)。
然后书中提到编译器总会合成一个析构函数。关于这个合成的析构函数有如下要点。

  • Unlike the copy constructor or assignment operator, the compiler always synthesizes a destructor for us. The synthesized destructor destroys each nonstatic member in the reverse order from that in which the object was created.
  • For each member that is of class type, the synthesized destructor invokes that member's destructor to destroy the object.
  • Destroying a member of built-in or compound type has no effect. In particular, the synthesized destructor does not delete the object pointed to by a pointer member.
  • An important difference between the destructor and the copy constructor or assignment operator is that even if we write our own destructor, the synthesized destructor is still run.

第二点是编译器合成的析构函数会自动执行 class type 成员的析构函数。第三点中的 built-in type 指 int float 等这种类型, compound type 指指针,引用这种类型,编译器合成的析构函数对这两种类型没有影响。由于编译器合成的析构函数负责成员变量的析构工作,对第四点就会觉得很理所当然,那就是虽然类中定义了析构函数,但是编译器合成的析构函数还是会执行。


■ 一个简单的内存池

经过上面的总结,我们知道 C++ 构造函数和析构函数完全与对象的生命周期同步。那么开发 C++ 内存池时,如何在已经分配好的内存空间上构造对象和析构对象呢。
一个内存池的基本逻辑有:内存池的空间管理、对象的构造、对象的回收、标识已分配对象的唯一 handle 。
下面举了一个简单的 MemObject ,只能构造一个对象,这样逻辑会很简单,让我们专注于对象的构造和回收。在 Alloc 时会在已经分配的内存上调用对象的习惯函数,在 Free 时会调用对象的析构函数,再设置 use_ 为 false ,使得这块内存再次被使用。

#include <new>
#include <stdio.h>
#include <stdlib.h>

template<typename T>
class MemObject {
public:
    MemObject() 
        : use_(false), handle_(0) {
        pObj_ = static_cast<T*>(malloc(sizeof(T)));
    }

    ~MemObject() {
        free(pObj_);
    }

    T* Alloc(unsigned int &hdl) {
        if (use_) {
            printf("object is using\n");
            return NULL;
        }
        hdl = ++handle_;
        use_ = true;
        new(pObj_) T();
        return pObj_;
    }

    void Free(unsigned int hdl) {
        if (!use_ || hdl != handle_) {
            printf("invalid free, use:%d hdl:%d\n", use_, handle_);
            return;
        }
        use_ = false;
        pObj_->~T();
    }

    T* Get(int hdl) {
        if (!use_ || hdl != handle_) {
            printf("invalid get, use:%d hdl:%d\n", use_, handle_);
            return NULL;
        }
        return pObj_;
    }

private:
    MemObject(const MemObject&) {}
    MemObject& operator=(const MemObject&) {}

    T *pObj_;
    bool use_;
    unsigned int handle_;
};

struct Foo {
    Foo() {
        printf("Foo ctor\n");
    }

    ~Foo() {
        printf("Foo ~ctor\n");
    }

    void Test() {
        printf("Test in Foo\n");
    }
};

int 
main() {
    MemObject<Foo> mo;
    Foo *ptr;
    unsigned int hdl1, hdl2;

    ptr = mo.Alloc(hdl1);
    ptr->Test();
    mo.Free(hdl1);

    printf("\n");
    mo.Free(hdl1);
    ptr = mo.Alloc(hdl2);
    ptr->Test();
    mo.Alloc(hdl1);
    mo.Free(hdl2);
    return 0;
}

编译 g++ -o t test.cpp 后,运行结果如下。

$ ./t.exe
Foo ctor
Test in Foo
Foo ~ctor

invalid free, use:0 hdl:1
Foo ctor
Test in Foo
object is using
Foo ~ctor
点赞
收藏
评论区
推荐文章
Wesley13 Wesley13
2年前
java 面试知识点笔记(六)垃圾回收 下篇
问:Object的finalize()方法的作用是否与C的析构函数作用相同?与C的析构函数不同,析构函数调用是确定的,而finalize是不确定的将未被引用的对象放置于FQueue队列(垃圾收集器确定一个对象死亡时需要至少两次标记过程。第一次是可达性分析,没有引用则会标记并且判断是否执行finalize方法,如果对象覆
Wesley13 Wesley13
2年前
C++中基类虚析构函数的作用及其原理分析
虚析构函数的理论前提是执行完子类的析构函数,那么父类的虚构函数必然会被执行。那么当用delete释放一个父类指针所实例化的子类对象时,如果没有定义虚析构函数,那么将只会调用父类的析构函数,而不会调用子类的虚构函数,导致内存的泄漏。故: 继承时,要养成的一个好习惯就是,基类析构函数中,加上virtual。知识背景     
Wesley13 Wesley13
2年前
C++基类的析构函数定义为虚函数的原因
1:每个析构函数只会清理自己的成员(成员函数前没有virtual)。2:可能是基类的指针指向派生类的对象,当析构一个指向派生类的成员的基类指针,这时程序不知道这么办,可能会造成内存的泄露,因此此时基类的析构函数要定义为虚函数;基类指针可以指向派生类的对象(多态),如果删除该指针delete\\p,就会调用该指针指向的派生类的析构函数,而派生类
Stella981 Stella981
2年前
Boost Python官方样例(三)
导出C类(纯虚函数和虚函数)大致做法就是为class写一个warp,通过get\_override方法检测虚函数是否被重载了,如果被重载了调用重载函数,否则调用自身实现,最后导出的时候直接导出warp类,但是类名使用class,析构函数不需要导出,因为它会被自动调用纯虚函数编写C函
Wesley13 Wesley13
2年前
C++中构造函数和析构函数
构造函数定义它是一种特殊的方法。主要用来在创建对象时初始化对象,即为对象成员变量赋初始值,总与new运算符一起使用在创建对象的语句中。另外,一个类可以有多个构造函数,我们可以根据其参数个数的不同或参数类型的不同来区分它们(这就是构造函数的重载)特点1.构造函数的命名必须和类名完全相同;2.构造函数的功能主要用于在类的对象创建时定义
Wesley13 Wesley13
2年前
C++类有继承时,析构函数必须为虚函数
C类有继承时,析构函数必须为虚函数。如果不是虚函数,则使用时可能存在内在泄漏的问题。假设我们有这样一种继承关系:!(https://oscimg.oschina.net/oscnet/5f0452c79b70794f2e4689cffa37f5a99f1.png)如果我们以这种方式创建对象:SubClasspObj
Wesley13 Wesley13
2年前
C++ 什么时候调用析构函数
析构函数是在对象消亡时,自动被调用,用来释放对象占用的空间。有四种方式会调用析构函数:1.生命周期:对象生命周期结束,会调用析构函数。2.delete:调用delete,会删除指针类对象。3.包含关系:对象Dog是对象Person的成员,Person的析构函数被调用时,对象Dog的析构函数也被调用。4.
Wesley13 Wesley13
2年前
C++进阶
///任何时候都不要在构造函数或析构函数中调用虚函数/classdog{public:stringm_name;
Easter79 Easter79
2年前
Thinking in C++ Notes 拷贝构造函数
1.默认拷贝构造函数不会调用构造函数,而通过位拷贝来实现。2.可以使用多个构造函数,并使用默认参数。3.小心使用默认构造函数,当属性中有指针时,会造成多次析构而产生错误Reference:http://blog.csdn.net/lwbeyond/article/details/6202256(https://www.oschina
Wesley13 Wesley13
2年前
VC++知识点整理
1.内联函数定义:定义在类体内的成员函数,即函数的函数体放在类体内特点:在调用处用内联函数体的代码来替换,用于解决程序的运行效率问题。一定要在调用之前定义,并且内联函数无法递归调用。2.构造函数与析构函数构造函数:用于为对象分配内存空间,对类的成员变量进行初始化,并执行其他内部管理操作。可以接受参