unique_ptr
智能指针的核心设计思路是利用对象的生命周期来管理资源(特别是内存),从而避免资源泄露和其他相关问题。
unique_ptr
提供了严格的所有权管理机制,通过独占的方式管理其指向的对象。也就是说unique_ptr
拥有其指向的对象,确保同一时间只有一个unique_ptr
拥有该对象。当这个unique_ptr
被销毁(例如,超出作用域)时,指向的对象也随即被销毁。
使用unique_ptr
需要包含头文件#include
unique_ptr
的结构
#include
template <typename T, typename D = std::default_delete<T>>
class unique_ptr {
public:
// 基础构造函数,接受一个指向已分配对象的指针。
explicit unique_ptr(T* p = nullptr) noexcept : ptr(p) {}
// 析构函数,负责释放 unique_ptr 拥有的对象。
~unique_ptr() {
if (ptr) {
D deleter;
deleter(ptr); // 默认使用 delete 删除器
}
}
// 移动构造函数,接受一个右值引用至其他 unique_ptr。
// “窃取”资源,将源 unique_ptr 置为空状态。
unique_ptr(unique_ptr&& other) noexcept : ptr(other.ptr) {
other.ptr = nullptr;
}
// 移动赋值操作符,接受一个右值引用至其他 unique_ptr。
// 释放当前对象,接管新对象的资源,将源 unique_ptr 置为空状态。
unique_ptr& operator=(unique_ptr&& other) noexcept {
if (this != &other) {
reset(); // 释放当前指针
ptr = other.ptr; // 接管资源
other.ptr = nullptr; // 将源 unique_ptr 置空
}
return *this;
}
// 禁用拷贝构造函数
unique_ptr(const unique_ptr&) = delete;
// 禁用拷贝赋值操作符
unique_ptr& operator=(const unique_ptr&) = delete;
// 解引用操作符,返回所指对象的引用。
T& operator*() const {
return *ptr;
}
// 箭头操作符,允许通过 unique_ptr 访问其所拥有对象的成员。
T* operator->() const noexcept {
return ptr;
}
// 释放 unique_ptr 对象的所有权,返回原始指针,unique_ptr 不再负责对象的删除。
T* release() noexcept {
T* old_ptr = ptr;
ptr = nullptr;
return old_ptr;
}
// 释放 unique_ptr 当前拥有的对象,并可选地接受一个新的对象指针。
void reset(T* p = nullptr) noexcept {
if (ptr) {
D deleter;
deleter(ptr); // 删除当前对象
}
ptr = p; // 接受新的对象指针(如果提供的话)
}
private:
T* ptr; // 内部原始指针,指向 unique_ptr 所拥有的对象。
};
T
是指针指向的对象类型。D
是用于删除所拥有对象的策略,即删除器。默认情况下,使用的是 default_delete
,它会用 delete
操作符来删除对象。测试类AA的定义:
class AA
{
public:
string m_name;
AA() { cout << m_name << "调用构造函数AA()。\n"; }
AA(const string & name) : m_name(name) { cout << "调用构造函数AA("<< m_name << ")。\n"; }
~AA() { cout << m_name << "调用了析构函数~AA(" << m_name << ")。\n"; }
};
方法一:直接初始化
unique_ptr<AA> p1(new AA("天梦"));
方法二:使用 std::make_unique
(C++14 及以后版本推荐使用)
auto p1 = make_unique<AA>("天梦"); // 使用 make_unique (类型推断)
unique_ptr<AA> p2 = make_unique<AA>("天梦"); // 明确指定类型
方法三:使用已存在的指针(不推荐)
AA* p = new AA("西施");
unique_ptr<AA> p0(p); // 用已存在的地址初始化,这种方式不推荐因为它可能导致所有权不明确。
unique_ptr
重载了解引用操作符 *
和箭头操作符 ->
,这意味着它们可以像普通指针一样使用,但它们提供了额外的内存管理功能。
auto p = make_unique<AA>("天梦");
p->method(); // 调用对象的方法
(*p).method(); // 同上
不支持拷贝构造和拷贝赋值,因为 unique_ptr
的设计目的是提供对其所指对象的唯一所有权。
下面具体解释一下为什么
unique_ptr
不支持拷贝构造和拷贝赋值。我认为
unique_ptr
它核心是提供了对动态分配内存的“独占所有权”。这种所有权模型具有严格限制,为了避免资源泄露和与其他资源共享相关的问题。
- 防止资源的多重删除:如果说
unique_ptr
能够被复制,那么你最终会得到多个智能指针实例指向同一个资源。这本身并不坏,但问题在于每个实例可能都认为自己“拥有”资源,因此,当这些实例中其中一个离开作用域销毁其拥有的资源后,会导致其他指向同一资源的unique_ptr
实例成为野指针,他们也可能尝试删除已经不复存在的资源,从而导致未定义行为,通常是程序崩溃。- 强化唯一所有权语义:
unique_ptr
的核心思想是它是资源的唯一所有者。允许复制会破坏这种唯一性,因为复制的结果是有两个所有者指向同一个资源。通过禁止复制,unique_ptr
确保了所有权的概念始终得到维护,你可以明确地知道资源的生命周期和所有者身份。- 异常安全:使用原始指针的代码通常对异常情况的处理不够,可能会导致资源泄漏。例如,如果在执行复制操作的过程中抛出异常,程序可能会处于一种状态,其中某些已分配资源没有被清理。而
unique_ptr
通过确保资源只有一个所有者来避免这种情况,即使在异常情况下,也会在unique_ptr
对象销毁时释放资源。
AA* p = new AA("天梦");
unique_ptr<AA> pu2 = p; // 错误,不能把普通指针直接赋给智能指针。
unique_ptr<AA> pu3 = new AA("天梦"); // 错误,不能把普通指针直接赋给智能指针。
unique_ptr<AA> pu2 = pu1; // 错误,不能用其它unique_ptr拷贝构造。
unique_ptr<AA> pu3;
pu3 = pu1; // 错误,不能用=对unique_ptr进行赋值。
但是,你可以通过移动语义转移所有权:
unique_ptr<AA> p1 = make_unique<AA>("天梦");
unique_ptr<AA> p2 = std::move(p1); // p1 释放所有权,p2 成为新的所有者
unique_ptr
对象。get()
方法返回裸指针。unique_ptr
管理不是new
分配的内存。+
, -
, ++
, --
)。请看简单示例:
#include
#include
using namespace std;
class AA
{
public:
string m_name;
AA() { cout << m_name << "调用构造函数AA()。\n"; }
AA(const string & name) : m_name(name) { cout << "调用构造函数AA("<< m_name << ")。\n"; }
~AA() { cout << m_name << "调用了析构函数~AA(" << m_name << ")。\n"; }
};
int main() {
AA* p = new AA("天梦"); // 定义原始指针p,分配内存并构造AA对象。
unique_ptr<AA> pu(p); // 创建智能指针对象,接管原始指针p的内存管理权。
cout << "裸指针的值是: " << p << endl; // 显示原始指针的内存地址。
cout << "pu输出的结果是: " << pu.get() << endl; // 显示智能指针管理的对象的内存地址。
cout << "pu.get() 输出的结果是: " << pu.get() << endl; // 同上,显示智能指针中存储的原始指针地址。
cout << "pu的地址是: " << &pu << endl; // 显示智能指针自身的地址。
return 0;
}
其中pu
输出的结果是智能指针管理的对象的内存地址。这是因为unique_ptr
重载了operator<<
,返回的是unique_ptr
所指向的对象的内存地址,就像它是一个裸指针一样。
当 unique_ptr
作为函数参数时,通常传递引用或者通过 std::move
转移所有权,因为它们不支持拷贝语义。
void some_function(unique_ptr<AA>& ptr) {
// 不会转移所有权的函数
}
void some_function_take_ownership(unique_ptr<AA> ptr) {
// 接受所有权的函数
}
// 调用
auto p = make_unique<AA>("天梦");
some_function(p); // 传递引用,p 仍然拥有对象
some_function_take_ownership(std::move(p)); // 转移所有权,p 不再拥有对象
unique_ptr
的使用技巧将一个unique_ptr
赋给另一个时,如果源unique_ptr
是一个临时右值,编译器允许这样做。如果源unique_ptr
将存在一段时间,编译器禁止这样做。一般用于函数的返回值。
unique_ptr<AA> p0; p0 = unique_ptr<AA>(new AA ("天梦")); //使用匿名对象赋值。
我们使用移动赋值操作符将临时
unique_ptr
的所有权赋给p0
。在这个操作完成后,p0
现在拥有这个AA
对象,而临时的unique_ptr
不再拥有任何对象。
用nullptr
给unique_ptr
赋值将释放对象,空的unique_ptr == nullptr
。
unique_ptr<AA> p (new AA("天梦")); if(p != nullptr) cout<< "p is not null" << endl; p = nullptr; if(p == nullptr) cout<< "p is null" << endl;
如果你使用
nullptr
给std::unique_ptr
赋值,它将释放其当前所拥有的对象(如果有的话),并将内部指针设置为nullptr
。这可以被用作一种释放unique_ptr
所拥有的资源的方法。
release()释放对原始指针的控制权,将unique_ptr
置为空,返回裸指针(可以用于把unique_ptr
传递给子函数,子函数将负责释放对象)
std::move()可以转移对原始指针的控制权。(可用于把unique_ptr传递给子函数,子函数形参也是unique_ptr)
#include
#include using namespace std; class AA { public: string m_name; AA() { cout << m_name << "调用构造函数AA()。\n"; } AA(const string & name) : m_name(name) { cout << "调用构造函数AA("<< m_name << ")。\n"; } ~AA() { cout << "调用了析构函数~AA(" << m_name << ")。\n"; } }; // 函数func1()需要一个指针,但不对这个指针负责。 void func1(const AA* a) { cout << a->m_name << endl; } // 函数func2()需要一个指针,并且会对这个指针负责。 void func2(AA* a) { cout << a->m_name << endl; delete a; } // 函数func3()需要一个unique_ptr,不会对这个unique_ptr负责。 void func3(const unique_ptr<AA> &a) { cout << a->m_name << endl; } // 函数func4()需要一个unique_ptr,并且会对这个unique_ptr负责。 void func4(unique_ptr<AA> a) { cout << a->m_name << endl; } int main() { unique_ptr<AA> pu(new AA("tianmeng")); cout << "开始调用函数。\n"; //func1(pu.get()); // 函数func1()需要一个指针,但不对这个指针负责。 //func2(pu.release()); // 函数func2()需要一个指针,并且会对这个指针负责。 //func3(pu); // 函数func3()需要一个unique_ptr,不会对这个unique_ptr负责。 func4(move(pu)); // 函数func4()需要一个unique_ptr,并且会对这个unique_ptr负责。 cout << "调用函数完成。\n"; if (pu == nullptr) cout << "pu是空指针。\n"; }
reset()
释放对象。
void reset(T * _ptr= (T *) nullptr); pp.reset(); // 释放pp对象指向的资源对象。 pp.reset(nullptr); // 释放pp对象指向的资源对象 pp.reset(new AA("bbb")); // 释放pp指向的资源对象,同时指向新的对象。
swap()
交换两个unique_ptr
的控制权
pp1.swap(pp2); // 交换两个 unique_ptr 的所有权
unique_ptr
也可象普通指针那样,当指向一个类继承体系的基类对象时,也具有多态性质,如同使用裸指针管理基类对象和派生类对象那样。
#include
#include
using namespace std;
class Base {
public:
virtual ~Base() = default;
virtual void show() const {
cout << "Base class" << endl;
}
};
class Derived : public Base {
public:
void show() const override {
cout << "Derived class" << endl;
}
};
int main() {
unique_ptr<Base> basePtr = make_unique<Derived>();
basePtr->show(); // Outputs: Derived class
return 0;
}
unique_ptr
不是绝对安全,如果程序中调用exit()
退出,全局的unique_ptr
可以自动释放,但局部的unique_ptr
无法释放。
#include
#include using namespace std; class AA { public: string m_name; AA() { cout << m_name << "调用构造函数AA()。\n"; } AA(const string & name) : m_name(name) { cout << "调用构造函数AA("<< m_name << ")。\n"; } ~AA() { cout << "调用了析构函数~AA(" << m_name << ")。\n"; } }; unique_ptr<AA> p (new AA("全局变量")); int main(){ unique_ptr<AA> p1(new AA("局部变量")); exit(0); }
unique_ptr
提供了支持数组的具体化版本。
数组版本的
unique_ptr
,重载了操作符[],操作符[]返回的是引用,可以作为左值使用。
声明unique_ptr
管理的数组:unique_ptr<int[]> myArray(new int[5]);
或者使用现代的
std::make_unique
函数(从 C++14 开始支持数组版本):auto myArray = std::make_unique<int[]>(5);
与裸指针数组类似,你可以使用
[]
操作符来访问数组中的每个元素。由于返回的是引用,你可以用它作为左值
shared_ptr
shared_ptr
共享它所指向的对象,多个shared_ptr
可以指向(关联)相同的的对象,并且在内部采用计数机制来实现。
当新的shared_ptr
与对象关联时,引用计数加一。
当shared_ptr
超出作用域时,引用计数减1。当引用计数变为0时,则表示没有任何shared_ptr
与对象关联,则释放该对象。
方法一:直接使用构造函数
shared_ptr<AA> p0(new AA("西施"));
方法二:使用mark_shared
shared_ptr<AA> p0 = make_shared<AA>("天梦"); // C++11标准,效率更高。
shared_ptr<int> pp1=make_shared<int>(); // 数据类型为int。
shared_ptr<AA> pp2 = make_shared<AA>(); // 数据类型为AA,默认构造函数。
shared_ptr<AA> pp3 = make_shared<AA>("天梦"); // 数据类型为AA,一个参数的构造函数。
shared_ptr<AA> pp4 = make_shared<AA>("天梦",8); // 数据类型为AA,两个参数的构造函数。
方法三:采用已有的裸指针
AA* p1 = new AA("天梦");
shared_ptr<AA> p2(p1);
方法四:复制构造和赋值函数
shared_ptr<AA> p3 = p0; // 使用赋值操作符
shared_ptr<AA> p4(p0); // 使用复制构造函数
shared_ptr
重载了 *
和 ->
,这意味着你可以像使用裸指针一样使用它。use_count()
方法来查询当前有多少个 shared_ptr
共享同一个对象。而 unique()
方法则检查是否只有一个 shared_ptr
指向该对象。shared_ptr
支持赋值,左值的shared_ptr
的计数器将减1,右值shared_ptr
的计算器将加1。get()
方法可以返回内部的裸指针,但通常应该避免使用它,除非真的需要裸指针。shared_ptr
,这会导致在程序结束时该对象被多次删除,从而引起未定义的行为。与unique_ptr
相同
shared_ptr
的使用技巧nullptr
给shared_ptr
赋值将把计数减1,如果计数为0,将释放对象,空的shared_ptr==nullptr
。std::move()
可以转移对原始指针的控制权。还可以将unique_ptr
转移成shared_ptr
。reset()
改变与资源的关联关系。swap()
交换两个shared_ptr
的控制权。shared_ptr
也可象普通指针那样,当指向一个类继承体系的基类对象时,也具有多态性质,如同使用裸指针管理基类对象和派生类对象那样。shared_ptr
不是绝对安全,如果程序中调用exit()
退出,全局的shared_ptr
可以自动释放,但局部的shared_ptr
无法释放。shared_ptr
的引用计数是线程安全的,但在多线程环境中修改shared_ptr
或其所指向的对象时仍需要加锁。unique_ptr
足以解决问题,那么优先选择unique_ptr
。因为unique_ptr
比shared_ptr
更高效,使用的资源也更少。在C++中,尽管智能指针可以自动删除所指向的对象,但在某些情况下,可能需要以特定的方式删除这些对象,例如,当对象是在非标准堆上分配的,或者与其他资源(如文件句柄或数据库连接)相关联时。这时,自定义删除器就派上了用场。
删除器是什么?
默认情况下,智能指针,如shared_ptr
和unique_ptr
,在其生命周期结束时使用delete
操作符来释放其管理的对象。但是,你可以提供一个自定义的删除器,以便更改智能指针的行为。
删除器可以是:
operator()
的类或结构。它可以保持状态,并可以作为删除器使用。注意:智能指针使用删除器时,只要在模板签名里指定删除器的类型。
如:
unique_ptr <AA,*decltype*(deletefunc)*> p4(*new* AA("p4"),deletefunc); *//* *普通函数*
下面是详细示例:
#include
#include
#include
#include
using namespace std;
class AA
{
public:
string m_name;
AA() { cout << m_name << "调用构造函数AA()。\n"; }
AA(const string & name) : m_name(name) { cout << "调用构造函数AA("<< m_name << ")。\n"; }
~AA() { cout << "调用了析构函数~AA(" << m_name << ")。\n"; }
};
void deletefunc(AA *a){ // 自定义删除器 普通函数
cout<<"自定义删除器(全局函数)"<<endl;
delete a;
}
struct deleteclass{ // 自定义删除器 仿函数
void operator()(AA *a){
cout<<"自定义删除器(仿函数)"<<endl;
delete a;
}
};
auto deletelambda = [](AA *a){ // 自定义删除器 lambda表达式
cout<<"自定义删除器(lambda表达式)"<<endl;
delete a;
};
int main(){
shared_ptr <AA> p1(new AA("p1"),deletefunc); // 普通函数
shared_ptr <AA> p2(new AA("p2"),deleteclass()); // 仿函数
shared_ptr <AA> p3(new AA("p3"),deletelambda); // lambda表达式
unique_ptr <AA,decltype(deletefunc)*> p4(new AA("p4"),deletefunc); // 普通函数
unique_ptr <AA,void (*)(AA *)> p5(new AA("p5"),deletefunc); // 普通函数
unique_ptr <AA,deleteclass> p6(new AA("p6")); // 仿函数
unique_ptr <AA,decltype(deletelambda)> p7(new AA("p7"),deletelambda); // lambda表达式
}
shared_ptr
存在的问题首先,我们要明确shared_ptr
的主要优点:自动引用计数。这意味着每当有一个新的shared_ptr
指向同一个资源,这个资源的引用计数会增加,当某个shared_ptr
不再存在,计数会减少。只有当引用计数为0时,资源才会被释放。然而,这种机制有一个重要的弱点,就是循环引用。
循环引用
循环引用发生在两个或多个shared_ptr
对象相互引用,导致他们之间形成了一个引用环。在这种情况下,涉及到的所有对象的引用计数都不会将至为0,这就意味着这些对象永远不会被自动删除,从而导致内存泄漏。
#include
#include
using namespace std;
class B;
class A {
public:
shared_ptr<B> b_ptr;
~A() {
cout << "A destructor called!" << endl;
}
};
class B {
public:
shared_ptr<A> a_ptr;
~B() {
cout << "B destructor called!" << endl;
}
};
int main() {
{
shared_ptr<A> a = make_shared<A>();
shared_ptr<B> b = make_shared<B>();
a->b_ptr = b; // A对象引用B对象
b->a_ptr = a; // B对象引用A对象
} // a和b的析构函数都不会被调用,因为存在循环引用
cout << "End of main()" << endl;
}
在上述示例中,我们在A和B两个类中,分别创建了一个shared_ptr
指向另一个类的对象,并在main函数中创建A和B的shared_ptr
实例,互相引用。
由于A
的实例持有指向B
的shared_ptr
,并且B
的实例持有指向A
的shared_ptr
,所以两者形成了一个引用循环。当我们退出作用域后,尽管a
和b
的生命周期已经结束,但由于存在循环引用,它们的引用计数都不为0,因此析构函数不会被调用,从而导致内存泄漏。
weak_ptr
是什么为了解决shared_ptr
可能导致的循环引用问题,C++11引入了weak_ptr
,与shared_ptr
不同,weak_ptr
不会增加或减少其所观察对象的引用计数。这意味着weak_ptr
可以观察一个对象,但不会导致该对象的生命期延长或缩短。
功能和用途:
weak_ptr
主要与shared_ptr
一起使用,以观察shared_ptr
所拥有的资源。weak_ptr
观察的资源被销毁(例如,所有的shared_ptr
都被销毁了)时,weak_ptr
可以检测到这一点。weak_ptr
是shared_ptr
的一个"安全伙伴",使开发人员能够安全地管理复杂的对象关系,避免循环引用导致的内存泄漏。weak_ptr
由于weak_ptr
没有重载的->
和*
操作符,您不能直接通过weak_ptr
来访问其观察的对象。但您可以通过以下方式与weak_ptr
互动:
shared_ptr
或另一个weak_ptr
赋值给当前的weak_ptr
。这样,weak_ptr
就可以观察(但不共享)该资源。weak_ptr
指向的资源是否仍然存在。如果资源已经被销毁(即与之相关的所有shared_ptr
都已经被销毁),则expired()
返回true
。weak_ptr
中最常用的函数。它尝试获取指向对象的shared_ptr
。如果对象仍然存在(即其引用计数不为0),lock()
返回一个有效的shared_ptr
。否则,它返回一个空的shared_ptr
。这是一个线程安全的操作。weak_ptr
不再指向任何对象,即它将其内部指针置为空。weak_ptr
对象的内容。使用weak_ptr
的常见场景:
shared_ptr
,可能会出现循环引用的问题。在这种情况下,您可以使用weak_ptr
来打破这种循环。weak_ptr
是一个好选择。weak_ptr
。下面是一个简单示例:
#include
#include
using namespace std;
class Sample {
public:
Sample() {
cout << "Sample constructor called!" << endl;
}
~Sample() {
cout << "Sample destructor called!" << endl;
}
};
int main() {
// 使用 shared_ptr 创建一个 Sample 对象
shared_ptr<Sample> sp1 = make_shared<Sample>();
// 使用 weak_ptr 指向该 Sample 对象
weak_ptr<Sample> wp1;
// 1. operator=()
wp1 = sp1; // 使用 operator=() 将 shared_ptr 赋值给 weak_ptr
// 使用 weak_ptr 指向同一个 Sample 对象
weak_ptr<Sample> wp2 = wp1;
// 2. expired()
if (wp1.expired()) {
cout << "wp1 is expired." << endl;
} else {
cout << "wp1 is not expired." << endl;
}
// 3. lock()
shared_ptr<Sample> sp2 = wp1.lock(); // 提升 weak_ptr 为 shared_ptr
if (sp2) {
cout << "Successfully locked wp1!" << endl;
}
// 释放 sp1 指向的资源
sp1.reset();
if (wp1.expired()) {
cout << "wp1 is now expired." << endl;
}
// 4. reset()
wp2.reset(); // 将 wp2 置为空
if (wp2.expired()) {
cout << "wp2 is expired." << endl;
}
// 5. swap()
wp1.swap(wp2); // 交换 wp1 和 wp2 的内容
if (wp2.expired()) {
cout << "After swap, wp2 is still expired." << endl;
}
if (wp1.expired()) {
cout << "After swap, wp1 is now expired." << endl;
}
return 0;
}