众所周知,C++指针是一把双刃剑,使用时存在的问题有:
- 忘记释放或发生异常引起内存泄露
- 重复释放同一块内存
- 出现野指针
为了给我们提供更好的内存管理,c++11新增了以下这3种智能指针。
原理
智能指针的原理比较简单,它是一个资源管理类,封装了原始指针,内部维护着一份引用计数,记录着当前有多少个类对象共享同一份内存。由于智能指针本身是栈上对象,当作用域结束时会调用析构函数,此时引用计数减1,当计数为0时,则释放原始指针指向的堆内存。
使用
shared_ptr
shared_ptr的特点是共享所有权,即同一时刻可以有多个shared_ptr指向同一块堆内存。shared_ptr内部维护着两个指针,一个原始指针指向共享对象,另一个指针指向control block,它管理着引用计数等共享数据,本身也是在堆内存中。
当存在多个shared_ptr共享一个堆内存对象时:
shared_ptr使用std::make_shared来构造对象,构造一个shared_ptr有几种方式:
int main() {
//构造, use_count()输出引用计数
std::shared_ptr<int> p1 = std::make_shared<int>(1);
std::cout << p1.use_count() << std::endl; // 1
//拷贝构造,引用计数+1
std::shared_ptr<int> p2 = p1;
std::cout << p1.use_count() << std::endl; // 2
std::cout << p2.use_count() << std::endl; // 2
//移动构造,p1的所有权给p3,总的引用计数不变
auto p3 = std::move(p1);
std::cout << p1.use_count() << std::endl; // 0
std::cout << p3.use_count() << std::endl; // 2
}
unique_ptr
unique_ptr的特点是独占所有权,即同一时刻只有一个unique_ptr指向这块堆内存。所以unique_ptr不支持拷贝语义,只支持移动语义。
//C++11 没有提供 std::make_unique,自行实现
template<typename T, typename ...Args>
std::unique_ptr<T> make_unique( Args&& ...args ) {
return std::unique_ptr<T>( new T( std::forward<Args>(args)... ) );
}
int main() {
//构造
std::unique_ptr<int> p1 = make_unique<int>(1);
// 非法,unique_ptr无拷贝构造
// std::unique_ptr<int> p2 = p1;
//移动构造,p1的所有权给p2
auto p2 = std::move(p1);
}
weak_ptr
weak_ptr一般作为shared_ptr的辅助指针,当 weak_ptr 指针的指向和某一 shared_ptr 指针相同时,并不会改变shared_ptr所指堆内存空间的引用计数。
其中一个使用场景是解决shared_ptr存在的循环引用导致的内存泄露问题。
当shared_ptr指针析构释放T对象时,weak_ptr指针还可以继续观察保留下来的control block数据。
int main()
{
std::shared_ptr<int> p1 = std::make_shared<int>(1);
std::weak_ptr<int> p2(p1);
std::cout << p2.use_count() << std::endl; //输出1
// 如果weak_ptr已经过期(指针为空,或者指向的堆内存已经被释放),则lock函数会返回一个空的shared_ptr指针
// 反之,此函数返回一个和weak_ptr指向相同的shared_ptr指针
std::cout << *(p2.lock()) << std::endl;
return 0;
}
性能对比
- 内存占用:shared_ptr是原始指针大小的两倍,而unique_ptr默认情况和原始指针相同。
- 运行速度:考虑到线程安全,shared_ptr中引用计数是原子操作,相比unique_ptr慢。
一般情况优先考虑使用unique_ptr,比如在忘记漏写delete或者出现异常安全的场景上非常适合,如果需要有多个对象同时管理同一块内存时,则使用shared_ptr。
异常安全的例子如下,如果handle方法出现异常,那么后面的delete方法将不会执行,而使用unique_ptr的方式就不需要去try-catch主动delete。
void func()
{
Test* test = new Test();
test->handle();
delete test;
}
实践踩坑
智能指针虽好,但用的不好还是免不了踩坑。下面列举几个例子
额外拷贝&非法引用
class Test
{
public:
void print()
{
std::cout << "call print: " << this << std::endl;
}
~Test()
{
std::cout << "destroy: " << this << std::endl;
}
};
std::shared_ptr<Test> GetTest()
{
return std::make_shared<Test>();
}
int main()
{
//方式1 额外拷贝
auto test1 = *GetTest();
test1.print();
//方式2 非法引用
auto& test2 = *GetTest();
test2.print();
//方式3
auto test2 = GetTest();
test2->print();
// return 0;
}
代码中有三种方式接收getTest方法返回的shared_ptr对象。
方式1的输出是:
destroy: 0x7ff6c6c05a08
call print: 0x7ffee29d0c88
destroy: 0x7ffee29d0c88
日志显示destroy了两次,是不同的对象。方式1的问题在于操作符*返回的是一个引用,使用auto会调用一次拷贝构造函数生成一个新的实例,造成了额外的拷贝开销。
方式2的输出是:
destroy: 0x7fa741405a08
call print: 0x7fa741405a08
日志显示在调用print前对象已经destroy了、原因是使用auto&虽然减少了额外拷贝,但是操作符*返回的结果并没有用智能指针接收,所以函数GetTest返回后之智能指针就销毁掉了,造成了非法引用。
方式3才是正确的使用姿势。
重复释放同一块内存
//复用之前例子的Test类
static void fun2(Test* rawTest)
{
std::shared_ptr<Test> testObj2(rawTest);
testObj2->print();
}
static void fun1()
{
auto testObj = std::make_shared<Test>();
fun2(testObj.get());
}
int main(){
fun1();
}
这里fun1新建一个shared_ptr对象,通过get()把原始指针传递给fun2,fun2又新建一个shared_ptr对象,导致出现两个独立非共享的shared_ptr,即此时存在两个control block,析构时原始指针指向的对象会被delete两次导致崩溃。
同样的case也经常出现在下面这个例子中,比如一个类的成员函数想获得指向自身的shared_ptr
class Test
{
public:
void print()
{
std::cout << "call print: " << this << std::endl;
}
~Test()
{
std::cout << "destroy: " << this << std::endl;
}
std::shared_ptr<Test> GetTestPtr()
{
return std::shared_ptr<Test>(this);
}
};
int main()
{
auto testObj1 = std::make_shared<Test>();
auto testObj2 = testObj1->GetTestPtr();
}
原因同上个例子,这里恰当的方式是继承自C++提供的std::enable_shared_from_this,调用shared_from_this返回shared_ptr
class Test: public std::enable_shared_from_this<Test>
{
public:
void print()
{
std::cout << "call print: " << this << std::endl;
}
~Test()
{
std::cout << "destroy: " << this << std::endl;
}
std::shared_ptr<Test> GetTestPtr()
{
return shared_from_this();
}
};
总结
C++11引入智能指针,是为了方便管理一个对象的生命期,在实际使用中,我们应该具体情况具体分析,避免不分场景只使用shared_ptr的情况。以上就是所有内容,大家有兴趣可以尝试实现一个shared_ptr智能指针。