智能指针
哇哦,终于学到了智能指针!!
智能指针存在的意义
可以改善传统指针的一些不便之处。那么传统的指针有哪些不便之处呢?
- 需要手动管理内存
int *p = new int();
//...
delete p; //手动释放
- 容易发生内存泄漏(忘记释放、出现异常无法释放等)
int *p = new int();
//...
//delete p; //忘记释放
try {
int *p = new int();
throw 0;
delete p; //出现异常,导致没有释放
}
catch (int &exception) {
cout << "done exception" << endl;
}
- 释放之后产生野指针
int *p = new int();
delete p;
p = nullptr; //如果不置位空指针,就会变成野指针
智能指针就是为了解决上述问题而生的。
智能指针的种类
-
auto_ptr:属于C++98标准,在C++11中已经不推荐使用(有缺陷:不能用于数组) -
shared_ptr:属于C++11标准 -
unique_ptr:属于C++11标准
auto_ptr怎么用
auto_ptr在之后的开发不推荐使用,但是毕竟是智能指针之父,还是要了解一下内部原理。
定义一个Person类,写了一个成员变量,构造函数和析构函数。然后声明一个test函数,里面包括:申请堆空间,操作对象,释放堆空间,野指针清零。
class Person {
public:
int m_age;
Person() {
cout << "Person()" << endl;
}
~Person() {
cout << "~Person()" << endl;
}
};
void test() {
Person *p = new Person();
p->m_age = 10;
p->m_age = 20;
delete p;
p = nullptr;
}
我们在main函数中调用完test函数,就会完成上述步骤:包括创建对象时调用构造函数,释放对象时调用析构函数。
int main(int argc, char *argv[])
{
test();
getchar();
return 0;
}
但,这是传统指针的做法。有很多麻烦我们也看到了,比如:必须在所有对象操作完之后才释放,还要清空指针等等。
那如果我们使用智能指针,会达到什么样的效果呢?我们重写test函数
void test() {
auto_ptr<Person> p(new Person());
p->m_age = 10;
p->m_age = 20;
}
这里说明一下智能指针的构造方式:
auto_ptr<泛型> p(new 泛型());
注意一个细节:p并不是指针,而是对象类型,是auto_ptr类的对象。且p后面的括号必须传入一个地址,也就是相当于p要“指向”的内存区域
这样的写法可以理解为:智能指针p指向了堆空间的泛型对象
。
我们可以看到,我们并没有释放p所指向的那块对象内存。依然完成了析构函数的调用。这是怎么回事呢?
智能指针之所以可以自动的delete对象内存,是因为智能指针内部有这样一个功能:当智能指针销毁的时候,所指向的对象也被delete。这个功能是在auto_ptr类的内部写好的。
也就是说:p在内存中销毁了,指向的对象内存就自动回收了。因为p定义在test函数内部,所以函数调用完毕之后,p的内存立马释放,p就销毁了,然后new Person就自动回收了。
了解了原理,我们自己实现一个智能指针:
智能指针auto_ptr的自实现
智能指针的神奇之处,就在于智能指针对象销毁的时候,所指向的对象内存也跟着销毁。这是怎么办到的呢?
几处细节:
- 需要使用模板类,因为将来我的智能指针指向的类应该是泛型的
- 智能指针内部需要有一个泛型的指针,指向即将传进来的泛型对象堆空间(T *m_object = new T() )
- 由于我们在使用智能指针的时候,需要将new T传入智能指针对象的构造函数,因此构造函数必须是T类型的指针作为参数。然后需要将传进来的new T赋值给m_object,因此这里选择初始化列表赋值即可
-
智能指针对象销毁则释放所指向的堆空间
。首先智能指针对象销毁,那就会调用智能指针对象的析构函数,所以释放所指向的堆空间的操作需要在该析构函数里完成。直接在析构函数里delete掉刚才指向new T的指针m_object即可
template<class T>
class SmartPointer
{
T *m_object; //接收传进来的new T()
public:
SmartPointer(T *obj);
~SmartPointer();
private:
};
template<class T>
SmartPointer<T>::SmartPointer(T *obj):m_object(obj)
{
//暂时不用做任何事情
//外面new Person()传进来,赋值给形参Person *obj --> Person *obj = new Person() --> Person *m_object = new Person()
//这就完成了对象的堆空间内存申请,指向堆空间的是Person指针m_object
//因此,智能指针对象p销毁的时候,会调用~SmartPointer(),只要在该析构函数中释放掉Perosn *m_object
//就可以实现:智能指针对象销毁时立马释放所指向的堆空间内存
}
template<class T>
SmartPointer<T>::~SmartPointer()
{
if (m_object == nullptr) return;
delete m_object;
}
但,想要复刻智能指针,还差一步。智能指针对象既然是指向了T对象内存,那就应该可以通过箭头“->”访问T类型的成员。我们知道,想要通过箭头“->”访问T类中的成员,就必须是T类型的指针。所以,这里在智能指针类里重载一下运算符“->”即可
// p->就会返回T类型的指针,那就可以通过箭头调用T中的成员
T *operator->() {
return m_object;
}
这样才完整了。
void test() {
SmartPointer<Person> p(new Person());
p->m_age = 10; //能箭头调用Person类里的成员变量,那就必须是Person *类型
}
总结:智能指针不过就是对传统指针的再次封装!
通俗的讲智能指针:在创建时本质是智能指针类的对象,但我们完全可以当做指向T类对象的“指针”。
auto_ptr内存变化
我们在test函数内部设置了一个断点
此时,p还没有被销毁,指向0x00000022ba67f6930,里面存放m_age = 10。
当断点执行完test函数之后,按理说p应该被销毁,指向的内存也应该被回收
然而p并没有被销毁,也没有被置为nullptr,
甚至p指向的对象内存空间也没有被回收
(但是,析构函数确实被调用了 ),这是怎么回事呢?
share_ptr怎么用
不推荐使用auto_ptr,不仅过时了,而且还存在一些问题。share_ptr的出现就是为了解决auto_ptr的问题。
auto_ptr存在的问题
auto_ptr智能指针不可以指向数组对象。
我们看到auto_ptr的源码,delete时就是默认的直接detele 指针。我们知道释放数组对象,必须是delete[],所以auto_ptr就注定了不能指向数组。
share_ptr可以指向数组
share_ptr解决了auto_ptr不能指向数组的问题。要注意,在泛型的时候,要声明为T[ ],才可以指向数组对象。
void test() {
shared_ptr<Person[]> p(new Person[5]);
p[4].m_age = 10;
}
多个 share_ptr可以指向同一个对象
多个 share_ptr可以指向同一个对象,当最后一个share_ptr在作用域范围内结束时,对象才会被自动释放。
{
shared_ptr<Person> p1(new Person());
shared_ptr<Person> p2 = p1;
shared_ptr<Person> p3 = p2;
shared_ptr<Person> p4 = p3;
//p1-p4全部指向new
}
这样写就代表,p1~p4智能指针对象都指向了Person对象。我们通过内存也可以查看这一点:他们指向的内存区域地址都是一样的,由于没有初始化m_age,因此m_age的内容是0xcdcdcdcd,也就是堆空间的初始化内容。
多个指向对象的智能指针 share_ptr全部销毁后,对象内存才会被回收
cout << 1 << endl;
{
shared_ptr<Person> p4;
{
shared_ptr<Person> p1(new Person());
shared_ptr<Person> p2 = p1;
shared_ptr<Person> p3 = p2;
p4 = p3;
//p1-p4全部指向new
}
cout << 2 << endl; //此时p1~p3全部销毁了,但是p4还没有被销毁,因此对象内存就不会被回收
}//执行完最后一个智能指针p4所在的作用域之后,p4销毁,至此全部指向Person对象的智能指针销毁
cout << 3 << endl;//此时,Person对象的内存才被回收
可以通过已经存在的share_ptr智能指针初始化一个新的share_ptr智能指针
shared_ptr<Person> p4;
{
shared_ptr<Person> p1(new Person());
shared_ptr<Person> p2 = p1; //利用已经存在的p1初始化新的p2
shared_ptr<Person> p3 = p2; //同理
p4 = p3;
}
share_ptr原理
-
一个share_ptr会对一个对象产生强引用(strong reference) -
被指向的对象会存在一个与share_ptr对应的
强引用计数
,记录这当前对象被多少个share_ptr强引用着(被share_ptr指着)
使用智能指针下的use_count()成员函数获得当前对象被多少个智能指针指向
{
shared_ptr<Person> p1(new Person());
cout << p1.use_count() << endl; //1,因为上句代码执行完,有1个智能指针指向Person(p1)
shared_ptr<Person> p2 = p1;
cout << p2.use_count() << endl; //2,因为上句代码执行完,有2个智能指针指向Person(p1,p2)
shared_ptr<Person> p3 = p2;
cout << p1.use_count() << endl; //3,使用p3或是p1调用都行,只要是已经存在的智能指针,且指向的是一个对象
}
打印1、2、3,没有问题
{
shared_ptr<Person> p3;
{
shared_ptr<Person> p1(new Person());
cout << p1.use_count() << endl; //1
shared_ptr<Person> p2 = p1;
cout << p2.use_count() << endl; //2
p3 = p2;
cout << p3.use_count() << endl;//3
}
cout << p3.use_count() << endl;//1,因为p1、p2都销毁了,到这的时候,只有p3还在,所以打印1
}
打印1、2、3、1,没有问题
-
当有一个share_ptr销毁时(比如作用域结束),对象的强引用计数就会-1 -
也就是说:当一个对象的强引用计数为0时,该对象就要被回收内存了。(因为没有任何的share_ptr指向了,说明智能指针也都销毁了)
share_ptr的循环引用问题
我们知道了有一个share_ptr指向对象的话,该对象的
强引用计数
就加1。当强引用计数为0的时候,对象才会被回收内存空间。
但是一旦出现循环引用问题,share_ptr智能指针就会发生内存泄漏,是一个Bug。那什么是循环引用呢?举个例子就明白了了
首先声明两个类:Person和Car。Person里有Car类型的智能指针,意味着人拥有车;Car里面有Person类型的智能指针,意味着车可以载人。实际需求中一定存在这种交叉包含的类。
class Person {
public:
shared_ptr<Car> m_car;
Person() {
cout << "Person()" << endl;
}
~Person() {
cout << "~Person()" << endl;
}
};
class Car {
public:
shared_ptr<Person> m_person;
Car() {
cout << "Car()" << endl;
}
~Car() {
cout << "~Car()" << endl;
}
};
紧接着,我们创建指向Person对象的智能指针person_ptr,创建指向Car对象的智能指针car_ptr。
{
shared_ptr<Person> person_ptr(new Person()); //创建指向Person对象的智能指针person_ptr
shared_ptr<Car> car_ptr(new Car()); //创建指向Car对象的智能指针car_ptr
person_ptr->m_car = car_ptr; //相当于shared_ptr<Car> m_car = car_ptr
car_ptr->m_person = person_ptr; //相当于shared_ptr<Person> m_person = person_ptr
}
然后我们使用已经创建好的Car类型智能指针初始化person_ptr->m_car(也就是初始化shared_ptr< Car> m_car,但是是初始化在Person对象里的m_car)。同理,使用已经创建好的Person类型智能指针初始化car_ptr->m_person(也就是初始化shared_ptr< Person> m_person,但是是初始化在Car对象里的m_person)
我们通过内存可以看到,car_ptr和person_ptr->m_car(shared_ptr< Car> m_car)指向的相同区域,也就是该Car对象
强引用计数
为2。
用一幅图来表达上述循环引用的逻辑:
当离开作用域之后,栈空间存在的person_ptr和car_ptr被销毁,但是堆空间的强引用还在,因此各自的强引用计数为1,不是为0。
所以,就导致了,作用域结束后,Person对象和Car对象的析构都不会被调用
那怎么解决这个问题呢?通过使用弱引用智能指针weak_ptr去解决这个问题
weak_ptr解决循环引用问题
我们只需在循环引用的两个类中,任意一个类里面声明智能指针为弱引用就可以解决这个问题。
class Person {
public:
weak_ptr<Car> m_car;
Person() {
cout << "Person()" << endl;
}
~Person() {
cout << "~Person()" << endl;
}
};
class Car {
public:
shared_ptr<Person> m_person;
Car() {
cout << "Car()" << endl;
}
~Car() {
cout << "~Car()" << endl;
}
};
一旦,变成弱引用,在作用域结束后,强引用计数不会保留,直接变成0。所以Car对象直接消失,Car对象消失了,就没有指针指向Person对象了,那么Person对象也会消失。就实现了对象内存全部回收。
总结:如果存在循环引用的需求,智能指针一定不要全部用shared_ptr,必须将其中一个类里面的智能指针设置为weak_ptr。
unique_ptr
unique字面意思:唯一的。相对于share_ptr,可以确保同一时间只有一个unique_ptr智能指针指向对象。另外,unique_ptr也会对对象产生一个强引用。
不可以这样:
只能这样:
同一时间(同一作用域),只能使用一个unique_ptr指向对象。反过来说,不同的作用域就可以使用不同的unique_ptr指向这个相同的对象了。即,我们可以在当前作用域将unique_ptr的指向权移交给另外一个作用域的unique_ptr智能指针。
C++通过std::move函数转移unique_ptr的指向权
unique_ptr<Person> p0;
{
unique_ptr<Person> p1(new Person());
p0 = std::move(p1); //将Person对象由p1指向转为p0指向
}
// 转为p0指向该Person对象,p1将唯一指向权转交给p0
总之:想要唯一指向就选择unique_ptr,其余都使用share_ptr就行。