毫无疑问,智能指针相比于普通的裸指针(也就是我们直接用 new出来的对象的指针)更加智能,最明确的体现在于,可以自动帮你管理内存泄漏的问题,也就是说是,使用智能指针,不需要你手动去delete一个指针;
简单的说:只能指针就是对普通的裸指针进行了一层包装,包装之后,就使得这个指针更加智能,能够自动在合适时间帮你去释放内存;
C++标准库提供了四种智能指针的使用:
std::auto_ptr; c++98就有的一种智能指针,但是现在被遗弃,完全被std::unique_ptr所取代;
下面三种都是C++11提供的新智能指针;
std::unique_ptr; 一种独占式智能指针,同一个时间内只能有一个指针指向该对象;
std::shared_ptr;多个指针可以指向同一个对象的指针;
std::weak_ptr;一种辅助std::shared_ptr指针而存在的;
使用智能指针时候,记得包含头文件#include
shared_ptr指针用共享所有权的方式来管理所指向对象的生命周期的;也就是说:一个对象不仅可以被一个单独的shared_ptr所指向,也可以被多个shared_ptr所指向,多个shared_ptr相互协作,共同管理所指向对象的生命周期,当所指向对象不被需要时候,就把所指向对象释放掉它的内存。
而我们使用shared_ptr前:需要思考一个问题,所指向的对象是否需要多个指针所指向,也就是多个指针可以共享一个对象(多个指针指向同一份内存的意思);
shared_ptr管理内存的原理是:使用引用计数的方式。这种方式:可以在指向一份对象的最后一个智能指针shared_ptr不再需要指向该内存时候,就释放该对象的空间;
那我们思考一个问题:最后一个指向该对象内存的shared_ptr指针是什么时候才会销毁该对象内存空间
1.该shared_ptr指针被析构的时候,也就是该指针的生命周期结束时候;
2.这个shared_ptr指向其他对象的时候;
我们要知道智能指针就是一个模板,本质智能指针是一个对象,之所以叫它为指针,因为该智能指针类里面重载了->
运算符,使这个对象能够像指针一样,指向它所需要的内存;
所以智能指针的初始化方式就是和容器的初始化方式差不多:基本形式如下
shared_ptr<指向的类型>智能指针名
;
比如:
shared_ptr<string> p; //指向一个类型为string的智能指针p1,
//这种方式为默认初始化,也就是会给智能指针赋值为nullptr;
//指向一个类型为int类型的智能指针, 并且指向该对象的初始值为10;
shared_ptr<int> p1(new int(10));
//指向一个类型为int类型的智能指针, 并且指向该对象的初始值为0;
shared_ptr<int> p2(new int());
我们要知道,指向的对象的初始化方式使由该对象决定的,也就是new 后面的类型加上()里面的方式。
shared_ptr<int> p = new int(10); //这种方式使错误的,因为shared_ptr是不支持隐式构造的;
//也就是说: = 号右边是 int* 类型, = 左边是 shared_ptr类型,类型不对应是不行;
对于智能指针作为返回值的方式也是:下面的方式也是不行
shared_ptr<int> fun(int x)
{
return new int(x); //这种方式也是错误的,原因也是不支持隐式构造,也就是类型不对应
}
起始我们可以用普通的裸指针去初始化智能指针,但是这种方式是不被推荐的,最好不要使用:
int* p = new int();
shared_ptr<int> p1(p);
上面的方式没有错误,但是不建议这么使用
其实我们可以使用一个特别的函数模板make_shared的函数去初始化shard_ptr指针,这种方式初始化也是被认为最安全,最高效的一种分配方式.(ps:虽然我不知道高效在哪里,但是看书资料是这么说的)
make_shared返回值是指向该对象的shared_ptr指针。
//类似:shared_ptr p (new int(10));
shared_ptr<int> p = std::make_shared<int>(100);
//类似shared-ptr p(new string(5,'a'));
shared_ptr<string> p = std::make_shared<string>(5,'a');
//还可以结合 auto使用
auto p = std::make_shared<string>(5,'a'); //这种写法比较简洁
make_shared函数的参数,是根据指向对象的初始化方式的参数。
但是,使用make_shared函数来初始化,shared_ptr指针的话,那么该shared_ptr指针就无法自定义删除器了。(至于什么是删除器,后面再讲);
我们知道shared_ptr是通过引用计数来管理指向对象内存空间的释放的。
那么只要shared_ptr的引用计数为0时候,那么就会自动释放指向该对象的内存空间;
//此时指向int类型的共享指针p1的引用计数为1
shared_ptr<int> p1 = std::make_shared<int>(10);
//此时指向int类型的共享指针p2的引用计数为2,同时p1的引用计数也是2
auto p2(p1);
可以这么理解:指向同一份对象内存的每个shared_ptr都关联着一个引用计数,当有新的shared_ptr指向该同一份对象内存空间时候,那么所有指向该同一份内存对象的shared_ptr都会增加1个引用计数;
减少也是同理;
shared_ptr作为形参的引用计数理解,此时的形参不是引用的方式
void fun(shared_ptr<int> p )
{
//代码逻辑
//...
}
int main()
{
shared_ptr<int> p1(new int(10)); //引用计数有一个
fun(p1); //当传参给fun函数形参p时候,在fun函数内部,形参还没销毁时候,
//这个shared_ptr的引用计数就是2,当p出来作用域的时候,
//那么引用计数就会减1,也就是说形参p是对象生命周期结束了,相当于引用计数没变化
return 0;
}
所以总的来说,形参是赋值的方式接收实参的话,那么引用计数就是现+1后减1,相当于没变化;
那么也很容易理解另一种情况就是,以shared_ptr引用的方式去接收实参,引用计数就是实实在在的没发生变化;
void fun(shared_ptr<int>& p )
{
//代码逻辑
//...
}
int main()
{
shared_ptr<int> p1(new int(10)); //引用计数有一个
fun(p1); //当传参给fun函数形参p时候,由于是引用的方式,所以引用计数没变,还是1,
return 0;
}
还有一种以返回值的方式:shared_ptr做函数的返回值,以值的方式接收返回值:
这种情况:分两种:
第一:当调用该函数时候,没有变量去接受该返回值,那么引用计数不变;其实这个不变,也算是变化的了,因为返回时候,相当于是赋值拷贝一份shared_ptr对象,那么这个赋值过去的share_ptr就会指向同一份对象的内存,此时引用计数就会+1,但是由于没有变量接收返回值,所以引用计数又减1,所以相当于没有变化;
第二:当调用该函数时候,有变量去接收返回值,那么引用计数+1;
//第一种情况:
shared_ptr<int> fun(shared_ptr<int>& p )
{
//代码逻辑
//...
return p;
}
int main()
{
shared_ptr<int> p1(new int(10)); //引用计数有一个
fun(p1); //没人接收fun函数返回值,引用计数还是1,
return 0;
}
//第2种情况:
shared_ptr<int> fun(shared_ptr<int>& p )
{
//代码逻辑
//...
return p;
}
int main()
{
shared_ptr<int> p1(new int(10)); //引用计数有一个
auto p2 = fun(p1); //有人接收,引用计数还是1,
return 0;
}
引用计数减少的情况:
第一:当指向同一份对象的shared_ptr指向另一个对象空间时候,此时引用计数就会减少
shared_ptr<int> p1(new int(10)); //引用计数为1
auto p2(p1); //引用计数为2,p1和p2都是2
p2 = std::make_shared<int>(20); //此时p2的引用计数为1,是因为它指向了新的空间,
//p1的引用计数为1,是因为p2指向了新的对象空间导致的;
第二:当shared_ptr的指针,离开了作用域后,调用自己的析构函数,此时,引用计数也会减少;
shared_ptr<int> p1(new int(10)); //引用计数为1
auto p2(p1); //引用计数为2,p1和p2都是2
void fun()
{
shared_ptr p3(p1); //引用计数为3
shared_ptr p4(p2); //引用计数为4
}
//当p3,p4离开作用域后,引用计数又变为2了
use_count成员函数使用来统计有多少个shared_ptr指针指向同一份内存空间对象的;
shared_ptr<int> p1(new int(10));
int nums = p1.use_count(); //nums = 1此时有一个引用计数
shared_ptr<int> p2(p1);
int nums = p2.use_count(); //nums = 2此时有一个引用计数
shared_ptr<int> p3(p2);
int nums = p3.use_count(); //nums = 3此时有一个引用计数
有一个细节:就是shared_ptr的对象,用哪个调用use_count函数都是可以的,p1,p2,p3调用use-count都是可以的
这个成员函数主要是判断:shared_ptr指针是否只有一个智能指针指向该对象,如果是:返回true,如果不是:返回false;
shared_ptr<int> p1(new int(10));
if(p1.unique()) //此时条件成立,因为只有一个引用计数
{
//输出这个结果
cout<<"只有一个shared_ptr指针指向同一份内存空间"<<endl;
}else
{
cout<<"多个shared_ptr指向同一份内存空间"<<endl;
}
shared_ptr<int> p2(p1);
if(p1.unique()) //此时不成立条件成立,因为只有2个引用计数
{
cout<<"只有一个shared_ptr指针指向同一份内存空间"<<endl;
}else //输出这个结果
{
cout<<"多个shared_ptr指向同一份内存空间"<<endl;
}
reset成员函数就是重置shared_ptr指针的的意思。
reset成员有两个重载版本: 第一个无参数的版本:重置该shared_ptr为空,同时引用计数减一,如果减到0就释放指针指向的内存空间;
第二个有参数的版本:重置shared_ptr指向为该参数的内存空间对象中,并且原来的内存空间对象的引用计数减一,如果减到0那么就释放该内存空间;
无参数的reset函数
shared_ptr<int> p1(new int(10)); //引用计数为1
p1.reset(); //p1指向空,由于引用计数会减一,减了之后变为0,就释放了该内存
shared_ptr<int> p1(new int(10)); //引用计数为1
shared_ptr p2(p1); //引用计数为2
p1.reset(); //p1指向空,由于引用计数会减一,减了之后变为1,p2的引用计数为1,P2指向的内存空间没有被释放
有参数的版本reset函数
shared_ptr<int> p1(new int(10)); //引用计数为1
p1.reset(new int(20)); //p1指向新开辟的对象内存空间,由于引用计数会减一,
//所以原来P1指向的对象的内存空间被 释放了
shared_ptr<int> p1(new int(10)); //引用计数为1
shared_ptr p2(p1); //引用计数为2
p1.reset(new int(20)); //p1指向新开辟的对象内存空间,由于引用计数会减一,
//减了之后变为1,p2的引用计数为1,P2指向的内存空间没有被释放
C++的智能指针初始化的第二个参数,可以指定自定义的删除器,其实这个删除器就是一个函数指针,并且是单参数的函数指针,当然,你也可以传lambda表达式。
如果不指定第二个初始化的参数,那么就是使用默认的删除器,也就是直接delete的版本;
为什么要指定自己的删除器呢?
因为智能指针在管理数组指针时候,需要释放数组的内存,假如使用默认的删除器,也就是直接delete,那么就会导致内存泄漏了,所以需要自己指定自己删除器,去释放数组内存;
class A
{
public:
A()
{
cout<<"A()构造函数执行"<<endl;
}
~A()
{
cout<<"A()析构函数执行"<<endl;
}
};
int main()
{
//shared_ptr p(new A[10]); //试图开辟10个A类的数组空间,用智能指针P去指向它;
//但是这会报错,报错原因就是默认删除器使用的delete p,
//这样只能析构一个数组元素,剩下的9个没有析构成功
//而我们需要的是delete[]p的方式释放内存,所以要自己指定删除器
shared_ptr<A> p(new A[10],[](A* p){
delete[] p;}); //用lambda表达式指定删除器
//这样就可以释放干净数组的内存了
//其实,删除器还有一种是C++ 标准库提供的类模板std::default_delete
//这种方式也可以用来删除数组
shared_ptr<A> p2(new A[10], std::default_delete<A[]>());
return 0;
}
在C++17提供了一种更加方便的方式来管理数组的,但是这种在C++11 和14都是不支持的,所以可能老的编译器会报错.
只在<>
尖括号 和()
小括号里面的类型都加上[ ]
中括号即可。
shared_ptr<A[]> p(new A[10]); //c++17就开始支持这种写法来管理数组
什么是循环引用?什么又是weak_ptr;
什么是weak_ptr指针
1.首先我们得知道weak_ptr:是一种辅助shared_ptr的智能指针;
也就是说,weak_ptr本身是不可以被单独使用的;
不可以被单独使用的意思:weak_ptr
这种方式是不可以创建weak_ptr对象的,这是错误的用法;p(new int(10))
2.weak_ptr的对象只能指向一个由shared_ptr创建的对象,但是weak_ptr是不管理shared_ptr指针指向的对象内存的空间生存周期的;这个weak_ptr是不会增加shared_ptr的引用计数的。
也就是说shared_ptr所指向的对象该释放空间就释放空间,和weak_ptr没有关系,尽管weak_ptr还是指向该对象的内存空间,只要shared_ptr的引用计数为0,那么就会释放该对象内存空间;
我们知道weak_ptr就是用来辅助shared_ptr使用的,那么是如何辅助呢?
首先我们得认识什么是循环引用得问题。
那么我们现来设一个场景类:一个人类,有一辆车;一个车类,需要有一个人;
在People类设计一个成员变量 shared_ptr
类型的指针;
在Car类设计一个成员变量 shared_ptr
类型的指针;
#include
#include
using namespace std;
class Car; //前置声明,使得People类里面认识Car类
class People
{
public:
shared_ptr<Car> _car;
People()
{
cout << "People的构造函数执行" << endl;
}
~People()
{
cout << "People的析构函数执行" << endl;
}
};
class Car
{
public:
shared_ptr<People> _people;
Car()
{
cout << "car的构造函数执行" << endl;
}
~Car()
{
cout << "car的析构函数执行" << endl;
}
};
void test()
{
shared_ptr<People> people(new People()); //开辟 People的堆空间
shared_ptr<Car> car(new Car()); //开辟 Car的堆空间
//再让类里的成员变量互相指向对方的shared_ptr指针
people->_car = car; //这会使得指向 car 对象的shared_ptr有2个引用计数
car->_people = people;//这会使得指向 People 对象的shared_ptr有2个引用计数
}
int main()
{
test();
return 0;
}
一旦我调用上面的test函数,你猜会输出什么结果?是否由正常的两次构造函数,和两次析构函数的调用呢?
很明显,当我执行这行代码的时候,并没有显示正确的两次析构函数,也就是说,这段代码出现了一个很严重的问题,那就是内存泄漏了。
这个也是循环引用带来的问题,导致内存泄漏,那么我们总结以下什么是循环引用呢?
也就是shared_ptr管理资源内存时候,互相指向的问题,你的shared_ptr指向我的sahred_ptr,我的shared_ptr又指向你的shared_ptr;
画个图更好理解上面的代码
我们能够很清晰的看到,这里有一个循环的圈子在相互引用这,当我们的people和car 的共享指针声明周期结束时候,也就是在栈空间销毁时候,就会导致共享指针的引用计数减1,但是我们发现仅仅是减1,没有减到0,在堆空间中还是有成员变量的共享指针相互指向对方,这就导致了对象的空间没有被释放的问题;导致了析构函数无法被执行;
那么我们如何解决这个问题呢?
其实很好解决,只要通过weak_ptr来解决即可,只要在任意一个类中把shared_ptr换成weak——ptr就可以解决循环引用带的问题了,其他代码都不需要变动。
比如我在People类中修改了shared_ptr
为weak_ptr
,当然你也可以在Car类修改,只要修改其中一个就可以了;
#include
#include
using namespace std;
class Car; //前置声明,使得People类里面认识Car类
class People
{
public:
weak_ptr<Car> _car; //修改了成了weak_ptr
People()
{
cout << "People的构造函数执行" << endl;
}
~People()
{
cout << "People的析构函数执行" << endl;
}
};
class Car
{
public:
shared_ptr<People> _people;
Car()
{
cout << "car的构造函数执行" << endl;
}
~Car()
{
cout << "car的析构函数执行" << endl;
}
};
void test()
{
shared_ptr<People> people(new People()); //开辟 People的堆空间
shared_ptr<Car> car(new Car()); //开辟 Car的堆空间
//再让类里的成员变量互相指向对方的shared_ptr指针
people->_car = car; //这里并不会使得new 的car对象引用计数变为2,依旧是1,因为这是个weak_ptr指针
car->_people = people;//这会使得指向 People 对象的shared_ptr有2个引用计数
}
int main()
{
test();
return 0;
}
那么原理是什么呢? 原理很简单,画个图就明白了
一旦栈空间的car共享指针离开作用域,那么就会就会释放 new Car对象,因为new Car的共享指针car只有1个引用计数,那么new Car对象就会调用自己的析构函数,一旦调用自己的析构函数,那么就导致new Car对象里面成员变量_people共享指针的引用计数少1,由于在栈空间people的共享指针也离开作用域,那么也就是说new People的共享指针引用计数也会少1,如此一来,由原来的两个引用计数变成0,那么就会释放 new People的空间了.
unique_ptr指针就是一种独占式的指针,也就是说,执行一个对象内存时候,只能有一个unique指针指向,不可以有多个;
所以说:基本没什么区别和shared_ptr的用法,那我们只要来分析一些常见的错误即可;
不可以拷贝构造;
不可以赋值初始化;
不可以赋值拷贝;
unique_ptr<string> p1(new string("hello world!"));
//以下三种赋值方式都不行,因为这是一个独占式指针,只能有一个指针指向该对象string的内存单元;
unique_ptr<string> p2(p1);
unique_ptr<string> p3 = p1;
unque_ptr<string> p4;
p4 = p1;
C++ 14还提供一种使用make_unique的函数模板进行初始化unique_ptr指针的,但是C++11是不支持这种写法的
unique_ptr<int> p = std::make_unique<int>(100);
假如程序中要使用多个指针指向同一个对象,选用shared_ptr;
假如程序中要使用单个指针指向同一个对象,选用unique_ptr;