Skip to content

Latest commit

 

History

History
838 lines (736 loc) · 30.4 KB

ch30.md

File metadata and controls

838 lines (736 loc) · 30.4 KB

Chapter30 使用newdelete管理超对齐数据

自从C++11起,你可以使用alignas修饰符指定 超对齐(over-aligned types) 类型, 它们比默认的对齐方式有更大的对齐。例如:

struct alignas(32) MyType32 {
    int i;
    char c;
    std::string s[4];
};

MyType32 val1;              // 32字节对齐
alignas(64) MyType32 val2;  // 64字节对齐

注意对齐数必须是2的幂,并且指定任何小于默认情况的对齐数都会导致错误。

然而,C++11和C++14中超对齐数据的 动态/堆分配(dynamic/heap allocation) 并没有 被正确处理。使用运算符new创建超对齐类型将会默认忽略要求的对齐数, 这意味着一些64字节对齐的类型可能只有8字节或16字节对齐。

这个问题在C++17中被修复。新的行为现在提供带对齐数的new重载来允许你为超对齐数据 提供自己的实现。

30.1 使用带有对齐的new运算符

例如使用如下的超对齐类型:

struct alignas(32) MyType32 {
    int i;
    char c;
    std::string s[4];
};

new表达式现在保证请求的堆内存按照要求对齐(超对齐也支持):

MyType32* p = new MyType32;     // 自从C++17起保证是32字节对齐
...

在C++17之前,这个请求并不保证是32字节对齐。

像往常一样,没有初始值时对象会被默认初始化,这意味着默认构造函数会被调用而基础类型 的子对象将会有未定义的值。因此,推荐的方式是使用列表初始化,在最后加上空的花括号 来保证基础类型会初始化为默认值:0/false/nullptr

MyType32* p = new MyType32{};   // 对齐并初始化

30.1.1 不同的动态/堆内存机制

注意请求对齐的内存可能会导致从不相交的内存分配机制获取内存。 因此,一个对对齐内存的请求还需要指定一个相应的释放内存的请求。 如果 可能 的话会使用C11函数aligned_alloc() (现在在C++17中也可以使用)来分配内存。在这种情况下,可以使用free()释放内存, 和使用malloc()分配内存时没有什么区别。

然而,不同平台上也允许不同的newdelete实现, 这导致释放默认对齐数据和超对齐数据时需要使用不同的内部函数。 例如,在Windows上,通常使用_aligned_malloc(),它要求 使用_aligned_free()来释放内存。

与C标准不同,C++标准分析了这个情况并因此从概念上假设有两个互不相交、 不可互操作的 内存机制(memory arenas) ,一个用于默认对齐的数据,另一个用于超对齐数据。 所以编译器从根本上知道了该如何正确处理这种情况:

std::string* p1 = new std::string;  // 使用默认对齐内存操作
MyType32* p2 = new MyType32;        // 使用超对齐内存操作
...
delete p1;                          // 使用默认内存操作
delete p2;                          // 使用超对齐内存操作

然而,正如我们将在这一章的剩余内容中看到的那样,有时程序员必须自己保证正确性。

30.1.2 带对齐的new表达式

还有一种为特定的new表达式请求特定对齐的方式。例如:

#include <new>  // for align_val_t
...

std::string* p = new(std::align_val_t{64}) std::string; // 64字节对齐
MyType32* p = new(std::align_val_t{64}) MyType32{};     // 64字节对齐
...

类型在std::align_val_t在头文件<new>中以如下方式定义:

namespace std {
    enum class align_val_t : size_t {
    };
}

提供std::align_val_t是为了给相应operator new()的实现传递对齐请求。

带对齐的new会影响delete

注意在C++中operator new()可以按照如下方式实现:

  • 作为 全局 函数(默认提供不同的重载,程序员可以进行替换)
  • 作为 类型特定 的实现,可以由程序员提供,并且比全局的重载有更高的优先级

因为不同的内存机制使用的实现可能不同,

所以想正确地处理它们需要特别小心。 问题在于当使用new表达式指定特殊的对齐时,编译器不能根据类型推断出是否需要和 需要什么样的对齐。因此,程序员必须指明要调用什么样的delete

不幸的是,没有可以传递额外参数的placement delete操作符:

