一文讲懂 C++ 内存调试和智能指针

1 Memory debugging with Dr. Memory

现在,让我们来演示如何使用一种工具来自动检测内存泄漏以及许多其他类型的内存相关错误。

我们将使用的工具名为 “Dr Memory”。它是完全跨平台的,因此无论学生使用何种操作系统或 CPU 架构,它都能在每个学生的电脑上运行。内存博士可以检测各种内存错误,包括:

  • Memory leaks - 内存泄漏
  • Reading from uninitialized memory - 读取未初始化的内存
  • Accessing unallocated memory - 访问未分配内存
  • Attempting to free the same memory block more than once - 多次尝试释放同一内存块、

要使用记忆博士,请从网站 "下载 "部分的相关链接下载最新版本,并按照网站 "文档 "部分的相关说明进行安装。我们将使用以下测试程序:

int main()
{
    double *p = new double[1000];
    if (p[0] == 0) // ERROR: Reading from uninitialized memory address!
        p[1] = 0;
    p[1000] = 0; // ERROR: Writing to unallocated memory address!
    // ERROR: We did not free the allocated memory!

    double *t = new double[1000];
    delete[] t;
    delete[] t; // ERROR: Attempting to free the same memory block twice!
}

编译该程序时,与调试时一样,使用参数 -ggdb3,再加上以下附加参数:

  • -static-libgcc-static-libstdc++ 指示编译器将 C 和 C++ 标准库的代码直接放在可执行文件中(作为静态链接库),而不是使用所有 C++ 程序共享的单独文件(共享库)。这将大大增加可执行文件的大小,但允许内存调试器直接访问标准库的代码。
  • -fno-inline 将禁用函数内联,这可能会干扰内存调试。
  • -fno-omit-frame-pointer 指示编译器不要在函数中省略名为帧指针的指针,这也会干扰内存调试。

添加这些参数后,tasks.jsonargs 字段应该与下面相似:

"args": [
    "${workspaceFolder}\\*.cpp",
    "-o",
    "${workspaceFolder}\\CSE701.exe",
    "-Wall",
    "-Wextra",
    "-Wconversion",
    "-Wsign-conversion",
    "-Wshadow",
    "-Wpedantic",
    "-std=c++20",
    "-ggdb3",
    "-static-libgcc",
    "-static-libstdc++",
    "-fno-inline",
    "-fno-omit-frame-pointer"
],

你可以选择终端 > 运行编译任务…或按下 Ctrl+Shift+B,使用这些参数编译程序,而无需实际运行它。请注意,虽然程序中有 4 个非常严重的错误,但编译器不会发出任何警告!如果运行程序,会因为重复删除数组 t 而崩溃。但我们不会直接运行程序,而是通过内存博士来检测内存问题。

