C++智能指针

为什么使用智能指针?

智能指针使用的是RAII的思想,资源的获取和释放和对象的声明周期绑定(通过类),避免手动释放内存,对象声明周期结束自动调用析构函数释放,避免内存泄漏

比如悬空指针(指针指向的地址已经被销毁),智能指针能避免这种情况发生。

C++98提供了auto_ptr模板的解决方案 C++11新增了unique_ptr、shared_ptr、weak_ptr

auto_ptr(C++17中被完全移除/弃用)

auto_ptr是C++98中定义的智能指针模板,可以将new获得的地址赋值给auto_ptr类型的指针,当对象过期时,调用析构函数中的delete释放内存。

1
2
3
4
5
// auto_ptr声明方式:auto_ptr<类型> 变量名(new 类型);
#include<memory>
auto_ptr<string> str_ptr(new string("这是一个字符串"));
auto_ptr<vector<int>> ve_ptr(new vector<int>());
auto_ptr<int> array_ptr(new int[10]);

对于类

1
2
3
4
5
6
7
8
class T
{
public:
int Display(){}
}
T *tptr = new T; //只调用构造函数,不会调用析构函数,不会释放new出来的内存,需要手动delete
auto_ptr<T> tptr1(new T); //等到tptr1的声明周期结束,还会调用析构函数(管理指针的类的析构),再牵扯调用类的析构
cout << tptr1->Display() << (*tptr).Display() << endl; //智能指针重载了 *(返回普通对象) 和 ->(返回指针对象) 运算符

智能指针常用的三个函数

1
2
3
4
5
6
7
8
9
10
auto_ptr<T> tptr(new T);
// get() 获取智能指针托管的指针地址
T *tmp = tptr.get(); // 一般不用这个操作
cout << tmp->Display();
// release() 取消智能指针对动态内存的托管
T *tmp2 = tptr.release(); //tptr不再指向原内存地址,指向NULL
delete tmp2;
//reset() 重置智能指针托管的内存地址,如果地址不一致,原来的会被析构掉
tptr.reset(); //释放掉内存,将tptr指向NULL
tptr.reset(new T()); //释放掉内存,指向新的内存

auto_ptr从C++11之后被抛弃的主要原因(被unique_ptr取代了)

  • 复制或赋值都会改变资源的所有权
  • 在STL容器中使用非常危险,因为容器中的元素必须支持可复制和赋值(别把指针传入容器,就算使用了std::move()避免了,在容器中修改值也寄了)
  • 不支持对象数组的内存管理(🚫auto_ptr<int[]> array(new int[5]);)因为auto_ptr并不会使用delete[],会错误的使用delete释放,引发内存泄漏甚至崩溃(访问非法内存)

unique_ptr

  • 同auto_ptr的特性一样,有排他所有权模式:两个指针不能指向同一个资源
  • 无法进行左值unique_ptr复制构造,也无法进行左值复制赋值操作,但是允许临时右值赋值构造和赋值
  • 保存指向某个对象的指针,当它本身离开作用域时会自动释放它指向的对象
  • 在容器中保存指针是安全的

使用 std::move 可以把左值转换成右值

1
2
3
4
5
6
7
8
9
10
11
12
//无法进行左值复制赋值操作,但允许临时右值赋值构造和赋值
unique_ptr<string> p1(new string("I'm Li Ming!"));
unique_ptr<string> p2(new string("I'm age 22."));
unique_ptr<string> p3(std::move(p1)); //p1被置为NULL,p3里是原p1指向的地址
p1 = std::move(p2); //同上,道理一样

//在 STL 容器中使用unique_ptr,不允许直接赋值
vec.push_back(std::move(p3));
vec[0] = std::move(vec[1]);

//支持对象数组的内存管理
unique_ptr<int[]> array(new int[5]);

除上述用法,其它与auto_ptr无差别。

初始化方法

1
2
std::unique_ptr<std::string> p1(new std::string("Hello"));
std::unique_ptr<std::string> p1 = std::make_unique<std::string>("Hello"); //C++14开始

使用 make_unique 减少了手动使用new的需求;它将对象的构造函数和内存分配结合在一起,避免了潜在的内存泄漏;支持数组的安全初始化,自动使用delete[]释放数组;提供更好的(一丢丢) 性能和内存管理。

内存管理陷阱

1
2
3
4
5
unique_ptr<string> p1, p2;
string *str = new string("内存管理陷阱");
p1.reset(str);
p2.reset(str); //p2接管str指针,会先取消p1的托管,此时p1指向了NULL
cout << *p1 << endl; //再使用p1时,会引发异常(内存错误)

shared_ptr

多个指针变量共享内存。记录引用特定内存对象的智能指针数量,当复制时,引用计数+1,当智能指针析构时,引用计数-1,如果计数为0,代表已经没有指针指向这块内存,我们就可以释放了。

1
2
element_type* _Ptr{nullptr};    //指向申请的内存
_Ref_count_base* _Rep{nullptr}; //指向引用计数

1.引用计数的使用

调用use_count函数可以获得当前托管指针的引用计数

1
2
3
4
5
6
shared_ptr<T> p1;
shared_ptr<T> p2(new T);
p1 = p2;
cout << p1.use_count() << endl; //2
shared_ptr<T> p3(p2);
cout << p1.use_count() << endl; //3

2.构造

