Modern C++——唯一所有权的明确

历史问题

在C++98及更早版本中,内存管理是一个极具挑战性的任务,尤其是在涉及动态内存分配时。由于C++语言本身提供了灵活的内存操作能力,程序员需要手动管理new和delete的使用,以确保所有动态分配的内存最终都能被正确释放。然而,这种灵活性也带来了风险,特别是在复杂的程序中,很容易出现内存管理问题。

一个显著的问题是内存所有权的不明确性。在C++98中,没有内置的机制来自动跟踪和管理对象的内存所有权。当多个指针或对象引用指向同一块动态分配的内存时,很容易发生内存被多次释放(double free)或未被释放(memory leak)的情况。此外,如果一块内存被释放后仍然被引用(use after free),则会导致未定义行为,进而可能引发程序崩溃或数据损坏。

这些问题在大型或复杂的项目中尤为突出,因为内存的使用和释放可能分散在多个函数或类中。程序员需要非常小心地维护内存的所有权关系,并确保在任何时候都不会有多个指针指向同一块内存或遗漏释放内存的操作。然而,即使是最有经验的程序员,也难以完全避免这类错误。

比如下面的代码,我们构造了一个Custom对象,但是使用它的是一个线程函数。那么这个对象的所有权该是main函数还是线程函数process_raw_ptr?似乎我们认为这个对象既可以属于main,也可以属于process_raw。这样,我们既可以在main函数中释放这个对象,也可以在process_raw中释放。 这种灵活的特点,带来了更多的隐患。于是在Modern C++,引入了“唯一所有权”来明确谁持有这个对象,进而就明确了谁来释放这个对象。

void process_raw(Custom* ptr) {
    std::cout << "Processing value: " << ptr->get_value() << std::endl;
}

int main() {
    Custom* custom_raw_ptr = new Custom(10);
    std::cout << "Starting thread" << std::endl;
    std::thread t1(process_raw, custom_raw_ptr);
    t1.join();

    // custom_raw_ptr is still pointing to the allocated memory
    if (custom_raw_ptr) {
        std::cout << "custom_raw_ptr is still valid" << std::endl;
    }

    // Manually delete the pointer to avoid memory leak
    delete custom_raw_ptr;
    custom_raw_ptr = NULL;

	return 0;
}

std::unique_ptr

std::unique_ptr 是一种智能指针,用于独占地管理动态分配的对象。比如上面的例子,如果我们改成std::unique_ptr来实现,可以如下

void process(std::unique_ptr<Custom> ptr) {
    std::cout << "Processing value: " << ptr->get_value() << std::endl;
}

int main() {
    std::unique_ptr<Custom> unique_ptr_custom = std::make_unique<Custom>(30);
    std::thread t1(process, std::move(unique_ptr_custom));

    // unique_ptr_custom is now nullptr
    if (!unique_ptr_custom) {
        std::cout << "unique_ptr_custom is now nullptr" << std::endl;
    }
    
    t1.join();
    
	return 0;
}

std::unique_ptr<Custom> unique_ptr_custom = std::make_unique<Custom>(30);这句构建的unique_ptr_custom唯一所有权暂时属于main函数。因为“唯一所有权”具有唯一性,所以它只能被“转移”,不能被“复制”。于是我们在std::thread t1(process, std::move(unique_ptr_custom));中看到,它的所有权被转移到process函数中。此时main函数不再拥有它的所有权。

自动析构管理对象

也正因为“唯一所有权”的明确,让它管理的对象的释放权得以明确——最后一个唯一所有权拥有者来释放它所管理的对象。而我们可以借助std::unique_ptr的析构来自动完成这个过程。

      ~unique_ptr() noexcept
      {
	static_assert(__is_invocable<deleter_type&, pointer>::value,
		      "unique_ptr's deleter must be invocable with a pointer");
	auto& __ptr = _M_t._M_ptr();
	if (__ptr != nullptr)
	  get_deleter()(std::move(__ptr));
	__ptr = pointer();
      }

对于已经失去唯一所有权的unique_ptr,它所管理的对象的指针也是空——因为移动函数会重置失去唯一管理权的unique_ptr的内部数据。

而对于持有唯一管理权的unique_ptr,则会调用“删除器”来析构对象。

为什么需要“删除器”?在大多数情况下,std::unique_ptr 的默认删除器已经足够处理动态分配的内存,尤其是对于单个对象(使用 delete)和数组(使用 delete[])。然而,有些情况下你可能需要自定义删除器,例如:

  • 自定义内存管理:如果你使用了自定义的内存分配器,那么你可能需要一个相应的自定义删除器来释放内存。
  • 资源管理:如果 std::unique_ptr 管理的资源不是通过 new 分配的内存,而是其他类型的资源(如文件句柄、网络连接等),你需要自定义删除器来正确释放这些资源。
  • 调试和日志记录:在调试和日志记录过程中,你可能希望在删除对象时记录一些信息,这时可以使用自定义删除器。

删除器样例

#include <iostream>
#include <memory>
#include <thread>

class Custom {
public:
    Custom(int val) : value(val) {
        std::cout << "Custom constructor: " << value << std::endl;
    }
    ~Custom() {
        std::cout << "Custom destructor: " << value << std::endl;
    }
    void display() const {
        std::cout << "Value: " << value << std::endl;
    }
private:
    int value;
};

// 自定义删除器
struct CustomArrayDeleter {
    void operator()(Custom* ptr) const {
        std::cout << "CustomArrayDeleter called" << std::endl;
        delete[] ptr;
    }
};

void process_array(std::unique_ptr<Custom[], CustomArrayDeleter> custom_array, int size) {
    for (int i = 0; i < size; ++i) {
        custom_array[i].display();
    }
}

int main() {
    const int array_size = 5;

    // 使用 unique_ptr 管理 Custom 对象的数组,并设置自定义删除器
    std::unique_ptr<Custom[], CustomArrayDeleter> custom_array(new Custom[array_size] {
        Custom(1), Custom(2), Custom(3), Custom(4), Custom(5)
    });

    // 创建线程并将 unique_ptr 传递给线程函数
    std::thread t(process_array, std::move(custom_array), array_size);

    // 等待线程完成
    t.join();

    return 0;
}

适用场景

  • 独占所有权:
    当你希望一个对象只能有一个所有者时,使用 std::unique_ptr。
    例如,管理资源的类成员变量。

  • 避免资源泄漏:
    使用 std::unique_ptr 可以确保在异常情况下自动释放资源,避免内存泄漏。

  • 性能优化:
    std::unique_ptr 的大小和性能与原始指针相同,因为它不需要引用计数。

代码地址

https://github.com/f304646673/cpulsplus/tree/master/smart_pointer/unique_ptr

总结

我们看到C++98的灵活性带来了很多困扰,比如文中提及的“唯一所有权”归属不明,导致对象的释放位置具有随意性。而Modern C++提供了std::unique_ptr来明确所有权归属,这给C++带来了更多的安全性和易维护性。当然,代价是我们要多学习一些概念。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

breaksoftware

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

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

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

打赏作者

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

抵扣说明:

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

余额充值