内存安全(部分内容)

内存泄露

在内存中申请了空间,但是并没有在合适的时机释放它,就会造成内存泄露。

内存泄露本质上就是对内存的使用“只增不减”,内存的可用空间一点点减少、泄露出去,像是一个只漏水但是不进水的杯子。

在内存中申请空间之后,并不会自动释放掉这块内存,内存的释放分两种情况:

  1. 开发者使用delete析构函数进行手动释放。(通常,内置类型delete,自定义类型使用析构。对自定义类型使用delete,也是会调用其析构函数)
  2. 程序结束后操作系统自动释放。
    值得注意的是,等待程序结束后由操作系统自动释放,其实已经无关”安全”了,因为操作系统并不关系程序运行时内存究竟是如何分配的,它要做的就是在程序结束后将所有内存全部收回。

内存有必要手动释放吗?为什么不能等待操作系统自动回收呢?

考虑一下边界/极端情况:
使用工厂模式生产对象时,如果工厂一直生产对象,但是并不释放已使用完毕的对象,会造成内存的不断泄露。

内存泄露的危害

1、频繁GC:系统分配给每个应用的内存资源都是有限的,内存泄漏导致其他组件可用的内存变少后,一方面会使得GC的频率加剧,再发生GC的时候,所有进程都必须等待,GC的频率越高,用户越容易感应到卡顿。另一方面内存变少,可能使得系统额外分配给该对象一些内存,而影响整个系统的运行情况。

2、导致程序运行崩溃:一旦内存不足以为某些对象分配所需要的空间,将会导致程序崩溃,造成体验差。

数组越界、指针越界、野指针

数组越界、指针越界、野指针的原理与现象是比较类似的:

访问到了操作系统并未开辟的内存空间(一说“垃圾内存”)。

在初始化变量、申请内存空间的时候,操作系统除了将数据写入对应的内存,还有一个行为,就是设置该内存块的访问权限。

危害

访问未分配访问权限的内存本身就是违法的,该内存中保存的数据又是未知的,因此这种情况下,根本不知道内存情况,是一种极其危险的状态。

数组越界:

对于数组的访问,超过了其变量范围。
如通过下标访问时,下标超过了数组设定的大小;++或–操作时,超过了数组的最大、最小范围。

指针越界:

指针指向了超出其变量的作用范围。
如:

父类指针指向子类对象时,访问子类新增成员或方法或访问重写的方法(不允许,但是是否是由于指针越界引起,存疑)(只允许静态联翩)。

且对于子类重写了的方法的调用,会根据指针类型来判断,父类指针指向子类对象,调用的还是父类指针的成员函数实现方法,而非子类重写之后的方法。

解决方法:

  1. 强制类型转换(可能失败,且不安全)
  2. 动态转换,dynamic_cast(安全)
  3. 对于访问重写的方法,可以提前设置为虚函数,此时执行的是重写后的。

野指针:

指向了没有访问权限的内存的指针。通常见于:delete一块空间之后,指向该空间的指针没有置空,该指针就成为了野指针。

为了避免以上现象,程序员需要养成良好的编程习惯。

C++11智能指针

为了解决上述 因为申请内存空间而可能造成的内存泄露野指针等问题,C++11新特性中,智能指针应运而生。

智能指针对于内存的管理,是利用对象的生命周期进行的。

当智能指针离开作用域时,会自动正确地销毁动态分配的内存对象,防止出现内存泄露。

C++库中的智能指针

头文件#include <memory>

unique_ptr

独占式指针,同一时刻只能有一个指针指向同一个对象。因此不允许unique_ptr之间进行拷贝,只能通过转移构造函数进行转移。

使用该指针管理独占性资源

常用方法介绍:

  1. unique_ptr::get() 获取其保存的原生指针,尽量不要使用

  2. unique_ptr::bool() 判断是否拥有指针

  3. unique_ptr::release() 释放所管理指针的所有权,返回原生指针。但并不销毁原生指针。通常用来初始化另一个智能指针。

  4. unique_ptr::reset() 释放并销毁原生指针。如果参数为一个新指针,将管理这个新指针。

  5. unique_ptr::swap()交换两个智能指针所指向的对象

