智能指针的作用
RAII
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
- 不需要显式地释放资源。
- 采用这种方式,对象所需的资源在其生命期内始终保持有效。
在c++中,动态内存的管理式通过一对运算符来完成的:new,在动态内存中为对象分配空间并返回一个指向该对象的指针,我们可以选择对对象进行初始化;delete,接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。动态内存的使用很容易出现问题,因为确保在正确的时间释放内存是极其困难的。有时使用完对象后,忘记释放内存,造成内存泄漏的问题。
内存泄漏举例
#include <iostream>
#include <string>
#include <memory>
using namespace std;
// 动态分配内存,没有释放就return
void memoryLeak1() {
string *str = new string("动态分配内存!");
return;
}
// 动态分配内存,虽然有些释放内存的代码,但是被半路截胡return了
int memoryLeak2() {
string *str = new string("内存泄露!");
// ...此处省略一万行代码
// 发生某些异常,需要结束函数
if (1) {
return -1;
}
/
// 另外,使用try、catch结束函数,也会造成内存泄漏!
/
delete str; // 虽然写了释放内存的代码,但是遭到函数中段返回,使得指针没有得到释放
return 1;
}
int main(void) {
memoryLeak1();
memoryLeak2();
return 0;
}
memoryLeak1函数中,new了一个字符串指针,但是没有delete就已经return结束函数了,导致内存没有被释放,内存泄露!
memoryLeak2函数中,new了一个字符串指针,虽然在函数末尾有些释放内存的代码delete str,但是在delete之前就已经return了,所以内存也没有被释放,内存泄露!
使用指针,我们没有释放,就会造成内存泄露。但是我们使用普通对象却不会!
思考:如果我们分配的动态内存都交由有生命周期的对象来处理,那么在对象过期时,让它的析构函数删除指向的内存,这看似是一个 very nice 的方案?
智能指针就是通过这个原理来解决指针自动释放的问题!
所谓的智能指针本质就是一个类模板,它可以创建任意的类型的指针对象,当智能指针对象使用完后,对象就会自动调用析构函数去释放该指针所指向的空间。
智能指针的基本框架,所有的智能指针类模板中都需要包含一个指针对象,构造函数和析构函数。
- C++98 提供了 auto_ptr 模板的解决方案
- C++11 增加unique_ptr、shared_ptr 和weak_ptr
定义和使用
- 智能指针的使用跟普通指针类似,可以使用运算符“ * " 和 ” -> "去获得指向的对象,因此,我们就需要在类中重载" * " 和" -> "函数。
- 当程序结束时,此时ptr1和ptr2指针被销毁时,对象ptr1和ptr2会自动调用析构函数去释放所指向的资源,这是智能指针特点。
- 由于我的类中没有定义拷贝构造函数和赋值重载函数,那么我们只能调用类中原生的拷贝构造函数和赋值重载函数。那么就会程序就会出现崩溃的问题
- ptr2和ptr1指向的同一块空间,当ptr2被销毁时,它会调用它的析构函数去delete该资源对象,当ptr1被销毁时,也会去调用它的析构函数去释放ptr1所指向的资源.所以,当程序结束时,ptr2被先被销毁,同时释放ptr2所指向的资源,然后ptr1被销毁,也去释放该资源对象,那么如下的资源对象同时被释放两次,所以程序就会被崩溃掉。(资源对象被释放后,如果再去释放该资源,程序就会崩溃)
综上所述,我们不能使用原生的拷贝构造函数和赋值重载函数,并且定义的拷贝构造函数和赋值重载函数需要考虑只能释放一次资源对象。
智能指针常用函数
- get() 获取智能指针托管的指针地址 但我们一般不会这样使用,因为都可以直接使用智能指针去操作,除非有一些特殊情况。
// 定义智能指针
auto_ptr<Test> test(new Test);
Test *tmp = test.get(); // 获取指针返回
cout << "tmp->debug:" << tmp->getDebug() << endl;
- release() 取消智能指针对动态内存的托管 也就是智能指针不再对该指针进行管理,改由管理员进行管理!
// 定义智能指针
auto_ptr<Test> test(new Test);
Test *tmp2 = test.release(); // 取消智能指针对动态内存的托管
delete tmp2; // 之前分配的内存需要自己手动释放
- reset() 重置智能指针托管的内存地址,如果地址不一致,原来的会被析构掉 reset函数会将参数的指针(不指定则为NULL),与托管的指针比较,如果地址不一致,那么就会析构掉原来托管的指针,然后使用参数的指针替代之。然后智能指针就会托管参数的那个指针了。
// 定义智能指针
auto_ptr<Test> test(new Test);
test.reset(); // 释放掉智能指针托管的指针内存,并将其置NULL
test.reset(new Test()); // 释放掉智能指针托管的指针内存,并将参数指针取代之
CPP库中的智能指针
auto_ptr
auto_ptr是c++98版本库中提供的智能指针,该指针解决上诉的问题采取的措施是管理权转移的思想,也就是原对象拷贝给新对象的时候,原对象就会被设置为nullptr,此时就只有新对象指向一块资源空间。

