《C++ Primer》第12章 动态内存(一)

news2025/1/16 11:06:47

参考资料:

  • 《C++ Primer》第5版
  • 《C++ Primer 习题集》第5版

我们的程序目前只用过静态内存栈内存。静态内存用来保存局部 static 对象、类 static 成员、定义在任何函数之外的变量;栈内存用来保存定义在函数内的非 static 对象。分配在静态内存和栈内存的对象由编译器自动创建和销毁,

除了静态内存和栈内存,每个程序还拥有一个内存池,被称作自由空间(free store)堆(heap)。程序用堆来存储动态分配(dynamic allocate)的对象。动态对象的生存周期由程序控制,当动态对象不再使用时,代码必须显式地销毁它们。

12.1 动态内存与智能指针(P400)

在 C++ 中,动态内存的管理是通过一对运算符完成的:new 在动态内存中为对象分配空间并返回一个指向该对象的指针delete 接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。

动态内存的使用很容易出现问题:有时我们会忘记释放内存;有时我们会在尚有指针引用内存的情况下就释放内存。

为了更容易和更安全地使用动态内存,新标准库提供了两种智能指针(smart pointer)类型来管理动态对象。与普通指针的主要不同点在于,智能指针可以自动释放对象shared_ptr 允许多个指针指向同一个对象;unique_str “独占”所指对象。此外,标准库还定义了一个名为 weak_ptr 的伴随类,它是一种弱引用,指向 shared_ptr 所管理的对象。这三种类型都定义在头文件 memory 中。

12.1.1 shared_ptr类(P400)

智能指针是模板,我们在创建智能指针时必须提供指针指向的类型:

shared_ptr<stirng> p1;    // 空指针
shared_ptr<list<int>> p2;

智能指针的使用方式和普通指针类似:解引用一个智能指针返回它所指的对象;在条件判断中使用智能指针,就是检测它是否为空:

if(p1 && p1->empty()){
    *p1 = "hi";
}
8b7dbc740845e684a16e8c36b74ae96

make_shared函数

分配和使用动态内存最安全的做法是调用一个定义在头文件 memory 中,名为 make_shared 的标准库函数,该函数在动态内存中分配并初始化一个对象,返回指向此对象的 shared_ptr

使用 make_shared 时,必须指定要创建的对象的类型:

// p3指向值为7的int对象
shared_ptr<int> p3 = make_shared<int>(7);
// p4指向值为“999”的string对象
shared_ptr<string> p4 = make_shared<string>(3, '9');
// p5指向值为0(值初始化)的int对象
shared_ptr<int> p5 = make_shared<int>();

类似顺序容器的 emplace 成员,make_shared 用参数来构造对象,如果我们不传递任何参数,对象就会进行值初始化。

shared_ptr的拷贝和赋值

每个 shared_ptr 对象都会记录有多少个 shared_ptr 指向相同的对象:

auto p = make_shared<int>(7);
auto q(p);
cout << p.use_count() << ' ' << q.use_count();    // 输出2 2

我们可以认为每个 shared_ptr 都有一个关联的计数器,称为引用计数(reference count)。当我们拷贝一个 shared_ptr 时(如拷贝构造、参数传递、作为函数返回值),它所关联的计数器会递增;当一个 shared_ptr 被赋予新值或被销毁时,它所关联的计数器会递减。

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

auto r = make_shared<int>(42);
r = q; // 递增q所指对象的引用计数
       // 递减r原来所指对象的引用计数
       // r原来所指对象的计数器变为0,自动释放

shared_ptr自动销毁所管理的对象

shared_ptr 通过析构函数自动销毁对象。

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

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

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

  • 程序不知道自己需要使用多少对象
  • 程序不知道对象的准确类型
  • 程序需要在多个对象间共享数据

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

vector<int> v1 = { 0,1,2 };
vector<int> v2;
v2 = v1;
v1.clear();
cout << v2.size();    // 输出为3

假定我们要定义一个 Blob 类,保存一组元素,希望 Blob 对象的不同拷贝之间共享元素

定义StrBlob

