动态内存

静态内存:用来存储static变量与全局变量。

栈内存:用来存储普通局部变量。

内存池(自由空间,堆):用来存储动态分配的对象。这些动态对象的生存期由程序显式控制。

管理动态内存很棘手。

使用动态内存的原因

  1. 程序不知道自己需要使用多少对象(容器)
  2. 程序不知道所需对象的准确类型
  3. 程序需要在多个对象间共享数据

创建与销毁

new,为对象分配空间并返回指向该对象的指针,使用默认初始化或默认构造函数。

格式为new type,type后可接()或{}来调用构造函数或列表初始化。可以在有单一初始化器时使用auto。

当没有只有内存时,new会抛出bad_alloc,可以使用定位new——在new后增加(nothrow)来避免throw,此时会返回空指针,二者都定义于中。

delete,接受一个动态对象的指针,销毁该对象,并释放其对应内存,但内存的地址仍然可能存在在其他指针中,此时要么删除该指针,要么重置nullptr使其不成为空悬指针。

对象必须是动态的或者空指针且未释放过,否则行为未定义。

若是没有释放一个已经不再使用的空间,并且已经没有指针指向它了(内存泄漏),这会导致内存泄漏,也就是这一部分的内存将再也不能被释放,除非程序结束。

必须显式释放。

由于内置类型没有折构函数,所以一个内置类型的指针即便离开了作用域其内存空间并不会有什么变化。

因此需要及时delete,但是很难,最好使用智能指针自动释放。

智能指针

负责自动释放所指向的对象。

shared_ptr

shared_ptr是一个类。

一般使用make_shared来初始化一个shared_ptr,其参数与类型的某个构造函数相同。

虽然使用make_shared初始化时其自身返回的shared_ptr已经引用计数为1,但是初始化赋值给对象时引用计数并不会增加,可能是在返回时直接被删除了。

在拷贝和赋值,离开作用域或销毁时,其引用计数都会实时改变。

shared_ptr的实现方式实际上是让其指向一个真正存储信息的指针,引用计数内容也保存在其中。(chat)

当引用计数为0时,shared_ptr会自动调用折构函数,也就是销毁对象,释放内存。

对于shared_ptr,由于只要有一个指针不销毁,那么其占用的空间就会一直存在,因此要记得销毁不再需要的shared_ptr。

作用域运算符{}是不依赖于任何其他的关键字存在的,可以随时使用。

列表初始化返回的实际是一个initializer_list,可以当做是常量数组。

当希望一个类共用一个对象并且不因为一个类对象的销毁而使其他产生问题,即可使用shared_ptr。

在这里static与shared_ptr的区别在于static可能会存在某个类对象删除了该static元素而产生未定义,而shared_ptr即便删除了也没有问题。

与new结合使用

当希望使用new来赋值shared_ptr时,由于智能指针的构造函数是explicit的,因此不能将普通指针转为智能指针,必须显式转换,使用直接初始化。即name(new type)。当在return时使用也一样。

但最好不要混用智能指针与普通指针。

当一个普通指针被托管给shared_ptr后,不要再使用普通指针的方式访问它,因为它可能已经被shared_ptr释放了。

.get()函数会返回智能指针所指向的普通指针,用于在某些只能传递普通指针的地方,但是当shared_ptr销毁时,其也会销毁。

智能指针与其他

当程序异常结束时,直接管理的内存并不会自动释放,造成内存泄漏,而shared_ptr则会由于作用域结束而自动释放,保证程序的正常运行。

当使用没有折构函数的类时,也可以用智能指针来保证内存的释放。

在其他情况下,如关闭连接时,也可以传入新定义的delete并使用智能指针来保证会关闭连接(在不需要的时候)。

注意最好不要使用相同的内置指针来初始化或reset多个智能指针,并注意get的使用。

unique_ptr

unique_ptr拥有对象。定义时必须将其绑定到一个new返回的指针上,但是仍然需要使用直接初始化。另外,不能对其进行拷贝与赋值,但是可以通过release与reset来拷贝,且可以拷贝或赋值一个将要销毁的unique_ptr,通过return的方式。需要传入删除器的类型是因为其会影响其类型与构造方式等。

weak_ptr

weak_ptr也是一种shared_ptr,不过其自身并不计入引用计数。

由于weak_ptr可能指向为空,因此不能直接访问其指向对象,需要通过.lock()来间接访问shared_ptr来获取其访问对象。

动态数组

在使用时用new type[num],可以将type[num] using 一下。返回的是一个指向第一个元素的指针。

动态数组并不是数组类型,因此不能for,不能begin,end,也不能使用下标运算符。

初始化的方式仍然可以使用()进行值初始化或列表初始化,不同的是这里不能使用auto。

有趣的是,动态数组的大小可以为0,此时将返回一个类似于数组尾后指针的指针,不能解引用。

释放时使用delete[] p。p指向动态数组的第一个元素,并倒序删除。若p是指向其他元素的,则将未定义。

可以使用unique_ptr来管理动态数组。由于是数组,因此其他运算符没有意义,但如release,reset等仍然可用,通过在类型后加[]来表示定义一个数组,此时它将可以用下标来访问元素。release时会自动调用delete[]。

shared_ptr不提供管理动态数组。但是通过传入删除器的方式仍然可以使其来管理,但很愚蠢,通过.get()来获取其首元素来访问数组,也就是原本传入shared_ptr时使用的指针。

allocator

new与delete都是将内存操作与对对象的操作绑在一起,但则会造成不灵活,且可能会造成不必要的浪费。如定义了一个string,然后读入这个string,在这个过程中string被赋值了两次。

另外,更重要的是,如果一个类没有默认构造函数,那么就不能动态分配数组。

allocator定义于

用allocate来分配内存,会返回指向内存块首的指针,deallocate来释放内存,p与n必须与allocate时完全一致。construct构造对象,destroy销毁对象,即调用折构函数。

必须先destroy再deallocate,因为deallocate之后对象仍然存在,如果此时进行destroy,那么其折构函数访问对象成员时就会出问题,因为已经释放了。

只对construct了的对象进行destroy,且只有对于已经construct了的内存对象才能进行普通的操作。


动态内存
https://lhish.github.io/project/hide/动态内存/
作者
lhy
发布于
2024年6月30日
许可协议