热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

C++学习笔记(12)动态内存、智能指针、new和delete、动态数组、allocator

C++学习笔记(12)动态内存、智能指针、new和delete、动态数组、allocator参考书籍:《C++Primer5th》API:动态内存管理静态内

C++ 学习笔记(12)动态内存、智能指针、new和delete、动态数组、allocator

参考书籍:《C++ Primer 5th》
API:动态内存管理


  • 静态内存:
    • 保存局部static对象、类static数据成员、定义在函数外的变量。使用前分配,程序结束时销毁。
  • 栈内存:
    • 保持函数内的非static对象。存在于程序块中。
  • 堆内存(动态分配):
    • 程序控制动态对象。

12.1 动态内存与智能指针

  • 智能指针(smart pointer):负责自动释放所指向的对象。
    • shared_ptr:允许多个指针指向同一个对象。
    • unique_ptr:独占一个对象。
    • weak_ptr:弱引用,指向shared_ptr所管理的对象(不对shared_ptr产生影响)。
    • auto_ptr(C++17后抛弃):具有unique_ptr部分特性,不能在容器中保存,也不能作为函数返回值。

12.1.1 shared_ptr

  • 使用原因:
    • 常用于允许多个对象共享相同的状态。
    • 不知道需要多少对象。
    • 不知道所需对象准确类型(继承等)。

shared_ptr<string> p;
if(p && p->emty()) // 检查p是否为空,以及指向的字符串是否为空
*p = "hi"; // 解引用p,赋予新值
  • make_shared:动态分配一个类型,参数就该类构造函数的参数列表。

  • 引用计数(reference count):shared_ptr的一个关联的计数器。

    • 拷贝shared_ptr时,计数器递增:
      • 一个shared_ptr初始化另一个shared_ptr。
      • 作为参数传递。
      • 作为函数返回值。
    • shared_ptr赋予新值或者被销毁时,计数器递减:
      • 局部shared_ptr离开作用域时。
auto p = make_shared<int>(12);      // p 指向一个int,计时器为1。
auto q(p); // q 和 p 指向同一个对象,计数器为2。

auto r = make_shared<int>(12); // r 指向一个int,计数器为1。
r = q; // r被赋值,指向 q。
// q 的计数器递增(为3)。
// r 计数器递减(为0), r 原来指向对象没有引用者,被自动释放。
  • shared_ptr 的析构函数会递减它所指向的对象的引用计数。如果引用计数变为0,其析构函数就会销毁对象,释放内存。
void function()
{

shared_ptr p =make_shared(666); // 计数器为 1。
return p; // 返回 p 时,实际上是返回p的拷贝值,计数器递增为 2。
} // p 离开作用域,计数器递减,变回 1。
  • 如果将shared_ptr存放于容器中,对于不需要用的元素,记得要使用erase删除,这样才能释放内存。

12.1.2 直接管理内存

  • new:返回指向该对象的指针。
int *p1 = new int;      // 默认初始化,*p1的值未定义
int *p2 = new int(); // 值初始化为0,*p2为0

auto p3 = new auto(1); // 正确,推出p1为int *类型
auto p4 = new int{ 1 }; // 正确
auto p5 = new auto{ 1 }; // 错误,不能使用new auto加上花括号
  • delete:释放动态分配的内存,也可以是空指针。释放非new分配的内存或者同一指针释放多次的行为是未定义的。
  • 在指针被delete之后,会变成空悬指针(dangling pointer),即指向一块已经失效的内存的指针。可以将指针赋予nullptr。

12.1.3 shared_ptr 和 new 结合使用

  • 接收指针参数的智能指针构造函数是explicit的,即不能隐式将内置指针转换成智能指针,必须使用直接初始化形式初始化一个智能指针。
shared_ptr p1 = new int(123);   // 错误,因为new产生的是内置指针,且智能指针无法隐式转换。
shared_ptr p2(new int(123)); // 正确,使用直接初始化。

if (!p2.unique()) // 如果p2不是唯一用户(计数器为1),重新分配。
p2.reset(new int(123)); // 正确,指向新对象。

  • 不要混合使用普通指针和智能指针。
  • 不要用get初始化另一个智能指针或赋值。
  • get返回普通指针,指向智能指针管理的对象。不能delete这个指针。
void process(shared_ptr<int> p) {}      // 在函数结束后,p的计数器会递减(也可能销毁)

int *x(new int(123)); // x是普通指针
process(x); // 错误,不能将普通指针隐式转换成智能指针。
process(shared_ptr<int>x); // 正确,但是在函数执行完后,x的内存被释放。
int i = *x; // 错误,x指向内容已经被释放

12.1.4 智能指针和异常