要运行 Dr Memory,首先在工作区文件夹下创建一个名为 drmemory 的子文件夹,然后进入 VS Code 集成终端(Ctrl+`),输入

drmemory -logdir ./drmemory -batch -- CSE701.exe

在 Linux 或 Mac 上,应删除 .exe 扩展名。此外,这还假定 Dr Memory 的二进制文件夹已添加到系统的 PATH 环境变量中,安装程序会自动执行该操作。如果不成功,你可能需要手动将该文件夹添加到 PATH。

参数 -logdir ./drmemory 将指示内存博士将日志存储在 drmemory 子文件夹中,而不是工作区之外的其他文件夹中。-batch 将指示 Dr. Memory 运行后不要自动打开结果文件。最后,--CSE701.exe 表示调试器应运行程序 CSE701.exe

终端将显示大量信息,包括程序中检测到的任何错误。程序执行完毕后,drmemory 子文件夹下会创建一些新文件和文件夹。其中一个文件夹名为 DrMemory-CSE701.exe.XXXXX.000XXXXX 是某个数字。第一次运行内存博士时,它可能会重启以自动生成一些所需的文件,在这种情况下会出现两个编号不同的文件夹。

打开 DrMemory-CSE701.exe.XXXXX.000 文件夹,然后打开 results.txt 文件。该文件包含的信息与输出到终端的信息相同,但以文件形式阅读更方便。如果你有两个文件夹,那么其中一个会包含一个空的 results.txt 文件,你可以放心地忽略它。
下面是我在 results.txt 中得到的结果:

Error #1: UNADDRESSABLE ACCESS beyond top of stack: reading 0x000000000065fab0-0x000000000065fab8 8 byte(s)
# 0 .text                                   [C:/_/M/mingw-w64-crt-git/src/mingw-w64/mingw-w64-crt/crt/pesect.c:230]
# 1 _pei386_runtime_relocator               [C:/_/M/mingw-w64-crt-git/src/mingw-w64/mingw-w64-crt/crt/pseudo-reloc.c:477]
# 2 __tmainCRTStartup                       [C:/_/M/mingw-w64-crt-git/src/mingw-w64/mingw-w64-crt/crt/crtexe.c:279]
# 3 .l_start                                [C:/_/M/mingw-w64-crt-git/src/mingw-w64/mingw-w64-crt/crt/crtexe.c:212]
# 4 KERNEL32.dll!BaseThreadInitThunk
Note: @0:00:00.401 in thread 14880
Note: 0x000000000065fab0 refers to 744 byte(s) beyond the top of the stack 0x000000000065fd98
Note: instruction: or     $0x0000000000000000 (%rcx) -> (%rcx)

Error #2: UNINITIALIZED READ: reading register xmm0
# 0 main               [C:/Users/barak/CSE 701/main.cpp:4]
Note: @0:00:00.425 in thread 14880
Note: instruction: ucomisd %xmm0 %xmm1

Error #3: UNADDRESSABLE ACCESS beyond heap bounds: writing 0x0000000001d54620-0x0000000001d54628 8 byte(s)
# 0 main               [C:/Users/barak/CSE 701/main.cpp:6]
Note: @0:00:00.427 in thread 14880
Note: instruction: movsd  %xmm0 -> (%rax)

Error #4: INVALID HEAP ARGUMENT to free 0x0000000001d54640
# 0 replace_operator_delete_array_nothrow               [d:\drmemory_package\common\alloc_replace.c:2999]
# 1 main                                                [C:/Users/barak/CSE 701/main.cpp:11]
Note: @0:00:00.444 in thread 14880
Note: memory was previously freed here:
Note: # 0 replace_operator_delete_array_nothrow               [d:\drmemory_package\common\alloc_replace.c:2999]
Note: # 1 main                                                [C:/Users/barak/CSE 701/main.cpp:10]

Error #5: POSSIBLE LEAK 26 direct bytes 0x0000000001d401c0-0x0000000001d401da + 0 indirect bytes
# 0 replace_malloc                    [d:\drmemory_package\common\alloc_replace.c:2577]
# 1 msvcrt.dll!realloc               +0x193    (0x00007ffb13fb9f44 <msvcrt.dll+0x19f44>)
# 2 msvcrt.dll!unlock                +0x40c    (0x00007ffb13fdb68d <msvcrt.dll+0x3b68d>)
# 3 msvcrt.dll!_getmainargs          +0x30     (0x00007ffb13fa7a01 <msvcrt.dll+0x7a01>)
# 4 pre_cpp_init                      [C:/_/M/mingw-w64-crt-git/src/mingw-w64/mingw-w64-crt/crt/crtexe.c:171]
# 5 msvcrt.dll!initterm              +0x42     (0x00007ffb13fda553 <msvcrt.dll+0x3a553>)
# 6 __tmainCRTStartup                 [C:/_/M/mingw-w64-crt-git/src/mingw-w64/mingw-w64-crt/crt/crtexe.c:269]
# 7 .l_start                          [C:/_/M/mingw-w64-crt-git/src/mingw-w64/mingw-w64-crt/crt/crtexe.c:212]
# 8 KERNEL32.dll!BaseThreadInitThunk

Error #6: LEAK 8000 direct bytes 0x0000000001d526e0-0x0000000001d54620 + 0 indirect bytes
# 0 replace_operator_new_array               [d:\drmemory_package\common\alloc_replace.c:2929]
# 1 main                                     [C:/Users/barak/CSE 701/main.cpp:3]

FINAL SUMMARY:

DUPLICATE ERROR COUNTS:
    Error #   1:      2

SUPPRESSIONS USED:

ERRORS FOUND:
      2 unique,     3 total unaddressable access(es)
      1 unique,     1 total uninitialized access(es)
      1 unique,     1 total invalid heap argument(s)
      0 unique,     0 total GDI usage error(s)
      0 unique,     0 total handle leak(s)
      0 unique,     0 total warning(s)
      1 unique,     1 total,   8000 byte(s) of leak(s)
      1 unique,     1 total,     26 byte(s) of possible leak(s)
ERRORS IGNORED:
      4 unique,     4 total,  74793 byte(s) of still-reachable allocation(s)
         (re-run with "-show_reachable" for details)
Details: C:\Users\barak\CSE 701\drmemory\DrMemory-CSE701.exe.6900.000\results.txt

让我们来看看这些错误:

  • 据我所知,error #1 和error #5 并不是我们程序本身的错误。它们可能是误报。
  • error #2 是在 main.cpp 第 4 行检测到的 “未初始化读取”。这一行是 if (p[0] == 0),它读取未初始化数组的第一个元素,这意味着它将读取一个垃圾值。
  • error #3 是在 main.cpp 第 6 行检测到的 8 字节(一个双)的 “超出堆边界的不可删除访问”。这是 p[1000] = 0 行,它写入了一个 1000 元素数组中的第 1001 个元素(从零开始计算),因此超出了分配内存的范围。
  • error #4 是在 main.cpp 第 11 行检测到的 “INVALID HEAP ARGUMENT to free”。这是第二个 delete[] t,它试图释放一个已经被释放的内存块。事实上,日志告诉我们该内存在第 10 行已经被释放。
  • error #6 是在 main.cpp 第 3 行检测到的 8000 字节的 “泄漏”(1000 个双字节,即整个数组 p)。这一行 double *p = new double[1000],分配了数组。事实上,我们忘记删除这个数组了。

警告:正如前面所强调的,C 和 C++ 让程序员手动管理内存,而不是像大多数高级语言那样自动管理内存,这可以大大提高性能,但也极易出错。例如,即使您没有在程序中明确使用动态内存分配,您仍有可能使用未初始化的值或访问超出数组边界的内存。因此,使用内存调试工具(如内存博士)检查与内存相关的错误非常重要。

修正所有错误(我会让你自己想办法),重新编译程序,然后再次运行内存博士。你会发现所有的错误(除了任何可能的误报,如上面的 #1 和 #5)都会消失。
完成内存调试后,应删除本节开头添加到 tasks.json 中的所有新编译器参数。

2 “Resource Acquisition Is Initialization”

资源获取即初始化(RAII)是 C++ 编程中一个非常重要的概念。从本质上讲,它意味着每个对象都应管理自己的资源,而且只能以这种方式管理资源。这些资源包括内存、文件、磁盘空间、网络连接以及其他任何有限的资源。

您可以将 RAII 视为封装和类不变性概念的扩展:

  • 每个对象不仅封装了数据和处理数据的函数,还封装了存储和处理数据所需的所有资源。
  • 资源分配被认为是类的不变量,因此如果分配得当,就可以认为资源已经分配完毕并可供使用。

一种可能发生内存泄漏的常见情况是,在一个类中分配内存,然后将指向已分配内存块的指针传递给另一个类。在这种情况下,很难保证内存会被正确地解除分配;它可能最终根本不会被解除分配,从而导致内存泄漏,或者被解除分配多次,从而导致崩溃。

相反,您应该遵循 RAII 原则,只在一个类中分配内存和访问分配的内存。内存应作为构造函数的一部分进行分配,并作为析构函数的一部分进行解分配。其他类绝不能直接访问内存,而只能通过内存管理类的成员函数来访问。这样既能大大简化程序中内存分配的工作方式,又能减少可能导致内存泄漏和其他内存相关错误的机会。

3 Smart pointers

确保不会发生内存泄漏的一种方法是使用 STL vector,它可以根据需要在上自动分配、重新分配和取消分配内存。不过,我们也看到这样做会带来性能上的损失。在现代 C++ 中,避免内存泄漏的更复杂、更优化的方法是使用智能指针。

使用 new 操作符分配内存时,不要将结果存储在必须确保稍后删除的原始指针中,而是将其存储在智能指针中。智能指针对象现在拥有原始指针,当智能指针离开作用域时,例如当代码块或函数结束时,它会自动重新分配相关的内存块。

智能指针是 C++ 垃圾收集的替代品,但与高级语言中的垃圾收集不同,智能指针基本上没有性能开销,因此完全没有理由不使用智能指针。

使用智能指针,你就不必再担心手动删除内存,尤其是不必担心删除语句因异常或其他不可预见的情况而无法执行,而内存泄漏通常就是这样发生的。无论发生什么情况,内存都会自动被删除。

头文件 中定义了三种类型的智能指针:

unique_ptr 是唯一的智能指针,这意味着同一个原始指针不能被多个 unique_ptr 对象拥有。智能指针需要唯一的原因是,如果一个原始指针由两个不同的智能指针对象管理,那么它们在超出范围时都会试图删除同一个原始指针,这将导致程序崩溃。unique_ptr 本质上与原始指针一样高效,是最常用的智能指针。
shared_ptr 是一种共享智能指针,这意味着同一个原始指针可以被多个 shared_ptr 对象所拥有。通过在幕后跟踪有多少个 shared_ptr 对象引用同一个原始指针,可以保证原始指针只被删除一次,即在其所有所有者都退出作用域之后。不过,这种称为引用计数的过程会增加复杂性并影响性能,因此除非万不得已,否则不建议使用 shared_ptr。
如果想访问现有的 shared_ptr,而又不想参与引用计数,则可以使用 weak_ptr。

我们只讨论 unique_ptr。它的使用方法如下:

unique_ptr<T> pointer(new T);

这里,T 是分配对象的类型,pointer 是智能指针的名称。我们还可以为一个(未初始化的)数组分配内存:

unique_ptr<T[]> pointer(new T[size]);

其中 size 是数组的大小。如果我们想将数组初始化为零,可以像往常一样在 new 操作符后添加 () 来实现:

unique_ptr<T[]> pointer(new T[size]());

一旦定义了 unique_ptr,我们就可以正常工作了,因为我们知道,当智能指针离开作用域(通常意味着声明它的对象被销毁)时,我们分配的对象或数组将自动为我们解除分配。
我们可以使用以下成员函数:

get() 返回智能指针拥有的原始指针,如果没有原始指针,则返回 nullptr。
如果智能指针拥有一个对象,则 bool 运算符(即把智能指针放在 if 语句中的结果)返回 true,否则返回 false。
reset(pointer) 释放当前拥有的指针(如果拥有指针),并指示智能指针拥有原始指针。
赋值操作符 = 是一个移动操作符,用于在两个 unique_ptr 对象之间转移所有权。它必须与函数 move() 结合使用。语法为 p1 = move(p2),其结果是 p1 将拥有之前由 p2 拥有的原始指针,而 p2 不再拥有任何原始指针。

此外

如果智能指针指向单个对象,即模板形式为 unique_ptr<T>,则可以使用 * 来取消引用原始指针,并且可以使用 -> 来访问对象的成员,这与任何指向对象的指针都是一样的。
如果智能指针指向一个数组,即模板的形式为 unique_ptr<T[]>,则可以像访问任何数组一样,使用 [] 访问元素。

我们将在下面的程序中演示智能指针的使用:

#include <iostream>
#include <memory>
#include <string>
using namespace std;

template <typename T>
void print_smart_pointer(const T &ptr, const string &name, const uint64_t &size)
{
    if (ptr)
    {
        cout << "The smart pointer " << name << " owns a raw pointer with the address " << ptr.get() << ".\n";
        cout << "The elements of the array at that address are: ";
        for (uint64_t i = 0; i < size; i++)
            cout << ptr[i] << ' ';
        cout << '\n';
    }
    else
        cout << "The smart pointer " << name << " does not own a raw pointer.\n";
}

template <typename T>
void print_smart_pointers(const T &ptr1, const T &ptr2, const uint64_t &size)
{
    print_smart_pointer(ptr1, "ptr1", size);
    print_smart_pointer(ptr2, "ptr2", size);
}

template <typename T>
void set_elements(const T &ptr, const uint64_t &size)
{
    for (uint64_t i = 0; i < size; i++)
        ptr[i] = i * i;
}

int main()
{
    constexpr uint64_t size = 5;

    unique_ptr<int64_t[]> ptr1, ptr2;
    cout << "Initial state:\n";
    print_smart_pointers(ptr1, ptr2, size);

    ptr1.reset(new int64_t[size]);
    cout << "\nAfter ptr1.reset(new int64_t[size]):\n";
    set_elements(ptr1, size);
    print_smart_pointers(ptr1, ptr2, size);

    ptr2 = move(ptr1);
    cout << "\nAfter ptr2 = move(ptr1):\n";
    print_smart_pointers(ptr1, ptr2, size);
}

Output:

Initial state:
The smart pointer ptr1 does not own a raw pointer.
The smart pointer ptr2 does not own a raw pointer.

After ptr1.reset(new int64_t[size]):
The smart pointer ptr1 owns a raw pointer with the address 0x1a23dffa030.
The elements of the array at that address are: 0 1 4 9 16
The smart pointer ptr2 does not own a raw pointer.

After ptr2 = move(ptr1):
The smart pointer ptr1 does not own a raw pointer.
The smart pointer ptr2 owns a raw pointer with the address 0x1a23dffa030.
The elements of the array at that address are: 0 1 4 9 16

警告: 应尽可能避免使用 new 创建一个原始指针,然后再通过构造函数或 reset() 成员函数将其赋值给智能指针,因为在 new 和将原始指针赋值给智能指针之间总是有可能发生错误,从而导致内存泄漏。最好直接在智能指针的构造函数或 reset() 成员函数的参数中使用 new,以确保智能指针立即拥有原始指针。

换句话说,要避免做这样的事情:

double *raw_pointer = new double[1000];
do_some_things();
// BAD: An error could have occurred before we assigned the smart pointer!
unique_ptr<double[]> smart_pointer(raw_pointer);

or

unique_ptr<double[]> smart_pointer;
double *raw_pointer = new double[1000];
do_some_things();
// BAD: An error could have occurred before we assigned the smart pointer!
smart_pointer.reset(raw_pointer);

取而代之的是:

// GOOD: Use new directly inside the argument to the constructor, so the smart pointer is assigned immediately, with no intermediate steps.
unique_ptr<double[]> smart_pointer(new double[1000]);
// We can still use the raw pointer, but only AFTER assigning the smart pointer, using get():
double *raw_pointer = smart_pointer.get();

或者这样:

unique_ptr<double[]> smart_pointer;
// GOOD: Use new directly inside the argument to the reset member function, so the smart pointer is assigned immediately, with no intermediate steps.
smart_pointer.reset(new double[1000]);
// We can still use the raw pointer, but only AFTER assigning the smart pointer, using get():
double *raw_pointer = smart_pointer.get();

4 Performance of smart pointers

我们可以使用下面的程序比较向量、unique_ptr 和原始指针的性能:

#include <chrono>
#include <iostream>
#include <memory>
#include <vector>
using namespace std;

class timer
{
public:
    void start()
    {
        start_time = chrono::steady_clock::now();
    }

    void end()
    {
        elapsed_time = chrono::steady_clock::now() - start_time;
    }

    double seconds() const
    {
        return elapsed_time.count();
    }

private:
    chrono::time_point<chrono::steady_clock> start_time = chrono::steady_clock::now();
    chrono::duration<double> elapsed_time = chrono::duration<double>::zero();
};

int main()
{
    const uint64_t size = 1'000'000'000;
    timer tmr;
    cout.precision(2);
    cout << fixed;

    {
        tmr.start();
        vector<int64_t> test(size);
        for (uint64_t i = 0; i < size; i++)
            test[i] = i;
        tmr.end();
    }
    cout << "vector, access via []:              " << tmr.seconds() << " seconds.\n";

    {
        tmr.start();
        vector<int64_t> test(size);
        int64_t *ptr_to_test = test.data();
        for (uint64_t i = 0; i < size; i++)
            ptr_to_test[i] = i;
        tmr.end();
    }
    cout << "vector, access via raw pointer:     " << tmr.seconds() << " seconds.\n";

    {
        tmr.start();
        unique_ptr<int64_t[]> test(new int64_t[size]);
        for (uint64_t i = 0; i < size; i++)
            test[i] = i;
        tmr.end();
    }
    cout << "unique_ptr, access via []:          " << tmr.seconds() << " seconds.\n";

    {
        tmr.start();
        unique_ptr<int64_t[]> test(new int64_t[size]);
        int64_t *ptr_to_test = test.get();
        for (uint64_t i = 0; i < size; i++)
            ptr_to_test[i] = i;
        tmr.end();
    }
    cout << "unique_ptr, access via raw pointer: " << tmr.seconds() << " seconds.\n";

    {
        tmr.start();
        int64_t *test = new int64_t[size];
        for (uint64_t i = 0; i < size; i++)
            test[i] = i;
        tmr.end();
        // Note: Here we must free the memory manually since we are not using a smart pointer!
        delete[] test;
    }
    cout << "Pure raw pointer:                   " << tmr.seconds() << " seconds.\n";
}

我在这里使用的定时器类与上文讨论编译器优化时使用的定时器类相同。每个数组的大小为 8 GB(10 亿个元素,每个元素 8 字节),以便获得更具统计意义的测量结果。如果您的系统内存总量小于 16GB,那么在运行此代码前,应相应减小数组的大小。

由于程序总共分配了 32 GB 内存,这将耗尽我电脑上的所有内存,因此我将每个部分都放在了代码块 {} 中。当矢量或 unique_ptr 对象的代码块结束时,它们就会退出作用域,而内存也会由于智能指针的存在而自动去分配;如果你留意内存的使用情况,就会很容易发现这一点。为了保持一致性,我将手动分配的数组也放在了自己的代码块中;当然,当该代码块结束时,数组不会被自动解除分配,这就是我添加手动删除的原因。

在没有进行任何编译器优化的情况下运行这个程序,我发现:

vector, access via []: 3.88 seconds.
vector, access via raw pointer: 3.76 seconds.
unique_ptr, access via []: 8.26 seconds.
unique_ptr, access via raw pointer: 2.35 seconds.
Pure raw pointer: 2.20 seconds.

我们可以看到,unique_ptr 本质上与原始指针一样快,但前提是我们必须直接访问原始指针,而不是通过智能指针的 [] 操作符。在后一种情况下,它的速度实际上是向量的两倍!这可能是由于每次调用[]都会累积开销。因此,最好调用一次 get(),然后使用获得的原始指针为数组赋值,就像我们在这里所做的那样。

在分配数组的类中实现这种优化的一种方法是,将智能指针和通过 get() 获得的原始指针作为类成员存储。智能指针将用于确保内存的正确解分配,而原始指针将用于访问实际的数组元素。我将在下一节更新的矩阵类中采用这种方法。

我们还可以看到,通过原始指针(通过数据获得)访问矢量要比通过 [] 操作符访问矢量稍快一些。不过,即便如此,它仍然比访问手动分配的数组慢了近一倍。这主要是因为,正如我之前所强调的,矢量在构建时会自动将所有元素初始化为零,这是在浪费时间,因为我们无论如何都要将它们重新初始化为其他值。

有趣的是,如果我在所有新建语句(即 new int64_tsize)中添加(),以强制它们也初始化为零,就会发现类似的结果。这可能是因为编译器对初始化进行了优化(尽管没有启用优化标志)。

如果启用编译器优化,情况就会大为改观。在 tasks.json 中使用 -O3 参数(该参数指示 GCC 使用所有可用的优化方法)后,两种访问方法([] 与原始指针)之间的差异消失了,无论是对于 vector 还是 unique_ptr:

vector, access via []: 1.25 seconds.
vector, access via raw pointer: 1.23 seconds.
unique_ptr, access via []: 0.93 seconds.
unique_ptr, access via raw pointer: 0.93 seconds.
Pure raw pointer: 0.90 seconds.

现在,在所有情况下,unique_ptr 的速度基本上与纯粹的原始指针一样快,尽管向量仍然落后很多,因为即使在最大优化情况下,双初始化也没有被优化掉。

正如我之前所说,我的性能优化理念是,永远不要相信编译器会替我完成工作,而应始终编写可自行优化的代码,不依赖编译器的优化。因此,在需要最高性能的情况下,我的建议是始终使用 unique_ptr,但通过原始指针访问数组。

总之,智能指针提供了与原始指针完全相同的性能,但它们的使用是 100% 安全的,因此在现代 C++ 中,除了在科学编程中可能永远不会遇到的特殊情况外,根本没有理由手动使用 new 和 delete。

还要注意的是,使用矢量而不是智能指针有很多很好的理由,因为矢量知道自己的大小,可以很容易地调整大小,提供了可以与 STL 算法一起使用的迭代器,可以与基于范围的 for 循环一起使用,还有很多其他方便的特性。

矢量的唯一缺点是双重初始化会降低性能,但在很多情况下,你确实希望将其初始化为零,而且在任何情况下,对性能的影响实际上只在初始化或调整数组大小时才有意义,因为在初始化后访问数组一样快。

最后,我想说的是,我们在本节中遇到的与性能相关的行为似乎是 GCC 特有的。在 Windows 和 Linux 上,当使用 g++ 编译且未进行编译器优化时,我得到了类似的结果。然而,当使用 MSVC(Microsoft Visual C++ 编译器)编译并禁用所有优化 (/Od) 时,得到的结果却截然不同:

vector, access via []: 3.07 seconds.
vector, access via raw pointer: 2.91 seconds.
unique_ptr, access via []: 2.52 seconds.
unique_ptr, access via raw pointer: 2.23 seconds.
Pure raw pointer: 2.01 seconds.

这可能是因为 MSVC 以不同的方式实现了向量和智能指针,在这种特殊情况下,开箱即可获得更好的性能。使用最大优化 (/O2),我得到了与 GCC 中使用 -O3 时类似的结果。这给我的启示是,你应该经常自己进行性能测试,看看哪种方法在特定编译器和操作系统下效果最好。

有关智能指针的更多信息,请参阅 C++ 参考资料Microsoft 的 C++ 参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

疯狂的码泰君

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值