如果auto_ptr调用拷贝构造函数或者赋值重载函数后,如果再去使用原来的对象的话,那么整个程序就会崩溃掉(因为原来的对象被设置为nullptr),这对程序是有很大的伤害的.所以很多公司会禁用auto_ptr智能指针。
在STL容器中使用auto_ptr存在着重大风险,因为容器内的元素必须支持可复制和可赋值
unique_ptr
unique_ptr是c++11版本库中提供的智能指针,它直接将拷贝构造函数和赋值重载函数给禁用掉,因此,不让其进行拷贝和赋值。
特性
- 基于排他所有权模式:两个指针不能指向同一个资源
- 无法进行左值unique_ptr复制构造,也无法进行左值复制赋值操作,但允许临时右值赋值构造和赋值
- 保存指向某个对象的指针,当它本身离开作用域时会自动释放它指向的对象。
- 在容器中保存指针是安全的
无法进行左值复制赋值操作,但允许临时右值赋值构造和赋值

在 STL 容器中使用unique_ptr,不允许直接赋值

auto_ptr 与 unique_ptr智能指针的内存管理陷阱
auto_ptr<string> p1;
string *str = new string("智能指针的内存管理陷阱");
p1.reset(str); // p1托管str指针
{
auto_ptr<string> p2;
p2.reset(str); // p2接管str指针时,会先取消p1的托管,然后再对str的托管
}
// 此时p1已经没有托管内容指针了,为NULL,在使用它就会内存报错!
cout << "str:" << *p1 << endl;
这是由于auto_ptr 与 unique_ptr的排他性所导致的
为了解决这样的问题,我们可以使用shared_ptr指针指针
shared_ptr
引用计数器
unique_ptr 这种排他型的内存管理并不能适应所有情况,有很大的局限!如果需要多个指针变量共享怎么办?
如果有一种方式,可以记录引用特定内存对象的智能指针数量,当复制或拷贝时,引用计数加1,当智能指针析构时,引用计数减1,如果计数为零,代表已经没有指针指向这块内存,那么我们就释放它 这就是 shared_ptr 采用的策略



