一、shared_ptr

最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数。此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。

1 shared_ptr<int> p3 = make_shared<int>(42);
2 shared_ptr<string> p4 = make_shared<string>(10, '9');
3 shared_ptr<int> p5 = make_shared<int>();

如果我们不传递参数,对象就会进行值初始化。

shared_ptr内部有一个引用计数变量,记录有多少个其他shared_ptr指向相同的对象。计数器变为0,会自动释放自己所管理的对象。

shared_ptr在无用之后一定要销毁,否则程序仍会正确执行,但会浪费内存。shared_ptr在无用之后仍然保留的一种可能情况是,你将shared_ptr存放在一个容器中,随后重排了容器,从而不再需要某些元素。

在这种情况下,你应该确保用erase删除那些不再需要的shared_ptr元素。比如unique算法,重排输入序列,覆盖相邻的重复元素,返回一个指向不重复元素之后位置的迭代器。

 

二、new与delete

new

默认情况下,动态分配的对象是默认初始化。在类型名之后跟一对空括号进行值初始化。

对于类类型来说,不管采用什么形式,对象都会通过默认构造函数来初始化。

Tip:出于与变量初始化相同的原因,对动态分配的对象进行初始化通常是个好主意。

1 auto p1 = new auto(obj);          // 正确
2 auto p2 = new auto(a,b,c);       // 错误

由于编译器要用初始化器的类型来推断要分配的类型,只有当括号中仅有单一初始化器时才能使用auto。

默认情况下,如果new不能分配所要求的内存空间,它会抛出一个类型为bad_alloc的异常。我们可以改变使用new的方式来阻止它抛出异常。

1 int *p1 = new int;                       // 如果分配失败,new抛出std::bad_alloc
2 int *p2 = new (nothrow) int;      // 如果分配失败,new返回一个空指针

这种形式的new是定位new(placement new)。

 

delete

delete p;     // p必须指向一个动态分配的对象或是一个空指针

通常情况下,编译器不能分辨一个指针指向的是静态还是动态分配的对象。类似的,编译器也不能分辨一个指针所指向的内存是否已经被释放了。

虽然一个const对象的值不能被改变,但它本身是可以被销毁的。想要释放一个const动态对象,只要delete指向它的指针即可:

1 const int *pci = new const int(1024);
2 delete pci;       // 正确

Tip:内置指针指向的动态内存在销毁内置指针之前必须显示释放。

使用new和delete管理动态内存存在三个常见问题:

1.忘记delete内存。

2.使用已经释放掉的对象。

3.同一块内存释放两次。

Tip:坚持只使用智能指针,就可以避免所有这些问题。

当我们delete一个指针后,指针变为无效,但在很多机器上指针仍然保存着动态内存的地址。

空悬指针是指向一块曾经保存数据对象但现在已经无效的内存的指针,本质上和未初始化指针类似。

有一种方法可以避免空悬指针的问题:释放内存后马上销毁指针。

如果我们需要保留指针,可以在delete之后将nullptr赋予指针,这样就清楚地指出指针不指向任何对象。

 

三、shared_ptr和new结合使用

如果我们不初始化一个智能指针,它就会被初始化为一个空指针。

接受指针参数的智能指针构造函数是explicit的。因此,我们不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式来初始化一个智能指针:

1 shared_ptr<int> p1 = new int(1024);     // 错误
2 shared_ptr<int> p2(new int(1024));      // 正确

出于相同的原因,一个返回shared_ptr的函数不能在其返回语句中隐式转换一个普通指针:

1 shared_ptr<int> clone(int p)
2 {
3   return new int(p);      // 错误
4 }

我们必须将shared_ptr显示绑定到一个想要返回的指针上:

1 shared_ptr<int> clone(int p)
2 {
3   return shared_ptr<int>(new int(p));      // 正确
4 }

默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用delete释放它所关联的对象。

我们可以将智能指针绑定到一个指向其他类型的资源的指针上,但是为了这样做,必须提供自己的操作来代替delete。

Notes:

1.不要混合使用普通指针和智能指针

shared_ptr可以协调对象的析构,但这仅限于其自身的拷贝(也是shared_ptr)之间。这也是为什么我们推荐使用make_shared而不是new的原因。

这样,我们就能在分配对象的同时就将shared_ptr与之绑定,从而避免了无意中将同一块内存绑定到多个独立创建的shared_ptr上。

 1 void process(shared_ptr<int> ptr)
 2 {
 3     // 使用ptr
 4 }
 5 
 6 shared_ptr<int> p(new int(42));
 7 process(p);
 8 int i = *p;     // 正确
 9   