1
2
3
4
5
6
7
8
9
10
11
12
shared_ptr<T> p1;
T *tptr = new T(1);
p1.reset(tptr); //托管tptr,不需要delete tptr了

shared_ptr<T> p2(new T(2));
shared_ptr<T> p3(p1);

shared_ptr<T[]> p4; //C++17后支持
shared_ptr<T[]> p5(new T[5]{3,4,5,6,7}); //C++17后支持

shared_ptr<T> p6(NULL, DestructT()); //空的指针,接受一个DestructT()类型的删除器,用它释放内存
shared_ptr<T> p7(new T(8), DestructT());

3.初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//构造函数
shared_ptr<int> up1(new int(10));
shared_ptr<int> up2(up1);
//使用make_shared初始化对象,分配内存效率更高(推荐)
shared_ptr<int> up3 = make_shared<int>(2);
shared_ptr<string> up4 = make_shared<string>("string");
shared_ptr<T> up5 = make_shared<T>(9);
//赋值
shared_ptr<int> up6(new int(10));
shared_ptr<int> up7(new int(11));
up6 = up7; //int(10)的引用计数-1,int(11)的引用计数+1,up2共享int(11)给up1
//主动释放对象
shared_ptr<int> up8(new int(10));
up8 = nullptr; //int(10)的引用计数-1,计数归零内存释放
up8 = NULL; //作用同上
//重置
p.reset(); //将p重置为空指针,所管理的对象引用计数-1
p.reset(p1); //将p重置为p1,p管理的对象计数-1,p接管的p1的对象计数+1
p.reset(p1,d); //同上,并使用d作为删除器
//交换
std::swap(p1,p2); //交换p1和p2管理的对象,原对象的引用计数不变
p1.swap(p2); //同上,swap()是shared_ptr中实现(重写)的,不需要加std

shared_ptr使用陷阱

shared_ptr作为被管控的对象的成员时,小心因循环引用造成无法释放资源。

比如A类中有B类的智能指针,B类中有A类的智能指针,相互持有。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Boy()
{
void setGirlFriend(shared_ptr<Girl> _girlFriend) {
this->girlFriend = _girlFriend;
}
shared_ptr<Girl> girlFriend;
}
class Girl()
{
void setBoyFriend(shared_ptr<Boy> _boyFriend) {
this->boyFriend = _boyFriend;
}
shared_ptr<Boy> boyFriend;
}
void useTrap()
{
shared_ptr<Boy> spBoy(new Boy());
shared_ptr<Girl> spGirl(new Girl());
// 陷阱用法
//spBoy->setGirlFriend(spGirl);
spGirl->setBoyFriend(spBoy);
// 此时boy和girl的引用计数都是2
}

解读:当useTrap()被调用,会初始化两个共享智能指针,spBoy指向Boy()内存,spGirl指向Girl()内存,然后进入交叉引用阶段,Boy()中有一个gF指针指向Girl(),Girl()中有一个bF指针指向Boy()。

img

可以使用weak_ptr避免这种情况的发生。

weak_ptr

目的是为了配合shared_ptr而引用的一种智能指针来协助其工作的,它只可以从一个shared_ptr或者另一个weak_ptr对象构造,它的构造和析构不会引起计数的增加或减少。

weak_ptr没有重载 * 和 ->,但是可以使用lock获得一个可用的shared_ptr对象。

1
2
3
4
5
6
7
8
9
weak_ptr wpGirl_1;
weak_ptr wpGirl_2(spGirl); //使用shared_ptr构造
wpGirl_1 = spGirl; //允许共享指针赋值给弱指针

wpGirl_1.use_count(); //也可以获得引用计数

shared_ptr<Girl> sp_girl; //必要的时候可以转换成共享指针
sp_girl = wpGirl_1.lock(); //使用lock()
sp_girl = NULL;

上述问题代码改成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Boy()
{
void setGirlFriend(shared_ptr<Girl> _girlFriend) {
this->girlFriend = _girlFriend;
}
shared_ptr<Girl> girlFriend;
}
class Girl()
{
void setBoyFriend(shared_ptr<Boy> _boyFriend) {
this->boyFriend = _boyFriend;
}
weak_ptr<Boy> boyFriend;
}
void useTrap()
{
shared_ptr<Boy> spBoy(new Boy());
shared_ptr<Girl> spGirl(new Girl());
// 陷阱用法
//spBoy->setGirlFriend(spGirl);
spGirl->setBoyFriend(spBoy);
// 此时boy和girl的引用计数都是2
}

直接解决问题!

weak_ptr的expired函数用法:判断当前weak_ptr智能指针是否还有托管的对象,有返回false,无返回true

如果返回true,相当于use_count()=0,已经没有托管对象了。可以在使用指针前加以判断,保证指针是有效的。

1
2
3
weak_ptr<int> g;
if(!g.expired()) cout << 1 << endl;//有效
else cout << 0 << endl;

智能指针的使用陷阱

  • 不要把一个原生指针给多个智能指针管理;
  • 记得使用u.release()的返回值,这个返回值是这块内存的唯一索引,没有释放的话就内存泄漏了;
  • 禁止使用delete智能指针get()得到的指针;
  • 禁止使用任何类型智能指针get()返回的地址去初始化另一个智能指针。

C++智能指针
https://kevin-aron.github.io/categories/C++11新特性/C-智能指针/
作者
Iuk
发布于
2025年3月4日
许可协议