C++智能指针
摘要:本文系统介绍了C++智能指针的概念、类型及应用。传统手动内存管理存在内存泄漏、野指针等问题,智能指针通过RAII机制自动管理内存生命周期。主要分析了四种智能指针:已弃用的auto_ptr存在所有权转移缺陷;unique_ptr实现独占所有权,禁止拷贝但支持移动;shared_ptr通过引用计数实现共享所有权;weak_ptr作为观察者解决循环引用问题。文章详细比较了各类智能指针的所有权模型、
一、什么是智能指针?
- 使用new操作符手动分配内存时,就必须时刻警惕,在恰当的时机使用delete释放内存 ,一旦稍有差池,忘记释放内存,内存泄漏便会如幽灵般悄然出现,随着程序的持续运行,被泄漏的内存不断堆积,最终可能导致系统内存枯竭,程序无奈崩溃。
- 在内存已经释放后,指针仍在,且被错误使用,悬空指针便会现身,这同样可能引发程序崩溃或出现难以预测的错误行为。
二、为什么使用智能指针
在 C++ 的编程世界中,内存管理一直是让人又爱又恨的存在。手动管理内存赋予了开发者极高的控制权,能让程序在性能上达到极致;但稍有不慎,就会掉进内存泄漏、野指针等陷阱中,让程序变得极其不稳定。智能指针的出现,就像是一道光照进了这个略显混乱的内存管理世界。
1.在 C++ 中,使用new分配内存后,如果忘记使用delete释放,内存泄漏就会悄然而至。比如在一个复杂的函数中:
void Myfunction()
{
int* ptr = new int(10);
//这里有大量的代码逻辑
//很可能疏忽而忘记释放ptr
}
2.野指针的问题同样棘手
void Myfunction()
{
int* ptr = new int(10);
delete ptr;
//此时ptr成为了野指针
//如果后续不小心再次使用ptr,就会导致未定义行为
int value = *ptr;
}
3.重复释放内存,对同一块内存进行多次释放,程序也会崩溃。
为了解决这些问题,C++ 引入了智能指针。智能指针利用 RAII(Resource Acquisition Is Initialization,资源获取即初始化)机制,将内存的管理和对象的生命周期绑定在一起。当智能指针对象创建时,它获取资源(即分配内存);当智能指针对象销毁时,它自动释放所指向的内存,无需手动调用delete。这就像是给内存分配和释放过程加上了一个可靠的 “管家”,极大地降低了内存管理出错的风险 。
三、智能指针的类型?
在 C++ 的智能指针家族中,包含了auto_ptr(已弃用)、unique_ptr、shared_ptr和weak_ptr ,每种智能指针都有其独特的设计目的和应用场景,接下来让我们深入了解一下它们。
1.auto_ptr(已弃用)
auto_ptr是 C++98 引入的智能指针,也是智能指针的 “鼻祖”,它采用所有权模式,当auto_ptr对象过期时,其析构函数会自动使用delete释放所指向的内存 ,这在一定程度上简化了内存管理,降低了内存泄漏的风险。然而,auto_ptr存在一些严重的缺陷,这也是它被弃用的原因。首先,auto_ptr在赋值和拷贝时会发生所有权转移,原指针会变为空指针。例如:以下程序会崩溃:
#include<iostream>
#include<memory>
using namespace std;
int main()
{
auto_ptr<int> p1(new int(10));
auto_ptr<int> p2 = p1;
cout << "*p1=" << *p1 << endl;
cout << "*p2=" << *p2 << endl;
return 0;
system("pause");
}
结果:

