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

2023-12-14 18:00:29

参考资料:

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

12.1.5 unique_ptr(P417)

unique “拥有”它所指向的对象,某个时刻只能有一个 unique_ptr 指向一个给定对象。

381a5b5e846786f8c43170926fca976

当我们定义一个 unique_ptr 时,需要将其绑定到一个 new 返回的指针上,且必须采用直接初始化的形式:

unique_ptr<double> p1;    // 空unique_ptr
unique_ptr<int> p2(new int());    // 指向一个值初始化的int

unique_ptr 不支持普通的拷贝和赋值操作:

unique_ptr<string> p1(new string("hello"));
unique_ptr<string> p2(p1);    // 错误
unique_ptr<string> p3;
p3 = p2;    // 错误

我们可以通过 releasereset 将指针的所有权从一个(非 constunique_ptr 转移给另一个 unique_ptr

unique_ptr<string> p1(new string("hello"));
unique_ptr<string> p2(p1.release());
unique_ptr<string> p3;
p3.reset(p2.release());

传递unique_ptr参数和返回unique_ptr

不能拷贝 unique_ptr 的规则有一个例外:我们可以拷贝或赋值一个将要被摧毁的 unique_ptr

unique_ptr<int> clone(int p) {
	unique_ptr<int> ret(new int(p));
	return ret;
}    // 正确

unique_ptr传递删除器

unique 默认使用 delete 释放指向的对象。与 shared_ptr 不同的是,我们需要在构造 unique_ptr 时提供删除器的类型:

unique_ptr<objT, delT> p(new objT, fcn);

以上一篇笔记中提到的网络连接类为例:

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

12.1.6 weak_ptr(P420)

weak_ptr 是一种不控制所指向对象生存期的智能指针,指向由一个 shared_ptr 管理的对象。将 weak_ptr 绑定到一个 shared_ptr 不会改变 shared_ptr 的引用计数,一旦最后一个指向对象的 shared_ptr 被销毁,即使有 weak_ptr 指向对象,对象也还是会被释放。

a99c3cc2f6bec742c0f663d36af300e

当我们创建一个 weak_ptr 时,要用 shared_ptr 来初始化它:

auto p = make_shared<int>();
weak_ptr<int> wp(p);

上述代码中 wp 不会改变 p 的引用计数。由于 wp 指向的对象可能被释放掉,我们不能使用 weak_ptr 直接访问对象,而必须调用 lock

if(shared_ptr<int> np = wp.lock()){    // 如果np不为空则条件成立
    ...
}

核查指针类

为了展示 weak_ptr 的用途,我们为 StrBlob 类定义一个伴随指针类 StrBlobPtr ,类中保存一个 weak_ptr ,指向 StrBlobdata 成员。使用 weak_ptr 可以阻止用户访问一个不再存在的 vector

class StrBlobPtr {
public:
	StrBlobPtr(): curr(0) {}
	StrBlobPtr(StrBlob &a, size_t sz = 0) :wptr(a.data), curr(sz){}
	string &deref() const;
	StrBlobPtr &incr();
private:
	shared_ptr<vector<string>> check(size_t, const string &) const;
	weak_ptr<vector<string>> wptr;
	size_t curr;    // 在数组中的当前位置
};

StrBlobPtrcheck 成员和 StrBlob 中的同名成员不同,它还要额外检查指向的 vector 是否存在:

shared_ptr<vector<string>>
StrBlobPtr::check(size_t i, const string &msg)const {
	auto ret = wptr.lock();
	if (!ret)
		throw runtime_error("unbound StrBlobPtr");
	if (i >= ret->size())
		throw out_of_range(msg);
	return ret;
}

指针操作

我们定义 derefincr 用来解引用和递增 StrBlobPtr

string &StrBlobPtr::deref()const {
	auto p = check(curr, "dereference past end");
	return (*p)[curr];
}

StrBlobPtr &StrBlobPtr::incr() {
	check(curr, "increment past end of StrBlobPtr");
	++curr;
	return *this;
}

由于我们在初始化 StrBlobPtr 时需要用到 StrBlob 中的 data 成员,所以我们要将 StrBlobPtr 声明成 StrBlob 的友元。

12.2 动态数组(P423)

C++ 和标准库提供了两种一次分配一个对象数组的方法。在大多数情况下,我们应该使用容器而非动态数组,使用容器的类可以使用默认版本的拷贝、赋值、析构操作,而使用动态数组的类必须定义自己版本的操作。

new和数组(P423)

为了让 new 分配一个对象数组,我们要在类型名后跟一对方括号,在其中指明要分配的对象的数目:

int *pia = new int[get_size()];    // 方括号中必须为整型,但不必为常量

也可以用类型别名来分配数组:

using arrT = int[1024];
int *p = new arrT;

分配一个数组会得到一个元素类型的指针

无论用 new T[] 还是类型别名,我们得到的都是一个指向数组元素类型的指针,而不是一个数组。下面的代码验证了这个事实:

int x = 0;
decltype(new int[10]) p1 = &x;    // 正确
int arr[10];
decltype(arr) p2 = &x;    // 错误

动态数组并不是数组类型

初始化动态分配对象的数组

默认情况下,new 分配的对象,不论是单个对象还是数组,都是默认初始化的。要对数组中的元素执行值初始化,可以在大小后跟一对圆括号:

int *pia1 = new int[10];    // 10个默认初始化的int
int *pia2 = new int[10]();    // 10个值初始化的int

在新标准中,我们还可以提供初始值列表:

int *pia3 = new int[10] {0, 1, 2, 3};

动态分配一个空数组是合法的

char arr[0];    // 错误
char *cp = new char[0];    // 正确

当我们用 new 分配一个大小为 0 的数组时,new 返回一个合法的非空指针。

释放动态数组

为了释放动态数组,我们也要在 delete 后跟一对方括号:

delete p;    // p必须指向一个动态分配的对象或为空
delete [] pa;    // pa必须指向一个动态分配的对象数组或为空

数组中的元素按逆序销毁。如果我们在 delete 一个数组时忽略了方括号或在 delete 一个对象时使用了方括号,结果是未定义的。

前面提到,当我们使用类型别名来定义数组类型时,在 new可以不使用方括号,但是在 delete 时则必须使用方括号:

using arrT = int[1024];
auto p = new arrT;
delete[] p;

此处产生一个疑问,既然前面提到,new[] 得到的仅仅是一个指针,而并不是一个数组,那么 delete[] 是怎么知道需要释放多少空间的呢?答案见C++中delete是如何获知需要释放的内存(数组)大小的? - 知乎 (zhihu.com)

智能指针和动态数组

标准库提供了一个可以管理 new 分配的数组的 unique_ptr 版本:

unique_ptr<int[]> up(new in[10]);
up.release();    // 自动使用delete[] 
4540dd42fbdbdd927fbe0ab578c2939

unique_str 指向一个数组时,我们不能使用点运算符箭头运算符,但我们可以使用下标运算符访问数组中的元素。

shared_ptr 不支持直接管理动态数组。如果希望使用 shared_ptr 管理动态数组,必须定义自己的删除器:

shared_ptr<int> sp(new int[10], [](int *p) {delete[] p; });

如果未提供删除器,shared_ptr 将使用 delete 释放一个动态数组,这个行为是未定义的。由于 shared_ptr 不支持下标运算符,为了访问访问数组中的元素,必须用 get 获得一个内置指针:

for (size_t i = 0; i != 10; ++i) {
	*(sp.get() + i) = i;
}

12.2.2 allocator类(P427)

new 在灵活性上有一些局限,因为它将内存分配和对象构造组合在一起了。当分配一大块内存时,我们通常希望将内存分配和对象构造分离,而将内存分配和对象构造组合在一起可能造成不必要的浪费:

// 初始化了n个string,但某些string可能永远用不到
string *const p = new string[n];

此外,没有默认构造函数的类不能用 new 分配动态数组。

allocator

allocator 类定义在头文件 memory 中,它帮助我们将内存分配和对象构造分离开来。

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

allocator分配未构造的内存

auto q = p;    // 顶层const被忽略
alloc.construct(q++);
alloc.construct(q++, 10, 'c');
alloc.construct(q++, "hi");

当我们用完对象后,必须对每个元素调用 destroy 销毁它们:

while(q != p){
    alloc.destroy(--q);    // 释放真正构造的string
}

调用 deallocate 释放内存:

alloc.deallocate(p, n);

拷贝和填充未初始化内存的算法

标准库还为 allocator 类定义了两个伴随算法,定义在头文件 memory 中:

d40ebd3f94a185266f1296424db3535
allocator<string> alloc;
vector<string> vs = {"hello", "hi", "him"};
auto p = alloc.allocate(vs.size() * 2);
auto q = uninitialized_copy(vs.begin(), vs.end(), p);
uninitialized_fill_n(q, vs.size(), "world");
d40ebd3f94a185266f1296424db3535
allocator<string> alloc;
vector<string> vs = {"hello", "hi", "him"};
auto p = alloc.allocate(vs.size() * 2);
auto q = uninitialized_copy(vs.begin(), vs.end(), p);
uninitialized_fill_n(q, vs.size(), "world");

文章来源:https://blog.csdn.net/MaTF_/article/details/134926364
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。