由于还没有学习模板的相关知识,所以我们先定义一个管理 string 的类,命名为 StrBlob

class StrBlob {
public:
	using size_type = vector<string>::size_type;
	StrBlob();
	StrBlob(initializer_list<string> il);
	size_type size() const { return data->size(); }
	bool empty() const { return data->empty(); }
	void push_back(const string &t) { data->push_back(t); }
	void pop_back();
	string &front();
	string &back();
private:
    // 使用shared_ptr实现数据共享
	shared_ptr<vector<string>> data;
	void check(size_type i, const string &msg) const;
};

StrBlob构造函数

StrBlob::StrBlob() :data(make_shared<vector<string>>()) { }
StrBlob::StrBlob(initializer_list<string> il):
	data(make_shared<vector<string>>(il)){ }

元素访问成员

void StrBlob::check(size_type i, const string &msg)const {
	if (i >= data->size()) {
		throw out_of_range(msg);
	}
}
string &StrBlob::front() {
	check(0, "front on empty StrBlob");
	return data->front();
}
string &StrBlob::back() {
	check(0, "back on empty StrBlob");
	return data->back();
}
void StrBlob::pop_back() {
	check(0, "pop_back on empty StrBlob");
	data->pop_back();
}

StrBlob的拷贝、赋值和销毁

StrBlob 使用默认版本的拷贝、赋值和析构函数。

12.1.2 直接管理内存(P407)

使用new动态分配和初始化对象

在堆中分配的内存是无名的,因此 new 无法为其分配的对象命名,而是返回一个指向该对象的指针:

int *pi = new int;

默认情况下,动态分配的对象执行默认初始化。我们可以使用直接初始化来初始化一个动态分配的对象:

int *pi = new int(7);
int *ps = new string(3, '9');
vector<string> *pv = new vector<string>{"hi", "hello"};

也可以对动态分配的对象进行值初始化,只需在类型名后面跟一对空括号即可:

int *pi1 = new int;    // 默认初始化
int *pi2 = new int();    // 值初始化

我们可以使用 auto 从初始化器推断我们要分配的对象的类型,但仅支持单一初始化器:

string str = "hello";
auto p1 = new auto(str);    // p为string*
auto p2 = new auto{str, str};    // 错误 

动态分配的const对象

const int *pci = new const int(1024);

内存耗尽

如果 new 不能分配所要求的空间,它会抛出一个类型为 bad_alloc 的异常:

int *p1 = new int;    // 分配失败则抛出bad_alloc异常
int *p2 = new (nothrow) int;    // 分配失败则返回空指针

bad_allocnothrow 都定义在头文件 new 中。

释放动态内存

delete 销毁给定指针指向的对象,释放对应的内存:

delete p;

指针值和delete

我们传递给 delete 的指针必须指向动态分配的内存,或者是一个空指针。释放一块 new 分配的内存,或多次释放相的指针值的行为是未定义的。

const 对象的值不能改变,但本身可以被销毁

const int *pci = new const int(7);
delete pci;

动态对象的生存期直到被释放为止

对于一个由内置指针管理的动态对象,直到被显式释放前它都是存在的:

Foo* factory(T arg){
    return new Foo(arg);
}
void use_factory(T arg){
    Foo *p = factory(arg);
}

use_factory 返回时,p 被销毁,但其指向的动态内存却没有被释放

12.1.3 shared_ptrnew结合使用(P412)

我们可以用 new 返回的指针来初始化智能指针:

shared_ptr<int> p(new int(7));

接受指针参数的智能指针构造函数是 explicit 的,因此我们必须使用直接初始化形式:

shared_ptr<int> p1 = new int(1024);    // 错误,不能隐式转换
shared_ptr<int> p2(new int(1024));
shared_ptr<int> clone(int p){
    return new int(p);
}    // 错误
shared_ptr<int> clone(int p){
    return shared_ptr<int>(new int(p));
}    // 正确

默认情况下,智能指针使用 delete 释放它关联的对象。我们也可以提供自己的操作来替代 delete

a4c3163f1daac42942400ebb4a79d21 7cd6f77ca6fd1481c203a455baede58