void f() 
{
shared_ptr<int> sp(new int(123)); // 智能指针分配一个新对象
int *ip = new int(456); // new创建指针

// 中间抛出异常,而且没有捕获,函数直接结束

delete ip; // 被跳过,没有执行,ip没有被释放
} // sp在函数结束时被释放内容,正确
  • 默认情况下,shared_ptr 被销毁时,对指针进行delete。
  • 删除器(deleter)函数:用于代替原来delete,即自定义销毁操作。详见std:: shared_ptr::shared_ptr

  • 基本规范:

    • 不使用内置指针值初始化(或 reset)多个智能指针。
    • 不delete get()返回的指针。
    • 不使用 get() 初始化(或 reset)另一个智能指针。
    • 记住get()返回的指针,在对应最后的智能指针被销毁时,会失效。
    • 如果智能指针管理的资源不是new分配的,要自定义一个删除器。

12.1.5 unique_ptr

  • 同shared_ptr,初始化时,必须用直接初始化形式。
  • unique_ptr 只占有一个对象,所以不支持普通拷贝或者赋值操作。但可以拷贝或赋值一个将要被销毁的unique_ptr。
unique_ptr<int> p1(new int(123));
unique_ptr<int> p2(p1); // 错误,不支持拷贝
unique_ptr<int> p3;
p3 = p1; // 错误,不支持赋值

// 但可以拷贝或赋值一个将要被销毁的 unique_ptr

unique_ptr<int> p2(p1.release()); // 正确,将p1的指向转给p2。p1为空,p2指向原来p1指向的对象

unique_ptr<int> clone(int value)
{
unique_ptr<int> ret(new int(value));
return ret; // 正确,返回一个局部对象(unique_ptr)的拷贝
}

  • release:放弃了指向原来的对象,指针为空值。
  • reset:释放了指向对象的内存。

12.1.6 weak_ptr

  • 一种不控制所指向对象生存期的智能指针,指向shared_ptr管理的对象。

auto p = make_shared<int>(123);
weak_ptr<int> wp(p); // wp弱共享p。p的计数器没改变。

if( shared_ptr<int> np = wp.lock() ) // 判断wp指向对象是否为空,不为空成立且获取(通过weak_ptr 获取 shared_ptr)
{
// 这个块区间内,np和p共享对象。
}

12.2 动态数组

12.2.1 new和数组

  • 虽然用 new T[ ] 分配的内存称为“动态数组”,但实际上得到的是元素类型的指针,而不是数组类型的对象
  • 因为上面这原因,所以也不能用begin或end,或者是范围for语句。
int *p1 = new int[10];      // 10个为初始化的int
int *p2 = new int[10](); // 10个默认初始化为0的int
int *p3 = new int[10]{ 1, 2, 3, 4 }; // 同数组列表初始化
  • 不能在括号中给出初始化器,所以不能用auto分配数组。
  • 可以用任意表达式来缺点要分配的对象的数目。
auto ap = new auto[10](1);      // 错误,不能在括号内给出初始化器,所以auto也无法判断

int v = 3; // 一个变量
int arr1[v]; // 错误,不能用非常量表达式设置维度
int arr2[0]; // 错误,不能使用0作为维度
int *ip = new int[v]; // 正确,可以用任意表达式分配数组
int *ip = new int[0]; // 正确,但是ip不能解引用,是一个合法的非空指针

delete [] ip; // 释放ip动态数组
  • 释放动态数组时,数组中元素按逆序销毁,即最后一个元素先销毁,再到倒数第二个,依此类推。

  • unique_ptr 可以管理动态数组。
  • C++17前,shared_ptr 不直接支持,需要提供删除器。C++17后支持管理动态数组。见std::shared_ptr::operator[]
unique_ptr<int[]> up(new int[10]);
for (size_t i = 0; i <10; i++)
up[i] = i;
up.reset(); // 自动调用delete[ ] 销毁其指针

// C++17前,使用shared_ptr,必须提供删除器
shared_ptr<int> sp(new int[10], [](int *p) { delete[] p; });
for (size_t i = 0; i != 10; ++i)
*(sp.get() + i) = i; // 使用get获取一个内置指针
sp.reset(); // 使用自定义的删除器

12.2.2 allocator 类

  • 提供可以将内存分配和对象构造分开的操作,分配的内存是原始的、未构造的。
  • destroy接收一个指针,指向对象执行的析构函数。

allocator<string> alloc;
auto const p = alloc.allocate(n); // 分配n个未初始化的string

auto q = p; // q指向最后构造的元素之后的位置
alloc.construct(q++); // *q为空字符串
alloc.construct(q++, 5, 'c'); // *q为ccccc(使用了初始化器)
alloc.construct(q++, "hi"); // *q为hi

while( q != p )
alloc.destroy(--q); // 销毁掉前面构造的string们

alloc.deallocate(p, n); // 释放掉内存,n与构造时的大小必须相同

  • 可以拷贝和填充为初始化内存,见上表。
vector<int> vi{ 1, 2, 3 };
allocator<int> alloc;

auto p = alloc.allocate(vi.size() * 2); // 分配比vi占用空间大一倍的动态内存:3 * 2 = 6 个int
auto q = uninitialized_copy(vi.begin(), vi.end(), p); // 拷贝vi,从p开始
uninitialized_fill_n(q, vi.size(), 66); // 剩余的填充66

