浅谈new与::operator new

目录

前言

1.为什么C++要引入new/delete?

[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)的回答不尽完美,甚至漏洞百出,但好在因此学到了新东西。

感谢你的阅读。

相关推荐
梅见十柒7 小时前
UNIX网络编程笔记:共享内存区和远程过程调用
linux·服务器·网络·笔记·tcp/ip·udp·unix
我命由我123459 小时前
Word - Word 查找文本中的特定内容
运维·经验分享·笔记·word·运维开发·文档·文本
崔高杰9 小时前
大模型训练中对SFT和DPO的魔改——PROXIMAL SUPERVISED FINE-TUNING和Semi-online DPO论文阅读笔记
论文阅读·笔记
受之以蒙10 小时前
Rust & WebAssembly 实践:构建一个简单实时的 Markdown 编辑器
笔记·rust·webassembly
~黄夫人~11 小时前
Nginx Ubuntu vs CentOS 常用命令对照表---详解笔记
运维·笔记·学习·nginx·ubuntu·centos
ZZHow102412 小时前
React前端开发_Day10
前端·笔记·react.js·前端框架·web
会思考的猴子13 小时前
UE5 PCG 笔记(三) Normal To Density 节点
笔记·ue5
叮咚前端16 小时前
vue3笔记
前端·javascript·笔记
Source.Liu19 小时前
【Rust】 2. 数据类型笔记
开发语言·笔记·rust