在现代C++编程中,智能指针(如std::unique_ptrstd::shared_ptr)已经成为资源管理的最佳实践。然而,如何高效、安全地创建这些智能指针,仍然是一个值得深入探讨的问题。《Effective Modern C++》的条款二十一明确指出,应当优先使用std::make_uniquestd::make_shared,而非直接使用new关键字。本文将详细解读这一建议背后的原因,并分析其适用场景与局限性。


一、为什么避免直接使用new

在C++中,直接使用new关键字来动态分配内存,虽然功能强大,但也伴随着一些潜在的问题。以下是直接使用new的几个主要缺点:

1. 代码冗余

当我们直接使用new时,通常需要显式地指定类型两次:一次用于智能指针的声明,另一次用于new操作符。例如:

std::unique_ptr<Widget> upw2(new Widget);

这种重复的代码不仅增加了编写成本,还容易在类型变更时引发错误。

2. 异常安全性问题

如果在将new返回的指针赋值给智能指针之前发生异常,那么new分配的内存将无法被正确释放,导致内存泄漏。例如:

process(std::shared_ptr<Widget>(new Widget), badFun());

在这段代码中,如果badFun()抛出异常,那么new Widget分配的内存将永远无法被释放。

3. 性能问题

对于std::shared_ptr,直接使用new会导致两次内存分配:一次用于对象本身,另一次用于控制块(control block)。这会带来额外的开销。


二、std::make_uniquestd::make_shared的优势

为了解决上述问题,C++标准库提供了两个便捷的函数:std::make_unique(C++14引入)和std::make_shared(C++11引入)。它们不仅简化了代码,还提升了程序的安全性和性能。

1. 代码简洁性

使用std::make_uniquestd::make_shared可以显著减少代码量,并避免类型冗余。例如:

// 使用std::make_unique
auto upw = std::make_unique<Widget>();

// 使用std::make_shared
auto spw = std::make_shared<Widget>();

与直接使用new相比,这些代码不仅更简洁,而且更易于维护。

2. 异常安全性

std::make_uniquestd::make_shared将内存分配和智能指针的创建作为一个整体操作来处理。如果在内存分配过程中发生异常,函数将直接抛出异常,而不会留下未释放的内存。例如:

std::shared_ptr<Widget> spw = std::make_shared<Widget>();

即使在内存分配过程中发生异常,也不会有任何内存泄漏的风险。

3. 性能优化

对于std::make_shared,它通过一次性分配内存(包括对象和控制块),避免了两次内存分配的开销。这在处理大量共享指针时,可以显著提升性能。例如:

// 使用new会导致两次内存分配
std::shared_ptr<Widget> spw(new Widget);

// 使用make_shared只会分配一次内存
auto spw = std::make_shared<Widget>();

三、std::make_uniquestd::make_shared的局限性

尽管std::make_uniquestd::make_shared在大多数情况下表现优异,但它们也有一些无法完成的任务。以下是它们的主要局限性:

1. 不支持自定义析构器

std::make_uniquestd::make_shared无法接受自定义的析构器(deleter)。如果你需要为智能指针指定特定的内存释放逻辑,那么直接使用new仍然是唯一的选择。

2. 无法直接接受初始化列表

std::make_uniquestd::make_shared无法直接接受大括号初始化(std::initializer_list)。例如:

// 直接使用初始化列表会导致编译错误
auto spv = std::make_shared<std::vector<int>>{10, 20};

不过,可以通过间接方式实现:

auto p = {10, 20};
auto spv = std::make_shared<std::vector<int>>(p);

3. 与自定义内存管理的类不兼容

对于那些自定义了operator newoperator delete的类,std::make_shared可能无法正常工作。这是因为std::shared_ptr除了管理对象内存外,还需要管理控制块内存。如果控制块的内存管理方式与对象内存不一致,可能会导致不可预知的问题。

4. 内存释放的延迟问题

对于std::make_shared,由于对象内存和控制块内存是一次性分配的,因此即使std::shared_ptr的引用计数为0,只要还有std::weak_ptr引用该对象,内存就不会被释放。这在某些内存敏感的场景下可能会带来性能问题。


四、适用场景总结

尽管std::make_uniquestd::make_shared存在一些局限性,但在大多数情况下,它们仍然是创建智能指针的最佳选择。以下是一些适用场景的总结:

  • 优先选择std::make_uniquestd::make_shared的场景:

    • 当需要创建std::unique_ptrstd::shared_ptr时。
    • 当需要避免类型冗余和内存泄漏风险时。
    • 当希望提升代码的可读性和安全性时。
  • 避免使用std::make_uniquestd::make_shared的场景:

    • 当需要使用自定义析构器时。
    • 当需要直接传递大括号初始化列表时。
    • 当处理自定义内存管理的类时。
    • 当内存释放的延迟可能带来性能问题时。

五、结论

std::make_uniquestd::make_shared是现代C++编程中的重要工具,它们通过减少代码冗余、提升异常安全性以及优化性能,显著简化了智能指针的创建过程。然而,正如任何工具一样,它们也有自己的局限性。在实际编程中,我们需要根据具体场景灵活选择,以充分发挥它们的优势,同时规避潜在的风险。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