参考资料:
- 《C++ Primer》第5版
- 《C++ Primer 习题集》第5版
12.1.5 unique_ptr
(P417)
unique
“拥有”它所指向的对象,某个时刻只能有一个 unique_ptr
指向一个给定对象。
![381a5b5e846786f8c43170926fca976](https://i-blog.csdnimg.cn/blog_migrate/beb983daa49ea8e06b4e6528fc1b3151.jpeg)
当我们定义一个 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; // 错误
我们可以通过 release
或 reset
将指针的所有权从一个(非 const
)unique_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
指向对象,对象也还是会被释放。
当我们创建一个 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
,指向 StrBlob
的 data
成员。使用 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; // 在数组中的当前位置
};
StrBlobPtr
的 check
成员和 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;
}
指针操作
我们定义 deref
和 incr
用来解引用和递增 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](https://i-blog.csdnimg.cn/blog_migrate/9bd699a234f300beda1e451f13dd9e7f.jpeg)
当 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](https://i-blog.csdnimg.cn/blog_migrate/fdcacde1c0201f195a5ae1a0117fbc22.jpeg)
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](https://i-blog.csdnimg.cn/blog_migrate/ff09301c69ef3223e77a7a6df23339fd.jpeg)
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](https://i-blog.csdnimg.cn/blog_migrate/ff09301c69ef3223e77a7a6df23339fd.jpeg)
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");