delete(std::align_val_t{64}) p; // ERROR:不支持placement delete

因此,你必须直接调用相应的operator delete(),这意味着:

  • 你必须知道都实现了哪些重载,这样才能调用正确的那一个。
  • 在调用operator delete()之前,你必须显式调用析构函数。

事实上,如果没有特定类型的delete定义,那你必须调用析构函数和全局的delete:

std::string* p = new(std::align_val_t{64}) std::string; // 64字节对齐
...
p->~basic_string();                                     // 析构对象
::operator delete(p, std::align_val_t{64});             // 释放内存

注意std::string的析构函数名叫~basic_string()

如果有类型特定的delete定义,你可以在调用析构函数之后调用它:

MyType32* p = new(std::align_val_t{64}) MyType32{};     // 64字节对齐
...
p->~MyType32();                                         // 析构对象
MyType32::operator delete(p, std::align_val_t{64});     // 释放内存

如果你不知道类型的详细情况,对于一个类型T的对象你可以调用下列函数之一:

void T::operator delete(void* ptr, std::size_t size, std::align_val_t align);
void T::operator delete(void* ptr, std::align_val_t align);
void T::operator delete(void* ptr, std::size_t size);
void T::operator delete(void* ptr);
void ::operator delete(void* ptr, std::size_t size, std::align_val_t align);
void ::operator delete(void* ptr, std::align_val_t align);
void ::operator delete(void* ptr, std::size_t size);
void ::operator delete(void* ptr);

这样很复杂,我将会在稍后详细解释什么时候应该调用哪一个。 现在,我推荐你按照如下指导方针:

  • 完全不要在new表达式中直接使用超对齐。而是创建自己的辅助类型来代替。
  • 提供使用相同内存机制的operator new()operator delete()实现 (这样使用普通的delete也可以工作)。
  • 提供operator delete()的类型特定的实现来匹配那些 特定类型的operator new(),并且直接调用它们而不是使用delete表达式。

注意你不能使用typedef或者using声明定义别名:

using MyType64 = alignas(64) MyType32;  // ERROR
typedef alignas(64) MyType32 MyType64;  // ERROR
...
MyType64* p = new MyType64              // ERROR

这是因为typedef或者using声明只是原本类型的别名, 而这里按照不同规则对齐的对象的类型是不同的。

如果你想让一个对齐的new表达式返回nullptr而不是 抛出std::bad_alloc异常,你可以像下面这样:

// 分配一个64字节对齐的string(失败时返回nullptr):
std::string* p = new(std::align_val_t{64}, std::nothrow) std::string;
if (p != nullptr) {
    ...
}

实现placement delete

如果你不得不写在new调用中使用超对齐的(泛型)代码并且不知道那些类型是否有 自己的operator delete,你可以实现自己的placement delete

你可以简单的实现如下的类型特征来判断类型是否定义了operator delete

#include <type_traits>  // for true_type, false_type, void_t

// 主模板
template<typename, typename = std::void_t<>>
struct HasDelete : std::false_type {
};

// 部分特化版(可能被SFINE'd away):
template<typename T>
struct HasDelete<T, std::void_t<decltype(T::operator delete(nullptr))>> : std::true_type  {
};

这里,我们使用了std::void_t<>来SFNAE away掉调用类型特化的operator delete 时无效的特化。

通过使用std::void_t<>,我们可以定义自己的placement delete辅助函数:

template<typename TP, typename... Args>
void placementDelete(TP* tp, Args&&... args)
{
    // 析构对象:
    tp->~TP();

    // 使用正确的delete操作符释放内存:
    if constexpr(HasDelete<TP>::value) {
        TP::operator delete(tp, std::forward<Args>(args)...);
    }
    else {
        ::operator delete(tp, std::forward<Args>(args)...);
    }
}

并且像下面这样使用这个辅助函数:

std::string* p1 = new(std::align_val_t{64}) std::string;    // 64字节对齐
MyType32* p2 = new(std::align_val_t{64}) MyType32{};        // 64字节对齐
...
placementDelete(p1, std::align_val_t{64});
placementDelete(p2, std::align_val_t{64});

30.2 实现内存对齐分配的new()运算符

在C++中你可以自定义调用newdelete时分配和释放内存的实现。 这个机制现在还支持传递一个对齐数参数。