似乎没有 shared_ptr<T> p(p2, d) 这个构造函数,书上是不是写错了🤔

不要混用普通指针和智能指针

shared_ptr 可以协调对象的析构,但这仅限于其自身的拷贝。考虑下面的函数:

void process(shared_ptr<int> ptr){
    ...
}

process 采用值传递,实参会拷贝到 ptr 中,导致引用计数递增。如果我们尝试混用普通指针和 shared_ptr

int *x = new int(1024);
process(x);    // 错误,不能将int*隐式转换为shared_ptr<int>
process(shared_ptr<int>(x));    // 合法,但x指向的内存会被释放!
// 此时x已经变成空悬指针

当我们将一个 shared_ptr 绑定到一个普通指针后,就不应该再使用该普通指针了。

也不要使用get初始化另一个智能指针或为智能指针赋值

智能指针定义了名为 get 的成员函数,返回一个内置指针,指向智能指针管理的对象。

虽然编译器不会给出报错信息,但将另一个智能指针绑定到 get 返回的指针是错误的:

shared_ptr<int> p1 = make_shared<int>(7);    // 引用计数为1
int *q = p1.get();
{
	// 两个独立的shared_ptr指向相同的内存,引用计数均为1
	shared_ptr<int> p2(q);
}    // p2被销毁,进而导致p1指向的内存被释放
int foo = *p1;    // 未定义

不要 delete 通过 get 得到的指针,也不要用 get 得到的指针初始化另一个智能指针或者为另一个智能指针赋值。

其他shared_ptr操作

我们可以用 reset 来将一个新的指针赋予一个 shared_ptr

p = new int(1024);    // 错误
p.reset(new int(1024));

reset 常常与 unique 一起使用:

if(!p.unique())
    p.reset(new string(*p));    // 如果p不是唯一用户,则分配新的拷贝
*p += newVal;    // p为唯一用户,可以随意修改对象的值

练习

49942a32ac8ee82cc72d81b1bf3a8d6

这道题涉及到了 explicit 构造函数、参数传递等问题,有些细节我还不是很清楚,目前只能给出一种相对合理的理解。假设有函数 f(int a) ,然后我们调用它 f(b) ,此时参数初始化的过程等价于执行 int a = b 。所以上面题目中的 b) 实际上执行了 shared_ptr<int> ptr = temptemp为临时量,类型为 int* ,而这条语句上执行的是拷贝初始化(尽管编译器可能优化为直接初始化),这不符合 explicit 的要求。

12.1.4 智能指针和异常(P415)

使用智能指针可以确保在异常发生后资源能被正确释放:

void f(){    // 普通指针
    int *ip = new int();
    // 此时代码抛出一个异常,且在f中未被捕获
    // ip被销毁,其指向的内存没有被释放
    delete ip;    
}

void f(){    // 智能指针
    shared_ptr<int> sp(new int());
    // 此时代码抛出一个异常,且在f中未被捕获
    // sp被销毁的同时,其管理的内存也被释放
}

智能指针和哑类

有些类的析构函数并不负责释放资源,特别是为 C 和 C++ 两种语言设计的类,通常要求用户显式释放所使用的资源。

假设我们正在使用一个 C 和 C++ 都使用的网络库:

struct destination;    // 表示我们正在连接什么
struct connection;    //使用连接所需的信息
connection connect(destination *);    // 打开连接
void disconnect(connection);    // 关闭给定的连接
void f(destination &d /* 其他参数 */) {
	connection c = connect(&d);
	// 如果在f退出前忘记调用disconnect,就无法关闭c了
}

使用 shared_ptr 可以有效解决上述问题。

使用我们自己的释放操作

为了用 shared_ptr 来管理一个 connection ,我们必须定义一个删除器(deleter) 函数来代替 delete

void end_connection(connection *p) { disconnect(*p); }
void f(destination &d /* 其他参数 */) {
	connection c = connect(&d);
    shared_ptr<connection> p(&c, end_connection);
    // 当p被销毁时,调用end_connection
}

