智能指针总结
对于编译器来说,智能指针实际上是一个栈对象,并非指针类型,在栈对象生命期即将结束时,智能指针通过析构函数释放由它管理的堆内存。所有智能指针都重载了“
operator->
”操作符,直接返回对象的引用,用以操作对象。访问智能指针原来的方法则使用“.”操作符。
访问智能指针包含的裸指针则可以用get()
函数。由于智能指针是一个对象,所以if (my_smart_object)
永远为真,要判断智能指针的裸指针是否为空,需要这样判断:if(my_smart_object.get())
。 智能指针包含了reset()
方法,如果不传递参数(或者传递NULL
),则智能指针会释放当前管理的内存。如果传递一个对象,则智能指针会释放当前对象,来管理新传入的对象。
上一个博客我们已经自己做了几个资源管理对象,其实智能指针的构造也大多就那几种类型,但是却更加复杂了许多,而且C++自带的智能指针已经很不错了,我们也不用都用自己写的资源管理对象。所以我们就来测试一下C++的各种资源管理对象——智能指针。
本文主要讲解参见的智能指针的用法。包括:std::auto_ptr、std::unique_ptr、std::shared_ptr、std::weak_ptr
首先我们先构建一个对象,用来测试各个智能指针的特性:
class Simple
{
private:
size_t value;
public:
Simple(const Simple& rhs) :value(rhs.value)
{
cout << "creat simple" << endl;
};
Simple() :value()
{
cout << "creat simple" << endl;
};
explicit Simple(size_t v) :value(value)
{
cout << "creat simple" << endl;
};
void DoSomething()
{
cout << "hello" << endl;
}
~Simple()
{
cout << "delete Simple" << endl;
}
};
智能指针
1、std::auto_ptr
std::auto_ptr
属于STL
,自然就包含在namespace std
中,包含头文件#include<memory>
便可以使用。std::auto_ptr
能够方便的管理单个堆内存对象。
以下是一段测试代码:
void test()
{
auto_ptr<Simple> auto_p(new Simple(1));
if (auto_p.get()) //判断源指针是否为空
{
auto_p->DoSomething();//用->调用智能指针管理的对象中的函数
auto_p->name = "auto_ptr";// 用->对智能指针管理的对象中的成员变量进行赋值
auto_p.get()->DoSomething();//输出已被修改的name成员变量
(*auto_p).name = "change"; //使用*操作符直接获取资源对象,然后对资源对象进行操作
}
}
执行结果如下:
creat simple
hellonone
helloauto_ptr
delete Simple
一切正常,不需要delete
就可以自动将管理的资源进行析构,是不是很完美?
但是看一下下面的代码:
void test()
{
auto_ptr<Simple> auto_p(new Simple(1));
if (auto_p.get())
{
auto_ptr<Simple> auto_p_1;
auto_p->DoSomething();//正常
auto_p_1 = auto_p;
auto_p_1->DoSomething();//正常
auto_p->DoSomething();//错误
}
}
我们调试之后可以发现,在auto_p_1 = auto_p;
之后auto_p
内部管理的内存就消失了,完全转移到了auto_p_1
身上。
这个其实和我们上一篇博客实现的控制权转移的资源管理对象非常相似,在std::auto_ptr
的赋值和拷贝构造过程中,它会将源智能指针的资源夺取然后交给新智能指针,控制权发生了转移,然后源智能指针就是指向了一个空的内存空间。所以报错。
所以,尽量不要使用operator =
操作,如果用了,就不要再使用先前对象。
再观察如下代码:
void test()
{
auto_ptr<Simple> auto_p(new Simple(1));
if (auto_p.get())
{
auto_p.release();
}
}
输出如下:
creat simple
我们创建出来的对象没有被析构,当我们不想让 auto_p
继续生存下去,我们调用 release()
函数释放内存,结果却导致内存泄露(在内存受限系统中,如果auto_p
占用太多内存,我们会考虑在使用完成后,立刻归还,而不是等到 auto_p
结束生命期后才归还)。
正确的代码应该这样:
auto_ptr<Simple> auto_p(new Simple(1));
if (auto_p.get())
{
auto_p.reset();//释放auto_t管理的内存
}
或者这样:
void test()
{
auto_ptr<Simple> auto_p(new Simple(1));
if (auto_p.get())
{
Simple* p = auto_p.release();
delete p;
}
}
原来 std::auto_ptr
的 release()
函数只是让出内存所有权,这显然不符合 C++ 编程思想。
总结:
std::auto_ptr 可用来管理单个对象的内存,但是,请注意如下几点:
(1) 尽量不要使用
operator=
和拷贝构造。如果使用了,请不要再使用先前对象。
(2) 记住 release()函数不会释放对象,仅仅归还所有权。
(3)std::auto_ptr
最好不要当成参数传递(读者可以自行写代码确定为什么不能)。
(4) 由于std::auto_ptr
的operator=
问题,有其管理的对象不能放入std::vector
等容器中。
(5) ……
2、std::unique_ptr
std::unique_ptr
属于 STL
库,定义在 namespace std
中,包含头文件#include<memory>
便可以使用。
std::unique_ptr
跟 std::auto_ptr
一样,可以方便的管理单个堆内存对象,特别的是,std::unique_ptr
独享所有权,避免了 std::auto_ptr
恼人的几个问题。
std::unique_ptr
具备如下特性:
std::unique_ptr
可以像auto_ptr
一样使用由于
std::unique_ptr
是独享所有权的,所以明确拒绝用户写auto_p_1 = auto_p
之类的语句
以下是一个unique_ptr
的使用示例:
#include <iostream>
#include <memory>
struct Foo {
Foo() { std::cout << "Foo::Foo\n"; }
~Foo() { std::cout << "Foo::~Foo\n"; }
void bar() { std::cout << "Foo::bar\n"; }
};
void f(const Foo &foo)
{
std::cout << "f(const Foo&)\n";
}
int main()
{
std::unique_ptr<Foo> p1(new Foo); // p1 拥有 Foo
if (p1) p1->bar();
{
std::unique_ptr<Foo> p2(std::move(p1)); // 现在 p2 拥有 Foo
f(*p2);
p1 = std::move(p2); // 所有权还给了 p1
std::cout << "destroying p2...\n";
}
if (p1) p1->bar();
// p1 离开作用域时, Foo 实例会自动销毁
}
输出:
Foo::Foo
Foo::bar
f(const Foo&)
destroying p2...
Foo::bar
Foo::~Foo
常用函数
函数 | 介绍 |
---|---|
unique_ptr<T>up(T* res) | 将资源res 交给up 来管理,资源不使用时会自动释放。 |
p | p 可以作为条件判断,若p 指向一个对象,则为true ,否则为false 。 |
*p | 对shared_ptr<T> 对象解引用,得到的是由p 管理的T 类型对象的引用。 |
p->mem | 等价于(*p).mem 。(比如所指对象的成员数据) |
p.get() | 返回p 中保存的T*指针。 |
swap(p,q) | 交换p,q 各自内部的T* 指针,注意p,q 类型要相同。 |
p.swap(q) | 同上。 |
p = q | 这是无效操作,unique_ptr 不能进行赋值。 |
unique_ptr<T>sp(q) | 这是无效操作,unique_ptr 不能进行拷贝。 |
p = nullptr | 释放p 指向的对象,并将p置为控。 |
p.release() | p 放弃对指针的控制权,返回资源指针,并将p 置为空。 |
p.reset(T* q) | 释放p 指向的对象,并将p 置为空,若传入参数q ,则让p 指向q 该指针。 |
p.reset(q.release()) | 释放掉p 指向的对象,q 将自身指向对象的控制权转移给p ,然后q 自身置为空。 |
3、std::shared_ptr
同样的,这个资源管理对象我们也在前面实现过,通过引用计数的方式共享资源的所有权(多个std::shared_ptr
可以管理同一个堆对象),并且保证对象被析构。当指向资源的所有的std::shared_ptr
都被释放之后,他们共同管理的资源也会被析构。
在典型的实现中,std::shared_ptr
只保存两个指针:
- 指向被管理对象的指针
- 指向控制块(control block)的指针
控制块是一个动态分配的对象,其中包含:
- 指向被管理对象的指针或被管理对象本身
- 删除器
- 分配器(allocator)
- 拥有被管理对象的 shared_ptr 的数量
- 引用被管理对象的 weak_ptr 的数量
通过 std::make_shared
和 std::allocate_shared
创建 shared_ptr
时,控制块将被管理对象本身作为其数据成员;而通过构造函数创建 shared_ptr
时则保存指针。
shared_ptr
持有的指针是通过 get()
返回的;而控制块所持有的指针/对象则是最终引用计数归零时会被删除的那个。两者并不一定相等。
shared_ptr
的析构函数会将控制块中的 shared_ptr
计数器减一,如果减至零,控制块就会调用被管理对象的析构函数。但控制块本身直到 std::weak_ptr
计数器同样归零时才会释放。
下面是对std::shared_ptr
的使用和测试(官方案例):
#include <iostream>
#include <memory>
#include <thread>
#include <chrono>
#include <mutex>
struct Base
{
Base() { std::cout << " Base::Base()\n"; }
// 注意:这里可以使用非虚的析构器
~Base() { std::cout << " Base::~Base()\n"; }
};
struct Derived: public Base
{
Derived() { std::cout << " Derived::Derived()\n"; }
~Derived() { std::cout << " Derived::~Derived()\n"; }
};
void thr(std::shared_ptr<Base> p)
{
std::this_thread::sleep_for(std::chrono::seconds(1));
std::shared_ptr<Base> lp = p; // 类型安全,即使
// 共享的 use_count 增加
{
static std::mutex io_mutex;
std::lock_guard<std::mutex> lk(io_mutex);
std::cout << "local pointer in a thread:\n"
<< " lp.get() = " << lp.get()
<< ", lp.use_count() = " << lp.use_count() << '\n';
}
}
int main()
{
std::shared_ptr<Base> p = std::make_shared<Derived>();
std::cout << "Created a shared Derived (as a pointer to Base)\n"
<< " p.get() = " << p.get()
<< ", p.use_count() = " << p.use_count() << '\n';
std::thread t1(thr, p), t2(thr, p), t3(thr, p);
p.reset(); // 从 main 中释放所有权
std::cout << "Shared ownership between 3 threads and released\n"
<< "ownership from main:\n"
<< " p.get() = " << p.get()
<< ", p.use_count() = " << p.use_count() << '\n';
t1.join(); t2.join(); t3.join();
std::cout << "All threads completed, the last one deleted Derived\n";
}
可能的输出如下(内存地址不一定相同):
Base::Base()
Derived::Derived()
Created a shared Derived (as a pointer to Base)
p.get() = 0xc99028, p.use_count() = 1
Shared ownership between 3 threads and released
ownership from main:
p.get() = (nil), p.use_count() = 0
local pointer in a thread:
lp.get() = 0xc99028, lp.use_count() = 3
local pointer in a thread:
lp.get() = 0xc99028, lp.use_count() = 4
local pointer in a thread:
lp.get() = 0xc99028, lp.use_count() = 2
Derived::~Derived()
Base::~Base()
All threads completed, the last one deleted Derived
可以看到,我们对shared_ptr
的赋值操作都会引起资源的共享而不是资源的拷贝或者其支配权的转移,所以,由上面的测试我们可以总结出几个shared_ptr
的特点:
shared_ptr
之间的拷贝构造和赋值都只会引起元对象将资源的支配权分享给新对象,而不是资源的拷贝或者其支配权的转移shared_ptr
可以知道当前使用这个资源的对象的个数shared_ptr
更改一个对象管理的资源,相当于更改所有使用此资源的shared_ptr
对象管理的资源- 当最后一个管理此资源的对象也被删除了,那么这个资源也将被
delete
掉 - 我们可以使用
release()
放弃对资源的控制权,并且将自身指针指向空
常用函数
函数 | 介绍 |
---|---|
shared_ptr<T>sp(T* res) | 将资源res 交给sp 来管理,资源不使用时会自动释放。 |
shared_ptr<T>sp = make_spared(params) | 用params 参数构造T类型的对象并且交给sp |
p | p可以作为条件判断,若p指向一个对象,则为true ,否则为false。 |
*p | 对shared_ptr<T> 对象解引用,得到的是由p管理的T类型对象的引用。 |
p->mem | 等价于(*p).mem |
p.get() | 返回p中保存的T*指针 |
swap(p,q) | 交换p,q各自内部的T*指针,注意p,q类型要相同 |
p.swap(q) | 同上 |
p = q | p,q 必须都是shared_ptr 指针,并且各自管理的指针类型能相互转换。此操作会递减p的引用次数,递增q的引用次数;若p的引用次数变为0,则其管理的原内存会自动释放。 |
shared_ptr<T>sp(q) | 使得sp 引用q 所占资源。 |
p.reset() , p.reset(q) | 放弃p 对资源的引用,若为唯一引用,还会释放内存。若传有参数q,则让p引用q所占资源。 |
p.use_count() | 返回与p 共享的智能指针数量;可能很慢,主要用于调试。 |
p.unique() | 若p.use_count() 为1,返回true ,否则返回false |
4、std::weak_ptr
weak_ptr
是一种不控制所指向对象生命期的智能指针,它指向一个由 shared_ptr
管理的对象。将一个 weak_ptr
绑定到一个 shared_ptr
不会改变 shared_ptr
的引用次数。
一旦最后一个指向对象的shared_ptr
被销毁,对象就会被释放
即使这个时候存在 weak_ptr
指向了该对象,对象还是会被释放。所以我们可以把 weak_ptr
看作是一种”弱引用”。
函数 | 介绍 |
---|---|
weak_ptr<T>wp | 空 weak_ptr 可以指向类型为T 的对象 |
weak_ptr<T>wp(sp) | 与shared_ptr sp指向相同的对象,T 必须可以转化为sp 指向对象的类型。 |
w = p | p 可以是weak_ptr 或者shared_ptr 。赋值后w ,p 共享对象。但是w 不干涉对象声明周期。 |
w.reset() | m 置为空。 |
w.use_count() | 与m 共享资源的shared_ptr 个数。 |
w.expired() | 若w.use_count() 为0,返回true ,否则返回false。 |
w.lock() | 如果expired 为true ,返回一个空的shared_ptr ;否则返回指向w 对象的shared_ptr 。 |
以下是一个示例(如何通过锁来保证指针的有效性):
#include <iostream>
#include <memory>
std::weak_ptr<int> gw;
void f()
{
if (auto spt = gw.lock())
{ // 使用之前必须复制到 shared_ptr
std::cout << *spt << "\n";
}
else
{
std::cout << "gw is expired\n";
}
}
int main()
{
{
auto sp = std::make_shared<int>(42);
gw = sp;
f();
}
f();
}
输出:
42
gw is expired
以下是std::weak_ptr
的一些特性:
std::weak_ptr
在访问所引用的对象前必须先转换为 std::shared_ptr
。
std::weak_ptr
用来表达临时所有权的概念:当某个对象只有存在时才需要被访问,而且随时可能被他人删除时,可以使用 std::weak_ptr 来跟踪该对象。
需要获得临时所有权时,则将其转换为 std::shared_ptr
,此时如果原来的 std::shared_ptr
被销毁,则该对象的生命期将被延长至这个临时的 std::shared_ptr
同样被销毁为止。
此外,std::weak_ptr
还可以用来避免 std::shared_ptr
的循环引用。