C++智能指针

1.内存泄漏


这里的a的内存是编译器自动分配的,就是用指针p去指向它,在函数调用完后,编译器会自动释放a的内存。
而用new关键字(或者malloc)来动态请求一些内存时,就需要我们手动去释放内存。

new对应delete,malloc对应free。当我们没有手动释放内存时,编译器是不会帮我们去释放的。这里的delete是释放p指向的内存,不是删除p本身。所以在delete之后,还可以把p指向别的地方。如下:

new不只可以动态请求单个元素的内存,还可以请求多个元素组成的数组。如下:

在delete时记得加上方括号,但方括号里不用写元素个数,因为编译器会帮我们记下来,我们创建了多少个元素。

小知识点: 局部变量会分配在栈内存里,当函数调用结束后,局部变量就会自动释放,编译器会自动去完成释放,所以就不用我们去释放。但堆内存就用于动态分配,就需要我们手动去请求并释放这块内存。不然这块内存就会一直存在,造成内存泄漏。而且这块内存别人也访问不了,就会造成内存的浪费。要是这样一直下去,内存将会被榨干,从而造成系统崩溃。

补充知识点: 对于同一块内存,不能删两次,这样的行为属于undefined behavi。可能会没事,但也可能产生严重的后果。

当删完内存之后,可以把p赋值为nullptr,相比于NULL,它是类型安全的,因为这某些情况下,编译器会把NULL和int类型搞混。

2.智能指针

(1)共享指针(shared_ptr)


命名空间下使用。

定义和初始化

等同于

推荐使用make_shared,效率更高,在C++17之前的编译器下也更安全。

shared_ptr(共享指针),会有一个引用计数器,每当一个共享指针指向同一片空间时,计数器就会加1。当计数器减少到为0时,该片空间就会被自动释放。当有一块资源,同时有裸指针和共享指针指向它时,当共享指针减少到为0时,这块空间还是会自动释放,此时再用裸指针去访问这块资源时,就会变成未定义的行为(undefined behavior),可能就会导致比较严重的后果。所以一个良好的编程习惯就是,在使用共享指针时要避免和裸指针混用

shared_ptr 有reset(重置)函数,当直接调用时,没有参数时,会使该共享指针所指向的资源计数器减1,而本身就不再指向原来的物体。上面的代码中,调用三次reset之后,也就是Ball的计数器的值减少为0,这是Ball就会自动释放。

reset也可以接收一个指针,sp指向一个新的Ball,旧的Ball的计数值就会减少1,如果此时减少完为0,旧的Ball就会自动释放。

默认情况下,shared_ptr会用delete来删除资源,但我们也可以自定义一个删除函数。

在文件指针计数降为0时,是要自动去关闭这个文件,而不是delete这个指针。

别名(Aliasing)(shared_ptr的一个特殊用法)

用常规方法定义了一个指向Foo的共享指针f。再定义了一个指向Bar类型的共享指针b,它的第一个参数是f,第二个参数是指向f的一个成员bar,这个就是shared_ptr的别名,其结果就是b拥有了对f指向物体的管理权。我们在定义b之后,f所指向的引用计数会加1,如果b不消失,那么f指向的物体就不会被删除。但是b的数据指针指向的却是bar,是f的一个成员。当我们像普通指针使用b的时候,我们实际上访问的是Bar实例,而不是Foo实例。通常这个技巧用于访问类的成员变量,我们访问的是一个实例的成员,但是我们不希望我们在用这个实例的时候,该实例被删除,所以我们增加了对这个实例的控制权,但是我们仍然访问的是成员。


注意: shared_ptr由于有引用计数,所以难免会用额外的内存和性能开销。所以在一些对性能要求比较苛刻的场景下,共享指针可能就不太适用。

(2)独享指针(unique_ptr)

这是一种零开销的智能指针,既能帮忙管理内存,又没有额外的性能开销。非常推荐在任何合适的情况下去使用。

unique_ptr独占某个资源的管理权。当独享指针被销毁或者reset的时候,其绑定的资源也会被自动释放。既然是独享,就不能有两个unique_ptr同时指向一份资源,所以unique_ptr也就不能支持复制操作。

当unique_ptr被销毁(如,当跳出局部的作用域时)时,其绑定的资源就会自动释放,不用我们再手动delete,非常方便。

unique_ptr有两个好处: 1.在相应的场景下,不再需要手动地释放内存;2.避免一些因为异常引起的bug。

上面这段代码中虽然手动delete了p,但仍然可能引起内存泄露,因为如果p->foo()这一步抛出了异常,delete将不会被执行。但如果我们使用unique_ptr就不会出现这个问题,即便过程中抛出了异常,资源最后也会释放。

unique_ptr释放资源的方法:

1.

可以用reset方法来释放unique_ptr下的资源,也可以在释放的同时指向另外一份资源。
第一行会释放up下的资源,并把up设置为nullptr;
第二行会释放up下的资源,并把up指向一个新的Ball实例。