为了正确使用智能指针,我们必须坚持一些基本规范:

  • 不使用相同的内置指针初始化或 reset 多个智能指针。
  • delete get() 返回的指针。
  • 使用 get() 返回的指针时,记住最后一个对应的指针销毁后,指针就变为无效了。
  • 如果智能指针管理的资不是 new 分配的内存,记住传递一个删除器。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1291333.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

GPT 中文提示词技巧:参照 OpenAI 官方教程

前言 搜了半天什么 prompt engineering 的课&#xff0c;最后会发现 gpt 官方其实是有 prompt 教程的。因此本文主要是学习这篇教程。 概述 - OpenAI API 部分案例是参考&#xff1a;根据吴恩达老师教程总结出中文版prompt教程_哔哩哔哩_bilibili up主的内容。 一、尽可能清…

批量AI创作文案的工具,批量AI创作文章的软件

人工智能&#xff08;AI&#xff09;的应用不断拓展&#xff0c;其中批量AI创作逐渐成为许多文本创作者和企业编辑的热门选择。面对海量的文章需求&#xff0c;批量AI创作工具能够高效、快速地生成大量文本内容&#xff0c;从而减轻创作者的工作负担。本文将专心分享批量AI创作…

一对一互相聊天

服务端 package 一对一用户;import java.awt.BorderLayout; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.ServerSocket; import java.net.Socket; import java.util.Vector;…

day14_java中的IO流(详解)

IO流 学习IO&#xff0c;我们必须要知道硬盘上的文件(文件夹)在java中的表现形式 一、File文件 Java中 提供了一个类 File 供我们使用; File &#xff1a; 文件和目录&#xff08;文件夹&#xff09;路径名的抽象表示 1、构造方法&#xff1a; File(File parent, String …

【环境搭建】ubuntu22安装ros2

基于某种特殊需求&#xff0c;从Ubuntu16到22目前都尝试过安装ros、ros2 参考1&#xff1a;http://t.csdnimg.cn/DzvSe 参考2&#xff1a;http://t.csdnimg.cn/sOzr1 1.设置locale sudo apt update && sudo apt install locales sudo locale-gen en_US en_US.UTF-8 s…

Linux---逻辑卷管理

本章主要介绍逻辑卷的管理。 了解什么是逻辑卷创建和删除逻辑卷扩展逻辑卷缩小逻辑卷逻辑卷快照的使用 前面介绍了分区的使用&#xff0c;如果某个分区空间不够&#xff0c;想增加空间是非常困难的。所以&#xff0c;建议尽可能使用逻辑卷而非普通的分区&#xff0c;因为逻辑卷…

VideoAssembler 一种新颖的方法,生成具有多样化内容的视频的方法

文章目录 摘要方法代码 VideoAssembler: Identity-Consistent Video Generation with Reference Entities using Diffusion Model 本文提出了VideoAssembler&#xff0c;一种新颖的方法&#xff0c;生成具有多样化内容的视频。它可以保留实体的保真度&#xff0c;并生成可控的内…

从零开始的c语言日记day40——字符函数和字符串函数——内存函数

常用函数介绍 求字符串长度 strlen 长度不受限制的字符串函数 Strcpy Strcat strcmp 长度受限制的字符串函数介绍 strncpy strncat strncmp 字符串查找 Strstro strtok 错误信息报告 strerror 字符操作 内存操作函数 memcpy memmove memset Memcmp 使用Asser…

Mendix版的电商京东首页长什么样儿?

前言 世界需要大前端。大前端需要Mendix。 近日经常有企业IT侧的朋友反应&#xff0c;自家需要一个神奇的内容管理平台&#xff0c;来快速打造随需应变的公司官网&#xff0c;亦或企业官微&#xff0c;如果能在小程序里呈现产品营销类的功能更好。首先要肯定的是&#xff0c;…

nodejs+vue+微信小程序+python+PHP的Sd球鞋销售平台的设计与实现-计算机毕业设计推荐

