目录
[2.operator new与operator delete函数](#2.operator new与operator delete函数)
[Placement New(定位new表达式)](#Placement New(定位new表达式))
前言
在写上一篇博客"vector的模拟实现"时,我一直很好奇vector的private成员为什么要用三个封装后的迭代器指针,来替代原本数据结构顺序表中的成员变量,因为原本的数据结构成员是完全满足需求的,即:

(从数据结构的本质来讲,vector完全可以被理解为 C++ 版的、功能更强大的、高度自动化的顺序表。)
在学习了实际库中的vector代码后,我发现了未见过的东西:**"::operator new与::operator delete"。**在知道了它们的作用与使用场所后,或许我找到了用迭代器指针替代原本数据成员的原因。
1.为什么C++要引入new/delete?
C语言内存管理方式在C++中是可以继续使用,比如malloc、realloc等函数。但有些地方这些传统的空间申请函数就显得有些无能为力,而且使用起来比较麻烦。
是的,在C++的自定类型数据中,常需要在定义时顺便调用构造函数初始化。而原C语言的空间申请函数是没有这种功能,需要额外操作,于是C++提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理。
简而言之,什么C++要引入new/delete的原因在于:
new/delete 和 malloc/free最大区别是 new/delete对于【自定义类型】除了开空间时,还会调用构造函数和析构函数。
2.operator new与operator delete函数
首先介绍它们是什么:
它们是C++中的内存分配原语函数 。它们只负责分配和释放原始内存,不涉及对象的构造和析构。
它们的用法:
cpp
#include <new> // 包含 operator new 和 operator delete 的声明
// 分配内存
void* memory = ::operator new(size_t bytes);
// 释放内存
::operator delete(void* ptr);
示例
cpp
#include <iostream>
#include <new>
int main()
{
// 分配10个int大小的内存
void* int_memory = ::operator new(10 * sizeof(int));
std::cout << "内存分配成功,地址: " << int_memory << std::endl;
// 使用内存...
// 释放内存
::operator delete(int_memory );
std::cout << "内存已释放" << std::endl;
return 0;
}
它们与new/delete的关系:
new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过 operator delete全局函数来释放空间。
操作 | 做的事情 |
---|---|
int* p = new int(42); |
1. 调用 operator new(sizeof(int)) 分配内存 2. 在内存上调用 int 的构造函数 构造对象(设为42) |
delete p; |
1. 调用 p 的析构函数 析构对象 2. 调用 operator delete(p) 释放内存 |
void* mem = ::operator new(sizeof(int)); |
只分配内存,不调用构造函数 |
::operator delete(mem); |
只释放内存,不调用析构函数 |
简单说:new
/delete
= (operator new
/operator delete
+ 构造函数/析构函数调用)
注意:和new与delete,new[ ]与delete[ ]相同,operator new只能与operator delete匹配。
它们的实际作用
回到前言部分,为什么vector的private成员为什么要用三个封装后的迭代器指针,来替代原本数据结构顺序表中的成员变量呢?
在查看vecotr的实际实现代码中,我发现 vector类中涉及空间增删的函数,实际上都是调用的reserve函数,而在reserve函数中则使用了operator new/delete。
我们来看看reserve函数的模拟实现:
cpp
void reserve(size_t n)
{
size_t oldSize = size(), oldCapa = capacity();
if (n > oldCapa)
{
size_t newcapa = oldCapa == 0 ? 16 : oldCapa * 2;
while (newcapa < n)newcapa *= 2;
iterator newVec =(iterator)::operator new(newcapa * sizeof(T));
if (_start)
{
for (int i = 0; i < oldSize; ++i)
new(newVec + i)T(_start[i]);
for (int i = 0; i < oldSize; ++i)
_start[i].~T();
}
::operator delete(_start);
_start = newVec;
_finish = _start + oldSize;
_end = _start + newcapa;
}
else return;
}
**讨论:**在普通的reserve函数中,我们通常用new来申请空间,如下所示:
cpp
T* newcapa=new T[n];
可这带来一个问题:new在申请空间的同时会调用构造函数,可是这些空间我们真的需要全部初始化吗,换句话说这些空间我们真能全部用完吗?显然大部分场景是用不完的,因为vector的空间一般呈*2倍速度增长。那么这些不用的空间通过调用构造函数初始化,这不仅造成了一定的性能浪费,还让后续无法自定义使用这片内存,造成内存资源浪费。
于是,通过用operator new/delete替换之前的new/delete,既解决了性能损失,又满足了C++内存分配与对象构造分离的目的。
或许有读者注意到上述代码中的如下这段代码,这段代码实则揭开了为什么vector要使用三个指针作为成员变量的原因:
cpp
for (int i = 0; i < oldSize; ++i)
new(newVec + i)T(_start[i]);
已知C++程序的一个核心设计思想:将内存分配(Allocation)和对象构造(Construction)分离。通过使用operator new/delete确实做到了只分配空间,不调用构造函数的目的。那么什么时候调用构造函数呢?
------在reserve函数中,通过提前记录的oldSize精确控制调用构造函数的次数。而实际调用构造函数是通过**Placement New(定位new表达式)**实现的。
Placement New(定位new表达式)
它是什么?
Placement new 是一种特殊的 new 表达式,它在已分配的内存上构造对象。它不分配内存,只调用构造函数。
语法
cpp
new (address) Type(constructor_arguments);
address:一般传入指针;
Type:某数据类型,可以是内置类型,也可以是自定义类型;constructor_arguments,该类型的构造函数。
使用示例
cpp
#include <iostream>
#include <new>
class MyClass {
public:
int value;
MyClass(int v) : value(v) {
std::cout << "构造函数被调用,value = " << value << std::endl;
}
~MyClass() {
std::cout << "析构函数被调用,value = " << value << std::endl;
}
};
int main() {
// 1. 只分配内存,不构造对象
void* memory = ::operator new(sizeof(MyClass));
// 2. 在已分配的内存上构造对象
MyClass* obj = new (memory) MyClass(42);
std::cout << "对象值: " << obj->value << std::endl;
// 3. 显式调用析构函数
obj->~MyClass();
// 4. 释放内存
::operator delete(memory);
return 0;
}
operator new
设计时就考虑了与 placement new 的配合使用。这种组合提供了对对象构造和内存分配的完全控制。
现在回到上述有关reserve函数的讨论。
现在已知vector中有关的空间操作,全是由reserve函数完成的,其中reserve函数通过使用operator new/delete、定位new表达式完成了"内存分配和对象构造的分离"。
结合定位new表达式 的使用语法,有关"vector的private成员为什么要用三个封装后的迭代器指针,来替代原本数据结构顺序表中的成员变量"的答案也就呼之欲出了,或许原因之一就是为了满足位new表达式 的使用语法从而达到**"内存分配和对象构造的分离"**的目的。
同样是申请空间,为什么不使用原C语言malloc等函数,而设计出operator new函数呢?
可能的原因或许有很多,但作者认为或许与它们在面对异常时的反应不同:
operator new
的异常行为
-
当
operator new
无法分配内存时,它会抛出std::bad_alloc
异常,提醒程序员。 -
这与 C++ 的异常处理机制完美集成。
malloc
的错误处理
-
当
malloc
无法分配内存时,它返回NULL
(或 C++11 中的nullptr
),需要程序员自己检查。 -
这要求你检查返回值,使用 C 风格的错误处理。
总结
本文从对"vector的private成员为什么要用三个封装后的迭代器指针,来替代原本数据结构顺序表中的成员变量"疑问中,引出operator new/delete的介绍,以及之后定位表达式new的使用语法。
本文或许对vector为什么要用三个指针,替换原本的使用一个指针加两个size_t(data, size, capacity)的回答不尽完美,甚至漏洞百出,但好在因此学到了新东西。
感谢你的阅读。