10 int *x(new int(1024));
11 process(x);     //  错误
12 process(shared_ptr<int>(x));      // 合法的
13 int j = *x;     // 未定义的:x是一个空悬指针!

在上面的调用中,我们将一个临时shared_ptr传递给process。当这个调用所在的表达式结束时,这个临时对象就被销毁了。

销毁这个临时变量会递减引用计数,此时引用计数就变为0了。因此,当临时对象被销毁时,它所指向的内存会被释放。

当将一个shared_ptr绑定到一个普通指针时,我们就将内存的管理责任交给了这个shared_ptr。

一旦这样做了,我们就不应该再使用内置指针来访问shared_ptr所指向的内存了。

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

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

此函数是为了这样一种情况而设计的:我们需要向不能使用智能指针的代码传递一个内置指针。

使用get返回的指针的代码不能delete此指针。

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

1 shared_ptr<int> p(new int (42));
2 int *q = p.get();
3 {
4   shared_ptr<int>(q);      // 未定义:两个独立的shared_ptr指向相同的内存
5 }
6 int foo = *p;     // 未定义:p指向的内存已经被释放了

Tip:get用来将指针的访问权限传递给代码,你只有在确定代码不会delete指针的情况下,才能使用get。

特别是,永远不要用get初始化另一个智能指针或者为另一个智能指针赋值。

3.其他shared_ptr操作

我们可以将一个新的指针赋予一个shared_ptr:

1 p = new int(1024);            // 错误
2 p.reset(new int(1024));     // 正确

reset会更新引用计数。reset成员经常与unique一起使用,来控制多个shared_ptr共享的对象。

在改变底层对象之前,我们检查自己是否是当前对象仅有的用户。如果不是,在改变之前要制作一份新的拷贝:

1 if(!p.unique())
2   p.reset(new string(*p));      // 我们不是唯一用户;分配新的拷贝
3 *p += newVal;      // 我们是唯一用户,可以改变对象的值

 

四、删除器(deleter)

与管理动态内存类似,我们通常可以使用类似的技术来管理不具有良好定义的析构函数的类。

例如,假定我们正在使用一个C和C++都使用的网络库,使用这个库的代码可能是这样的:

 1 struct destination;                              // 表示我们正在连接什么
 2 struct connection;                              // 使用连接所需的信息
 3 connection connect(destination*);       // 打开连接
 4 void disconnect(connection);              // 关闭给定连接
 5 void f(destination &d /* 其他参数*/)
 6 {
 7     // 获得一个连接;记住使用完后要关闭它
 8     connection c = connect(&d);
 9     // 使用连接
10     // 如果我们在f退出前忘记调用disconnect,就无法关闭c了
11 }

 默认情况下,shared_ptr假定它们指向的是动态内存。因此,当一个shared_ptr被销毁时,它默认地对它管理的指针进行delete操作。

为了用shared_ptr来管理一个connection,我们必须首先定义一个函数来代替delete。这个删除器(deleter)函数必须能够完成对shared_ptr中保存的指针进行释放的操作。

在本例中,我们的删除器必须接受单个类型为connection*的参数。当我们创建一个shared_ptr时,可以传递一个(可选的)指向删除器函数的参数:

 1 void end_connection(connection *p)
 2 {
 3      disconnect(*p);
 4 }
 5 
 6 void f(destination &d /* 其他参数 */)
 7 {
 8      connection c = connect(&d);
 9      shared_ptr<connection> p(&c, end_connection);
10      // 使用连接
11      // 当f退出时(即使是由于异常而退出),connection会被正确关闭  
12 }

Tips:

1.不使用相同的内置指针值初始化(或reset)多个智能指针。

2.不delete get()返回的指针。

3.不使用get()初始化或reset另一个智能指针。

4.如果你使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了。

5.如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器。

 

五、unique_ptr

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

当unique_ptr被销毁时,它所指向的对象也被销毁。

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

1 unique_ptr<double> p1;
2 unique_ptr<int> p2(new int(42));

由于一个unique_ptr拥有它指向的对象,因此unique_ptr不支持普通的拷贝或赋值操作:

1 unique_ptr<string> p1(new string("Stegosaurus"));
2 unique_ptr<string> p2(p1);      // 错误
3 unique_ptr<string> p3;
4 p3 = p2;                        // 错误

