问题

C++的new操作符,底层使用的是malloc吗,在析构时,是如何确定自己需要释放内存的大小的?

回答
C++ `new` 操作符与 `malloc`:底层联系与内存管理奥秘

在C++中,`new` 操作符是用于动态分配内存和调用构造函数的关键机制。许多开发者会好奇 `new` 操作符的底层实现,以及它与C语言中的 `malloc` 函数之间的关系。同时,在对象生命周期结束时,`delete` 操作符是如何准确地释放内存的,这也是一个值得深入探讨的问题。

`new` 操作符的底层:与 `malloc` 的关系

C++ 的 `new` 操作符在底层,绝大多数情况下,确实是调用了C语言的 `malloc` 函数来实际分配内存。

这里的关键在于理解 C++ 的抽象层次。C++ 标准并没有强制规定 `new` 操作符必须使用 `malloc`,但这是最常见和标准化的实现方式。`new` 操作符除了内存分配,还包含了调用对象的构造函数的职责。

我们可以将 `new` 的过程分解为两个主要步骤:

1. 内存分配 (Memory Allocation):
这是 `new` 操作符的核心功能之一。它需要一块足够大的内存来存储将要创建的对象。
在大多数标准库实现中,这个内存分配部分就是通过调用 `malloc` 来完成的。`malloc` 是C标准库提供的函数,负责从堆(Heap)中分配指定字节数的内存块,并返回一个指向该内存块起始位置的指针。
与之相对应的,`delete` 操作符在释放内存时,则会调用 `free` 函数,它也是C标准库提供的,用于释放 `malloc` 分配的内存。

2. 对象构造 (Object Construction):
在内存分配成功之后,`new` 操作符会紧接着调用目标对象的构造函数。构造函数负责对象的初始化,设置对象的成员变量,执行任何必要的设置逻辑。
这个构造过程是C++特有的,而 `malloc` 本身只负责内存分配,不涉及对象的初始化。

举个例子:

```c++
class MyClass {
public:
MyClass() {
std::cout << "MyClass constructor called" << std::endl;
}
~MyClass() {
std::cout << "MyClass destructor called" << std::endl;
}
private:
int data;
};

int main() {
MyClass obj = new MyClass; // new 操作符在这里执行
// ... 使用 obj ...
delete obj; // delete 操作符在这里执行
return 0;
}
```

当我们执行 `new MyClass` 时,背后会发生类似以下的过程:

1. `new MyClass` 首先会向 `malloc` 请求分配一块足够存储 `MyClass` 对象内存的大小。`sizeof(MyClass)` 会给出这个大小。
2. `malloc` 在堆上找到一块合适的内存,并返回一个 `void` 指针。
3. C++ 运行时将 `void` 指针强制转换为 `MyClass` 类型。
4. 然后,在分配好的内存地址上,调用 `MyClass` 的构造函数。
5. 最终,`new` 返回一个指向已初始化 `MyClass` 对象的指针。

当执行 `delete obj` 时,过程则相反:

1. `delete obj` 首先会调用 `obj` 所指向的 `MyClass` 对象的析构函数 (`~MyClass()`)。
2. 析构函数执行完毕后,`delete` 会将内存释放回堆,这个过程通常是通过调用 `free` 函数完成的。

为什么要分开处理?

将内存分配和对象构造/析构分离,提供了极大的灵活性:

自定义内存分配: C++ 标准允许开发者重载全局的 `operator new` 和 `operator delete` 函数。这意味着你可以用自己的内存管理策略替换默认的 `malloc`/`free` 实现(例如,使用内存池、arena allocation 等),而无需改变 `new` 和 `delete` 的使用方式。
分离关注点: `malloc` 关注的是内存的原始分配,而构造函数关注的是对象的逻辑初始化。这种分离使得编译器和运行时库可以更有效地处理对象生命周期。

析构时如何确定需要释放内存的大小?