问题1:
在上述代码中,p1赋值给p2后,p1的所有权转移给了p2,p1变为空指针,此时访问p1会导致未定义行为,这在实际编程中很容易引发难以调试的错误。
问题2:
auto_ptr不支持数组的管理,它的析构函数使用delete而不是delete[],如果用auto_ptr管理 数组,会导致内存释放错误。此外,auto_ptr不符合 STL 容器对元素拷贝语义的要求,无法在 STL 容器中使用 。由于这些问题,auto_ptr在 C++11 中被弃用,逐渐被更优秀的智能指针所取代。
2.unique_ptr
unique_ptr是 C++11 引入的智能指针,它实现了独占式拥有概念,保证同一时间内只有一个智能指针可以指向该对象,有效避免了资源泄露。unique_ptr不允许拷贝和赋值,这是为了确保资源的独占性 。例如:
#include<iostream>
#include<memory>
using namespace std;
int main()
{
unique_ptr<int> p1(new int(10));
//auto_ptr<int> p2 = p1; //编译错误,不允许拷贝
unique_ptr<int> p3 = move(p1);
if (!p1)
{
cout << "p1 is nullptr" << endl;
}
cout << "p3=" << *p3<<endl;
return 0;
system("pause");
}
在这段代码中,尝试将p1赋值给p2会导致编译错误,而通过std::move将p1的所有权转移给p3后,p1变为空指针,p3拥有了资源的所有权 。
unique_ptr支持管理数组,它会使用delete[]来释放数组内存 。同时,unique_ptr还可以作为函数返回值,高效地将资源的所有权转移给调用者。例如:
#include<iostream>
#include<memory>
using namespace std;
unique_ptr<int> createint()
{
return unique_ptr<int>(new int(20));
}
int main()
{
unique_ptr<int>ptr = createint();
cout << *ptr << endl;
return 0;
system("pause");
}
在上述代码中,createInt函数返回一个unique_ptr<int>,将资源的所有权转移给main函数中的ptr 。
3.shared_ptr
shared_ptr实现了共享式拥有概念,允许多个智能指针指向相同对象,通过引用计数机制来管理对象的生命周期 。当一个shared_ptr指向对象时,引用计数加 1;当shared_ptr离开作用域或被重新赋值时,引用计数减 1,当引用计数为 0 时,对象会被自动释放 。例如:
#include<iostream>
#include<memory>
using namespace std;
int main()
{
shared_ptr<int> s1(new int(10));
shared_ptr<int> s2 = s1;
cout << "s1 use_count:" << s1.use_count() << endl;
cout << "s2 use_count:" << s2.use_count() << endl;
s1.reset();
cout << "s2 use_count:" << s2.use_count() << endl;
return 0;
system("pause");
}
注意:reset() 函数的默认行为是将 shared_ptr 持有的对象释放,并将 shared_ptr 重置为空指针。如果 reset() 不带参数,它将释放当前 shared_ptr 持有的对象,并将其重置为 nullptr。
在这段代码中,s1和s2共享同一个对象,引用计数为 2,当s1调用reset函数后,引用计数减 1,此时只有s2指向对象,引用计数变为 1 。
shared_ptr在多线程环境中使用时,需要注意线程安全问题,因为引用计数的修改不是原子操作,可能会导致数据竞争 。为了解决这个问题,可以使用std::atomic来实现原子操作,或者使用互斥锁来保护引用计数的修改 。此外,shared_ptr还支持自定义删除器,可以在对象被释放时执行一些额外的清理操作 。
4.weak_ptr
weak_ptr是一种不控制对象生命周期的智能指针,它指向一个由shared_ptr管理的对象,主要用于协助shared_ptr解决循环引用问题 。weak_ptr不会增加对象的引用计数,它的构造和析构不会影响对象的生命周期 。例如:
#include<iostream>
#include<memory>
using namespace std;
class B;
class A
{
public:
shared_ptr<B> pb1;
~A() {
cout << "A delete" << endl;
}
};
class B
{
public:
shared_ptr<A> pa1;
~B() {
cout << "B delete" << endl;
}
};
void func()
{
shared_ptr<A> pa(new A());
shared_ptr<B> pb(new B());
pa->pb = pb;
pb->pa = pa;
cout << "pa use_count: " << pa.use_count() << endl;
cout << "pb use_count: " << pb.use_count() << endl;
}
int main()
{
func();
return 0;
system("pause");
}
结果:

在上述代码中,A和B相互引用,形成了循环引用,导致pa和pb的引用计数永远不会为 0,对象无法被释放(A,B的对象都创建在堆上,两者互相依赖) 。如果将A中的std::shared_ptr<B> pb;改为std::weak_ptr<B> pb;,就可以打破循环引用,使对象能够正常释放 。
例如:
#include<iostream>
#include<memory>
using namespace std;
class B;
class A
{
public:
weak_ptr<B> pb1;
~A() {
cout << "A delete" << endl;
}
};
class B
{
public:
shared_ptr<A> pa1;
~B() {
cout << "B delete" << endl;
}
};
void func()
{
shared_ptr<A> pa(new A());
shared_ptr<B> pb(new B());
pa->pb = pb;
pb->pa = pa;
cout << "pa use_count: " << pa.use_count() << endl;
cout << "pb use_count: " << pb.use_count() << endl;
}
int main()
{
func();
return 0;
system("pause");
}
结果:

