C++ 动态内存

2023-05-13 00:13:17 来源:哔哩哔哩

我们的程序目前为止只使用过静态内存和栈内存。静态内存保存局部变量static对象,类static数据成员、以及定义在任何函数之外的变量。栈内存用来保存定义在函数内的非static对象。分配在静态或栈内存中的对象由编译器自动创建和销毁。对于栈对象,仅在其定义的程序块运行时才存在;static对象在使用之前分配,在程序结束时销毁。

除了静态内存和栈内存,每个程序还拥有一个内存池。这部分内存被称作自由空间。程序用堆来存储动态分配的对象——即那些在程序运行时分配的对象。动态对象的生存期由程序来控制,也就是说,当动态对象不再使用时,我们的代码必须显式的销毁他们。

动态内存与智能指针


(相关资料图)

在C++中,动态内存的管理是通过一对运算符完成的;new,在动态内存中为对象分配空间并返回一个指向该对象的指针,我们可以选择对对象进行初始化;delete,接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。

如果没有释放内存,会产生内存泄漏;如果在存有指针引用内存的情况下我们释放了他,就会产生引用非法内存的指针。

为了更容易且安全地使用动态内存,新的标准库提供了两种智能指针类型来管理动态对象。智能指针的行为类似常规指针,重要的区别是她负责自动释放所指的对象。标准库提供的这两种智能指针的区别在于管理底层指针的方式;shared_ptr允许多个指针指向同一个对象;unique_ptr则“独占”所指向的对象。标准库还定义了一个名为weak_ptr的伴随类,他是一种弱引用,指向shared_ptr所管理的对象。

上面三种类型都定义在memory中

shared_ptr类

类似vector,智能指针也是模板。

默认初始化的智能指针中保存一个空指针。

智能指针和普通指针使用方法类似。

make_shared函数

此函数在动态内存中分配一个对象并初始化他,返回指向此对象的shared_ptr。与智能指针一样,make_shared也定义在头文件memory中

使用make_shared必须指定要创建的对象的类型。

shared_ptr的拷贝和赋值

进行拷贝或赋值操作时,每个shared_ptr都会记录有多少个其他shared_ptr指向相同的对象

我们可以认为每个shared_ptr都有一个关联的计数器,通常称其为引用计数。无论何时我们拷贝一个shared_ptr,计数器都会递增。

