C++ 中的所有权语义

2025 年 5 月 13 日 | 阅读 7 分钟

C++ 中的所有权语义是定义内存和文件句柄等资源如何管理的基础概念。所有权确实对这些资源的生命周期有直接影响,这对于确保没有内存泄漏和最大限度地减少运行时错误的可能性至关重要。自 C++11 引入智能指针和移动语义以来,所有权已成为现代 C++ 资源管理的一个支柱。本章将深入探讨这些方面,以建立对 C++ 中所有权的全面理解。

所有权可分为两类

  • 独占所有权:所有权利由一个单一实体持有,该实体管理资源的整个生命周期。
  • 共享所有权:权利分布在多个实体之间,并且仅当最后一个实体释放其持有后,资源才会被释放。

RAII 和所有权

资源获取初始化 (RAII) 是体现所有权概念的核心 C++ 习惯用法。在 RAII 中,资源分配和释放与对象的生命周期相关联。C++ 通过确保资源在构造函数中获取,并在析构函数中释放,来强制执行可预测的所有权模式。

例如

在这种方法中,资源类本身处理其资源的生命周期。当资源类的对象创建时,资源将被获取,当它超出范围时,它将自动释放资源,即 RAII 原则。

原始指针和所有权问题

在 C++ 的早期版本中,所有权通常是通过原始指针来处理的。尽管原始指针简单灵活,但它们不会明确指示所有权,因此很容易导致双重删除、内存泄漏和悬空指针等问题。

例如,考虑以下代码:

代码可以工作,但如果我们忘记删除 ptr,就会导致内存泄漏。同样,如果我们删除两次,就会导致未定义行为。后者已通过 C++11 标准引入的智能指针得以解决。

智能指针和所有权

智能指针类专门用于自动化动态分配内存的管理。它们封装了原始指针,并负责在不再需要时解除分配内存,从而更有效地管理所有权。

std::unique_ptr 的独占所有权

std::unique_ptr 是资源的独占所有者。在任何给定时间,只有一个 std::unique_ptr 实例可以拥有一个资源,这强制执行了清晰的所有权语义,并且当 std::unique_ptr 超出范围时,资源会被自动删除。

std::unique_ptr 不能被复制,只能被移动,这强化了其独占所有权

移动后,ptr1 会失去资源的拥有权并被设置为 nullptr。

std::shared_ptr 的共享所有权

当需要共享资源的所有权时,可以使用 std::shared_ptr 函数。多个 std::shared_ptr 可以拥有一个资源,并且当最后一个 std::shared_ptr 超出范围时,资源将被自动释放。

在下面的情况下,sharedPtr1 和 sharedPtr2 共享整数的所有权。当 sharedPtr1 和 sharedPtr2 都被销毁时,资源将被释放。

std::weak_ptr 的弱所有权

std::weak_ptr 是一个非拥有引用,它可以观察但不拥有资源。它通常用于在共享所有权可能导致内存泄漏时打破循环引用。std::weak_ptr 不会增加 std::shared_ptr 的引用计数。

如果 sharedPtr 被销毁,资源将被释放,weakPtr 将不再有效。std::weak_ptr 函数还提供了一个 lock 函数,通过提供一个临时的 std::shared_ptr,使访问资源变得安全。

移动语义和所有权转移

自 C++11 起,移动语义通过允许“移动”而不是深层复制资源,实现了高效的所有权转移。移动通过右值引用 (&&) 实现。

移动 std::unique_ptr

std::unique_ptr 函数是一个很好的例子,因为它强制执行独占所有权;std::unique_ptr 对象只能通过移动而不能通过复制来转移。

移动后,ptr1 被设置为 nullptr,因为它的所有权已转移到 ptr2。

移动构造函数和移动赋值运算符

管理资源的类通常定义一个移动构造函数和一个移动赋值运算符,以实现安全的所有权转移。以下是一个具有移动语义的自定义类的示例:

通过移动语义,我们可以安全地转移 MyClass 中资源的拥有权,而无需进行昂贵的复制。

所有权和容器

所有权语义适用于 C++ 容器,如 std::vector 和 std::list。如果对象被添加到容器中,则适用所有权。容器会创建所包含对象的副本,除非它们包含智能指针或已被显式移动。

例如

这表明在上面的示例中,std::unique_ptr 已被移动到 vector 中,从而将所有权转移给了容器。

常见的所有权场景

  1. 资源池
    资源通常只分配一次,然后重新用于资源池。对于这种所有权,常见的用法场景是 std::shared_ptr,因为多个对象应该偶尔共享数据库连接或线程池。
  2. 管理依赖关系
    面向对象编程中的所有权语义用于控制类之间的依赖关系。例如,std::weak_ptr 函数可用于父子关系,以避免出现循环引用和内存泄漏。

示例

让我们举一个例子来说明 C++ 中的所有权语义。

输出

Ownership Semantics in C++

高级所有权模式和最佳实践

std::unique_ptr、std::shared_ptr 和 std::weak_ptr 的所有权语义可用于更复杂的高级资源管理模式,同时牢记它们的基本用法。

1. 自定义删除器:智能指针还支持指定自定义删除器。这对于内存以外的资源管理操作非常有用。例如,std::unique_ptr 函数还可以采用自定义删除器来管理文件句柄、套接字或任何需要不同清理例程的其他类型的资源。

2. Pimpl 习惯用法:使用 std::unique_ptr 将类的实现细节隐藏在 .cpp 文件中,从而减少依赖性,甚至加快编译速度。该习惯用法强制执行清晰的所有权,并将实现细节保留在一个私有位置。

3. 基于容器的仅移动类型:C++ 应用程序开发人员现在可以创建旨在不可复制但可移动的类,通过所有权语义,这会删除复制构造函数和赋值运算符。现在,在容器中使用仅移动类型将是高效的,因为像 std::vector 这样的容器将能够在不进行不必要复制的情况下转移所有权,从而限制资源消耗。

这些程序允许高性能、内存安全的 C++ 作者保留对资源的严格控制。因此,所有权语义是现代 C++ 编程的重要组成部分,它允许开发人员充分利用 RAII 和自动资源管理的强大功能。

多线程环境下的所有权

所有权语义在多线程应用程序中非常重要,尤其是在共享资源和并发访问资源时。在这种情况下,当多个线程共享资源时,通常会应用 std::shared_ptr 来进行安全的引用计数。这样,只要至少有一个线程需要该资源,该资源就可以保持有效。但是,如果所有线程同时修改资源,共享所有权很容易导致竞态条件。

这些情况通常使用 std::shared_ptr 和 std::mutex 或 std::atomic 来实现,以防止数据争用并确保线程安全。另一方面,std::weak_ptr 函数可用于安全地观察共享资源,而不会阻止其释放,这在访问仅是暂时的且不影响所有权生命周期的情况下很有用。

通过结合这些技术,开发人员可以实现健壮、线程安全的所有权管理,在并发应用程序中平衡效率和安全性。