C++ new

 

参考

概念

new是 C++中关键字, 用于分配内存并构造对象

T *p = new T();
graph LR;
    A(特点)
    A-->B(同时进行内存分配和对象构造)
    A-->C(若内存分配失败, 抛出 std::bad_alloc 异常)
    A-->D(可用于单个对象或数组创建)

函数

operator new

operator new是可重载函数, 负责从自由存储区(堆)中分配内存(类似于malloc函数), 不调用构造函数

void* operator new(std::size_t size) {
    // 调用全局::operator new 分配内存
    return ::operator new(size);
}
graph LR;
    A(operator new特点)
    A-->B(只负责内存分配, 不初始化对象)
    A-->C(可重载以自定义内存分配过程)
    A-->D(若分配失败也抛出 std::bad_alloc 异常, 除非使用 nothrow 版本)
重载perator new作用

(1) 内存池管理, 通过自定义 operator new, 可实现内存池来减少动态内存分配时开销

(2) 调试, 可在 operator new 中记录日志, 跟踪每次内存分配调用

(3) 性能优化, 对于特定类型对象, 可以通过自定义 operator new 来优化内存分配策略

(4) 异常处理, 可自定义 operator new 来处理分配失败时异常, 甚至返回 nullptr 而不是抛出异常

::operator new

全局 operator new, 用于分配内存

与operator new区别

(1) ::operator new 强制调用作用域为全局, 即标准库所提供默认版本

(2) operator new 作用域可能是全局, 也可是自定义重载版本

placement new

用于在指定内存地址上构造对象

new 操作符会在堆上分配内存并构造对象, 而 placement new 则允许指定一个已经分配好内存区域, 然后在区域上调用对象构造函数

// 指向已分配内存指针
void* memory = ....
// 在指定内存上构造对象
T* object = new (memory) T(args...);

流程

graph LR;
    X(开始)
    X-->A(分配内存)--> A1[/内存空间/]-->B
    B(初始化)-->B1[/生成对象/]-->C
    C(返回指针)-->D1[/指向对象指针/]-->Y
    Y(结束)

分配内存

new 调用 operator new 函数 默认从堆(自由存储区)中分配内存来存储对象

调用operator new

(1) operator new 内存分配失败时会抛出 std::bad_alloc 异常(而不像 malloc 返回 NULL)

(2) 自定义 operator new 函数会优先调用, 否则调用全局 ::operator new

void* operator new(std::size_t size) {
    // 分配 size 大小内存
    void* p = ::malloc(size);
    // 内存分配失败时抛出异常
    if (!p) {
        throw std::bad_alloc();
    }
    return p;
}
  • 示例, 调用自定义operator new
#include <iostream>

class UseNew {
public:
    UseNew() = default;
    ~UseNew() = default;

    // 重载 operator new
    void* operator new(std::size_t size) {
        std::cout << "operator new called, size: " << size << std::endl;
        // 调用全局 operator new
        return ::operator new(size);
    }
    // 重载 operator delete
    void operator delete(void* p) {
        std::cout << "operator delete called" << std::endl;
        // 调用全局 operator delete
        ::operator delete(p);
    }
};

int main() {
    UseNew* use = new UseNew();
    delete use;
    return 0;
}

初始化

分配内存后 new 会在刚分配内存上调用对象构造函数初始化, 本质是通过定位 new机制, 确保构造函数在分配内存地址上正确执行

构造对象

(1) 使用placement new 构造函数会在已经分配内存位置上构造对象

(2) 对象构造函数会根据传入参数初始化对象成员变量和状态

// 在已分配好内存 ptr 上调用构造函数
T* obj = new(ptr) T();

处理异常

(1) 若内存分配失败::operator new 会抛出 std::bad_alloc 异常

(2) 若构造函数抛出异常:会释放已经分配内存, 并传播异常, C++ 通过 RAII异常处理机制, 确保不会泄漏资源

// 1. 分配内存
T* obj = static_cast<T*>(operator new(sizeof(T)));

try {
    // 2. 定位 new, 调用构造函数
    new (obj) T();
} catch (...) {
    // 3. 构造失败, 释放内存
    operator delete(obj);
    // 4. 重新抛出异常
    throw;
}

返回指针

若内存分配和对象构造均成功, new返回指向已初始化对象指针, 此时对象已完全初始化

对比

与系统函数

操作符/函数 作用 调用构造函数 是否抛出异常
new 分配内存并构造对象 是(若内存分配失败)
operator new 只分配内存, 不构造对象, 允许类自定义内存分配策略 是(若内存分配失败)
::operator new 全局operator new, 只分配内存, 调用默认内存分配实现 是(若内存分配失败)

与malloc

申请位置不同

malloc

从堆上动态分配内存

new

操作符从自由存储区(free store)上为对象动态分配内存空间

自由存储区是 C++ 基于 new的抽象概念, 可以是堆还可以是静态存储区, 取决于 operator new 在哪里为对象分配内存, 凡通过 new所申请内存即为自由存储区

返回类型安全性

new

操作符内存分配成功时, 返回对象类型指针, 类型严格与对象匹配, 无须进行类型转换, 故 new 是符合类型安全性操作符

malloc

内存分配成功则是返回 void 指针, 需要通过强制类型转换将 void 指针转换成指定类型

内存分配失败时返回值

new, 内存分配失败时, 会抛出 bac_alloc 异常, 不会返回 NULL

malloc, 分配失败时返回 NULL

分配内存大小

new, 操作符申请内存分配时无须指定内存块大小, 编译器会根据类型信息自行计算

malloc, 则需要明确指出所需内存尺寸

是否调用构造/析构函数

new

new分配对象内存时会经历三个步骤,

(1) 调用 operator new 函数(对于数组是 operator new[])分配一块足够大原始, 未命名内存空间, 存储特定类型对象

(2) 编译器运行相应构造函数以构造对象,并为其传入初值

(3) 对象构造完成后,返回一个指向该对象指针

delete 操作符来释放对象内存时会经历两个步骤,

(1) 调用对象析构函数

(2) 编译器调用operator delete(或operator[] delete[])函数释放内存空间

malloc

不会调用构造函数, free也不会调用析构函数

对于数组处理

new

对数组会分别调用构造函数函数初始化每一个数组元素

C++ 提供new[]delete[] 来专门处理数组类型分配

释放对象时为每个对象调用析构函数, delete[] 要与 new[] 配套使用,不然会找出数组对象部分释放现象, 造成内存泄漏

malloc

只提供开辟出内存地址, 若动态分配一个数组内存,还需要手动自定数组大小

是否支持内存扩充

new, 不支持内存扩充

malloc, 在分配内存后若内存不足, 可使用 realloc 进行内存重新分配, 实现扩充