这是一个非常关键的问题,答案在于 `delete` 操作符的工作方式以及运行时库的支持。

核心在于:C++ 的 `delete` 操作符在执行时,并不会“自己计算”需要释放的内存大小。它依赖于 `new` 操作符在分配内存时所记录的信息。

让我们深入了解:

1. `new` 分配内存时记录的信息:
当 `new` 分配内存时(通常通过 `malloc`),它不仅仅是分配了对象本身所需的内存。
在许多实现中,为了支持正确释放,`new` 操作符会在 分配的内存块的起始地址之前(或之后,具体取决于实现)额外分配一小块“头部信息”。
这块头部信息通常会存储 分配的内存块的总大小。这个大小包括了对象本身占用的空间,以及可能存在的任何额外的对齐填充(padding)所需的空间,以及头部信息本身占用的空间。
换句话说,`new` 并不是直接返回 `malloc` 返回的原始指针给用户,而是返回一个 经过调整的指针,这个指针指向了对象实际的开始位置,而头部信息则位于用户不可见的地方。

一个简化的示意图:

```
┌─────────────────┐
│ Header (size info) │
├─────────────────┤
│ Object │ < User sees this address
├─────────────────┤
│ (Padding if any) │
└─────────────────┘
```

2. `delete` 操作符的工作流程:
当 `delete` 被调用,并传入一个指向对象的指针时(例如 `delete objPtr;`):
首先,它会找到该对象的析构函数并调用。 这是C++的保证。
然后,`delete` 操作符需要知道这个对象占用了多少内存空间,以便能够正确地将其归还给堆管理器。
它会通过对象的指针反向查找,找到之前由 `new` 分配的整个内存块。 通常,这是通过减去一个固定的偏移量(等于头部信息的大小)来完成的,从而获取到原始分配块的起始地址。
从这个原始内存块的起始地址,`delete` 操作符会读取之前存储在头部信息中的内存大小。
最后,`delete` 操作符调用 `free` 函数,并传入原始内存块的起始地址和之前读取到的内存大小,来释放整个内存块。

为什么不能仅仅依靠对象的大小?

你可能会想,为什么不能直接知道 `MyClass` 的大小,然后 `delete` 时 `free(objPtr, sizeof(MyClass))` 呢?

动态数组: 对于动态数组,例如 `int arr = new int[10];`。`delete[] arr;` 需要知道分配的总大小才能正确释放。如果只知道单个 `int` 的大小,就无法释放整个数组。
内存分配的实际大小: `new` 分配的内存可能比 `sizeof(Object)` 大。例如,为了满足内存对齐要求,堆管理器可能会分配比对象实际需要稍大的内存块。或者,如果 `new` 调用了自定义的分配器,分配的大小可能完全不同。
多态性(虚函数): 在涉及多态的情况下(即基类指针指向派生类对象),`delete` 操作符需要知道对象的实际类型来调用正确的析构函数。虽然这与内存大小的确定机制是独立的,但都依赖于运行时信息。

总结来说,确定释放内存大小的关键在于:

`new` 操作符在分配内存时,会“附加”记录内存块的实际大小信息。
`delete` 操作符在释放内存时,会通过对象的指针“回溯”找到这些信息,然后使用它们来调用底层的内存释放函数(如 `free`)。

这种机制使得 C++ 的动态内存管理既高效又安全,即使面对复杂的数据结构和动态数组,也能准确地处理内存的分配和释放。

网友意见

user avatar

不考虑重载new delete的特殊情况,只考虑一般情况。

new的底层的确是malloc,但是不是最底层,malloc下面还有底层,那就是系统API层。比如在Windows上就是heapalloc,同样 free对应heap free。堆管理器负责记录下每个分配的内存块的信息,这个信息和内存地址是关联的,你只要给出地址,它就能知道内存块的实际大小。