30.2.1 在C++17之前实现对齐的内存分配

在全局范围内,C++提供重载的operator new()operator delete(), 当没有类型特定的实现定义的时候会使用它们,如果有类型特定的实现那么会使用后者。 注意只要有一个类型特定的operator new(),那么处理这个类型时就会禁止 所有全局的operator new()deletenew[]delete[]也是这样)。

也就是说,每次你对类型T调用new时,要么会调用相应的类型特定的 T::operator new(),要么调用全局的::operator new()(当前者不存在时):

auto p = new T; // 尝试调用类型特定的operator new()(如果有的话)
                // 或者,如果不存在前者,就尝试调用全局的::operator new()

同样的道理,每次对类型T调用delete时,也是要么调用类型特定的 T::operator delete()要么调用全局的::operator delete()。 如果分配/释放的是数组那么会调用相应的特定类型的或者全局的operator new[]()operator delete[]()

在C++17之前,要求的对齐并不能自动传递给这些函数,默认的动态内存分配机制也不会考虑这些对齐。 一个超对齐类型必须使用它自己的operator new()operator delete()实现, 这样才能正确的在动态内存上对齐。更糟的是,还没有可移植的方式来实现这种超对齐的动态内存请求。

因此,你必须像下面一样定义:

#include <cstddef>  // for std::size_t
#include <cstring>
#if __STDC_VERSION >= 201112L
#include <stdlib.h> // for aligned_alloc()
#else
#include <malloc.h> // for _aligned_malloc() or memalign()
#endif

struct alignas(32) MyType32 {
    int i;
    char c;
    std::string s[4];
    ...
    static void* operator new (std::size_t size) {
        // 按照请求的对齐分配内存:
#if __STDC_VERSION >= 201112L
        // 使用C11的API:
        return aligned_alloc(alignof(MyType32), size);
#else
#ifdef _MSC_VER
        // 使用Windows的API:
        return _aligned_malloc(size, alignof(MyType32));
#else
        // 使用Linux的API:
        return memalign(alignof(MyType32), size);
#endif
#endif
    }

    static void operator delete (void* p) {
        // 释放对齐分配的内存:
#ifdef _MSC_VER
        // 使用Windows的特殊API:
        _aligned_free(p);
#else
        // C11/Linux可以使用通用的free():
        free(p);
#endif
    }

    // 自从C++14起:
    static void operator delete (void* p, std::size_t size) {
        MyType32::operator delete(p);   // 使用无大小的delete
    }
    ...
    // 定义数组需要的new[]和delete[]
}

注意自从C++14起,你可以为delete运算符提供一个size参数。 然而,有可能大小未知(例如不完全类型),也有些平台可以选择要不要给operator delete[] 传递一个大小参数。因此,自从C++14,你应该总是同时定义有大小和无大小的operator delete()重载。 用其中一个调用另一个通常是个好注意。

有了这个定义,下面代码的行为将是正确的:

#include "alignednew11.hpp"

int main()
{
    auto p = new MyType32;
    ...
    delete p;
}

自从C++17起,你可以省略对齐数据的分配和释放操作的实现。 下面的例子即使在没有为自定义类型定义operator new()operator delete()的 情况下也能正确工作:

#include <string>

struct alignas(32) MyType32 {
    int i;
    char c;
    std::string s[4];
    ...
};

int main()
{
    auto p = new MyType32;  // 自从C++17起分配32字节对齐的内存
    ...
    delete p;
}

30.2.2 实现类型特化的new()运算符

如果你必须提供自定义的operator new()operator delete()实现, 现在有了对超对齐数据的支持。在实践中,自从C++17起类型特定实现的相应代码类似于如下:

#include <cstddef>  // for std::size_t
#include <new>      // for std::align_val_t
#include <cstdlib>  // for malloc(), aligned_alloc(), free()
#include <cstring>

struct alignas(32) MyType32 {
    int i;
    char c;
    std::string s[4];
    ...
    static void* operator new (std::size_t size) {
        // 调用new获取默认对齐的数据:
        std::cout << "MyType32::new() with size " << size << '\n';
        return ::operator new(size);
    }
    static void* operator new (std::size_t size, std::align_val_t align) {
        // 调用new获取超对齐数据:
        std::cout << "MyType32::new() with size " << size
                  << " and alignment " << static_cast<std::size_t>(align)
                  << '\n';
        return ::operator new(size, align);
    }