1
2
3
4
5
6
7
8
9
10
11
12
std::unique_ptr<A> a1(new A());
A *origin_a = a1.get();//错误示例:尽量不要暴露原生指针
if(a1)
{
// a1 拥有指针
}

std::unique_ptr<A> a2(a1.release());//常见用法,转义拥有权
a2.reset(new A());//释放并销毁原有对象,持有一个新对象
a2.reset();//释放并销毁原有对象,等同于下面的写法
a2 = nullptr;//释放并销毁原有对象

初始化方法:

  1. 直接初始化:unique<T> myPtr(new T); 可以, 但不能通过隐式转换来构造,如unique<T> myPtr = new T() 因为unique_ptr构造函数被声明为explicit。
  2. 移动构造:unique<T> myOtherPtr = std::move(myPtr); 但不允许复制构造,如unique<T> myOther = myPtr; 因为unique是个只移动类型。只允许承接,不允许复制。(只能通过右值引用的方式交接管理权,不能进行拷贝)
  3. 通过 make_unique 构造:unique<T> myPtr = std::make_unique<T>(); //C++14支持的语法。但是 make 都不支持添加删除器,或者初始化列表
  4. 通过reset重置:如 std::unique_ptr up; up.reset(new T());
shared_ptr

共享式指针,同一时刻可以有多个指针指向同一个对象

std::shared_ptr 共同确保对象在不被需要时被销毁, 没有哪个特定的 std::shared_ptr 拥有该对象, 它的实现机制是引用计数,内部包含一个指向资源的裸指针,另一个指向该资源的引用计数 (控制块)的裸指针。

使用该指针管理共享型资源

1
2
std::shared_ptr<A> a1(new A());
std::shared_ptr<A> a2 = a1;//编译正常,允许所有权的共享

常用方法介绍:

  1. shared_ptr::get()获取其保存的原生指针,尽量不要使用
  2. shared_ptr::bool() 判断是否拥有指针
  3. shared_ptr::reset() 释放并销毁原生指针。如果参数为一个新指针,将管理这个新指针
  4. shared_ptr::unique() 如果引用计数为 1,则返回 true,否则返回 false
  5. shared_ptr::use_count() 返回引用计数的大小
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
std::shared_ptr<A> a1(new A());
std::shared_ptr<A> a2 = a1;//编译正常,允许所有权的共享

A *origin_a = a1.get();//错误示范:尽量不要暴露原生指针

if(a1)
{
// a1 拥有指针
}

if(a1.unique())
{
// 如果返回true,引用计数为1
}

long a1_use_count = a1.use_count();//引用计数数量

使用shared_ptr可能会带来的问题:互相引用时会造成死锁,最终导致内存泄露

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include <iostream>
#include <memory>

class B;

class A
{
public:
A()
{
std::cout << "A()" << std::endl;
}

~A()
{
std::cout << "~A()" << std::endl;
}

void setB(std::shared_ptr<B> sp)
{
this->sp = sp;
}

private:
std::shared_ptr<B> sp;
};

class B
{
public:
B()
{
std::cout << "B()" << std::endl;
}

~B()
{
std::cout << "~B()" << std::endl;
}

void setA(std::shared_ptr<A> sp)
{
this->sp = sp;
}

private:
std::shared_ptr<A> sp;
};

int main()
{
std::shared_ptr<A> pa(new A());
std::shared_ptr<B> pb = std::make_shared<B>(); //两种智能指针的创建方式
std::cout << "pa count:" << pa.use_count() << std::endl;
std::cout << "pb count:" << pb.use_count() << std::endl;
pa.reset();
pb.reset();
return 0;
}

image-20241021151052851 image-20241021145916509

使用setA、setB设定好互相引用之后:

image-20241021150011885

手动释放或者等待智能指针自动释放的时候,出对象A、B作用域之后,pa、pb执行释放,但是count的值为2,因此只能先断掉pa、pb并置空,并不会释放掉A、B的内存

image-20241021151136660

而失去了对于A、B的连接,直到程序结束前,都无法通过地址找到A、B了,内存泄露