例如我们用一个shared_ptr初始化另一个shared_ptr ,或将他作为参数传递给一个函数,以及作为函数的返回值时,他所关联的计数器都会递增。当我么你给shared_ptr赋予一个新值或是shared_ptr被销毁(例如一个局部的shared_ptr离开其作用域时,计数器就会递减

一旦一个shared_ptr的计数器变为0,他就会自动释放自己所管理的对象

shared_ptr自动销毁所管理的对象......

当指向一个对象的最后一个shared_ptr被销毁时,shared_ptr类会自动销毁此对象。他是通过另一个特殊的成员函数——析构函数完成销毁工作的。类似于构造函数,每个类都有一个析构函数,他负责控制此类型对象销毁时的操作。

析构函数一般用来释放对象所分配的资源。例如string的构造函数会分配内存来保存构成string的字符,而string的析构函数就负责释放这些内存。

shared_ptr的析构函数会递减他所指向对象的引用计数,如果引用计数变为0,shared_ptr的析构函数就会销毁对象并释放内存。

......shared_ptr还会自动释放相关联的内存

当动态对象不再被使用时,shared_ptr类会自动释放动态对象,这样动态内存的使用变得非常容易。

上面我们的factory函数指向一个Foo类型的动态分配对象,对象通过一个类型为T的参数初始化,由于factory返回一个shared_ptr,所以我们可以确保他分配的对象会在恰当的时刻被释放。

由于p时use_factory的局部变量,在use_factory结束时,它将被销毁。当p被销毁时,将递减其引用计数并检查他是否为0。

但如果有其他shared_ptr也指向这块内存,他就不会被释放

这个函数中,use_factory中的return语句向此函数的调用者返回一个p的拷贝。拷贝一个shared_ptr会增加所管理对象的计数。即使p被销毁,它所指向的内存还有其他使用者。所以不会销毁。

由于最后一个shared_ptr销毁前不会释放内存,保证shared_ptr无用之后不再保留就非常重要,不然就会浪费内存。(例如将shared_ptr元素存储在容器中,然后重排容器,对于不需要的容器应该确保erase掉不需要的元素

使用了动态生存期的资源的类

程序使用动态内存出于以下三种原因之一

程序不知道自己需要使用多少对象

程序不知道所需对象的准确类型

程序需要在多个对象间共享数据

容器类是出于第一种原因而是用动态内存的典型例子,我们后面会看到出于第二种原因而使用动态内存的例子。这里我们将定义一个类,它使用动态内存是为了让多个对象共享底层数据。

目前为止,我们使用过的类中,分配的资源都和对应对象生存期一致,例如每个vector“拥有”其自己的元素,当我们拷贝一个vector时,原vector和副本中的元素是相互分离的

由于一个vector分配的元素只有当这个vector存在时才存在,所以v2被销毁时,元素也被销毁。

但某些类分配的资源具有与原对象想独立的生存期。例如我们希望定义一个名为Blob的类,保存一组元素。和容器不同,我们希望Blob对象的不同拷贝之间共享相同的元素。即我们拷贝一个Blob对象时,原Blob对象及其拷贝应该引用相同的底层元素。

定义StrBlob类

最终我们会将Blob类实现为一个模板,但是我们后面才会讲到模板。因此我们先定义一个管理string的类,命名为StrBlob

实现一个新的集合类型最简单的就是使用某个标准库容器来管理元素。采用这种方法,我们可以借助标准库类型来管理元素所使用的内存空间。

但是我们不能在一个Blob对象中直接保存vector,因为如果我们将vector保存在Blob中,当b2离开作用域时,此vector也会被销毁同时元素也将不复存在。为了保证vector中的元素继续存在,我们将vector保存在动态内存中。

为了实现我们希望的数据共享,我们为每一个StrBlob设置一个shared_ptr来管理动态内存分配的vector。此成员记录有多少个StrBlob共享的vector,并在最后一个vector使用者被销毁时释放vector

我们实现了size、empty和push_back成员,这些成员通过指向底层vector的data成员来完成他们的工作。

StrBlob的构造函数

两个构造函数都适用初始化列表来初始化其data成员,令他指向一个动态分配的vector。

元素访问成员函数

pop_back、front、back操作访问vector中的元素,这些操作在试图访问元素之前必须检查元素是否存在。由于这些成员函数需要做相同的检查操作,我们为StrBlob定义了一个名为check的private工具函数,他检查一个给定索引是否在合法范围内。

除了索引,check还接受一个string参数,他将此参数传递给异常处理程序。

pop_back和元素访问成员函数首先调用check。如果check成功,这些成员函数继续利用底层vector的操作来完成自己的工作

StrBlob的拷贝、赋值和销毁

类似Sales_data类,StrBlob使用默认版本的拷贝、赋值和销毁成员函数来对此类型的对象进行这些操作。默认情况下,这些操作拷贝、赋值和销毁类的数据成员。我们StrBlob类只有一个数据成员,也就是shared_ptr类型。因此,当我们拷贝、赋值或销毁一个StrBlob对象时,它的shared_ptr成员会被拷贝、赋值或销毁。

对于StrBlob构造函数分配的vector,当最后一个指向它的StrBlob对象被销毁时,他会随之被自动销毁。

编辑:

最近更新

每日推荐

浙银申购是什么板块?浙银申购

浙银申购是什么板块?浙银申购是浙商银行发行的新股,股票代码是60...

铁路股票龙头股有哪些?铁路股

铁路股票龙头股有哪些?1、中国铁建(601186):铁路基建龙头股。在...

农机股票龙头股有哪些?农机展

农机股票龙头股有哪些?1、星光农机星光农机主要是进行收割机的生...

开高铁需要报考什么专业?开高

开高铁需要报考什么专业?1、铁道机车智能运用技术专业。2、高速铁...

在线教育的收入来源主要有哪些

在线教育的收入来源主要有哪些?1、内容收费2、增值服务收费3、平...

栏目排行