for (size_t i = 0; i 2; i++) // 结果为 1 2 3 66 66 66
cout <<*(p++) <


推荐阅读
  • 开机自启动的几种方式
    0x01快速自启动目录快速启动目录自启动方式源于Windows中的一个目录,这个目录一般叫启动或者Startup。位于该目录下的PE文件会在开机后进行自启动 ... [详细]
  • 本文详细介绍了 PHP 中对象的生命周期、内存管理和魔术方法的使用,包括对象的自动销毁、析构函数的作用以及各种魔术方法的具体应用场景。 ... [详细]
  • 本文详细介绍了在 CentOS 7 系统中配置 fstab 文件以实现开机自动挂载 NFS 共享目录的方法,并解决了常见的配置失败问题。 ... [详细]
  • Ihavetwomethodsofgeneratingmdistinctrandomnumbersintherange[0..n-1]我有两种方法在范围[0.n-1]中生 ... [详细]
  • 在多线程并发环境中,普通变量的操作往往是线程不安全的。本文通过一个简单的例子,展示了如何使用 AtomicInteger 类及其核心的 CAS 无锁算法来保证线程安全。 ... [详细]
  • OpenAI首席执行官Sam Altman展望:人工智能的未来发展方向与挑战
    OpenAI首席执行官Sam Altman展望:人工智能的未来发展方向与挑战 ... [详细]
  • ### 优化后的摘要本文对 HDU ACM 1073 题目进行了详细解析,该题属于基础字符串处理范畴。通过分析题目要求,我们可以发现这是一道较为简单的题目。代码实现中使用了 C++ 语言,并定义了一个常量 `N` 用于字符串长度的限制。主要操作包括字符串的输入、处理和输出,具体步骤涉及字符数组的初始化和字符串的逆序操作。通过对该题目的深入探讨,读者可以更好地理解字符串处理的基本方法和技巧。 ... [详细]
  • Web开发框架概览:Java与JavaScript技术及框架综述
    Web开发涉及服务器端和客户端的协同工作。在服务器端,Java是一种优秀的编程语言,适用于构建各种功能模块,如通过Servlet实现特定服务。客户端则主要依赖HTML进行内容展示,同时借助JavaScript增强交互性和动态效果。此外,现代Web开发还广泛使用各种框架和库,如Spring Boot、React和Vue.js,以提高开发效率和应用性能。 ... [详细]
  • PHP预处理常量详解:如何定义与使用常量 ... [详细]
  • 本文介绍了如何利用 Delphi 中的 IdTCPServer 和 IdTCPClient 控件实现高效的文件传输。这些控件在默认情况下采用阻塞模式,并且服务器端已经集成了多线程处理,能够支持任意大小的文件传输,无需担心数据包大小的限制。与传统的 ClientSocket 相比,Indy 控件提供了更为简洁和可靠的解决方案,特别适用于开发高性能的网络文件传输应用程序。 ... [详细]
  • 在当前的软件开发领域,Lua 作为一种轻量级脚本语言,在 .NET 生态系统中的应用逐渐受到关注。本文探讨了 Lua 在 .NET 环境下的集成方法及其面临的挑战,包括性能优化、互操作性和生态支持等方面。尽管存在一定的技术障碍,但通过不断的学习和实践,开发者能够克服这些困难,拓展 Lua 在 .NET 中的应用场景。 ... [详细]
  • 在 Linux 环境下,多线程编程是实现高效并发处理的重要技术。本文通过具体的实战案例,详细分析了多线程编程的关键技术和常见问题。文章首先介绍了多线程的基本概念和创建方法,然后通过实例代码展示了如何使用 pthreads 库进行线程同步和通信。此外,还探讨了多线程程序中的性能优化技巧和调试方法,为开发者提供了宝贵的实践经验。 ... [详细]
  • Python全局解释器锁(GIL)机制详解
    在Python中,线程是操作系统级别的原生线程。为了确保多线程环境下的内存安全,Python虚拟机引入了全局解释器锁(Global Interpreter Lock,简称GIL)。GIL是一种互斥锁,用于保护对解释器状态的访问,防止多个线程同时执行字节码。尽管GIL有助于简化内存管理,但它也限制了多核处理器上多线程程序的并行性能。本文将深入探讨GIL的工作原理及其对Python多线程编程的影响。 ... [详细]
  • 在开发过程中,我最初也依赖于功能全面但操作繁琐的集成开发环境(IDE),如Borland Delphi 和 Microsoft Visual Studio。然而,随着对高效开发的追求,我逐渐转向了更加轻量级和灵活的工具组合。通过 CLIfe,我构建了一个高度定制化的开发环境,不仅提高了代码编写效率,还简化了项目管理流程。这一配置结合了多种强大的命令行工具和插件,使我在日常开发中能够更加得心应手。 ... [详细]
  • Java 模式原型在游戏服务器架构中的应用与优化 ... [详细]
author-avatar
哭泣的玫瑰花丶_443
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有