虽然我们不能拷贝或赋值unique_ptr,但可以通过调用release或reset将指针所有权从一个(非const)unique_ptr转移给另一个unique_ptr:

1 // 将所有权从p1(指向string Stegosaurus)转移给p2
2 unique_ptr<string> p2(p1.release());     // release将p1置为空
3 unique_ptr<string> p3(new string("Trex"));
4 // 将所有权从p3转移给p2
5 p2.reset(p3.release());     // reset释放了p2原来指向的内存

release返回的指针通常被用来初始化另一个智能指针或给另一个智能指针赋值。

如果我们不用另一个智能指针来保存release返回的指针,我们的程序就要负责资源的释放:

1 p2.release();              // 错误:p2不会释放内存,而且我们丢失了指针
2 auto p = p2.release();     // 正确,但我们必须记得delete(p)

不能拷贝unique_ptr的规则有一个例外:我们可以拷贝或赋值一个将要被销毁的unique_ptr,最常见的例子是从函数返回一个unique_ptr:

1 unique_ptr<int> clone(int p)
2 {
3     return unique_ptr<int>(new int(p));      // 正确
4 }

还可以返回一个局部对象的拷贝:

1 unique_ptr<int> clone(int p)
2 {
3     unique_ptr<int> ret(new int(p));
4     // ...
5     return ret;
6 }

标准库的较早版本包含了一个名为auto_ptr的类,它具有unique_ptr的部分特性,但不是全部。

特别是,我们不能在容器中保存auto_ptr,也不能从函数中返回auto_ptr。

虽然auto_ptr仍是标准库的一部分,但编写程序时应该使用unique_ptr。

重载一个unique_ptr中的删除器会影响到unique_ptr类型以及如何构造(或reset)该类型的对象。

我们必须在尖括号中unique_ptr指向类型之后提供删除器类型。

在创建或reset一个这种unique_ptr类型的对象时,必须提供一个指定类型的可调用对象(删除器):

1 // p指向一个类型为objT的对象,并使用一个类型为delT的对象释放objT对象
2 // 它会调用一个名为fcn的delT类型对象
3 unique_ptr<objT, delT> p(new objT, fcn);

 重写连接程序,用unique_ptr来代替shared_ptr:

1 void f(destination &d /* 其他需要的参数 */)
2 {
3     connection c = connect(&d);     // 打开连接
4     // 当p被销毁时,连接将会关闭
5     unique_ptr<connection, decltype(end_connection)*> p(&c, end_connection);
6     // 使用连接
7     // 当f退出时(即使是由于异常而退出),connection会被正确关闭
8 }

C++11新标准引入了第二种类型说明符decltype,它的作用是选择并返回操作数的数据类型。

编译器分析表达式并得到它的类型,却不实际计算表达式的值:

1 decltype(f()) sum = x;     // sum的类型就是函数f的返回类型

由于decltype(end_connection)返回一个函数类型,所以我们必须添加一个*来指出我们正在使用该类型的一个指针。

 

六、weak_ptr

weak_ptr是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象。

将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。

 1 // 对于访问一个不存在元素的尝试,StrBlobPtr抛出一个异常
 2 class StrBlobPtr
 3 {
 4 public 5     StrBlobPtr(): curr(0) { }
 6     StrBlobPtr(StrBlob &a, size_t sz = 0);
 7         wptr(a.data), curr(sz) { }
 8     std::string& deref() const;
 9     StrBlobPtr& incr();     // 前缀递增
10 private:
11     // 若检查成功,check返回一个指向vector的shared_ptr
12     std::shared_ptr<std::vector<std::string>>
13         check(std::size_t, const std::string&) const;
14     // 保存一个weak_ptr,意味着底层vector可能会被销毁
15     std::weak_ptr<std::vector<std::string>> wptr;
16     std::size_t curr;     // 在数组中的当前位置
17 }

weak_ptr可以检查指针指向的vector是否还存在:

 1 std::shared_ptr<std::vector<std::string>>
 2 StrBlobPtr::check(std::size_t i, const std::string &msg) const
 3 {
 4     auto ret = wptr.lock();     // vector还存在吗?
 5     if (!ret)
 6         throw std::runtime_error("unbound StrBlobPtr");
 7     if (i >= ret->size())
 8         throw std::out_of_range(msg);
 9     return ret;     // 否则,返回指向vector的shared_ptr
10 }

 

七、动态数组

