【问题标题】:What really happens and who is responsible when a call to delete[] X command?当调用 delete[] X 命令时,真正发生了什么以及谁负责?
【发布时间】:2016-05-02 15:37:28
【问题描述】:

我试图找出在应用程序或进程运行并专门运行命令delete[] X 时实际执行这些操作的组件或模块(可能属于操作系统?)。

我的问题是在我阅读了delete[] X 之后提出的,我知道编译器有责任(根据其实现)知道要删除多少X 的对象。但是,编译器在运行时不是“活动的”!我的意思是,在编译时,编译器不知道用户在新命令中需要多少内存,所以在删除时也不知道,那么程序实际运行时在运行时实际发生了什么?

我读到的答案之一是所谓的运行时系统,它是什么?它是否连接到 CPU - 因为 CPU 最终会执行命令......或者可能是操作系统?

我看到的另一个答案是“由系统的分配器完成”(How does delete[] know how much memory to delete?) - 这个组件(操作系统、CPU)又在哪里?

【问题讨论】:

    标签: c++ compiler-construction


    【解决方案1】:

    编译器负责在需要时生成deletes 的代码。它发生时不需要运行。生成的代码可能是对执行以下操作的例程的函数调用:

    void delete_arr(object *ptr)
    {
        size_t *actual_start = ((size_t *)ptr) - 1;
        int count = *actual_start;
    
        for (int i = count-1; i >= 0; i--)
            destruct(ptr[i]);
    
        free(actual_start);
    }
    

    new[]被调用时,它实际上保存了分配内存旁边的元素数量。当您调用 delete[] 时,它会查找数量,然后删除该数量的元素。

    提供这些功能的库称为 C++ 标准库或 C++ 运行时环境。该标准没有说明什么构成运行时,因此定义可能会有所不同,但要点是它是支持运行 C++ 代码所需要的。

    【讨论】:

      【解决方案2】:

      C++ 运行时(间接)使用Operating System 原语来更改运行程序的processvirtual address space

      阅读更多关于computer architectureCPU modesoperating systemsOS kernelssystem callsinstruction setsmachine codeobject codelinkersname manglingcompilers, virtual memory.

      在我的Linux系统上,new(由C++标准库提供)一般构建在malloc(3)(由C标准库提供)之上,可以调用mmap(2)系统调用(在内核内部实现)更改虚拟地址空间(通过处理MMU)。而delete(来自 C++ 标准库)通常构建在 free(3) 之上,可能调用 munmap(2) 系统调用来更改虚拟地址空间。

      事情在细节上要复杂得多:

      • new 在使用malloc 分配内存后调用构造函数

      • delete 在使用free 释放内存之前调用析构函数

      • free 通常将已释放的内存区域标记为可供未来malloc 重用(因此通常不要使用munmap 释放内存

      • 所以malloc 通常会在向内核请求更多地址空间(使用mmap)之前重用之前释放的内存区域

      • 对于数组new[]delete[],内存区包含数组的大小,循环调用构造函数(new[])或析构函数(delete[]

      • 从技术上讲,当您编写SomeClass*p = new SomeClass(12); 时,首先使用::operator new(调用malloc)分配内存,然后使用12 作为参数调用SomeClass 的构造函数

      • 在编写delete p;时,会调用SomeClass的析构函数,然后使用::operator delete(调用free)释放内存

      顺便说一句,Linux 系统是由free software 组成的,所以我强烈建议你在你的机器上安装一些 Linux 发行版并使用它。因此,您可以研究libstdc++(标准C++ 库,它是GCC 编译器源代码的一部分,但由您的程序链接)的源代码libc(标准C库),内核。你也可以strace(1)你的C++程序和进程来了解system calls它在做什么。

      如果使用GCC,您可以通过使用g++ -Wall -O -fverbose-asm -S foo.cc 编译foo.cc C++ 源文件来获得生成的汇编程序代码,g++ -Wall -O -fverbose-asm -S foo.cc 会生成foo.s 汇编程序文件。您还可以使用g++ -Wall -O -fdump-tree-gimple -c foo.cc 在编译器中获得中间Gimple internal representation 的一些文本视图(您将获得一些foo.cc.*.gimple 以及可能许多其他GCC 转储文件)。您甚至可以使用 GCC MELT 工具在 Gimple 表示中搜索某些内容(我设计并实现了其中的大部分;useg++ -fplugin=melt -fplugin-arg-melt-mode=findgimple)。

      标准 C++ 库具有内部不变量和约定,C++ 编译器负责在发出汇编代码时遵循它们。所以编译器和它的标准 C++ 库是在密切合作下共同设计和编写的(你的 C++ 库实现中的一些肮脏的技巧需要编译器支持,也许通过编译器内置函数等......)。这并不特定于 C++:Ocaml 人员还共同设计和共同实现 Ocaml 语言及其标准库。

      C++ 运行时系统在概念上具有几层:C++ 标准库libstdc++、C 标准库libc、操作系统(以及最底层的硬件,包括 MMU)。所有这些都是实现细节,C++11 语言标准并没有真正提及它们。

      【讨论】:

      • 我认为您的回答将我引向了我想了解的内容。如果我没看错的话,有几层代码,包括使用 mmap func 的内核代码(Linux、Windows 等),所有这些“代码”实际上都被编译为汇编“代码”,因此 CPU 可以运行它。至于删除 - 编译后代码已经在汇编中。现在,当进程(代码)加载到内存时,CPU 实际运行它。 现在,在这个已经加载并运行的进程阶段 - 谁负责处理内存?
      • 你看起来很困惑,你应该花几天时间阅读几本关于计算机体系结构、指令集、操作系统设计、编译器的书。我们无法在几段中解释所有关于 SO 的内容。
      • 好的,你能给我一些起点、好书名、链接或某种特定的词来搜索起点吗?
      • 进入一些大学图书馆,或查看一些描述大学课程的网站。我不明白你不知道什么。您可以自行寻找其他材料。我还建议在您的笔记本电脑上安装 Linux。学习是一段漫长的旅程。你需要ten years。玩得开心,祝你好运。 GCC 是免费软件,所以好好研究一下
      • 我看到你添加了一些链接和引导词。非常感谢!我会仔细研究它们并尝试更深入地学习。谢谢!
      【解决方案3】:

      可能是helpfull

      • 每次调用全局 ::operator new() 都会获取传递的对象大小并添加额外数据的大小
      • 它将分配上一步推导出的大小的内存块
      • 它会将指针偏移到块中未被额外数据占用的部分,并将该偏移值返回给调用者

      ::operator delete() 将反向执行相同的操作-移动指针, 访问额外的数据,释放内存。

      通常delete[]在删除分配到堆中的对象数组时使用。据我所知, new [] 还在分配内存的开头添加了额外的数据,其中存储了有关 delete [] 运算符的数组大小的信息。也可以是useful:

      换句话说,一般情况下,由 new[] 分配的内存块在实际数据前面有两组额外的字节:以字节为单位的块大小(由 malloc 引入)和元素计数(由 new[] 引入])。如您的示例所示,第二个是可选的。第一个通常始终存在,因为它是由 malloc 无条件分配的。 IE。即使您只请求 20 个字节,您的 malloc 调用也会物理分配超过 20 个字节。这些额外的字节将被 malloc 用来存储块大小(以字节为单位)。 ...

      new[] 从 operator new[] 请求的“额外字节”并不用于“存储已分配内存的大小”,正如您所相信的那样。它们用于存储数组中元素的数量,以便 delete[] 知道要调用多少个析构函数。在您的示例中,析构函数是微不足道的。没有必要打电话给他们。因此,无需分配这些额外的字节并存储元素计数。

      【讨论】:

        【解决方案4】:

        它分两个阶段工作。一种是编译器做的事情看起来很神奇,然后堆做的事情也看起来很神奇。

        也就是说,直到你意识到诀窍。然后魔法就消失了。

        但是先让我们回顾一下当您执行new X[12] 时会发生什么;

        编译器在后台编写的代码在概念上如下所示:

        void* data = malloc(12 * sizeof(X))
        for (int i=0; i != 12; ++i) {  
          X::Ctor(data);
          data += sizeof(X); 
        }
        

        其中Ctor(void* this_ptr) 是一个秘密函数,它设置this 指针调用X 的构造函数。在这种情况下是默认的。

        所以在销毁时,我们可以撤消这个,只要我们能把这 12 个藏在容易找到的地方......

        我猜你已经猜到了……

        任何地方!真的!例如,它可以存储在对象开始之前。

        第一行变成了这 3 行:

        void* data = malloc((12 * sizeof(X)) +sizeof(int));
        *((int*)data) = 12;
        data += sizeof(int);
        

        其余的保持不变。

        当编译器看到delete [] addr 它知道addr 之前的4 个字节它可以找到对象计数。还需要调用free(addr - sizeof(int))

        顺便说一句,这在本质上与mallocfree 使用的技巧相同。至少在过去我们有简单的分配器的时候。

        【讨论】:

          【解决方案5】:

          当您使用new 关键字时,程序会从堆上的操作系统请求一块内存来保存对象。返回指向该内存空间的指针。如果不使用new,编译器会将对象放在堆栈上,并且在编译期间,这些对象的内存会在堆栈上对齐。使用new 创建的任何对象在不再需要时都需要删除,因此指向堆块的原始指针不会丢失非常重要,因此您可以对其调用 delete。当您使用delete[] 时,它将释放数组中的所有块。例如,如果您创建了char* anarray = new char[128],则使用 delete[];如果您创建了string *str = new string(),您将使用 delete,因为字符串被称为对象,而 char* 是指向数组的指针。

          编辑:一些对象重载了删除操作符,因此您的对象可以支持正确释放动态内存,因此可以让一个对象负责确定它的行为

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2021-05-13
            • 2010-09-30
            相关资源
            最近更新 更多