2.

还可以用release把unique_ptr跟资源解绑,release返回资源的裸指针同时把unique_ptr设置成nullptr,此时就需要自己手动管理内存了。

3.

直接把unique_ptr赋值为nullptr,其绑定的资源会自动释放。

小知识点:
由于unique_ptr独占一块内存的控制权,所以它不支持普通拷贝。

上面这段代码的第二三行会导致编译的时候报错,因为我们不能复制控制权。但是我们可以转移控制权。

这段代码的release就释放了up1的控制权,返回了资源的裸指针,然后用来给up2初始化,up2就获得了up2的控制权,这就是转移的效果。我们也可以使用std::move来达成同样的目的。

在默认情况下,unique_ptr使用new和delete来分配和释放内存。但也可以自定义创建和删除函数。

unique_ptr使用自定义删除函数比shared_ptr更复杂,原因在于unique_ptr绑定释放函数是在编译期,避免了运行时绑定的时间损耗。删除函数的类型本身变成unique_ptr实例的一部分。这是unique_ptr 0额外开销的特性决定的。shared_ptr的自定义函数在运行时绑定,用户用起来更简单,由于shared_ptr的引用计数已经有运行时消耗了,再增加一点也就无所谓了,所以出于这种考虑,对unique_ptr和shared_ptr的自定义释放函数做出了不同的设计。

如何在函数间传递unique_ptr?

unique_ptr性能好,没有什么额外的开销,所以推荐在任何合适的情况下去使用,但是由于它禁止复制操作,所以它在作为函数参数在函数间传递的时候有一些注意事项。

当作为参数直接传递给函数,由于unique_ptr不能复制,所以上面这种情况会导致编译错误。
(1)如果我们只是需要传递unique_ptr指向的内容,而不需要传递它本身的话,我们就只是需要传递unique_ptr底下的资源就可以了。如这样:

(2)与此类似的还有第二种方法,我们把get()作为参数传递:

up.get()获得的是资源的裸指针,也能访问资源同时也避免unique_ptr的复制。以上情况只能让我们访问unique_ptr的资源,如果我们想通过函数改变unique_ptr本身呢?

(3)就需要把函数的参数设置为unique_ptr的引用,就像如此:

(4)还有一种方法可以传递unique_ptr,就是std::move,可以转移对unique_ptr的控制权。

上面代码,我们转移了up的控制权到函数中,因此原up变成了nullptr。以上就是在函数间传递独占指针的四个方法。

函数返回unique_ptr的情况

虽然作为参数传递有着限制,但是函数可以返回unique_ptr并且用来初始化,就像这段代码所展示的一样。

(3)弱指针(weak_ptr):

简单来说,weak_ptr是shared_ptr的伴侣,它是一个观察者,它对资源的引用是非拥有式的,也就是它没有资源的管理权,不能控制资源的释放。甚至想要访问资源的时候,也需要通过创建一个临时的shared_ptr。但它可以检查资源是否存在,在我们不想要额外控制资源但是又想检查资源是否存在的时候,可以使用weak_ptr。

何时用weak_ptr,以及为什么要用weak_ptr?


shared_ptr理论上可以自动释放内存,但是有一种特殊的情况可能会造成内存泄漏,就是环形依赖。上面的代码中,在main函数中,princinple是Teacher类型的共享指针,但是它的成员(school类型的共享指针)指向了School类型的university。然后School类型的university又指向了Teacher类型的principle,这样它们就互相指向对方了,就形成了一个环形依赖。

当我们运行上面的代码后,不会输出任何的语句,也就是principel和university指向的资源的没有被释放。这是因为当principel结束生命周期的时候,university还存在;当university结束生命周期的时候,principel也还存在,它们两个shared_ptr的引用计数仍然不为0,所以就不会自动删除物体。这两个物体循环引用锁死了,导致shared_ptr的引用计数不能正常降为0,这就造成了内存泄漏。而要解决这个问题就要使用weak_ptr。

weak_ptr的用法:

定义weak_ptr很简单,类似于shared_ptr和unique_ptr,但是初始化weak_ptr需要一个shared_ptr或weak_ptr,weak_ptr本身是依赖于shared_ptr存在的。当用weak_ptr来访问资源时,必须把它临时转换成一个shared_ptr。这也是weak_ptr的意义,用来获取临时管理权。weak_ptr本身存储的是对资源的引用,而且是一个非拥有的引用。意思就是weak_ptr不能删除对象本身,只能作为一个观察者,告诉你资源是否存在。weak_ptr指向的物体可以被其他的shared_ptr所删除,如果你想要weak_ptr来访问对象,就需要使用weak_ptr的lock方法,这个方法会返回一个shared_ptr,如果其指向的资源已经被释放了,就会返回一个nullptr。

如何解决上面环形依赖的问题,只需要把Teacher里的school改成weak_ptr就好了。