文章目录
- 一、引言
- 二、 原始指针的问题
- 1、原始指针的问题
- 2、智能指针如何解决这些问题
- 三、智能指针的类型
- 四、std::shared_ptr
- 1、shared_ptr使用
- 2、shared_ptr的使用注意事项
- 3、定制删除器
- 4、shared_ptr的优缺点
- 5、shared_ptr的模拟实现
- 五、std::unique_ptr
- 1、unique_ptr的使用
- 2、unique_ptr的使用注意事项
- 3、定制删除器
- 4、unique_ptr的优缺点
- 5、unique_ptr的模拟实现
- 六、std::weak_ptr
- 七、RAII模式:资源获取即初始化
- 八、再谈删除器
一、引言
智能指针是现代 C++ 编程中的重要概念,它们为程序员提供了一种更安全、更方便地管理动态内存的方式。在传统的 C++ 编程中,手动管理内存通常会导致一系列问题,例如内存泄漏、悬空指针以及释放已释放的内存等。智能指针的出现解决了这些问题,使得 C++ 编程更加健壮、安全和高效。
智能指针本质上是一种对象,它模拟了指针的行为,但具有自动管理内存的功能。通过使用智能指针,可以避免手动调用 new
和 delete
来分配和释放内存,从而减少了出错的可能性。
二、 原始指针的问题
1、原始指针的问题
在使用原始指针(raw pointers)时,程序员需要手动管理内存的生命周期,这包括分配和释放内存。这种管理方式非常直接但也很容易出错,常见的问题有:
-
内存泄漏(Memory Leaks):
- 当动态分配的内存不再需要时,程序员需要显式地释放它。如果忘记释放内存,或者由于某种原因(如异常)导致内存释放的代码没有被执行,那么这块内存就会被永久占用,导致内存泄漏。
- 内存泄漏在长时间运行的程序中尤为严重,因为它们会逐渐消耗所有可用的内存,最终导致程序崩溃。
-
悬空指针(Dangling Pointers):
- 如果一个指针被赋予了动态分配的内存地址,并且随后该内存被释放了,但是指针的值并没有被置为
nullptr
或重新分配其他地址,那么这个指针就被称为悬空指针。 - 使用悬空指针会导致不可预测的行为,因为这块内存可能已经被操作系统分配给其他部分使用,或者已经被其他代码覆盖。
- 如果一个指针被赋予了动态分配的内存地址,并且随后该内存被释放了,但是指针的值并没有被置为
-
双重释放(Double Deletion):
- 如果同一块内存被释放了两次,这通常会导致运行时错误,因为第二次释放尝试会试图操作一个已经被标记为“已释放”的内存块。
2、智能指针如何解决这些问题
智能指针是C++标准库提供的一种自动管理内存的机制,它们通过封装原始指针并提供额外的功能来自动处理内存的生命周期。
-
自动内存释放:
- 智能指针在析构时会自动释放它们所指向的内存,从而避免了内存泄漏的问题。
- 例如,
std::unique_ptr
在析构时会调用delete
来释放内存,而std::shared_ptr
则使用引用计数来确保当最后一个shared_ptr
被销毁时,内存才会被释放。
-
防止悬空指针:
- 智能指针在释放内存后会将其置为
nullptr
,从而避免了悬空指针的问题。 - 这意味着即使尝试访问一个已经被销毁的智能指针,它也会安全地返回,而不会导致未定义的行为。
- 智能指针在释放内存后会将其置为
-
防止双重释放:
- 由于智能指针在析构时只释放一次内存,因此它们可以防止双重释放的问题。
- 当将一个智能指针赋值给另一个智能指针时(例如,通过赋值或移动操作),原始的智能指针会自动放弃对内存的所有权,从而确保同一块内存不会被多次释放。
总的来说,智能指针通过自动管理内存的生命周期和提供额外的安全性检查来解决了原始指针常见的问题。然而,它们并不是万能的,仍然需要程序员谨慎使用以避免其他类型的错误。
三、智能指针的类型
当谈到C++中的智能指针时,通常指的是std::unique_ptr
、std::shared_ptr
和std::weak_ptr
这三种类型。它们在管理动态内存分配和资源所有权方面提供了更安全和方便的方法。这三种类型都定义在memory
头文件中。
- std::unique_ptr:
- 特点:
std::unique_ptr
提供了独占所有权的智能指针。这意味着同一时间只能有一个std::unique_ptr
指向同一个资源,当指针超出范围或被销毁时,它所指向的资源会被自动释放。 - 适用场景:当需要确保资源只有一个所有者时,
std::unique_ptr
是一个很好的选择。比如,当在函数中分配了一个资源,但是需要在函数返回后释放资源时,使用std::unique_ptr
可以确保资源在函数退出时被正确释放。
- 特点:
- std::shared_ptr:
- 特点:
std::shared_ptr
允许多个指针共享同一个资源。它使用引用计数来跟踪资源的所有者数量,并在没有所有者时释放资源。 - 适用场景:当需要多个指针共享同一资源,并且不清楚哪个指针会最后释放资源时,
std::shared_ptr
是一个很好的选择。比如,当需要在多个地方引用同一个对象,但不想手动跟踪所有权时,使用std::shared_ptr
可以简化管理。
- 特点:
- std::weak_ptr:
- 特点:
std::weak_ptr
是一种弱引用智能指针,它不增加资源的引用计数,指向std::shared_ptr
所管理的对象。它用于解决std::shared_ptr
可能导致的循环引用问题。 - 适用场景:当需要引用
std::shared_ptr
所管理的资源,但不希望增加资源的引用计数时,可以使用std::weak_ptr
。比如,在观察者模式中,观察者可能需要引用被观察者,但不应该影响被观察者的生命周期。
- 特点:
总的来说,选择哪种智能指针类型取决于需求和设计。如果需要确保资源只有一个所有者,使用
std::unique_ptr
;如果需要多个所有者,使用std::shared_ptr
;如果需要避免循环引用,使用std::weak_ptr
。
四、std::shared_ptr
std::shared_ptr
是 C++11 引入的一个智能指针,用于管理动态分配的对象。它的主要特点是可以共享所有权,并通过引用计数来管理资源的释放,它具有以下特点:
-
共享所有权:
std::shared_ptr
允许多个指针共享对同一资源的所有权。这意味着当最后一个指向资源的std::shared_ptr
被销毁时,资源才会被释放。 -
引用计数:
std::shared_ptr
内部维护一个引用计数器,用于跟踪有多少个std::shared_ptr
指向相同的资源。每当创建或销毁一个std::shared_ptr
时,引用计数都会相应地增加或减少。
使用场景:
- 多个所有者:当需要多个对象共享同一资源的所有权时,
std::shared_ptr
是一个很好的选择。比如,在设计图形用户界面(GUI)时,多个对象可能需要访问同一块内存或同一个文件资源。- 循环引用:
std::shared_ptr
可以用于解决循环引用的问题,因为它会自动处理对象之间的引用计数,确保在没有被引用时能够正确释放资源。
1、shared_ptr使用
make_shared<T>(args)
:返回一个shared_ptr
,指向一个动态分配的类型为T
的对象,使用args
初始化此对象。
shared_ptr<T>p(q)
:p
是shared_ptr q
的拷贝,此操作会递增q
中的计数,因为 p
和 q
现在都指向了相同的资源。这种操作允许多个智能指针共享同一块内存,同时确保在最后一个指针超出作用域时释放资源。q
中的指针必须能转换为T*
,即 q
所管理的资源类型能够隐式转换为 T
类型的指针。这通常是因为 q
的类型本身是 shared_ptr
,并且 T
类型是 q
中指针的类型或者可以从 q
中指针的类型隐式转换为 T*
。p=q
:p
和q
都是shared_ptr
,所保存的指针必须能相互转换。此操作会递减p
的引用计数,递增q
的引用计数;若p
的引用计数变为0,则将其管理的原内存释放。
p.unique
:若p.use count()
为1,返回true
;否则返回false
。
p.use_count()
:返回与p
共享对象的智能指针数量;可能很慢,主要用于调试。
std::shared_ptr
的 reset
函数用于重新分配被管理的资源,或者将其置为空(不管理任何资源)。reset
函数接受一个可选的参数,用于指定新的资源。如果不提供参数,则该 std::shared_ptr
将置为空。
以下是 std::shared_ptr
的 reset
函数的一般语法:
void reset(); // 重置为 nullptr,不管理任何资源
void reset(T* ptr); // 重置为指定的指针 ptr,开始管理该指针指向的资源
void reset(nullptr_t); // 重置为 nullptr,不管理任何资源
其中 T* ptr
是指向被管理的资源的原始指针,nullptr_t
是空指针类型。使用 reset
函数可以安全地在不同的 std::shared_ptr
之间转移资源的所有权,或者在不再需要资源时释放它。
以下是一些示例说明了 reset
函数的用法:
#include <iostream>
#include <memory>
int main() {
// 创建一个 shared_ptr 来管理动态分配的整数
std::shared_ptr<int> ptr(new int(42));
// 重新分配资源为一个新的整数
ptr.reset(new int(100));
// 释放资源,置为空指针
ptr.reset();
return 0;
}
在这个示例中,我们首先创建了一个 std::shared_ptr
来管理动态分配的整数。然后,我们使用 reset
函数将该 std::shared_ptr
重新分配为指向一个新的整数。最后,我们再次调用 reset
函数,这次没有传递任何参数,将该 std::shared_ptr
置为空指针。
2、shared_ptr的使用注意事项
当使用 new
创建对象时,可以将返回的指针包装在 shared_ptr
中,以确保对象的安全共享和自动内存管理。但是若补初始化一个智能指针,它就会被初始化成一个空指针。
shared_ptr<double> p1;
shared_ptr<int> p2(new int(1));
需要注意的是,接受指针参数的智能指针的构造函数是explicit
的。因此,我们不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式来初始化智能指针:
shared_ptr<double> p1 = new int(1024); //错误
shared_ptr<double> p2(new int(1024)); //正确
p1的初始化隐式地要求编译器用一个new
返回的int*
来创建一个shared_ptr
。
由于我们不能进行内置指针到智能指针间的隐式转换,因此这条初始化语句是错误的。出于相同的原因,一个返回shared_ptr
的函数不能在其返回语句中隐式转换一个普通指针:
shared_ptr<int>clone(int p){
return new int(p);
}//错误:不存在从 "int *" 转换到 "std::shared_ptr<int>" 的适当构造函数
我们必须将其显式绑定到一个想要返回的指针上:
shared_ptr<int>clone(int p){
return shared_ptr<int>(new int(p));
}//正确:显式地用int*创建shared ptr<int>
默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用 delete
释放它所关联的对象。我们可以将智能指针绑定到一个指向其他类型的资源的指针上,但是为了这样做,必须提供自己的操作来替代delete
。
**混合使用智能指针和普通指针可能导致内存管理问题。**智能指针会自动管理其所指向的内存资源,而普通指针则需要手动管理内存。混合使用时,可能会导致重复释放内存或者内存泄漏等问题。
重复释放内存:
#include <memory>
int main() {
int* rawPtr = new int(5);
{
std::shared_ptr<int> smartPtr(rawPtr);
// 这里会发生问题,因为当 unique_ptr 离开作用域时,它会尝试释放内存。
// 而 rawPtr 本身并不知道 smartPtr 已经释放了内存,因此可能导致重复释放。
}
delete rawPtr; // 这里会导致重复释放内存,造成未定义行为。
return 0;
}
不能使用get
初始化另一个智能指针或为智能指针赋值。这是因为智能指针的设计初衷是为了自动管理资源。使用get
方法获得底层指针,并且将其用于初始化另一个智能指针或者直接赋值给另一个智能指针,会导致资源的所有权问题。因为这样做会使得两个智能指针都认为自己拥有资源,从而可能导致重复释放资源或者其他未定义行为。
在混合使用智能指针和原始指针时,
get
函数提供了一种将指针传递给无法接受智能指针的代码的方法。但是,强调了这并不意味着可以安全地将get
返回的指针传递给另一个智能指针,因为这可能导致内存所有权混乱和未定义行为。举例说明,当使用get
返回的指针来初始化另一个智能指针时,每个智能指针都认为自己拥有该资源,这可能导致同一块内存被重复释放,或者在使用时发生未定义行为。因此,为了避免这种情况,强调了永远不要将get
返回的指针用于初始化另一个智能指针或为另一个智能指针赋值。
#include <iostream>
#include <memory>
int main() {
std::shared_ptr<int> ptr1(new int(42));
// 使用 get 返回的原始指针来初始化另一个智能指针
std::shared_ptr<int> ptr2(ptr1.get()); // 错误的做法!
// 此时,ptr1 和 ptr2 都认为自己拥有该资源,这会导致问题
// 当程序结束时,ptr1 和 ptr2 都尝试释放相同的内存,导致未定义行为
return 0;
}
在这个示例中,我们尝试使用 ptr1
的原始指针来初始化 ptr2
,这是一种错误的做法。现在两个 std::shared_ptr
都认为它们拥有相同的资源。
这样做可能导致内存重复释放的问题或者更糟糕的未定义行为。所以,对于 std::shared_ptr
,同样要避免使用 get
返回的指针来初始化另一个智能指针或为另一个智能指针赋值。
3、定制删除器
为了为 std::shared_ptr
定制删除器,可以在创建 std::shared_ptr
对象时,提供一个自定义的删除器函数对象。这个删除器函数对象将在 std::shared_ptr
的引用计数变为0时被调用,以释放所管理的资源。以下是一个简单的示例,演示了如何为 std::shared_ptr
提供自定义的删除器:
#include <iostream>
#include <memory>
// 自定义删除器函数对象
struct CustomDeleter {
void operator()(int* p) const {
std::cout << "Custom deleter is called.\n";
delete p; // 自定义的删除操作
}
};
int main() {
// 使用自定义删除器创建 shared_ptr
std::shared_ptr<int> ptr(new int(42), CustomDeleter());
// 手动重置 shared_ptr
ptr.reset(new int(100));
return 0;
}
在这个例子中,我们定义了一个名为 CustomDeleter
的结构体,它重载了调用运算符 operator()
,以执行我们自定义的删除操作。然后,在创建 std::shared_ptr
对象时,我们通过在括号中提供 CustomDeleter
的一个临时实例来指定这个自定义的删除器。当 std::shared_ptr
的引用计数为0时,CustomDeleter
中的 operator()
将被调用来释放所管理的资源。
需要注意的是,使用自定义删除器时,确保删除器能够正确释放所管理的资源,并且与
std::shared_ptr
的资源类型兼容。
std::shared_ptr
的 reset
函数在传递自定义删除器时的行为与不传递删除器时略有不同。当使用自定义删除器创建 std::shared_ptr
时,必须显式地指定新资源,以便 reset
函数能够知道要使用哪个删除器。
让我们看看如何使用自定义删除器来重新分配资源:
#include <iostream>
#include <memory>
// 自定义删除器函数对象
struct CustomDeleter {
void operator()(int* p) const {
std::cout << "Custom deleter is called.\n";
delete p; // 自定义的删除操作
}
};
int main() {
// 创建 shared_ptr,并传递自定义删除器
std::shared_ptr<int> ptr(new int(42), CustomDeleter());
// 使用 reset 重新分配资源,并传递自定义删除器
ptr.reset(new int(100), CustomDeleter());
return 0;
}
/*
运行结果:
Custom deleter is called.
Custom deleter is called.
*/
在这个示例中,我们使用自定义删除器创建了一个 std::shared_ptr
,然后使用 reset
函数重新分配了资源,并且仍然传递了相同的自定义删除器。这确保了在资源管理转移到新分配的整数时,仍然使用相同的删除器来释放旧资源。
4、shared_ptr的优缺点
- 优点:
- 自动管理资源生命周期,无需手动释放。
- 允许多个指针共享所有权,灵活性高。
- 可以避免循环引用导致的内存泄漏。
- 缺点:
- 额外的开销:
std::shared_ptr
内部需要维护引用计数,可能会带来额外的开销。 - 不能解决循环依赖:当存在 A 指向 B,B 指向 A 的情况时,即使使用了
std::shared_ptr
,仍然会导致资源无法释放的问题。
- 额外的开销:
循环依赖是指两个或多个对象之间相互依赖,形成一个环形结构。在 C++ 中,使用 std::shared_ptr
来管理资源的所有权时,循环依赖可能导致资源无法正确释放的问题,这被称为“循环引用”或“循环依赖”。
考虑以下情况:对象 A 拥有一个指向对象 B 的 shared_ptr
,而对象 B 同样拥有一个指向对象 A 的 shared_ptr
。这样一来,当没有其他对象持有 A 和 B 时,它们之间的引用计数永远不会降为零,因为彼此持有对方的指针,导致它们的析构函数永远不会被调用,从而资源无法释放,造成内存泄漏。
这种情况下,使用 std::weak_ptr
可以打破循环依赖。weak_ptr
是一种弱引用,它允许观察 shared_ptr
指向的对象,但不会增加其引用计数。通过在循环依赖中使用 weak_ptr
,可以防止引用计数永远不会降为零的情况发生,从而正确释放资源。下面是一个示例:
class B; // 前向声明
class A {
public:
std::weak_ptr<B> b_ptr;
A() {
std::cout << "A constructor\n";
}
~A() {
std::cout << "A destructor\n";
}
};
class B {
public:
std::weak_ptr<A> a_ptr;
B() {
std::cout << "B constructor\n";
}
~B() {
std::cout << "B destructor\n";
}
};
int main() {
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->b_ptr = b;
b->a_ptr = a;
// 此时引用计数仍然为 1,但是资源可以正确释放
return 0;
}
在这个例子中,类 A 持有类 B 的 std::weak_ptr
,而类 B 则持有类 A 的 std::weak_ptr
。这样一来,资源就可以正确释放。
class MyClass {
public:
MyClass() {
std::cout << "MyClass constructor called." << std::endl;
}
~MyClass() {
std::cout << "MyClass destructor called." << std::endl;
}
};
int main() {
// 创建一个智能指针,共享一个 MyClass 实例的所有权
std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>();
{
// 创建另一个智能指针,共享相同的 MyClass 实例的所有权
std::shared_ptr<MyClass> ptr2 = ptr1;
// 此时引用计数为 2
std::cout << "Reference count: " << ptr1.use_count() << std::endl;
} // ptr2 超出作用域,引用计数减少为 1
// ptr1 仍然指向相同的 MyClass 实例,引用计数为 1
std::cout << "Reference count: " << ptr1.use_count() << std::endl;
// ptr1 超出作用域,引用计数减少为 0,MyClass 实例被销毁
return 0;
}
在上面的示例中,std::shared_ptr
被用来管理 MyClass
的实例。ptr1
和 ptr2
共享对相同 MyClass
实例的所有权。当 ptr2
超出作用域时,引用计数减少为 1,但资源不会被释放,因为仍然有一个 std::shared_ptr
持有它。最后,当 ptr1
也超出作用域时,引用计数减少为 0,MyClass
实例被销毁。
5、shared_ptr的模拟实现
template<class T>
class shared_ptr {
public:
// RAII
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pcount(new int(1))
{}
template<class D>
shared_ptr(T* ptr ,D del)
:_ptr(ptr)
, _pcount(new int(1))
,_del(del)
{}
~shared_ptr() {
if (_ptr) {
release();
}
}
shared_ptr(const shared_ptr<T>& sp) {
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
}
void release() {
if (--(*_pcount) == 0) {
cout << "delete:" << _ptr << endl;
delete _pcount;
_del(_ptr);
//delete _ptr;
}
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
//if (&sp != this) //分析 为什么不行 sp1 = sp2; sp2是sp1构造的。
if (sp._ptr != _ptr) {
release();
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
}
return *this;
}
int use_count() { return *_pcount; }
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
T* get()const { return _ptr; }
private:
T* _ptr;
int* _pcount;
std::function<void(T*)> _del = [](T* ptr) {delete ptr; };
};
这个是一个简单的 shared_ptr
模板类的模拟实现。让我们来分析一下:
-
构造函数: 类中定义了两个构造函数。第一个构造函数接受一个指向
T
类型对象的指针,将其作为初始资源,并为计数器分配一个新的int
对象来跟踪引用计数。第二个构造函数类似于第一个,但还接受一个可调用对象作为删除器,用于释放资源。 -
析构函数: 析构函数释放资源。如果引用计数器为 0,则调用删除器释放资源。
-
拷贝构造函数: 拷贝构造函数复制指针和计数器,并增加引用计数。
-
release 函数: 减少引用计数,并在引用计数为 0 时释放资源。
-
赋值操作符重载: 赋值操作符重载实现了浅拷贝语义。如果两个
shared_ptr
指向不同的资源,则释放当前资源并复制新的资源,并增加新资源的引用计数。 -
use_count 函数: 返回当前引用计数的值。
-
重载 * 和 -> 运算符: 使得
shared_ptr
可以像指针一样操作。 -
get 函数: 返回指向的原始指针。
这个模拟实现中考虑了资源管理和拷贝语义,并且使用引用计数来跟踪共享资源的引用情况。
五、std::unique_ptr
std::unique_ptr
是 C++11 中引入的智能指针之一,它具有以下特点:
- 独占所有权(Exclusive Ownership):
unique_ptr
确保在任意时间点只有一个unique_ptr
实例可以指向一个特定的对象。当unique_ptr
被销毁时,它所管理的对象也会被销毁。即不允许拷贝。 - 移动语义(Move Semantics):
unique_ptr
支持移动语义,因此可以在不复制实际对象的情况下将所有权从一个unique_ptr
转移到另一个。这使得unique_ptr
在资源管理和传递所有权时非常高效。 - 自动释放资源(Automatic Resource Release):通过使用
unique_ptr
,可以确保在不再需要对象时自动释放资源,避免了手动管理内存的复杂性和潜在的内存泄漏。
一个
unique_ptr
“拥有”它所指向的对象。与shared_ptr
不同,某个时刻只能有一个unique_ptr
指向一个给定的对象。当unique_ptr
被销毁时,它所指向的对象也被销毁。
1、unique_ptr的使用
对于unique_ptr
,初始化不必使用直接初始化形式。它允许使用直接初始化形式,也可以使用拷贝初始化形式,因为 unique_ptr
有移动构造函数。这意味着可以通过赋值运算符或者其他可以转换为 unique_ptr
类型的函数返回值初始化 unique_ptr
。
std::unique_ptr<int> ptr(new int(42));
std::unique_ptr<int> ptr = std::make_unique<int>(42);
在第二种情况下,我们使用 std::make_unique
来创建一个新的 int
对象,并将返回的 std::unique_ptr
直接初始化为 ptr
。make_unique
是c++14引入的。
unique_ptr<double>pl; //可以指向一个double的unique_ptr
unique_ptr<int>p2(new int(42)); //p2指向一个值为 42的int
由于一个 unique_ptr
拥有它指向的对象,因此unique_ptr
不支持普通的拷贝构造和赋值构造操作:
unique_ptr<string> p1(new string("stegosaurus"));
unique_ptr<string>p2(p1); //错误:unique ptr不支持拷贝
unique_ptr<string>p3;
p3 = p2; //错误:unique ptr不支持赋值
下面我会举一个例子来说明std::unique_ptr
的移动构造和移动赋值,以及它不支持拷贝构造和拷贝赋值。
class MyClass {
public:
MyClass() {
std::cout << "MyClass constructor called." << std::endl;
}
~MyClass() {
std::cout << "MyClass destructor called." << std::endl;
}
};
int main() {
// 使用移动构造
std::unique_ptr<MyClass> ptr1(new MyClass); // 创建 ptr1
std::unique_ptr<MyClass> ptr2(std::move(ptr1)); // 移动构造 ptr2,ptr1变为空指针
// 使用移动赋值
std::unique_ptr<MyClass> ptr3(new MyClass); // 创建 ptr3
ptr1 = std::move(ptr3); // 移动赋值,ptr3变为空指针
// 以下代码会导致编译错误,因为 std::unique_ptr 不支持拷贝构造和拷贝赋值
// std::unique_ptr<MyClass> ptr4(ptr1); // 拷贝构造
// std::unique_ptr<MyClass> ptr5 = ptr3; // 拷贝赋值
return 0;
}
在这个例子中,我们首先创建了两个std::unique_ptr
对象ptr1
和ptr3
,并分别通过移动构造和移动赋值将它们的所有权转移给了ptr2
和ptr1
。然后,我们尝试使用拷贝构造和拷贝赋值来创建新的std::unique_ptr
对象,但是编译器会报错,因为std::unique_ptr
不支持拷贝操作。
当使用std::unique_ptr
时,有几个函数和操作值得详细解释:
-
reset()
reset()
函数用于释放unique_ptr
当前持有的指针,并将unique_ptr
置为空指针。这意味着它不再拥有任何资源。- 例如:
std::unique_ptr<int> ptr(new int(42)); ptr.reset(); // 释放资源并将ptr置为空指针
-
reset(nullptr)
reset(nullptr)
是reset()
函数的一种特例,它释放当前持有的指针,并将unique_ptr
置为空指针。- 例如:
std::unique_ptr<int> ptr(new int(42)); ptr.reset(nullptr); // 释放资源并将ptr置为空指针
-
release()
release()
函数放弃unique_ptr
对指针的控制权,并返回指针,但不会释放资源。这意味着在调用release()
之后,unique_ptr
将不再管理该资源,需要手动释放。该函数仅切断了这原来管理对象之间的联系。- 例如:
std::unique_ptr<int> ptr(new int(42)); int* rawPtr = ptr.release(); // 放弃控制权并返回指针 // 现在ptr为空指针,需要手动释放rawPtr指向的资源 delete rawPtr;
-
reset(q)
reset(q)
函数允许将unique_ptr
重新指向一个新的指针q
,释放当前持有的指针。如果q
不为空,则unique_ptr
开始管理q
指向的资源。- 例如:
std::unique_ptr<int> ptr(new int(42)); int* newPtr = new int(100); ptr.reset(newPtr); // 释放原始资源,开始管理newPtr指向的资源
虽然我们不能拷贝或赋值
unique_ptr
,但可以使用release
或reset
将指针的所有权从一个(非const
)unique_ptr
转移给另一个unique_ptr
。
2、unique_ptr的使用注意事项
当unique_ptr
作为函数返回值时,涉及到的是C++的移动语义。
移动语义允许将临时对象的资源“窃取”(也就是转移)给另一个对象,而不是执行深层的复制操作。这种转移是通过移动构造函数和移动赋值运算符来实现的,它们将资源从一个对象转移到另一个对象,而不是像拷贝构造函数和拷贝赋值运算符那样创建一个资源的完整副本。
在unique_ptr
的情况下,当函数返回一个unique_ptr
时,如果返回值被赋值给另一个unique_ptr
,则发生移动语义。例如,考虑以下情况:
std::unique_ptr<int> createInt(int a) {
return std::make_unique<int>(a);
}
int main() {
std::unique_ptr<int> ptr1 = createInt(42);
// 移动语义:将createInt返回的unique_ptr的资源转移给ptr1
return 0;
}
在这个例子中,createInt()
返回一个std::unique_ptr<int>
,它持有一个动态分配的int
对象。当createInt()
返回时,它的返回值会被移动到ptr1
中,这意味着指针所指向的资源所有权被转移,不会执行资源的深层复制。这样可以避免额外的内存分配和释放,提高程序的性能和效率。
返回
unique_ptr
的函数允许有效地管理资源的所有权,同时通过移动语义来避免不必要的资源复制。
3、定制删除器
当使用 unique_ptr
时,可以提供一个自定义的删除器,以便在释放指针时执行特定的操作。删除器是一个函数或函数对象,它接受指针并释放它所指向的资源。
与重载关联容器(set
)的比较操作类似,必须在指定unique_ptr
指向的类型后,提供删除器类型。在创建或重置unique_ptr
对象时,必须提供一个特定类型的可调用对象。这样做允许控制unique_ptr
销毁其持有对象时的行为,非常类似于重载关联容器的比较操作以控制排序行为。
-
unique_ptr<T> u1;
和unique_ptr<T, D> u2;
u1
是一个使用默认删除器delete
(std::default_delete<T>
) 的unique_ptr
。这意味着当u1
超出作用域或被显式释放时,它所管理的指针将被delete
释放。u2
是一个使用自定义删除器D
的unique_ptr
。这意味着当u2
被释放时,它所管理的指针将被传递给D
所指定的自定义删除函数或函数对象来释放。
-
unique_ptr<T, D> u(d);
- 在这种情况下,通过构造函数参数
d
,创建了一个带有自定义删除器D
的unique_ptr
对象u
。这意味着当u
被释放时,它所管理的指针将被传递给D
所指定的自定义删除函数或函数对象来释放。
- 在这种情况下,通过构造函数参数
通过提供自定义删除器,可以更灵活地控制 unique_ptr
如何管理其所拥有的资源的生命周期。
在 std::unique_ptr
中,可以通过以下方式来定制删除器:
-
函数指针或函数对象: 最简单的方式是通过函数指针或函数对象来指定删除器。这个删除器会在
std::unique_ptr
对象超出作用域时被调用,用于释放指针指向的资源。例如:void customDeleter(int* ptr) { delete ptr; } std::unique_ptr<int, decltype(&customDeleter)> ptr(new int(42), customDeleter); std::unique_ptr<int, std::function<void(int*)>> ptr(new int(42), customDeleter); //注意: std::cout << typeid(&customDeleter).name() << std::endl; std::cout << typeid(customDeleter).name() << std::endl; /* 输出结果: void (__cdecl*)(int * __ptr64) void __cdecl(int * __ptr64) */
或者使用 lambda 表达式:
std::unique_ptr<int, std::function<void(int*)>> ptr(new int(42), [](int* ptr) { delete ptr; });
-
函数对象(仿函数): 也可以创建一个函数对象(仿函数),其中实现了
operator()
,并将其传递给std::unique_ptr
的模板参数中。例如:struct CustomDeleter { void operator()(int* ptr) const { delete ptr; } }; std::unique_ptr<int, CustomDeleter> ptr(new int(42)); std::unique_ptr<int, CustomDeleter> ptr(new int(42), CustomDeleter());
-
Lambda 表达式: 可以使用 lambda 表达式作为删除器,这种方式非常方便。例如:
std::unique_ptr<int, std::function<void(int*)>> ptr(new int(42), [](int* ptr) { delete ptr; });
或者更简洁地:
auto deleter = [](int* ptr) { delete ptr; }; std::unique_ptr<int, decltype(deleter)> ptr(new int(42), deleter);
unique_ptr
的reset()
函数不接受删除器作为参数,因此在调用reset()
时,不会更改std::unique_ptr
对象的删除器。原来定义std::unique_ptr
对象时所指定的删除器会一直保留,除非销毁了原有的std::unique_ptr
对象并重新创建一个新的对象,并在创建时使用新的删除器。因此,如果想要更改
std::unique_ptr
的删除器,需要手动销毁原有的对象,并创建一个新的对象,并在创建新对象时指定新的删除器。
4、unique_ptr的优缺点
优点:
- 独占所有权:
unique_ptr
确保每个指针只有一个所有者,这意味着它独占了指向的资源。当std::unique_ptr
被销毁或者被赋予新的指针时,它会自动释放之前所指向的资源。这有助于避免资源泄漏。 - 轻量级:
unique_ptr
是一种轻量级智能指针,它不需要额外的运行时开销,因为它的功能主要是通过编译器支持的语言特性实现的。 - 移动语义支持:
unique_ptr
支持移动语义,因此可以通过移动而非复制来传递所有权。这可以提高性能,并且在某些情况下,移动语义可以避免不必要的资源复制或者资源转移。
缺点:
-
独占性质限制:
unique_ptr
的独占性质也可能是它的一个缺点,因为它不能共享所有权。如果需要在多个地方共享指针所有权,那么unique_ptr
就不适用。 -
不支持复制:
unique_ptr
不能进行复制,因为它的拷贝构造函数和拷贝赋值运算符被删除了。这意味着您不能直接将unique_ptr
传递给函数,除非您使用了移动语义或者显示地将其转移所有权。 -
使用限制: 对于一些复杂的场景,如循环引用的管理,
unique_ptr
可能不够灵活。在这种情况下,可能需要使用shared_ptr
或者其他更复杂的智能指针。
总体而言,unique_ptr
是一种非常有用的智能指针,特别适用于管理动态分配的资源,并且在性能和资源管理方面提供了很多优势。然而,它也有其使用上的限制,需要根据具体情况来选择最合适的智能指针类型。
下面是一个示例代码,演示了 unique_ptr
的基本用法:
class MyClass {
public:
MyClass() { std::cout << "MyClass Constructor" << std::endl; }
~MyClass() { std::cout << "MyClass Destructor" << std::endl; }
void someMethod() { std::cout << "Some Method of MyClass" << std::endl; }
};
int main() {
// 创建一个 std::unique_ptr,管理 MyClass 的对象
std::unique_ptr<MyClass> ptr1(new MyClass());
// 调用对象的方法
ptr1->someMethod();
// 移动 ptr1 到 ptr2
std::unique_ptr<MyClass> ptr2 = std::move(ptr1);
// 此时 ptr1 不再拥有对象的所有权
if (!ptr1) {
std::cout << "ptr1 is nullptr" << std::endl;
}
// ptr2 拥有对象的所有权
ptr2->someMethod();
// 当 ptr2 超出作用域时,对象会被销毁
return 0;
}
在这个示例中,unique_ptr
确保了 MyClass
对象的自动释放,无论是因为指针超出作用域还是因为移动指针。
5、unique_ptr的模拟实现
template<class T>
class unique_ptr {
public:
// RAII
unique_ptr(T* ptr)
:_ptr(ptr)
{}
~unique_ptr() {
reset();
}
// 删除拷贝构造函数和拷贝赋值运算符,确保只有一个 unique_ptr 可以管理资源
unique_ptr(const unique_ptr<T>&) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>&) = delete;
// 移动构造函数
unique_ptr(unique_ptr<T>&& other) noexcept
: _ptr(other.release())
{}
// 移动赋值运算符
unique_ptr<T>& operator=(unique_ptr<T>&& other) noexcept {
if (this != &other) {
reset(other.release());
}
return *this;
}
T& operator*() const { return *_ptr; }
T* operator->() const { return _ptr; }
// 返回指向被管理资源的原始指针
T* get() const noexcept { return _ptr; }
// 释放资源所有权
T* release() noexcept {
T* releasedPtr = _ptr;
_ptr = nullptr;
return releasedPtr;
}
// 重置 unique_ptr,释放当前资源并接管新资源
void reset(T* ptr = nullptr) noexcept {
if (_ptr != ptr) {
delete _ptr;
_ptr = ptr;
}
}
private:
T* _ptr;
};
让我们逐个解释这个类的各个部分:
-
构造函数和析构函数:构造函数接受一个原始指针作为参数,用于初始化
unique_ptr
,并且在析构函数中释放资源。这是 RAII(资源获取即初始化)的一个例子,确保资源在unique_ptr
生命周期结束时被正确释放。 -
删除拷贝构造函数和拷贝赋值运算符:通过将拷贝构造函数和拷贝赋值运算符声明为
delete
,禁止了unique_ptr
的拷贝,从而确保了资源的独占所有权。 -
移动构造函数和移动赋值运算符:通过移动构造函数和移动赋值运算符,
unique_ptr
可以从另一个unique_ptr
实例中获取资源的所有权,而不进行资源的复制。这提高了效率,并避免了资源的重复释放。 -
解引用和成员访问运算符重载:这些重载允许像使用原始指针一样使用
unique_ptr
,使得用户可以像操作普通指针一样访问所管理的资源。 -
get()
函数:返回指向被管理资源的原始指针,使得用户可以在需要时直接操作原始指针。 -
release()
函数:释放unique_ptr
对资源的所有权,并返回指向该资源的原始指针。这允许用户在不删除资源的情况下放弃对资源的所有权,常见于需要将资源传递给 C API 或延迟释放资源的情况。 -
reset()
函数:重置unique_ptr
,释放当前资源并接管新资源。如果传递了新的原始指针,则unique_ptr
会释放当前资源并获取新资源的所有权,如果未传递任何指针,则unique_ptr
会释放当前资源而不获取新资源,相当于将unique_ptr
重置为空指针。
六、std::weak_ptr
std::weak_ptr
是 C++ 中用于解决 std::shared_ptr
循环引用问题的工具。它是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr
管理的对象。
循环引用通常发生在两个或多个对象相互持有对方的
shared_ptr
实例时,导致它们的引用计数永远不会归零,从而无法释放它们的内存,造成内存泄漏。因为它们会增加资源的引用计数,导致资源无法被正确释放。
weak_ptr
允许我们观测由 shared_ptr
管理的对象,但不会增加引用计数。一旦最后一个指向对象的shared_ptr
被销毁,对象就会被释放。即使有weak_ptr
指向对象,对象也还是会被释放。因此这种智能指针”弱“共享对象。
下面是一个示例代码,演示了如何使用 weak_ptr
:
class B; // 提前声明 B 类
class A {
public:
void setB(std::shared_ptr<B> b) {
_b = b;
}
private:
std::weak_ptr<B> _b;
};
class B {
public:
void setA(std::shared_ptr<A> a) {
_a = a;
}
private:
std::weak_ptr<A> _a;
};
int main() {
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->setB(b);
b->setA(a);
// 使用 a 和 b,它们彼此共享资源,但不会导致循环引用
return 0;
}
在这个示例中,类 A
和 B
相互引用,但是其中的一个使用了 std::weak_ptr
。这样做可以确保在没有循环引用的情况下共享资源,并且不会导致内存泄漏。
七、RAII模式:资源获取即初始化
RAII(Resource Acquisition Is Initialization)是一种C++编程范式,它利用对象的生命周期来管理资源的获取和释放。其核心思想是:在对象的构造函数中获取资源,在析构函数中释放资源。这样做的好处是,只要对象在作用域内存在,资源就会被正确地管理,不会出现资源泄漏。
RAII 的原理和概念可以总结为以下几点:
-
资源获取即初始化:通过在对象的构造函数中获取资源,利用C++的对象生命周期机制来确保资源在对象生命周期内有效。
-
资源的释放由析构函数负责:在对象的析构函数中释放资源,无论对象是因为正常结束作用域而销毁还是因为异常而销毁,都会确保资源得到正确释放。
-
异常安全性:RAII能够保证在发生异常时资源能够被正确释放,不会出现资源泄漏。
RAII与智能指针有着密切的联系,智能指针本身就是 RAII 的一种实现。智能指针通过将资源(如内存、文件句柄等)的所有权与指针绑定,利用对象的析构函数来确保资源在适当时机释放。
例如,unique_ptr
它在对象被销毁时自动释放内存。通过将动态分配的内存与unique_ptr
对象绑定,可以很容易地实现 RAII。当unique_ptr
对象超出作用域时,其析构函数会被调用,自动释放所管理的内存。
{
std::unique_ptr<int> ptr(new int(42)); // 获取资源(动态分配的内存)
// 在此作用域内,ptr对象存在,资源有效
// 使用ptr指向的内存
std::cout << *ptr << std::endl;
} // ptr对象超出作用域,资源被释放
八、再谈删除器
删除器必须保存为一个指针或一个封装了指针的类。
虽然std::shared_ptr
和std::unique_ptr
都允许使用自定义删除器,但它们在定制删除器方面有一些不同之处:
-
unique_ptr:
unique_ptr
允许通过模板参数来指定删除器,这意味着删除器的类型可以作为unique_ptr
类模板的一部分。例如:std::unique_ptr<T, Deleter>
,其中Deleter
是一个函数对象或者函数指针,用于在指针超出范围时释放资源。- 删除器类型必须与所指向的指针类型匹配,即删除器必须接受指向所指类型的指针作为参数。通常,对于动态分配的单个对象,删除器的函数签名为
void operator()(T*)
;对于动态分配的数组,删除器的函数签名为void operator()(T[])
或void operator()(T*)
。 unique_ptr
的删除器默认使用std::default_delete
,它对应于delete
或delete[]
,取决于指针的类型。
-
shared_ptr:
shared_ptr
不直接支持定制删除器作为模板参数。相反,可以在创建shared_ptr
时传递一个额外的函数对象(或函数指针)参数作为删除器。这个删除器可以是std::default_delete
的自定义版本,也可以是完全不同的函数对象。- 删除器不需要与所指向的类型匹配,因为
shared_ptr
使用类型擦除技术来存储删除器和引用计数等信息。
总的来说,
unique_ptr
在设计上更加灵活,因为它允许在编译时指定删除器类型,并且删除器的类型必须与所指向的指针类型匹配。而shared_ptr
允许在运行时指定删除器,并且删除器不需要与指针类型匹配。
我们可以确定shared_ptr
不是将删除器直接保存为一个成员,因为删除器的类型直到运行时才会知道。实际上,在一个shared_ptr
的生存期中,我们可以随时改变其删除器的类型。我们可以使用一种类型的删除器构造一个shared_ptr
,随后使用reset
赋予此 shared_ptr
另一种类型的删除器。通常,类成员的类型在运行时是不能改变的。因此,不能直接保存删除器。而unique_ptr
则不行。我们再次对它们进行对比:
- 共享所有权 vs. 独占所有权:
std::shared_ptr
允许多个智能指针共享同一块资源,因此资源的生命周期由引用计数来管理。这意味着资源只在最后一个引用计数归零时才会被释放,因此删除器可能不会立即被调用。std::unique_ptr
拥有独占所有权,因此资源在指针被销毁时立即释放。这意味着删除器会在std::unique_ptr
超出作用域或被重置时立即被调用。
- 删除器类型:
- 对于
std::shared_ptr
,删除器类型可以是任何可调用对象,包括函数指针、函数对象和 lambda 表达式,因为std::shared_ptr
不会在编译时执行删除器类型检查。 - 对于
std::unique_ptr
,删除器类型必须作为模板参数之一,在编译时进行类型检查。这意味着必须在编译时提供删除器的确切类型。
- 对于
- 传递参数给删除器:
- 在
std::shared_ptr
中,删除器可以接受额外的参数,并且可以将这些参数传递给删除器函数,这样可以更灵活地管理资源。 - 在
std::unique_ptr
中,如果希望删除器接受额外的参数,则需要将这些参数捕获在lambda
表达式中,或者使用绑定器或者包装器。
- 在
让我们通过示例来说明在std::shared_ptr
和std::unique_ptr
中如何处理删除器参数:
示例1 - 在 std::shared_ptr
中传递额外参数给删除器:
void customDeleter(int* ptr, int extraParam) {
std::cout << "Custom deleter called with extra parameter: " << extraParam << std::endl;
delete ptr;
}
int main() {
int extraParam = 42;
// 使用 lambda 表达式捕获额外参数
auto deleter = [&extraParam](int* ptr) {
std::cout << "Lambda deleter called with extra parameter: " << extraParam << std::endl;
delete ptr;
};
// 创建 shared_ptr 并传递额外参数给删除器
std::shared_ptr<int> ptr(new int(10), std::bind(customDeleter, std::placeholders::_1, extraParam));
std::shared_ptr<int> ptr2(new int(20), deleter);
return 0;
}
在这个示例中,我们定义了一个自定义的删除器 customDeleter
,它接受一个指针和一个额外的参数。然后,我们使用 lambda 表达式或者 std::bind
来捕获额外的参数,并将捕获的参数传递给删除器。最后,我们创建了两个 std::shared_ptr
,并将删除器作为参数传递给它们,从而实现了在 std::shared_ptr
中传递额外参数给删除器。
示例2 - 在 std::unique_ptr
中使用 lambda 表达式捕获额外参数:
int main() {
// 额外参数
int extra_param = 10;
// 使用 lambda 表达式作为删除器,并捕获额外参数
auto customDeleter = [&extra_param](int* ptr) {
std::cout << "Deleting pointer with extra_param: " << extra_param << std::endl;
delete ptr;
};
// 创建 unique_ptr,并指定删除器
std::unique_ptr<int, decltype(customDeleter)> ptr(new int(42), customDeleter);
return 0;
}
在这个示例中,我们使用 lambda 表达式作为删除器,并捕获了外部定义的额外参数 extra_param
。这样,我们就可以在 lambda 表达式中使用这个额外参数来实现更灵活的资源管理。
示例3 - 在 std::unique_ptr
中使用 bind表达式捕获额外参数:
// 自定义删除器,接受额外参数
void customDeleter(int* ptr, int extra_param) {
std::cout << "Deleting pointer with extra_param: " << extra_param << std::endl;
delete ptr;
}
int main() {
// 额外参数
int extra_param = 10;
// 使用 std::bind 绑定函数和额外参数,创建删除器
auto deleter = std::bind(customDeleter, std::placeholders::_1, extra_param);
// 创建 unique_ptr,并指定删除器
std::unique_ptr<int, decltype(deleter)> ptr(new int(42), deleter);
return 0;
}
在这个示例中,我们使用 bind
将 customDeleter
函数与额外参数 extra_param
绑定,然后将绑定后的函数作为删除器传递给 unique_ptr
。这样,当 unique_ptr
被销毁时,删除器会正确地调用 customDeleter
函数,并传递额外参数。
- 管理资源的方式:
shared_ptr
在超出作用域时不会立即释放资源,而是在引用计数归零时才释放资源。因此,删除器可能不会立即被调用。unique_ptr
在超出作用域时立即释放资源,并在释放资源时调用删除器。
总的来说,虽然 std::shared_ptr
和 std::unique_ptr
都支持定制删除器,但由于它们管理资源的方式不同,因此删除器的行为也会有所不同。