1 智能指针的概述
毫无疑问,智能指针相比于普通的裸指针(也就是我们直接用 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<memory>
2 shared_ptr基础理解
shared_ptr指针用共享所有权的方式来管理所指向对象的生命周期的;也就是说:一个对象不仅可以被一个单独的shared_ptr所指向,也可以被多个shared_ptr所指向,多个shared_ptr相互协作,共同管理所指向对象的生命周期,当所指向对象不被需要时候,就把所指向对象释放掉它的内存。
而我们使用shared_ptr前:需要思考一个问题,所指向的对象是否需要多个指针所指向,也就是多个指针可以共享一个对象(多个指针指向同一份内存的意思);
shared_ptr管理内存的原理是:使用引用计数的方式。这种方式:可以在指向一份对象的最后一个智能指针shared_ptr不再需要指向该内存时候,就释放该对象的空间;
那我们思考一个问题:最后一个指向该对象内存的shared_ptr指针是什么时候才会销毁该对象内存空间 1.该shared_ptr指针被析构的时候,也就是该指针的生命周期结束时候; 2.这个shared_ptr指向其他对象的时候;
3 shared_ptr的初始化方式
我们要知道智能指针就是一个模板,本质智能指针是一个对象,之所以叫它为指针,因为该智能指针类里面重载了-> 运算符,使这个对象能够像指针一样,指向它所需要的内存;
3.1 默认初始化
所以智能指针的初始化方式就是和容器的初始化方式差不多:基本形式如下 shared_ptr<指向的类型>智能指针名 ; 比如:
shared_ptr<string> p;
3.2 配合 new的初始化
shared_ptr<int> p1(new int(10));
shared_ptr<int> p2(new int());
我们要知道,指向的对象的初始化方式使由该对象决定的,也就是new 后面的类型加上()里面的方式。
3.3 shared_ptr错误使用方式
shared_ptr<int> p = new int(10);
对于智能指针作为返回值的方式也是:下面的方式也是不行
shared_ptr<int> fun(int x)
{
return new int(x);
}
起始我们可以用普通的裸指针去初始化智能指针,但是这种方式是不被推荐的,最好不要使用:
int* p = new int();
shared_ptr<int> p1(p);
上面的方式没有错误,但是不建议这么使用
3.4 使用std:: make_shared函数来初始化
其实我们可以使用一个特别的函数模板make_shared的函数去初始化shard_ptr指针,这种方式初始化也是被认为最安全,最高效的一种分配方式.(ps:虽然我不知道高效在哪里,但是看书资料是这么说的)
make_shared返回值是指向该对象的shared_ptr指针。
shared_ptr<int> p = std::make_shared<int>(100);
shared_ptr<string> p = std::make_shared<string>(5,'a');
auto p = std::make_shared<string>(5,'a');
make_shared函数的参数,是根据指向对象的初始化方式的参数。
但是,使用make_shared函数来初始化,shared_ptr指针的话,那么该shared_ptr指针就无法自定义删除器了。(至于什么是删除器,后面再讲);
4 shared_ptr引用计数的增加和减少
我们知道shared_ptr是通过引用计数来管理指向对象内存空间的释放的。 那么只要shared_ptr的引用计数为0时候,那么就会自动释放指向该对象的内存空间;
shared_ptr<int> p1 = std::make_shared<int>(10);
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);
return 0;
}
所以总的来说,形参是赋值的方式接收实参的话,那么引用计数就是现+1后减1,相当于没变化;
那么也很容易理解另一种情况就是,以shared_ptr引用的方式去接收实参,引用计数就是实实在在的没发生变化;
void fun(shared_ptr<int>& p )
{
}
int main()
{
shared_ptr<int> p1(new int(10));
fun(p1);
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);
return 0;
}
shared_ptr<int> fun(shared_ptr<int>& p )
{
return p;
}
int main()
{
shared_ptr<int> p1(new int(10));
auto p2 = fun(p1);
return 0;
}
引用计数减少的情况: 第一:当指向同一份对象的shared_ptr指向另一个对象空间时候,此时引用计数就会减少
shared_ptr<int> p1(new int(10));
auto p2(p1);
p2 = std::make_shared<int>(20);
第二:当shared_ptr的指针,离开了作用域后,调用自己的析构函数,此时,引用计数也会减少;
shared_ptr<int> p1(new int(10));
auto p2(p1);
void fun()
{
shared_ptr p3(p1);
shared_ptr p4(p2);
}
5 shared_ptr常用的成员函数
5.1 use_count成员函数
use_count成员函数使用来统计有多少个shared_ptr指针指向同一份内存空间对象的;
shared_ptr<int> p1(new int(10));
int nums = p1.use_count();
shared_ptr<int> p2(p1);
int nums = p2.use_count();
shared_ptr<int> p3(p2);
int nums = p3.use_count();
有一个细节:就是shared_ptr的对象,用哪个调用use_count函数都是可以的,p1,p2,p3调用use-count都是可以的
5.2 unique成员函数
这个成员函数主要是判断: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())
{
cout<<"只有一个shared_ptr指针指向同一份内存空间"<<endl;
}else
{
cout<<"多个shared_ptr指向同一份内存空间"<<endl;
}
5.3 reset成员函数
reset成员函数就是重置shared_ptr指针的的意思。
reset成员有两个重载版本: 第一个无参数的版本:重置该shared_ptr为空,同时引用计数减一,如果减到0就释放指针指向的内存空间; 第二个有参数的版本:重置shared_ptr指向为该参数的内存空间对象中,并且原来的内存空间对象的引用计数减一,如果减到0那么就释放该内存空间;
无参数的reset函数
shared_ptr<int> p1(new int(10));
p1.reset();
shared_ptr<int> p1(new int(10));
shared_ptr p2(p1);
p1.reset();
有参数的版本reset函数
shared_ptr<int> p1(new int(10));
p1.reset(new int(20));
shared_ptr<int> p1(new int(10));
shared_ptr p2(p1);
p1.reset(new int(20));
6 指定删除器和指向数组的问题
C++的智能指针初始化的第二个参数,可以指定自定义的删除器,其实这个删除器就是一个函数指针,并且是单参数的函数指针,当然,你也可以传lambda表达式。 如果不指定第二个初始化的参数,那么就是使用默认的删除器,也就是直接delete的版本;
为什么要指定自己的删除器呢? 因为智能指针在管理数组指针时候,需要释放数组的内存,假如使用默认的删除器,也就是直接delete,那么就会导致内存泄漏了,所以需要自己指定自己删除器,去释放数组内存;
class A
{
public:
A()
{
cout<<"A()构造函数执行"<<endl;
}
~A()
{
cout<<"A()析构函数执行"<<endl;
}
};
int main()
{
shared_ptr<A> p(new A[10],[](A* p){
delete[] p;});
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]);
7 shared_ptr带来的循环引用问题–weak_ptr解决方案
什么是循环引用?什么又是weak_ptr;
什么是weak_ptr指针 1.首先我们得知道weak_ptr:是一种辅助shared_ptr的智能指针; 也就是说,weak_ptr本身是不可以被单独使用的; 不可以被单独使用的意思:weak_ptr<int> p(new int(10)) 这种方式是不可以创建weak_ptr对象的,这是错误的用法; 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> 类型的指针; 在Car类设计一个成员变量 shared_ptr<People> 类型的指针;
#include<iostream>
#include<memory>
using namespace std;
class 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());
shared_ptr<Car> car(new Car());
people->_car = car;
car->_people = people;
}
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<Car> _car 为weak_ptr<Car> _car ,当然你也可以在Car类修改,只要修改其中一个就可以了;
#include<iostream>
#include<memory>
using namespace std;
class Car;
class People
{
public:
weak_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());
shared_ptr<Car> car(new Car());
people->_car = car;
car->_people = people;
}
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的空间了.
8 unique_ptr的基本使用
unique_ptr指针就是一种独占式的指针,也就是说,执行一个对象内存时候,只能有一个unique指针指向,不可以有多个;
所以说:基本没什么区别和shared_ptr的用法,那我们只要来分析一些常见的错误即可;
不可以拷贝构造; 不可以赋值初始化; 不可以赋值拷贝;
unique_ptr<string> p1(new string("hello world!"));
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);
9 智能指针选取问题
假如程序中要使用多个指针指向同一个对象,选用shared_ptr; 假如程序中要使用单个指针指向同一个对象,选用unique_ptr;
|