类似的话题

  • 回答
    C++ `new` 操作符与 `malloc`:底层联系与内存管理奥秘在C++中,`new` 操作符是用于动态分配内存和调用构造函数的关键机制。许多开发者会好奇 `new` 操作符的底层实现,以及它与C语言中的 `malloc` 函数之间的关系。同时,在对象生命周期结束时,`delete` 操作符是.............
  • 回答
    在 C++ 中,`new` 和定位 `new` 操作符的返回值都是 `void`,这确实是它们能够被赋值给不同类型指针的关键。要理解这一点,我们需要深入 C++ 的内存管理和类型转换机制。 `new` 操作符:动态内存分配的通用接口首先,我们来看看 `new` 操作符。它的核心作用是在堆(heap).............
  • 回答
    在 C++ 中,当你在构造函数内 `new` 对象时,有几个重要的点需要考虑,以确保代码的健壮性和效率。这不仅仅是简单地分配内存,更关系到对象的生命周期管理、异常安全以及潜在的资源泄漏。核心问题:谁来管理这个 `new` 出来的对象的生命周期?这是你在构造函数内 `new` 对象时最先应该思考的问题.............
  • 回答
    在 C++ 中,当你有一个指针,然后让这个指针指向了新的内存地址,而它原来指向的内存地址是通过 `new` 分配出来的,那么原来被指向的那个对象的内存并不会“立刻”被释放。C++ 的内存管理机制需要你主动去处理。让我为你细致地讲讲这个过程,尽量去除那些生硬的、像 AI 才会用的表述。想象一下,你有一.............
  • 回答
    C++ 模板:功能强大的工具还是荒谬拙劣的小伎俩?C++ 模板无疑是 C++ 语言中最具争议但也最引人注目的一项特性。它既能被誉为“代码生成器”、“通用编程”的基石,又可能被指责为“编译时地狱”、“难以理解”的“魔法”。究竟 C++ 模板是功能强大的工具,还是荒谬拙劣的小伎俩?这需要我们深入剖析它的.............
  • 回答
    C++ 是一门强大而灵活的编程语言,它继承了 C 语言的高效和底层控制能力,同时引入了面向对象、泛型编程等高级特性,使其在各种领域都得到了广泛应用。下面我将尽可能详细地阐述 C++ 的主要优势: C++ 的核心优势:1. 高性能和底层控制能力 (Performance and LowLevel C.............
  • 回答
    C++ 的核心以及“精通”的程度,这是一个非常值得深入探讨的话题。让我尽量详细地为您解答。 C++ 的核心究竟是什么?C++ 的核心是一个多层次的概念,可以从不同的角度来理解。我将尝试从以下几个方面来阐述:1. 语言设计的哲学与目标: C 的超集与面向对象扩展: C++ 最初的目标是成为 C 语.............
  • 回答
    C++ 和 Java 都是非常流行且强大的编程语言,它们各有优劣,并在不同的领域发挥着重要作用。虽然 Java 在很多方面都非常出色,并且在某些领域已经取代了 C++,但仍然有一些 C++ 的独特之处是 Java 无法完全取代的,或者说取代的成本非常高。以下是 C++ 的一些 Java 不能(或难以.............
  • 回答
    好,咱们来聊聊 C++ 单例模式里那个“为什么要实例化一个对象,而不是直接把所有成员都 `static`”的疑问。这确实是很多初学者都会纠结的地方,感觉直接用 `static` 更省事。但这里面涉及到 C++ 的一些核心概念和设计上的考量,咱们一点点掰开了说。 先明确一下单例模式的目标在深入“`st.............
  • 回答
    在 C++ 标准库的 `std::string` 类设计之初,确实没有提供一个直接的 `split` 函数。这与其他一些高级语言(如 Python、Java)中普遍存在的 `split` 方法有所不同。要理解为什么会这样,我们需要深入探究 C++ 的设计哲学、标准库的演进过程以及当时的开发环境和需求.............
  • 回答
    C 扩展方法:一把双刃剑C 的扩展方法,顾名思义,允许我们为现有的类型添加新的方法,而无需修改原始类型的源代码。这种能力最初听起来像是魔法,能够让代码更加优雅、富有表现力,并且提升了代码的复用性。然而,正如许多强大的工具一样,扩展方法也是一把双刃剑,如果使用不当,可能会导致代码可读性下降、维护困难,.............
  • 回答
    C++ 的 `std::list`,作为 STL(Standard Template Library)中的一员,它是一种双向链表(doubly linked list)。它的核心特点在于,每个节点都存储了数据本身,以及指向前一个节点和后一个节点的指针。这使得 `std::list` 在某些特定场景下.............
  • 回答
    你问了一个非常关键的问题,而且问得非常实在。确实,C++ 的智能指针,尤其是 `std::unique_ptr` 和 `std::shared_ptr`,在很大程度上解决了 C++ 中常见的野指针和内存泄漏问题。这玩意儿在 C++ 世界里,堪称“救世主”般的存在。那么,为什么大家对 Rust 的内存.............
  • 回答
    C++ 中的常量后缀,顾名思义,就是用来标识字面量(literal)是何种类型的。虽然编译器通常能够通过字面量的形式推断出其类型,但在很多情况下,使用常量后缀能够明确表达开发者的意图,避免潜在的类型转换问题,并提升代码的可读性和健壮性。我们来详细探讨一下常量后缀在哪些情况下特别有用,并说明其背后的原.............
  • 回答
    CRTP,也就是Curiously Recurring Template Pattern(奇特的递归模板模式),在C++中,它是一种利用模板的静态分派特性来实现多态的一种精巧技巧。很多人听到“多态”首先想到的是虚函数和运行时多态,但CRTP带来的多态是“静态多态”,这意味着多态的决策是在编译期完成的.............
  • 回答
    C++ 运行时多态:性能的代价与权衡在 C++ 的世界里,我们常常惊叹于它的灵活性和表达力。其中,运行时多态(Runtime Polymorphism)是实现这一能力的关键机制之一,它允许我们在程序运行时根据对象的实际类型来决定调用哪个函数。这就像一个剧团的导演,在舞台上,他可以根据演员扮演的角色,.............
  • 回答
    C++的move构造,作为语言引入的一项重要特性,其设计初衷是为了解决资源管理中的性能瓶颈,特别是针对那些拥有昂贵资源(如堆内存、文件句柄、网络连接等)的对象。它允许我们将一个对象的资源“转移”到另一个对象,而不是通过昂贵的拷贝操作来复制这些资源。然而,随着这项特性的应用和深入理解,关于其设计是否“.............
  • 回答
    sizeof 关键字在 C++ 中,并不是一个普通的函数,而是一个编译时常量。理解它的实现,关键在于区分它在编译期和运行时的行为。1. 编译期的魔法:类型的大小计算当你使用 `sizeof` 关键字时,比如 `sizeof(int)` 或者 `sizeof(MyClass)`,编译器会立即在编译阶段.............
  • 回答
    C++ 的 `switch` 语句之所以不默认添加 `break` 语句,这是 C++ 设计者们经过深思熟虑后做出的一个选择,其背后有明确的理由和意图。理解这一点,需要我们深入到 `switch` 语句的本质和它与其他控制流语句的区别。 1. fallthrough(贯穿)的意图与灵活性C++ 的 .............
  • 回答
    咱们聊聊 C 里的接口,这玩意儿在实际开发中,那可是个顶顶重要的角色,但要是光看定义,可能觉得有点抽象。我试着把这些实际用法给你掰开了揉碎了讲讲,尽量避免那些“AI味儿”的说法,就跟咱们哥俩坐一块儿聊天一样。接口是啥?通俗点说,就是一份“合同”你可以把接口想象成一个约定,或者一份“合同”。这份合同规.............

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有