    static void operator delete (void* p) {
        // 调用delete释放默认对齐的数据:
        std::cout << "MyType32::delete() without alignment\n";
        ::operator delete(p);
    }
    static void operator delete (void* p, std::size_t size) {
        MyType32::operator delete(p);   // 使用无大小的delete
    }
    static void operator delete (void* p, std::align_val_t align) {
        // 调用delete释放超对齐的数据:
        std::cout << "MyType::32::delete() with alignment\n";
        ::operator delete(p, align);
    }
    static void operator delete (void* p, std::size_t size, std::align_val_t align) {
        MyType32::operator delete(p, align);    // 使用无大小的delete
    }

    // 定义数组需要的operator new[]和operator delete[]
    ...
}

理论上讲,我们只需要接受额外的对齐参数的重载,并在这些重载里调用相应的函数来 申请和释放对齐的内存。实现这一点的最有可移植性的方式是调用为超对齐/释放提供的全局函数:

static void* operator new (std::size_t size, std::align_val_t align) {
    ...
    return ::operator new(size, align);
}
...
static void operator delete (void* p, std::align_val_t align) {
    ...
    ::operator delete(p);
}

你也可以使用C11函数来直接处理对齐的分配:

static void* operator new (std::size_t size, std::align_val_t align) {
    ...
    return std::aligned_alloc(static_cast<size_t>(align), size);
}
...
static void operator delete (void* p, std::align_val_t align) {
    ...
    std::free(p);
}

然而,因为Windows的aligned_alloc()的问题,在实践中, 我们需要特殊的处理来保证可移植性:

static void* operator new (std::size_t size, std::align_val_t align) {
    ...
#ifdef _MSC_VER
    // Windows特定的API:
    return aligned_malloc(size, static_cast<size_t>(align));
#else
    // C++17标准的API:
    return std::aligned_alloc(static_cast<size_t>(align), size);
#endif
}

static void operator delete (void* p, std::align_val_t align) {
    ...
#ifdef _MSC_VER
    // Windows特定的API:
    _aligned_free(p);
#else
    // C++17标准的API:
    std::free(p);
#endif
}

注意所有的分配函数都以类型size_t接受参数,所以我们必须使用static cast把 值从std::align_val_t转换为size_t

另外,你应该用[[nodiscard]]属性标记operator new()

[[nodiscard]] static void* operator new (std::size_t size) {
    ...
}

[[nodiscard]] static void* operator new (std::size_t size, std::align_val_t align) {
    ...
}

虽然很罕见但也有可能直接调用operator new()(不使用new表达式)。 有了[[nodiscard]],编译器会检查调用者是否忘记了使用返回值, 如果是则可能会发生内存泄露。

operator new()何时被调用?

正如之前所解释的,我们现在可以有两个operator new()重载:

  • 只有size参数的版本,在C++17之前就已经存在,一般用来处理默认对齐 的数据。然而,如果处理超对齐数据的版本不存在它会被用作备选项。
  • 带有额外的align参数的版本,从C++17开始才有,一般用来处理超对齐数据的请求。

使用哪一个重载 并不一定 依赖于是否使用了alignas,而是依赖于 超对齐数据的平台特定的定义。

一个编译器根据一个通用的对齐数在默认对齐和超对齐之间切换, 你可以在新的预处理常量中找到它:

__STDCPP_DEFAULT_NEW_ALIGNMENT__

也就是说,当要求比这个常量更大的对齐时,new调用会从尝试调用

operator new(std::size_t)

转变为尝试调用

operator new(std::size_t, std::align_val_t)

因此,下面的代码的输出在不同平台上可能不同:

struct alignas(32) MyType32 {
    ...
    static void* operator new (std::size_t size) {
        std::cout << "MyType32::new() with size " << size << '\n';
        return ::operator new(size);
    }
    static void* operator new (std::size_t size, std::align_val_t align) {
        std::cout << "MyType32::new() with size " << size
                  << " and alignment " << static_cast<std::size_t>(align) << '\n';
        return ::operator new(size, align);
    }
    ...
};

auto p = new MyType32;