如上代码,sp1 = sp2; 和 shared_ptr< Person > sp3(sp1);就是在使用引用计数了。
sp1 = sp2; --> sp1和sp2共同托管同一个指针,所以他们的引用计数为2;
shared_ptr< Person > sp3(sp1); --> sp1和sp2和sp3共同托管同一个指针,所以他们的引用计数为3;
构造
1). shared_ptr< T > sp1; 空的shared_ptr,可以指向类型为T的对象
shared_ptr sp1;
Person *person1 = new Person(1);
sp1.reset(person1); // 托管person1
2). shared_ptr< T > sp2(new T()); 定义shared_ptr,同时指向类型为T的对象
shared_ptr sp2(new Person(2));
shared_ptr sp3(sp1);
3). shared_ptr sp4; 空的shared_ptr,可以指向类型为T[]的数组对象 C++17后支持
shared_ptr sp4;
4). shared_ptr sp5(new T[] { … }); 指向类型为T的数组对象 C++17后支持
shared_ptr sp5(new Person[5] { 3, 4, 5, 6, 7 });
5). shared_ptr< T > sp6(NULL, D()); //空的shared_ptr,接受一个D类型的删除器,使用D释放内存
shared_ptr sp6(NULL, DestructPerson());
6). shared_ptr< T > sp7(new T(), D()); //定义shared_ptr,指向类型为T的对象,接受一个D类型的删除器,使用D删除器来释放内存
shared_ptr sp7(new Person(8), DestructPerson());
初始化
- 构造函数
shared_ptr<int> up1(new int(10)); // int(10) 的引用计数为1
shared_ptr<int> up2(up1); // 使用智能指针up1构造up2, 此时int(10) 引用计数为2
- 使用make_shared 初始化对象,分配内存效率更高(推荐使用)
- make_shared函数的主要功能是在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr;
- 用法: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);
赋值
shared_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
主动释放对象
shared_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
重置
p.reset() ; 将p重置为空指针,所管理对象引用计数 减1
p.reset(p1); 将p重置为p1(的值),p 管控的对象计数减1,p接管对p1指针的管控
p.reset(p1,d); 将p重置为p1(的值),p 管控的对象计数减1并使用d作为删除器
p1是一个指针!
交换
p1和p2是智能指针
std::swap(p1,p2); // 交换p1 和p2 管理的对象,原对象的引用计数不变
p1.swap(p2); // 交换p1 和p2 管理的对象,原对象的引用计数不变
使用陷阱
shared_ptr作为被管控的对象的成员时,小心因循环引用造成无法释放资源
在使用shared_ptr智能指针时,要注意避免对象交叉使用智能指针的情况! 否则会导致内存泄露!
当然,这也是有办法解决的,那就是使用weak_ptr弱指针。
weak_ptr
weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。 同时weak_ptr 没有重载*和->但可以使用 lock 获得一个可用的 shared_ptr 对象。
弱指针的使用
- weak_ptr wpGirl_1; // 定义空的弱指针
weak_ptr wpGirl_2(spGirl); // 使用共享指针构造
wpGirl_1 = spGirl; // 允许共享指针赋值给弱指针 - 弱指针也可以获得引用计数;
wpGirl_1.use_count() - 弱指针不支持 * 和 -> 对指针的访问;
- 在必要的使用可以转换成共享指针 lock();
shared_ptr<Girl> sp_girl;
sp_girl = wpGirl_1.lock();
// 使用完之后,再将共享指针置NULL即可
sp_girl = NULL;
在类中使用弱指针接管共享指针,在需要使用时就转换成共享指针去使用即可
expired函数
判断当前weak_ptr智能指针是否还有托管的对象,有则返回false,无则返回true
如果返回true,等价于 use_count() == 0,即已经没有托管的对象了;当然,可能还有析构函数进行释放内存,但此对象的析构已经临近(或可能已发生)。
演示如何用 expired 检查指针的合法性。
在网上找了一段代码,加上自己的注释理解
智能指针的使用陷阱
- 不要把一个原生指针给多个智能指针管理;
int *x = new int(10);
unique_ptr< int > up1(x);
unique_ptr< int > up2(x);
// 警告! 以上代码使up1 up2指向同一个内存,非常危险
或以下形式:
up1.reset(x);
up2.reset(x);
- 记得使用u.release()的返回值;
在调用u.release()时是不会释放u所指的内存的,这时返回值就是对这块内存的唯一索引,如果没有使用这个返回值释放内存或是保存起来,这块内存就泄漏了.
- 禁止delete 智能指针get 函数返回的指针;
如果我们主动释放掉get 函数获得的指针,那么智能 指针内部的指针就变成野指针了,析构时造成重复释放,带来严重后果!
- 禁止用任何类型智能指针get 函数返回的指针去初始化另外一个智能指针!
shared_ptr< int > sp1(new int(10));
// 一个典型的错误用法 shared_ptr< int > sp4(sp1.get());