可以用一个表示数组类型的类型别名来分配一个数组:

1 typedef int arrT[42];
2 int *p = new arrT;

即使这段代码中没有方括号,编译器执行这个表达式时还是会用new[]。编译器执行如下形式:

1 int *p = new int[42];

当用new分配一个数组时,我们并未得到一个数组类型的对象,而是得到一个数组元素类型的指针。

即使我们使用类型别名定义了一个数组类型,new也不会分配一个数组类型的对象。

由于分配的内存并不是一个数组类型,因此不能对动态数组调用begin或end。也不能用范围for语句来处理(所谓的)动态数组中的元素。

这些函数使用数组维度(维度是数组类型的一部分)来返回指向首元素和尾后元素的指针。

默认情况下,new分配的动态数组是默认初始化的。在大小之后跟一对空括号可以对数组中的元素进行值初始化。

在新标准中,还可以提供一个元素初始化器的花括号列表。

如果初始化器数目小于元素数目,剩余元素将进行值初始化。如果初始化器数目大于元素数目,则new表达式失败,不会分配任何内存。

虽然我们用空括号对数组中元素进行值初始化,但不能在括号中给出初始化器,这意味着不能用auto分配数组。

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

1 char arr[0];                // 错误:不能定义长度为0的数组
2 char *cp = new char[0];     // 正确:但cp不能解引用

 数组中的元素按逆序销毁。

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

当我们使用一个类型别名来定义一个数组类型时,在释放一个数组指针时必须使用方括号:

1 typedef int arrT[42];
2 int *p = new arrT;
3 delete [] p;

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

为了用一个unique_ptr管理动态数组,我们必须在对象类型后面跟一对空方括号:

1 // up指向一个包含10个未初始化int的数组
2 unique_ptr<int[]> up(new int[10]);
3 up.release();     // 自动用delete[]销毁其指针

当一个unique_ptr指向一个数组时,我们不能使用点和箭头成员运算符。毕竟unique_ptr指向的是一个数组而不是单个对象。

当一个unique_ptr指向一个数组时,我们可以使用下标运算符来访问数组中的元素:

1 for (size_t i = 0; i != 10; ++i)
2     up[i] = i;

shared_ptr不直接支持管理动态数组。

如果希望使用shared_ptr管理一个动态数组,必须提供自己定义的删除器:

1 // 为了使用shared_ptr,必须提供一个删除器
2 shared_ptr<int> sp(new int[10], [] (int *p) { delete p; });
3 sp.reset();     // 使用我们提供的lambda释放数组,它使用delete[]

shared_ptr不直接支持动态数组管理这一特性会影响我们如何访问数组中的元素:

1 // shared_ptr未定义下标运算符,并且不支持指针的算术运算
2 for (size_t i = 0; i != 10; ++i)
3     *(sp.get() + i) = i;     // 使用get获取一个内置指针

shared_ptr未定义下标运算符,而且智能指针类型不支持指针算术运算。

 

八、allocator

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

它提供一种类型感知的内存分配方法,它分配的内存是原始的、未构造的。

当一个 allocator对象分配内存时,它会根据给定的对象类型来确定恰当的内存大小和对齐位置:

1 allocator<string> alloc;
2 auto const p = alloc.allocate(n);

在新标准库中,construct成员函数用来在给定位置构造一个元素并初始化构造的对象:

1 auto q = p;     // q指向最后构造的元素之后的位置
2 alloc.construct(q++);
3 alloc.construct(q++, 10, 'c');
4 alloc.construct(q++, "hi");

Tip:为了使用allocate返回的内存,我们必须用construct构造对象。使用未构造的内存,其行为是未定义的。

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

1 while (q != p)
2     alloc.destroy(--q);

一旦元素被销毁后,就可以重新使用这部分内存来保存其他string,也可以将其归还给系统:

1 alloc.deallocate(p, n);

标准库还为allocator类定义了两个伴随算法,可以在未初始化内存中创建对象。

1 // 分配比vi中元素所占用空间大一倍的动态内存
2 auto p = alloc.allocate(vi.size() * 2);
3 // 通过拷贝vi中的元素来构造从p开始的元素
4 auto q = uninitialized_copy(vi.begin(), vi.end(), p);
5 // 将剩余元素初始化为42
6 uninitialized_fill_n(q, vi.size(), 42);

 

内容来源于网络如有侵权请私信删除
你还没有登录,请先登录注册
  • 还没有人评论,欢迎说说您的想法!