如果默认的对齐数是32(或者更多并且能编译通过), 表达式new MyType32将会调用operator new()的第一个 只有一个大小参数的版本,因此输出将类似于:

MyType32::new() with size 128

如果默认的对齐小于32,那么将会调用有两个参数的第二个版本,因此输出将类似于:

MyType32::new() with size 128 and alignment 32

类型特定备选项

如果类型特定的operator new()没有提供std::align_val_t的重载, 那么将会使用没有这个参数的重载版本作为备选项。因此,一个只提供operator new() 重载的类(在C++17就支持)仍然可以编译并且和之前的行为相同 (注意全局operator new()不是这种情况):

struct NonalignedNewOnly {
    ...
    static void* operator new (std::size_t size) {
        ...
    }
    ... // 没有operator new(std::size_t, std::align_val_t align)
};

auto p = new NonalignedNewOnly; // OK:使用operator new(size_t)

反过来则不行。如果一个类型只提供了有对齐参数的重载,任何尝试默认对齐的new调用都会失败:

struct AlignedNewOnly {
    ... // 没有operator new(std::size_t)
    static void* operator new (std::size_t size, std::align_val_t align) {
        return std::aligned_alloc(static_cast<std::size_t>(align), size);
    }
};

auto p = new AlignedNewOnly;    // ERROR:没有默认对齐使用的operator new()

如果为该类型要求的对齐数小于默认的对齐数也会导致错误。

new表达式中要求对齐

如果你在new表达式中传递了要求的对齐数,那么传入的对齐数参数将会被一直传递下去 并且必须被operator new()支持。事实上,对齐数参数的处理就和其他new表达式 接受的额外参数一样:它们作为额外的参数传递给operator new()

因此,一个类似于这样的调用:

std::string* p = new(std::align_val_t{64}) std::string; // 64字节对齐

总是 会尝试调用:

operator new(std::size_t, std::align_val_t)

这里只有一个大小参数的重载将 不会 被用作备选项。

如果你对超对齐类型要求特定的对齐,程序的行为将会更加有趣。例如,如果你调用:

MyType32* p = new(std::align_val_t{64}) MyType32{};

并且MyType32本身就是超对齐的,那么编译器会首先尝试调用:

operator new(std::size_t, std::align_val_t, std::align_val_t)

以32作为第二个参数(该超对齐类型的一般对齐)和64作为第三个参数(这里要求的特定对齐数)。 它会回退到备选项

operator new(std::size_t, std::align_val_t)

第二个实参是要求的对齐数64。理论上讲,你还可提供三参数的重载来 实现这种为超对齐类型指定对齐数的情况下的分配操作。

再次提醒,注意如果你想让超对齐数据调用特殊的释放内存函数, 当在new表达式中传递对齐时时你必须 调用正确的分配内存函数:

std::string* p1 = new(std::align_val_t{64}) std::string{};
MyType32* p2 = new(std::align_val_t{64}) MyType32{};
...
p1->~basic_string();
::operator delete(p1, std::align_val_t{64});            // !!!
p2->~MyType32();
MyType32::operator delete(p2, std::align_val_t{64});    // !!!

这意味着这个例子中的new表达式将会调用

operator new(std::size_t size, std::align_val_t align);

delete表达式将会调用下列两个默认对齐数据的操作之一:

operator delete(void* ptr, std::align_val_t align);
operator delete(void* ptr, std::size_t size, std::align_val_t align);

和下面四个超对齐数据的操作之一:

operator delete(void* ptr, std::align_val_t typealign, std::align_val_t align);
operator delete(void* ptr, std::size_t size, std::align_val_t typealign,
                                             std::align_val_t align);
operator delete(void* ptr, std::align_val_t align);
operator delete(void* ptr, std::size_t size, std::align_val_t align);

再提醒一次,用户自定义的placement delete可能会有帮助。

30.3 实现全局的new()运算符

默认情况下,C++平台现在会提供很多的operator new()operator delete()重载 (包括相应的数组版本):

void* ::operator new(std::size_t);
void* ::operator new(std::size_t, std::align_val_t);
void* ::operator new(std::size_t, const std::nothrow_t&) noexcept;
void* ::operator new(std::size_t, std::align_val_t, const std::nothrow_t&) noexcept;

