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

参考资料:

  • 《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)

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

cpp 复制代码
shared_ptr<stirng> p1;    // 空指针
shared_ptr<list<int>> p2;

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

cpp 复制代码
if(p1 && p1->empty()){
    *p1 = "hi";
}

make_shared函数

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

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

cpp 复制代码
// 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 指向相同的对象:

cpp 复制代码
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 ,它就会自动释放自己管理的对象。

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

shared_ptr自动销毁所管理的对象

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

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

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

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

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

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

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

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

定义StrBlob

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

cpp 复制代码
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构造函数

cpp 复制代码
StrBlob::StrBlob() :data(make_shared<vector<string>>()) { }
StrBlob::StrBlob(initializer_list<string> il):
	data(make_shared<vector<string>>(il)){ }

元素访问成员

cpp 复制代码
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 无法为其分配的对象命名,而是返回一个指向该对象的指针:

cpp 复制代码
int *pi = new int;

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

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

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

cpp 复制代码
int *pi1 = new int;    // 默认初始化
int *pi2 = new int();    // 值初始化

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

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

动态分配的const对象

cpp 复制代码
const int *pci = new const int(1024);

内存耗尽

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

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

bad_allocnothrow 都定义在头文件 new 中。

释放动态内存

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

cpp 复制代码
delete p;

指针值和delete

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

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

cpp 复制代码
const int *pci = new const int(7);
delete pci;

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

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

cpp 复制代码
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 返回的指针来初始化智能指针:

cpp 复制代码
shared_ptr<int> p(new int(7));

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

cpp 复制代码
shared_ptr<int> p1 = new int(1024);    // 错误,不能隐式转换
shared_ptr<int> p2(new int(1024));
cpp 复制代码
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
似乎没有 shared_ptr<T> p(p2, d) 这个构造函数,书上是不是写错了🤔

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

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

cpp 复制代码
void process(shared_ptr<int> ptr){
    ...
}

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

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

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

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

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

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

cpp 复制代码
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

cpp 复制代码
p = new int(1024);    // 错误
p.reset(new int(1024));

reset 常常与 unique 一起使用:

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

练习

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

12.1.4 智能指针和异常(P415)

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

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

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

智能指针和哑类

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

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

cpp 复制代码
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

cpp 复制代码
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 分配的内存,记住传递一个删除器。
相关推荐
秃头佛爷25 分钟前
Python学习大纲总结及注意事项
开发语言·python·学习
待磨的钝刨26 分钟前
【格式化查看JSON文件】coco的json文件内容都在一行如何按照json格式查看
开发语言·javascript·json
XiaoLeisj2 小时前
【JavaEE初阶 — 多线程】单例模式 & 指令重排序问题
java·开发语言·java-ee
励志成为嵌入式工程师3 小时前
c语言简单编程练习9
c语言·开发语言·算法·vim
捕鲸叉4 小时前
创建线程时传递参数给线程
开发语言·c++·算法
A charmer4 小时前
【C++】vector 类深度解析:探索动态数组的奥秘
开发语言·c++·算法
Peter_chq4 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
记录成长java5 小时前
ServletContext,Cookie,HttpSession的使用
java·开发语言·servlet
前端青山5 小时前
Node.js-增强 API 安全性和性能优化
开发语言·前端·javascript·性能优化·前端框架·node.js
青花瓷5 小时前
C++__XCode工程中Debug版本库向Release版本库的切换
c++·xcode