weak_ptr

用来解决shared_ptr相互引用导致的死锁问题

weak_ptr是一个弱指针,它是与shared_ptr配合使用的。

所谓弱指针,主要体现在,当使用weak_ptr指向一个对象时,并不会增加该对象的引用计数,同样的,weak_ptr无法直接调用原生指针的方法,必须将其转化为一个shared_ptr才可以。

1
2
std::shared_ptr<A> a1(new A());
std::weak_ptr<A> weak_a1 = a1;//不增加引用计数

weak_ptr最大的意义就在于解决了shared_ptr在相互引用时内存泄露的问题。

将循环引用的一方改为weak_ptr,解决内存泄露。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//修改上述代码
#include <iostream>
#include <memory>

class B;

class A
{
public:
...
void setB(std::weak_ptr<B> sp)
{
this->sp = sp;
}

private:
std::weak_ptr<B> sp;
};

class B
{
public:
...
void setA(std::weak_ptr<A> sp)
{
this->sp = sp;
}

private:
std::weak_ptr<A> sp;
};
...
image-20241021153601781
智能指针是如何避免野指针问题的?

首先,智能指针本身就是一个”智能“指针了,不需要用户去手动释放(delete),而置空是delete的后续操作,因此从根本上来说,智能指针就不需要置空。没有前提,哪儿来的后续?

其次,如果想要手动主动释放智能指针,可以调用reset()方法,当参数为空时,认定为释放原内存,并将智能指针置空;如果参数为一个新的内存,则释放原内存,并接管新的内存。或者直接给智能指针置空,这一行为默认释放原内存、置空智能指针。

C++11动态内存与智能指针_智能指针怎么释放内存-CSDN博客

后话

此处为之前做的一些笔记,关于智能指针的前置内容

当在堆内存中new出一块空间时:

  1. 操作系统会在堆内存中申请一块空间,并将其设置为可用
  2. 在栈空间创建一个指针变量,承接堆内存申请的这块空间的地址

当手动delete该空间时:

  1. 操作系统会将该内存释放掉,本质上就是将使用权限回收,将该区域设置为不可用
  2. 此时,之前的指针指向的这块空间就是不可用的了,指针指向了一块不可用的指针,成为”野指针”
  3. 因此在delete之后,需要手动将指针置空,防止出现野指针。

当在堆内存开辟空间之后,只要不进行手动释放(delete),内存一直会被占用,直到程序结束,释放所有空间。

手动释放与程序结束自动释放存在区别:

自动释放:程序结束时,操作系统并不关心程序运行时到底进行了怎样的内存分配,要做的只是将所有的内存全部回收。

手动释放:在程序运行时,为了更合理利用内存空间(内存空间是有限的),需要手动释放掉一些已创建但使用完毕、后续不会使用的内存。

通常来看,只要能确保内存不会溢出,就算不进行手动释放(delete),等待程序结束时,让操作系统自动释放程序内存,也是可以的吧?

可以,但是存在很大的安全隐患!

此前我们学习了“工厂模式”,简单来讲就是一个不断生产各种对象的工厂结构。
当这种工厂运作起来的时候,可能会源源不断生成对象,如果这个过程中,只进行new新空间,而不delete就空间,那么内存很快就会爆满,等不到程序正常退出,内存就已经溢出了。

没有delete,你可能是忘了,也可能是故意的,但是结构就是内存溢出。

就算没有溢出,也会占用大量的内存资源,从而引发其他可能的问题。

所以为了杜绝以上现象,程序员必须:有new就有delete。

然而,delete了也不一定就确保了安全了:

  1. delete之后要记得将指针置空。
  2. 可能存在多个指向同一内存的指针,在某个指针delete之后,其余指针都变成野指针了,但是程序员可能不知道,在其他的地方使用其余指针进行了delete:delete的前提是需要用指针访问到想要销毁的内存空间,因此通过野指针访问未授权的空间,本身就是非法行为,程序直接就会崩溃。

问题出现在野指针,delete行为本身是正常的,比如多次delete一个空指针,是不会报错的。

智能指针主要解决的问题就是内存泄漏相关