void ::operator delete(void*) noexcept;
void ::operator delete(void*, std::size_t) noexcept;
void ::operator delete(void*, std::align_val_t) noexcept;
void ::operator delete(void*, std::size_t, std::align_val_t) noexcept;
void ::operator delete(void*, const std::nothrow_t&) noexcept;
void ::operator delete(void*, std::align_val_t, const std::nothrow_t&) noexcept;

void* ::operator new[](std::size_t);
void* ::operator new[](std::size_t, std::align_val_t);
void* ::operator new[](std::size_t, const std::nothrow_t&) noexcept;
void* ::operator new[](std::size_t, std::align_val_t, const std::nothrow_t&) noexcept;

void ::operator delete[](void*) noexcept;
void ::operator delete[](void*, std::size_t) noexcept;
void ::operator delete[](void*, std::align_val_t) noexcept;
void ::operator delete[](void*, std::size_t, std::align_val_t) noexcept;
void ::operator delete[](void*, const std::nothrow_t&) noexcept;
void ::operator delete[](void*, std::align_val_t, const std::nothrow_t&) noexcept;

如果你想实现自己的内存管理(例如,为了调试动态内存分配的调用),你不需要重写所有这些重载。 默认情况下只需要实现下面几个特定的函数就可以了,所有其他的函数(包括数组的版本)都是调用 这几个基本的函数之一:

void* ::operator new(std::size_t);
void* ::operator new(std::size_t, std::align_val_t);
void ::operator delete(void*) noexcept;
void ::operator delete(void*, std::size_t) noexcept;
void ::operator delete(void*, std::align_val_t) noexcept;
void ::operator delete(void*, std::size_t, std::align_val_t) noexcept;

理论上讲,默认的有大小的operator delete()版本只简单地调用无大小的版本。 然而,将来这一点可能会发生变化,因此这两种你都需要实现 (如果你没有这么做,有些编译器会给出一个警告)。

30.3.1 向后的不兼容性

注意C++17中下面的程序的行为会悄悄的发生变化:

#include <cstddef>  // for std::size_t
#include <cstdlib>  // for std::malloc()
#include <cstdio>   // for std::printf()

void* operator new (std::size_t size)
{
    std::printf("::new called with size: %zu\n", size);
    return ::std::malloc(size);
}

int main()
{
    struct alignas(64) S {
        int i;
    };

    S* p = new S;   // 只有在C++17之前才调用自己的operator new
}

在C++14中,全局的::operator new(size_t)重载会被所有new表达式调用, 这意味着程序总是有如下输出:

::new called with size: 64

自从C++17起,这个程序的行为会发生变化,因为现在默认的超对齐数据的重载

::operator new(size_t, align_val_t)

将会被调用,而我们 没有 替换这个版本。因此,程序将不会再输出上面那一行。

注意这个问题只适用于全局的operator new()。如果S有类型特定 的operator new(),那么这个运算符将用作超对齐数据的备选项, 这样的话这个程序的行为将和C++17之前的行为一样。

注意这里故意使用了printf()来避免当分配内存时std::cout又分配内存, 这会导致无限递归错误(最好情况下也是core dump)。

30.4 追踪所有::new调用

下面的程序演示了怎么联合内联变量和[[nodiscard]]一起 使用新的operator new()来追踪所有的::new调用。请看如下头文件:

#ifndef TRACKNEW_HPP
#define TRACKNEW_HPP

#include <new>      // for std::align_val_t
#include <cstdio>   // for printf()
#include <cstdlib>  // for malloc()和aligned_alloc()
#ifdef _MSC_VER
#include <malloc.h> // for _aligned_malloc()和_aligned_free()
#endif

class TrackNew {
private:
    static inline int numMalloc = 0;    // malloc调用的次数
    static inline size_t sumSize = 0;   // 总共分配的字节数
    static inline bool doTrace = false; // 开启追踪
    static inline bool inNew = false;   // 不追踪new重载里的输出
public:
    static void reset() {               // 重置new/memory计数器
        numMalloc = 0;
        sumSize = 0;
    }

    static void trace(bool b) {         // 开启/关闭trace
        doTrace = b;
    }