此网站系统的开发方式和信息管理方式&#xff0c;借鉴前人设计的信息和研发。以网站商品信息为主&#xff0c;购物商品为核心功能来进行设计和研发&#xff0c;把网站信息和技术整合&#xff0c;开发出一套Sd球鞋销售平台。用目前现有的新技术进行系统开发&#xff0c;   目…

linux云服务器开启防火墙注意事件

重要的事情先说三遍: linux云服务器开启防火墙要先获取到云服务器的管理界面控制权!! linux云服务器开启防火墙要先获取到云服务器的管理界面控制权!! linux云服务器开启防火墙要先获取到云服务器的管理界面控制权!! 也就是能打开这个页面: 为什么这么说呢?如果你…

这个蓄电池监测技术,超硬核!

随着科技的飞速发展&#xff0c;蓄电池作为一种重要的能量储存装置&#xff0c;已经深刻地影响着我们的日常生活和各个行业。 因此&#xff0c;为了确保蓄电池系统的可靠性、安全性和效率&#xff0c;监控技术的应用变得尤为迫切。 客户案例 工业备用电源 在工业领域&#x…

Spring Boot 实战 | Spring Boot整合JPA常见问题解决方案

专栏集锦&#xff0c;大佬们可以收藏以备不时之需&#xff1a; Spring Cloud 专栏&#xff1a;http://t.csdnimg.cn/WDmJ9 Python 专栏&#xff1a;http://t.csdnimg.cn/hMwPR Redis 专栏&#xff1a;http://t.csdnimg.cn/Qq0Xc TensorFlow 专栏&#xff1a;http://t.csdni…

中庸行者 - 华为机试真题题解

给定一个m * n的整数矩阵作为地图,短阵数值为地形高度; 中庸行者选择地图中的任意一点作为起点,尝试往上、下、左、右四个相邻格子移动; 移动时有如下约束: 中庸行者只能上坡或者下坡,不能走到高度相同的点不允许连续上坡或者连续下坡,需要交替进行,每个位置只能经过一次…

【Ajax】发送get请求获取接口数据

编写html实现通过Ajax发送get请求并获取数据 代码实现 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title…

冯·诺依曼的绝密程序,让世界上多了一个高薪职业!

1 一次偶遇 1944年春天&#xff0c;戈德斯坦上校在阿伯丁火车站台上偶遇大神冯诺依曼。 戈德斯坦壮起胆子&#xff0c;拘谨地和冯诺依曼聊起来。 幸运的是&#xff0c;冯诺依曼热情而友善&#xff0c;让戈德斯坦很放松。 当冯诺依曼得知戈德斯坦正在宾夕法尼亚大学研制电子计算…

打工人副业变现秘籍,某多/某手变现底层引擎-Stable Diffusion简介

Stable Diffusion是2022年发布的深度学习文本到图像生成模型,它主要用于根据文本的描述产生详细图像,尽管它也可以应用于其他任务,如

微信会员卡小程序 多门店系统 满足日常商家的收银功能 附带完整的搭建教程

大家好&#xff0c;今天罗峰来给大家分享一款微信会员卡小程序源码系统&#xff0c;多门店系统&#xff0c;附带完整的搭建教程。 以下是部分代码示例&#xff1a; 系统特色功能一览&#xff1a; 1.多门店管理&#xff1a;系统支持添加多个门店&#xff0c;每个门店可独立管理…

自动化定时发送天气提醒邮件

&#x1f388; 博主&#xff1a;一只程序猿子 &#x1f388; 博客主页&#xff1a;一只程序猿子 博客主页 &#x1f388; 个人介绍&#xff1a;爱好(bushi)编程&#xff01; &#x1f388; 创作不易&#xff1a;如喜欢麻烦您点个&#x1f44d;或者点个⭐&#xff01; &#x1f…

Redis和MySQL双写一致性实用解析

1、背景 先阐明一下Mysql和Redis的关系&#xff1a;Mysql是数据库&#xff0c;用来持久化数据&#xff0c;一定程度上保证数据的可靠性&#xff1b;Redis是用来当缓存&#xff0c;用来提升数据访问的性能。 关于如何保证Mysql和Redis中的数据一致&#xff08;即缓存一致性问题…