目录
主页🌏:R6bandito_
所属专栏📰:《C++新特性》
C++智能指针
在使用C/C++进行开发时,时常避免不了与内存资源管理斗智斗勇,例如下列这一代码:
void myfunction() {
auto *ptr_dub = new std::vector<double>;
for (int i = 0 ; i < 5000 ; i++) {
ptr_dub->push_back(static_cast<double>(i));
}
}
这段错误代码示例,你可能一眼就看出了其中的缺陷:我们在堆上开辟了一块动态内存数组,并且不断往内压入一块数据,我们在不断从堆空间上分配数据却从不收回!这导致了严重的内存泄漏。
当然想要解决掉这个问题也很简单,只需记住在该函数末尾delete
掉这块空间就行,就像这样:
void myfunction() {
auto *ptr_dub = new std::vector<double>;
for (int i = 0 ; i < 5000 ; i++) {
ptr_dub->push_back(static_cast<double>(i));
}
delete ptr_dub;
}
然而,再思考一下,真的如此简单吗?这样做真的妥当吗?当代码简单的时候你可能会拍着胸脯放心大胆的说自己绝对不会忘记,这样做就是最简单稳妥的办法。
但万一代码结构十分晦涩复杂呢?万一真的就忘了呢?而且这么做日后拓展怎么办?有没有日后在不经意间注释掉delete
导致内存泄漏的可能?或者原先一段没有任何问题的代码在经过修改之后却抛出异常导致内存泄漏?
很明显,这么草草了事是十分不妥的,比如说这种情况:
void myfunction() {
auto *ptr_dub = new std::vector<double>;
for (int i = 0 ; i < 5000 ; i++) {
ptr_dub->push_back(static_cast<double>(i));
}
/*
some_code
*/
throw std::logic_error("logic error");
/*
some_code
*/
delete ptr_dub;
}
当在执行流程中某一点抛出异常时,由于栈解退机制(详见《C++异常对象及栈回溯机制》),delete
语句将被忽略从而引发内存泄漏。
为了防止内存泄漏,可以使用RAII(Resource Acquisition Is Initializing,资源获取即初始化,采用对象来管理资源)机制来设计一个简单资源管理类:
class Resource {
public:
explicit Resource(std::vector<double> *&ptr_dub) {
auto *temp = ptr_dub;
ptr_dub = nullptr;
pointer_doublevec = temp;
}
Resource(const Resource &other) = delete;
Resource(Resource &&other) {
pointer_doublevec = other.pointer_doublevec;
other.pointer_doublevec = nullptr;
}
Resource &operator=(const Resource &other) = delete;
Resource &operator=(Resource &&other) {
pointer_doublevec = other.pointer_doublevec;
other.pointer_doublevec = nullptr;
return *this;
}
auto get() const {
return this->pointer_doublevec;
}
~Resource() {
if (pointer_doublevec != nullptr) {
delete pointer_doublevec;
pointer_doublevec = nullptr;
}
std::cout << "Resource deleted" << std::endl;
}
private:
std::vector<double> *pointer_doublevec;
};
上面我们自己实现了一个简单的以对象管理资源的方法,将我们开辟在堆上的动态数组交予其管理,便可在任何作用域离开该函数的情况下正确回收资源。
void myfunction() {
auto *ptr_dub = new std::vector<double>;
Resource my_resource(ptr_dub);
if (ptr_dub == nullptr) {
std::cout << "ptr_dub is nullptr." << std::endl;
}
for (int i = 0 ; i < 5000 ; i++) {
my_resource.get()->push_back(static_cast<double>(i));
}
}
--Output:
ptr_dub is nullptr.
Resource deleted
如果你想到了这种方法,那你基本已经掌握了智能指针的要领。
智能指针正是以RAII为核心思想,以对象管理资源的绝佳典范!
前面铺垫了这么多,都是为了强调智能指针在C++中对于资源管理的重大意义。接下来就逐一讲讲C++中几个可帮助管理动态内存分配的指针模板。
std::auto_ptr
智能指针是 C++ 中一种用于管理内存的指针类型,它可以自动管理指针所指向对象的生命周期。智能指针的主要作用是简化内存管理,避免手动释放内存,提高代码的可读性和可维护性。
关于C++中的智能指针,其实早在C11之前,在C98标准中便引入了最初一代的智能指针:auto_ptr
。该类型位于头文件<memory>
中,其主要应用方式与C11中新引入的std::unique_ptr
是很类似的。目前auto_ptr
已经在C11中已被废弃,在C++17被移除。
因此这里我们不着重讲auto_ptr
,只简单谈谈其缺点:
-
拷贝语义的二义性:
std::auto_ptr
的拷贝构造和拷贝赋值并不是传统意义上的拷贝,而是所有权的转移。这与开发者通常期望的拷贝行为不同,不仅违反了现代 C++ 的直观语义,且容易导致一些潜在的问题。
void Function() {
std::auto_ptr<int> p(new int(10));
std::auto_ptr<int> p1(p); //违背常规的潜在移动操作
if (p1.get() != nullptr && p.get() == nullptr) {
std::cout << "p moved to p1" << std::endl;
}
}
--Output:
p moved to p1
-
功能缺陷:
auto_ptr
不支持移动语义(C++98 不支持 std::move
)。删除器不可自定义,导致它在管理非 new
分配的资源(如文件句柄或数组)时显得有心无力。
-
容器的不兼容以及线程不安全性。
上述种种原因,都足以说明auto_ptr
已不再适用于当今快速变化的现代C++语言,因此标准委员会在C++11中推出了全新的std::unique_ptr以及std::shared_ptr等等更强大的新式智能指针模板类型。
std::unique_ptr
unqiue_ptr的构造
std::unique_ptr
是在c++11中被引入的auto_ptr
的平替版本。它也是一个 独占所有权 的智能指针,但相比 auto_ptr
,提供了更安全、清晰的语义。
接下来我们从该类型的构造入手,全面了解std::unqiue_ptr
.
以上是由官方给出的unique_ptr
的原型以及相关的构造函数。
/*
constexpr unique_ptr() noexcept;
constexpr unique_ptr( std::nullptr_t ) noexcept; (1)
*/
std::unique_ptr<int> p(nullptr);
std::unique_ptr<int> q;
std::cout << p.get() << std::endl;
std::cout << q.get() << std::endl;
--Output:
0000000000000000
0000000000000000