    //  被追踪的分配内存的实现:
    static void* allocate(std::size_t size, std::size_t align, const char* call) {
        // 追踪内存分配:
        ++numMalloc;
        sumSize += size;
        void* p;
        if (align == 0) {
            p = std::malloc(size);
        }
        else {
#ifdef _MSC_VER
            p = _aligned_malloc(size, align);       // Windows API
#else
            p = std::aligned_alloc(align, size);    // C++17 API
#endif
        }
        if (doTrace) {
            // 不要在这里使用std::cout,因为它可能在我们在处理内存分配时
            // 分配内存(最好情况也是core dump)
            printf("#%d %s ", numMalloc, call);
            printf("(%zu bytes, ", size);
            if (align > 0) {
                printf("%zu-byte aligned) ", align);
            } else {
                printf("def-aligned) ");
            }
            printf("=> %p (total: %zu bytes)\n", (void *) p, sumSize);
        }
        return p;
    }

    static void status() {  // 打印当前的状态
        printf("%d allocations for %zu bytes\n", numMalloc, sumSize);
    }
};

[[nodiscard]]
void* operator new (std::size_t size) {
    return TrackNew::allocate(size, 0, "::new");
}

[[nodiscard]]
void* operator new (std::size_t size, std::align_val_t align) {
    return TrackNew::allocate(size, static_cast<std::size_t>(align), "::new aligned");
}

[[nodiscard]]
void* operator new[] (std::size_t size) {
    return TrackNew::allocate(size, 0, "::new[]");
}

[[nodiscard]]
void* operator new[] (std::size_t size, std::align_val_t align) {
    return TrackNew::allocate(size, static_cast<std::size_t>(align), "::new[] aligned");
}

// 确保释放操作匹配:
void operator delete (void* p) noexcept {
    std::free(p);
}
void operator delete (void* p, std::size_t) noexcept {
    ::operator delete(p);
}
void operator delete (void* p, std::align_val_t) noexcept {
#ifdef  _MSC_VER
    _aligned_free(p);   // Windows API
#else
    std::free(p);       // C++17 API
#endif
}
void operator delete (void* p, std::size_t, std::align_val_t align) noexcept {
    ::operator delete(p, align);
}

#endif  // TRACKNEW_HPP

考虑在如下CPP文件中使用这个头文件:

#include "tracknew.hpp"
#include <iostream>
#include <string>

int main()
{
    TrackNew::reset();
    TrackNew::trace(true);
    std::string s = "string value with 26 chars";
    auto p1 = new std::string{"an initial value with even 35 chars"};
    auto p2 = new std(std::align_val_t{64}) std::string[4];
    auto p3 = new std::string [4] { "7 chars", "x", "or 11 chars",
                                    "a string value with 28 chars" };
    TrackNew::status();
    ...
    delete p1;
    delete[] p2;
    delete[] p3;
}

输出依赖于追踪器被实例化的具体时间和其他初始化分配了多少内存。 然而,它应该包含类似于如下这几行的输出:

#1 ::new (27 bytes, def-aligned) => 0x8002ccc0 (total: 27 Bytes)
#2 ::new (24 bytes, def-aligned) => 0x8004cd28 (total: 51 Bytes)
#3 ::new (36 bytes, def-aligned) => 0x8004cd48 (total: 87 Bytes)
#4 ::new[] aligned (100 bytes, 64-byte aligned) => 0x8004cd80 (total: 187 Bytes)
#5 ::new[] (100 bytes, def-aligned) => 0x8004cde8 (total: 287 Bytes)
#6 ::new (29 bytes, def-aligned) => 0x8004ce50 (total: 316 Bytes)
6 allocations for 316 bytes

在这个例子中,第一个输出是为s分配内存。注意根据std::string类的 内存分配策略这个值可能会更大。

之后的两行是因为第二个请求:

auto p1 = new std::string{"an initial value with even 35 chars"};

这个请求为核心的字符串对象分配了24字节,又为字符串的初始值分配了36个字节 (再提醒一次,不同平台上值可能不同)。

第三次调用请求一个64字节对齐的4个string的数组。 最后的调用也分配了两次:一次是为数组,一次是为最后一个string的初始值。 只有最后的string的值分配了内存,因为库的实现使用了经典的短字符串优化, 这是string通常有一个最多15个字符的数据成员,长度小于这个值时不会在堆上分配内存。 如果没有这个优化这里可能会有5次内存分配。