C++智能指针
一、什么是智能指针
智能指针就是帮我们 C++ 程序员管理动态分配的内存的,它会帮助我们自动释放 new 出来的内存,从而避免内存泄漏。
下面就是一个内存泄露的例子:
1 | |
思考:如果我们分配的动态内存都交由有生命周期的对象来处理,那么在对象过期时,让它的析构函数删除指向的内存,这看似是一个 very nice 的方案?
智能指针就是通过这个原理来解决指针自动释放的问题!
- C++98 提供了
auto_ptr模板的解决方案 - C++11 增加
unique_ptr、shared_ptr和weak_ptr
二、auto_ptr
2.1 用法
auto_ptr 是 C++ 98 定义的智能指针模板,其定义了管理指针的对象,可以将 new 获得(直接或间接)的地址赋给这种对象。当对象过期时,其析构函数将使用 delete 来释放内存!
用法:
1 | |
例如:
1 | |
例:
1 | |
2.2 常用内置函数
get():获取智能指针托管的指针地址
1
2
3
4
5// 定义智能指针
auto_ptr<Test> test(new Test);
Test *tmp = test.get(); // 获取指针返回
cout << "tmp->debug:" << tmp->getDebug() << endl;
但一般不会这样使用,因为都可以直接使用智能指针去操作,除非有一些特殊情况。
release():取消智能指针对动态内存的托管
1
2
3
4
5// 定义智能指针
auto_ptr<Test> test(new Test);
Test *tmp2 = test.release(); // 取消智能指针对动态内存的托管
delete tmp2; // 之前分配的内存需要自己手动释放
也就是智能指针不再对该指针进行管理,改由程序员进行管理!
重置智能指针托管的内存地址,如果地址不一致,原来的会被析构掉
1
2
3
4
5
6// 定义智能指针
auto_ptr<Test> test(new Test);
test.reset(); // 释放掉智能指针托管的指针内存,并将其置NULL
test.reset(new Test()); // 释放掉智能指针托管的指针内存,并将参数指针取代之
reset 函数会将参数的指针(不指定则为 NULL),与托管的指针比较,如果地址不一致,那么就会析构掉原来托管的指针,然后使用参数的指针替代之。然后智能指针就会托管参数的那个指针了。
2.3 使用注意
尽可能不要将
auto_ptr变量定义为全局变量或指针1
2// 没有意义,全局变量也是一样
auto_ptr<Test> *tp = new auto_ptr<Test>(new Test);除非自己知道后果,不要把auto_ptr 智能指针赋值给同类型的另外一个 智能指针;
1
2
3auto_ptr<Test> t1(new Test);
auto_ptr<Test> t2(new Test);
t1 = t2; // 不要这样操作...C++11 后
auto_ptr已经被“抛弃”,已使用unique_ptr替代!C++11 后不建议使用auto_ptr。
原因如下:
复制或者赋值都会改变资源的所有权
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21// auto_ptr 被C++11抛弃的主要原因
auto_ptr<string> p1(new string("I'm Li Ming!"));
auto_ptr<string> p2(new string("I'm age 22."));
cout << "p1:" << p1.get() << endl;
cout << "p2:" << p2.get() << endl;
// p2赋值给p1后,首先p1会先将自己原先托管的指针释放掉,然后接收托管p2所托管的指针,
// 然后p2所托管的指针制NULL,也就是p1托管了p2托管的指针,而p2放弃了托管。
p1 = p2;
cout << "p1 = p2 赋值后:" << endl;
cout << "p1:" << p1.get() << endl;
cout << "p2:" << p2.get() << endl;
/* output
p1:0x1071dd0
p2:0x1071e00
p1 = p2 赋值后:
p1:0x1071e00
p2:0
*/在 STL 容器中使用
auto_ptr存在着重大风险,因为容器内的元素必须支持可复制和可赋值1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23vector<auto_ptr<string>> vec;
auto_ptr<string> p3(new string("I'm P3"));
auto_ptr<string> p4(new string("I'm P4"));
// 必须使用std::move修饰成右值,才可以进行插入容器中
vec.push_back(std::move(p3));
vec.push_back(std::move(p4));
cout << "vec.at(0):" << *vec.at(0) << endl;
cout << "vec[1]:" << *vec[1] << endl;
// 风险来了:
vec[0] = vec[1]; // 如果进行赋值,问题又回到了上面一个问题中。
cout << "vec.at(0):" << *vec.at(0) << endl;
cout << "vec[1]:" << *vec[1] << endl;
/* output
vec.at(0):I'm P3
vec[1]:I'm P4
vec.at(0):I'm P4
vec[1]:
*/不支持对象数组的内存管理
1
auto_ptr<int[]> array(new int[5]); // 不能这样定义
三、unique_ptr
3.1 特性
特性:
- 基于排他所有权模式:两个指针不能指向同一个资源
- 无法进行左值
unique_ptr复制构造,也无法进行左值复制赋值操作,但允许临时右值赋值构造和赋值 - 保存指向某个对象的指针,当它本身离开作用域时会自动释放它指向的对象。
- 在容器中保存指针是安全的
unique_ptr 和 auto_ptr 用法几乎一样,除了一些特殊,见下:
a. 无法进行左值复制赋值操作,但允许临时右值赋值构造和赋值:
1 | |
b. 在 STL 容器中使用unique_ptr,不允许直接赋值 :
1 | |
c. 支持对象数组的内存管理:
1 | |
3.2 使用实例
构造:
1 | |
赋值:
1 | |
主动释放对象:
1 | |
放弃对象的控制权:
1 | |
重置:
1 | |
3.3 缺陷
1 | |
这是由于 auto_ptr 与 unique_ptr 的排他性所导致的!
为了解决这样的问题,我们可以使用 shared_ptr 指针!
四、shared_ptr
如果有一种方式,可以记录引用特定内存对象的智能指针数量,当复制或拷贝时,引用计数加1,当智能指针析构时,引用计数减1,如果计数为零,代表已经没有指针指向这块内存,那么我们就释放它!这就是 shared_ptr 采用的策略!
例:
1 | |
4.1 使用实例
调用
use_count函数可以获得当前托管指针的引用计数1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33shared_ptr<Person> sp1;
shared_ptr<Person> sp2(new Person(2));
// 获取智能指针管控的共享指针的数量 use_count():引用计数
cout << "sp1 use_count() = " << sp1.use_count() << endl;
cout << "sp2 use_count() = " << sp2.use_count() << endl << endl;
// 共享
sp1 = sp2;
cout << "sp1 use_count() = " << sp1.use_count() << endl;
cout << "sp2 use_count() = " << sp2.use_count() << endl << endl;
shared_ptr<Person> sp3(sp1);
cout << "sp1 use_count() = " << sp1.use_count() << endl;
cout << "sp2 use_count() = " << sp2.use_count() << endl;
cout << "sp2 use_count() = " << sp3.use_count() << endl << endl;
/* output
构造函数 no = 2
sp1 use_count() = 0
sp2 use_count() = 1
sp1 use_count() = 2
sp2 use_count() = 2
sp1 use_count() = 3
sp2 use_count() = 3
sp2 use_count() = 3
析构函数 no = 2
*/构造
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17shared_ptr<Person> sp1;
Person *person1 = new Person(1);
sp1.reset(person1); // 托管person1
shared_ptr<Person> sp2(new Person(2));
shared_ptr<Person> sp3(sp1);
// C++17后支持
shared_ptr<Person[]> sp4;
// C++17后支持
shared_ptr<Person[]> sp5(new Person[5] { 3, 4, 5, 6, 7 });
shared_ptr<Person> sp6(NULL, DestructPerson());
shared_ptr<Person> sp7(new Person(8), DestructPerson());初始化
1
2
3
4
5
6
7
8// 方式一:构造函数
shared_ptr<int> up1(new int(10)); // int(10) 的引用计数为1
shared_ptr<int> up2(up1); // 使用智能指针up1构造up2, 此时int(10) 引用计数为2
// 方式二:使用make_shared初始化对象,分配内存效率更高
shared_ptr<int> up3 = make_shared<int>(2); // 多个参数以逗号','隔开,最多接受十个
shared_ptr<string> up4 = make_shared<string>("字符串");
shared_ptr<Person> up5 = make_shared<Person>(9);赋值
1
2
3shared_ptrr<int> up1(new int(10)); // int(10) 的引用计数为1
shared_ptr<int> up2(new int(11)); // int(11) 的引用计数为1
up1 = up2; // int(10) 的引用计数减1,计数归零内存释放,up2共享int(11)给up1, int(11)的引用计数为2主动释放对象
1
2
3
4shared_ptrr<int> up1(new int(10));
up1 = nullptr ; // int(10) 的引用计数减1,计数归零内存释放
// 或
up1 = NULL; // 作用同上重置
1
2
3
4p.reset() ; 将p重置为空指针,所管理对象引用计数 减1
p.reset(p1); 将p重置为p1(的值),p 管控的对象计数减1,p接管对p1指针的管控
p.reset(p1,d); 将p重置为p1(的值),p 管控的对象计数减1并使用d作为删除器
// p1是一个指针!交换
1
2
3// p1 和 p2 是智能指针
std::swap(p1,p2); // 交换p1 和p2 管理的对象,原对象的引用计数不变
p1.swap(p2); // 交换p1 和p2 管理的对象,原对象的引用计数不变
4.2 缺陷
shared_ptr 作为被管控的对象的成员时,小心因循环引用造成无法释放资源!
1 | |
可以看出,程序结束了,但是并没有释放内存 。
当执行 useTrap 函数时,注意,是没有结束此函数,boy 和 girl 指针其实是被两个智能指针托管的,所以他们的引用计数是 2。
useTrap 函数结束后,函数中定义的智能指针被清掉,boy 和 girl 指针的引用计数减 1,还剩下 1,对象中的智能指针还是托管他们的,所以函数结束后没有将 boy 和 girl 指针释放的原因就是于此。
所以在使用 shared_ptr智能指针时,要注意避免对象交叉使用智能指针的情况! 否则会导致内存泄露!
当然,这也是有办法解决的,那就是使用 weak_ptr 指针。
五、weak_ptr
weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。 同时 weak_ptr 没有重载 * 和 -> 但可以使用 lock 获得一个可用的 shared_ptr 对象。
5.1 使用实例
弱指针的使用;
1
2
3weak_ptr wpGirl_1; // 定义空的弱指针
weak_ptr wpGirl_2(spGirl); // 使用共享指针构造
wpGirl_1 = spGirl; // 允许共享指针赋值给弱指针弱指针也可以获得引用计数;
1
wpGirl_1.use_count()弱指针不支持 * 和 -> 对指针的访问;
在必要的使用可以转换成共享指针 lock();
1 | |
使用示例:
1 | |