使用weak_ptr(弱引用)不增加B对象的引用计数。因此,B对象的引用计数为1,A对象的引用计数为2,若此时代码执行结束,栈空间上的pa指针先进行释放,A对象的引用计数减1为1,后释放pb指针,B对象的引用计数减1后为0,B对象释放内存空间,因此pa1成员函数也得到释放,A对象引用计数减1后为0,A对象也得到释放。因此不会产生内存泄漏。
四、智能指针间的区别
在所有权模型上,unique_ptr、shared_ptr 和 weak_ptr 有着本质的不同。unique_ptr 就像是一位独占资源的 “霸道总裁”,它对所指向的对象拥有独占所有权 ,在其生命周期内,不允许其他 unique_ptr 同时指向同一个对象。这一特性使得 unique_ptr 在管理需要明确单一所有权的资源时,表现出色,例如一个函数内部动态分配的临时对象,通过 unique_ptr 管理,能够确保在函数结束时,对象被正确释放,避免内存泄漏 。
shared_ptr 则像是一个资源共享的 “社交达人”,允许多个 shared_ptr 指向同一个对象,通过引用计数机制来共享对象的所有权 。每一个指向该对象的 shared_ptr 都会增加引用计数,当 shared_ptr 离开作用域或被重新赋值时,引用计数减 1,只有当引用计数降为 0 时,对象才会被销毁 。这种所有权模型适用于多个组件需要共享同一资源的场景,比如在一个多模块协作的程序中,多个模块可能需要访问同一个配置文件对象,就可以使用 shared_ptr 来管理这个配置文件对象的所有权 。
weak_ptr 更像是一个对对象的 “观察者”,它不拥有对象的所有权,只是对 shared_ptr 所管理的对象进行弱引用 。weak_ptr 的存在不会影响对象的引用计数,它主要用于解决 shared_ptr 之间可能出现的循环引用问题 。例如,在一个双向链表的实现中,如果节点之间使用 shared_ptr 相互引用,就会形成循环引用,导致节点无法被正确释放,而使用 weak_ptr 来表示其中一个方向的引用,就可以打破循环引用,使节点能够正常释放 。
内存管理方式
在内存管理方式上,auto_ptr(已弃用)、unique_ptr 和 shared_ptr 各有特点 。auto_ptr 在过期时,会自动使用 delete 释放所指向的内存,但由于其在赋值和拷贝时会发生所有权转移,容易引发悬空指针等问题,所以在 C++11 中已被弃用 。
unique_ptr 采用独占式的内存管理方式,当 unique_ptr 对象被销毁时,它所指向的内存会被立即释放 。这是因为 unique_ptr 不允许拷贝和赋值,确保了资源的独占性,从而保证了内存释放的及时性和正确性 。例如,在管理一个文件句柄时,可以使用 unique_ptr 来确保在不再需要文件句柄时,文件资源能够被及时关闭和释放 。
shared_ptr 通过引用计数来管理内存,只有当引用计数为 0 时,对象所占用的内存才会被释放 。这种内存管理方式使得 shared_ptr 在需要共享资源的场景中非常灵活,但也带来了一定的性能开销,因为每次引用计数的增减都需要进行额外的操作 。此外,在多线程环境下,由于引用计数的修改不是原子操作,需要额外的同步机制来保证线程安全 。
weak_ptr 本身并不直接管理内存,它主要用于协助 shared_ptr 解决循环引用问题 。当 weak_ptr 指向的对象被销毁后,weak_ptr 并不会自动变为空指针,而是需要通过 lock 函数来检查对象是否仍然存在 。如果对象已被销毁,lock 函数会返回一个空的 shared_ptr,从而避免了对已释放内存的访问 。
应用场景不同
不同的智能指针适用于不同的应用场景 。unique_ptr 适用于需要明确单一所有权的场景,例如在函数返回值中传递动态分配的对象,或者在类中管理成员对象的生命周期 。在一个工厂函数中,当创建一个新的对象并返回时,可以使用 unique_ptr 来确保对象的所有权能够安全地转移给调用者,并且在调用者不再需要该对象时,对象能够被正确释放 。
shared_ptr 适用于需要多个对象共享同一资源的场景,比如在实现一个全局的资源池时,多个模块可能需要从资源池中获取和使用资源,使用 shared_ptr 可以方便地管理资源的生命周期,确保在所有模块都不再使用资源时,资源才会被释放 。此外,在实现一些复杂的数据结构,如哈希表、链表等,当节点需要被多个地方引用时,也可以使用 shared_ptr 来管理节点的所有权 。
weak_ptr 主要用于解决 shared_ptr 之间的循环引用问题,以及在需要观察对象状态但不影响对象生命周期的场景中使用 。在一个观察者模式的实现中,观察者对象可能需要观察被观察对象的状态变化,但观察者对象的生命周期不应该影响被观察对象的生命周期,这时就可以使用 weak_ptr 来实现观察者对象对被观察对象的引用 。
更多推荐



所有评论(0)