动态内存——动态内存与智能指针

Posted acgame

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了动态内存——动态内存与智能指针相关的知识,希望对你有一定的参考价值。

  全局对象在程序启动时分配,在程序结束时销毁。对于局部自动对象,当我们进入其定义所在的程序块时被创建,在离开块时销毁。局部static对象在第一次使用前分配,在程序结束时销毁。

  除了自动和static对象外,C++还支持动态分配对象。动态分配的对象的生命期与它们在哪里创建是无关的,只有当显示地被释放时,这些对象才会销毁。

   静态内存用来保存局部static对象、类static数据成员以及定义在任何函数之外的变量。栈内存用来保存定义在函数内的非static对象。分配在静态或栈内存中的对象由编译器自动创建和销毁。对于栈对象,仅在其定义的程序块运行时才存在;static对象在使用之前分配,在程序结束时销毁。

  除了静态内存和栈内存,每个程序还拥有一个内存池。这部分内存被称作自由空间和堆。程序用来存储动态分配的对象——即,那些在程序运行时分配的对象。动态对象的生存期由程序来控制,也就是说,当动态对象不再使用时,我们的代码必须显示地销毁它们。

 

一、动态内存与智能指针

  在C++中,动态内存的管理是通过一对运算符来完成的:new,在动态内存中为对象分配空间并返回一个指向该对象的指针,我们可以选择对对象进行初始化;delete,接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。

  为了更容易同时也更安全地使用动态内存,新的标准库提供了两种智能指针类型来管理动态对象。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。新标准库提供的这两种智能指针的区别在于管理底层指针的方式:shared_ptr允许多个指针指向同一个对象;unique_ptr则“独占”所指向的对象。标准库还定义了一个名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象。这三种类型都定义在memory头文件中。

 

1、shared_ptr类

  智能指针也是模板。因此,当我们创建一个智能指针时,必须提供额外的信息——指针可以指向的类型。默认初始化的智能指针中保存着一个空指针。

技术分享图片
 1 #include <iostream>
 2 #include <memory>
 3 #include <string>
 4 
 5 int main()
 6 {
 7     std::shared_ptr<std::string> p; // shared_ptr,可以指向string
 8     if (p == nullptr)
 9     {
10         std::cout << "nullptr" << std::endl;
11     }
12     return 0;
13 }
View Code

技术分享图片

  智能指针的使用方式与普通指针类似。解引用一个智能指针返回它指向的对象。如果在一个条件判断中使用智能指针,效果就是检测它是否为空指针。

  shared_ptr和unique_ptr都支持的操作:

操作 说明
shared_ptr<T> sp 空智能指针,可以指向类型为T的对象
unique_ptr<T> up  
p 将p用作一个条件判断,若p指向一个对象,则为true
*p 解引用p,获得它指向的对象
p->mem 等价于(*p).mem
p.get() 返回p中保存的指针。要小心使用,若智能指针释放了其对象。返回的指针所指向的对象也就消失了
swap(p, q) 交换p和q中的指针
p.swap(q)  

  shared_ptr独有的操作:

操作 说明
make_shared<T>(args) 返回一个shared_ptr,指向一个动态分配的类型为T的对象。使用args初始化对象
shared_ptr<T> p(q) p是shared_ptr q的拷贝;此操作会递增q中的计数器。q中的指针必须能转换成T*
p = q p和q是shared_ptr, 所保存的指针必须能相互转换。此操作会递减p的引用计数,递增q的引用计数;若p的引用计数变为0,则将其管理的原内存释放
p.unique() 若 p.use_count()为1,返回true;否则返回false
p.use_count() 返回与p共享对象的智能指针数量;可能很慢,主要用于调试 

1)make_shared函数

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

  当要用make_shared时,必须指定想要创建的对象的类型。make_shared用其参数来构造给定类型的对象。如果我们不传递任何参数,对象就会进行值初始化

技术分享图片
 1 #include <iostream>
 2 #include <memory>
 3 #include <string>
 4 
 5 int main()
 6 {
 7     std::shared_ptr<std::string> p = std::make_shared<std::string>(3, 6);
 8     std::cout << *p << std::endl;
 9     return 0;
10 }
View Code

技术分享图片

 2)shared_ptr的拷贝和赋值

  当进行拷贝或赋值操作时,每个shared_ptr都会记录有多少个其他shared_ptr指向相同的对象。可以认为每个shared_ptr都有一个关联的计数器,通常称其为引用计数。无论何时我们拷贝一个shared_ptr,计数器都会递增(例如,当用一个shared_ptr初始化另一个shared_ptr,或将它作为参数传递给一个函数以及作为函数的返回值时,它所关联的计数器就会递增)。当我们给shared_ptr赋予一个新值或是shared_ptr被销毁(例如一个局部的shared_ptr离开其作用域)时,计数器会递减。

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

3)shared_ptr自动销毁所管理的对象

   当指向一个对象的最后一个shared_ptr被销毁时,shared_ptr类会自动销毁对象。它是通过另一个特殊的成员函数——析构函数来完成销毁工作的。

  shared_ptr的析构函数会递减它所指对象的引用计数。如果引用计数变为0,shared_ptr的析构函数就会销毁对象,并释放它所占的内存。

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

   由于在最后一个shared_ptr销毁前内存都不会释放,保证shared_ptr在无用之后不再保留就非常重要了。如果忘记了销毁程序不再需要的shared_ptr,程序仍会正确执行,但会浪费内存。shared_ptr在无用之后仍然保留的一种可能情况是,你将shared_ptr存放在一个容器中,随后重排了容器,从而不再需要某些元素。在这种情况下,你应该确保用erase删除那些不再需要的shared_ptr元素。

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

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

a、程序不知道自己要使用多少对象。

b、程序不知道所需对象的准确类型。

c、程序需要在多个对象间共享数据

  一个不同拷贝之间共享相同元素的类的例子:

技术分享图片
 1 #include <iostream>
 2 #include <memory>
 3 #include <string>
 4 #include <vector>
 5 #include <stdexcept>
 6 
 7 class StrBlob
 8 {
 9 public:
10     typedef std::vector<std::string>::size_type size_type;
11     StrBlob() = default;
12     StrBlob(std::initializer_list<std::string> items) 
13         :data(std::make_shared<std::vector<std::string>>(items)){}
14     size_type size()const{ return data->size(); }
15     bool empty() const { return data->empty(); }
16     void push_back(const std::string &s){ data->push_back(s); }
17     void pop_back()
18     {
19         check(0, "pop_back on empty StrBlob");
20         data->pop_back();
21     }
22     std::string &front()
23     {
24         check(0, "front on empty StrBlob");
25         return data->front();
26     }
27     std::string &back()
28     {
29         check(0, "back on empty StrBlob");
30         data->back();
31     }
32 private:
33     std::shared_ptr<std::vector<std::string>> data; // 多个对象间共享底层元素
34     void check(size_type i, const std::string &msg) const
35     {
36         if (i >= data->size())
37             throw std::out_of_range(msg);
38     }
39 };
40 int main()
41 {
42     StrBlob blob({ "QAQ", "hello" });
43     StrBlob cpy = blob;
44     std::cout << cpy.front() << std::endl;
45     return 0;
46 }
View Code

技术分享图片

 

2、直接管理内存

  C++语言定义了两个运算符来分配和释放动态内存。运算符new分配内存,delete释放new分配的内存。

  相对于智能指针,使用这两个运算符管理内存非常容易出错。而且,自己直接管理内存的类与使用智能指针的类不同,它们不能依赖对象拷贝、赋值和销毁操作的任何默认定义。

1)使用new动态分配内存和初始化对象

  在自由空间分配的对象是无名的,因此new无法为其分配的对象命名,而是返回一个指向该对象的指针:

技术分享图片
 1 #include <iostream>
 2 #include <memory>
 3 #include <string>
 4 #include <vector>
 5 
 6 int main()
 7 {
 8     int *p = new int; // p指向一个动态分配的、未初始化的无名对象
 9     return 0;
10 }
View Code

  默认情况下,动态分配的对象是默认初始化的,这意味着内置类型或组合类型的对象的值将是未定义的,而类类型对象将用默认构造函数进行初始化:

技术分享图片
 1 #include <iostream>
 2 #include <memory>
 3 #include <string>
 4 #include <vector>
 5 
 6 int main()
 7 {
 8     int *p = new int; // p指向一个未初始化的int
 9     std::string *ps = new std::string; // p指向一个空string
10     if (*ps == "")
11         std::cout << "null string" << std::endl;
12     return 0;
13 }
View Code

技术分享图片

  我们可以使用直接初始化方式来初始化一个动态分配的对象。可以使用传统的构造方式(使用圆括号),在新标准下,也可以使用列表初始化(使用花括号):

技术分享图片
 1 #include <iostream>
 2 #include <memory>
 3 #include <string>
 4 #include <vector>
 5 
 6 int main()
 7 {
 8     int *p = new int(1024);
 9     std::string *ps = new std::string(10, 9);
10     std::vector<int> *pv = new std::vector<int>{ 1, 2, 3, 4, 5, 6 };
11     std::cout << *p << std::endl;
12     std::cout << *ps << std::endl;
13     for (auto iter = pv->begin(); iter != pv->end(); ++iter)
14         std::cout << *iter << " ";
15     std::cout << std::endl;
16     return 0;
17 }
View Code

技术分享图片

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

技术分享图片
 1 #include <iostream>
 2 #include <memory>
 3 #include <string>
 4 #include <vector>
 5 
 6 int main()
 7 {
 8     int *p = new int();
 9     std::cout << *p << std::endl;
10     return 0;
11 }
View Code

技术分享图片

对于定义了自己的构造函数的类类型来说,要求值初始化是没有意义的;不管采用什么方式,对象都会通过默认构造函数来初始化。但对于内置类型,两种形式的差别就很大了:值初始化的内置类型对象具有着良好定义的值,而默认初始化的对象的值则是未定义的。

2)动态分配的const对象

  用new分配const对象是合法的:

技术分享图片
 1 #include <iostream>
 2 #include <memory>
 3 #include <string>
 4 #include <vector>
 5 
 6 int main()
 7 {
 8     const int *pci = new const int(1024); // 分配并初始化一个const int
 9     const std::string *pcs = new const std::string; // 分配并默认初始化一个const的空string
10     return 0;
11 }
View Code

一个动态分配的const对象必须进行初始化。对于一个定义了默认构造函数的类类型,其const动态对象可以隐式初始化,而其他类型的对象就必须显示初始化。由于分配的对象是const的,new返回的指针是一个指向const的指针。

3)内存耗尽

   虽然现代计算机都配备大容量内存,但是自由空间被耗尽的情况还是有可能发生。一旦一个程序用光了它所有可用的内存,new表达式就会失败。默认情况下,如果new不能分配所要求的内存空间,它就会抛出一个类型为bad_alloc的异常。我们可以改变使用new的方式来阻止它抛出异常:

技术分享图片
 1 #include <iostream>
 2 #include <memory>
 3 #include <string>
 4 #include <vector>
 5 
 6 int main()
 7 {
 8     int *p1 = new int; // 如果分配失败,new抛出std::bad_alloc
 9     int *p2 = new (std::nothrow) int; // 如果分配失败,new返回一个空指针
10     return 0;
11 }
View Code

我们称这种形式大的new为定位new。定位new允许我们向new传递额外的参数。在此例中,我们传递给它一个由标准库定义的名为nothrow的对象。如果将nothrow传递给new,我们的意图是告诉它不能抛出异常。如果这种形式的new不能分配所需内存,它会返回一个空指针。bad_alloc和nothrow都定义在头文件new中。

4)释放动态内存

  为了防止内存耗尽,在动态内存使用完毕后,必须将其归还给系统。我们通过delete表达式来将动态内存归还给系统。delete表达式接受一个指针,指向我们想要释放的对象:

技术分享图片
 1 #include <iostream>
 2 #include <memory>
 3 #include <string>
 4 #include <vector>
 5 #include <new>
 6 
 7 int main()
 8 {
 9     int *p = new int;
10     delete p; // p必须指向一个动态分配的对象或是一个空指针
11     std::cout << p << std::endl;
12     p = nullptr; // 指针delete后,不会变成空指针,将其设为空指针很有必要,以免误用
13     std::cout << p << std::endl;
14     delete p;
15     return 0;
16 }
View Code

技术分享图片

与new类型类似,delete表达式也执行两个动作:销毁给定的指针指向的对象;释放对应的内存。

5)指针值和delete

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

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

技术分享图片
 1 #include <iostream>
 2 #include <memory>
 3 #include <string>
 4 #include <vector>
 5 #include <new>
 6 
 7 int main()
 8 {
 9     const int *p = new const int(1024);
10     delete p;
11     return 0;
12 }
View Code

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

  由内置指针(而不是智能指针)管理的动态内存在被显示释放前一直都会存在。

7)delete之后重置指针值

  当我们delete一个指针后,指针值就变为无效了。虽然指针无效,但在很多机器上指针仍然保存着(已经释放了的)动态内存的地址。在delete之后,指针就变成了人们所说的空悬指针,即,指向一块曾经保存数据对象但现在已经无效的内存的指针。

  避免空悬指针:在指针即将离开其作用域之前释放掉它所关联的内存。这样,在指针关联的内存被释放掉之后,就没有机会继续使用指针了。如果我们需要保留指针,可以在delete之后将nullptr赋予指针,这样就清楚地指出指针不指向任何对象。

  动态内存的一个基本问题是可能有多个指针指向相同的内存。在delete内存之后重置指针的方法只对这个指针有效,对其他任何仍指向已释放的内存的指针是没有任何作用的

 

3、shared_ptr和new结合使用

   定义和改变shared_ptr的方法:

操作 说明
shared_ptr<T> p(q) p管理内置指针q所指向的对象;q必须指向new分配的内存,且能够转换为T*类型
shared_ptr<T> p(u) p从unique_ptr u那里接管了对象的所有权;将u置为空
shared_ptr<T> p(q, d) p接管了内置指针q所指向的对象的所有权。q必须能转换为T*类型。p将使用可调用对象d来代替delete
shared_ptr<T> p(p2, d) p是shared_ptr p2的拷贝,p将用可调用对象d来代替delete
p.reset()

若p是唯一指向其对象的shared_ptr,reset会释放此对象。

若传递了可选的参数内置指针q,会令p指向q,否则将p置空。

若还传递了参数d,将会调用d而不是delete来释放q

p.reset(q)  
p.reset(q, d)  

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

  默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用delete释放它所关联的对象。我们可以将智能指针绑定到一个指向其他类型的资源的指针上,但是为了这样做,必须提供自己的操作来代替delete。

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

  当将一个shared_ptr绑定到一个普通指针时,我们就将内存的管理责任交给了这个shared_ptr。一旦这样做了,我们就不应该再使用内置指针来访问shared_ptr所指向的内存,因为我们无法知道对象何时会被销毁。

技术分享图片
 1 #include <iostream>
 2 #include <memory>
 3 #include <string>
 4 #include <vector>
 5 #include <new>
 6 
 7 void process(std::shared_ptr<int> ptr)
 8 {
 9     std::cout << *ptr << std::endl;
10 } // ptr离开作用域,被销毁
11 
12 int main()
13 {
14     int *p(new int(1024)); // p是一个普通指针
15     process(std::shared_ptr<int>(p)); // 内存会被释放
16     int data = *p; // 未定义:p是一个空悬指针
17     std::cout << data << std::endl;
18     return 0;
19 }
View Code

技术分享图片

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

  智能指针类型定义了一个名为get的函数,它返回一个内置指针,指向智能指针管理的对象。此函数是为了这样一种情况而设计的:我们需要向不能使用智能指针的代码传递一个内置指针。使用get返回的指针的代码不能delete此指针。

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

技术分享图片
 1 #include <iostream>
 2 #include <memory>
 3 #include <string>
 4 #include <vector>
 5 #include <new>
 6 
 7 class Blob
 8 {
 9 public:
10     Blob() :x(0){}
11     Blob(int _x) :x(_x){}
12     ~Blob(){
13         std::cout << x << " ~Blob" << std::endl;
14     }
15     int x;
16 };
17 int main()
18 {
19     std::shared_ptr<Blob> p(new Blob(1024)); // 引用计数为1
20     Blob *q = p.get(); // 使用q时要注意,不要让它管理的指针被释放
21     {
22         std::shared_ptr<Blob> cp(q); // 两个独立的shared_ptr指向相同的内存
23     } // 程序块结束,q被销毁,它指向的内存被释放
24     std::cout << "------------------------" << std::endl;
25     Blob data = *p; // 未定义:p指向的内存已经被释放了
26     std::cout << data.x << std::endl;
27     return 0;
28 }
View Code

技术分享图片

在本例中,p和cp指向相同的内存。由于它们是相互独立创建的,因此各自的引用计数都是1。当cp所在的程序块结束时,cp被销毁,这会导致cp指向的内存被释放。从从而p变成一个空悬指针,当我们试图使用p时,将会发生未定义的行为。而且,当p被销毁时,这块内存会被第二次delete。

  注意:get用来将指针的访问权限传递给代码,你只有在确定代码不会delete指针的情况下,才能使用get。特别是,永远不要用get初始化另一个智能指针或者为另一个智能指针赋值

3)其他shared_ptr操作

  与赋值类似,reset会更新引用计数,如果需要的话,会释放p指向的对象。

技术分享图片
 1 #include <iostream>
 2 #include <memory>
 3 #include <string>
 4 #include <vector>
 5 #include <new>
 6 
 7 class Blob
 8 {
 9 public:
10     Blob() :x(0){}
11     Blob(int _x) :x(_x){}
12     ~Blob(){
13         std::cout << x << " ~Blob" << std::endl;
14     }
15     int x;
16 };
17 int main()
18 {
19     std::shared_ptr<Blob> p(new Blob(1024));
20     p.reset(new Blob(233));
21     std::cout << "------------" << std::endl;
22     return 0;
23 }
View Code

技术分享图片

 

4、智能指针和异常

  使用异常处理的程序能在异常发生后令程序流程继续,这种程序需要确保在异常发生后资源能被正确地释放。一个简单的确保资源被释放的方法是使用智能指针。

  如果使用智能指针,即使程序块过早结束,智能指针类也能确保在内存不再需要时将其释放。与之相对的,当发生异常时,我们直接管理的内存是不会自动释放的。如果使用内置指针管理内存,且在new之后在对应的delete之前发生了异常,则内存不会被释放。

1)智能指针和哑类

  包括标准库类在内的很多C++类都定义了析构函数,负责清理对象使用的资源。但是,不是所有的类都是这样良好定义的。特别是那些为C和C++两种语言设计的类,通常都要求用户显示地释放所使用的任何资源。

  那些分配了资源,而又没有定义析构函数来释放这些资源的类,可能会遇到与使用动态内存相同的错误——程序员非常容易忘记释放资源。类似的,如果在资源分配和释放之间发生了异常,程序也会发生资源泄露。

  与管理内存类似,我们通常可以使用类似的技术来管理不具有良好定义的析构函数的类。例如,加入我们正在使用一个网络库,代码如下:

技术分享图片
 1 #include <iostream>
 2 #include <memory>
 3 #include <string>
 4 #include <vector>
 5 
 6 class Connection //表示一个连接
 7 {
 8 public:
 9     Connection() :x(0){}
10     Connection(int _x) :x(_x){}
11     int x;
12 };
13 
14 Connection connect() // 一个连接函数
15 {
16     std::cout << __FUNCTION__ << std::endl;
17     return Connection(1024);
18 }
19 
20 void disconnect(Connection &b) // 一个断开连接的函数
21 {
22     std::cout << __FUNCTION__ << "--" << b.x << std::endl;
23 }
24 
25 void f()
26 {
27     Connection con = connect(); // 获得一个连接,记住使用完后要关闭它
28     // 一些处理
29     disconnect(con); //如果处理时发生了异常,连接就关闭不了了
30 }
31 
32 int main()
33 {
34     f();
35     return 0;
36 }
View Code

如果Connection有一个析构函数,就可以在f结束时由析构函数自动关闭连接。但是Connection没有析构函数。这个问题与使用shared_ptr避免内存泄露几乎是等价的。使用shared_ptr来保证Connection被正确关闭是一种有效的方法。

2)使用我们自己的释放操作

  默认情况下,shared_ptr假定它们指向的是动态内存。因此,当一个shared_ptr被销毁时,它默认地对它管理的指针进行delete操作。为了使用shared_ptr来管理一个Connection,我们必须首先定义一个函数来代替delete。这个删除器函数必须能够完成对shared_ptr中保存的指针进行释放的操作。

技术分享图片
 1 #include <iostream>
 2 #include <memory>
 3 #include <string>
 4 #include <vector>
 5 #include <stdexcept>
 6 
 7 class Connection //表示一个连接
 8 {
 9 public:
10     Connection() :x(0){}
11     Connection(int _x) :x(_x){}
12     int x;
13 };
14 
15 Connection connect() // 一个连接函数
16 {
17     std::cout << __FUNCTION__ << std::endl;
18     return Connection(1024);
19 }
20 
21 void disconnect(Connection *b) // 一个断开连接的函数
22 {
23     std::cout << __FUNCTION__ << "--" << b->x << std::endl;
24 }
25 
26 void end_connection(Connection *p) // 删除器函数
27 {
28     disconnect(p);
29 }
30 
31 void f()
32 {
33     Connection con = connect(); // 获得一个连接,记住使用完后要关闭它
34     std::shared_ptr<Connection> p(&con, end_connection);
35     std::cout << "something do" << std::endl;
36 }
37 
38 int main()
39 {
40 
41     f();
42     return 0;
43 }
View Code

技术分享图片

当p被销毁时,它不会对自己保存的指针指向delete,而是调用end_connection。如果f正常退出,那么p的销毁会作为结束处理的一部分。如果发生了异常,p同样会被销毁,从而连接关闭。

 3)智能指针陷阱

  使用智能指针的基本规范:

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

 

5、unique_ptr

  一个unique_ptr“拥有”它所指向的对象。与shared_ptr不同,某个时刻只能有一个unique_ptr指向一个给定对象。当unique_ptr被销毁时,它所指向的对象也被销毁。

  unique_ptr独有的操作:

操作 说明
unique_ptr<T> u1 空unique_ptr可以指向类型为T的对象。u1会使用delete来释放它的指针;u2会使用一个类型为D的可调用对象来释放它的指针
unique_ptr<T, D> u2  
unique_ptr<T, D> u(d) 空unique_ptr,指向类型为T的对象,用类型为D的对象d代替delete
u=nullptr 释放u指向的对象,将u置空
u.release() u放弃对指针的控制权,返回指针,并将u置空
u.reset() 释放u指向的对象
u.reset(q) 如果提供了内置指针q,令u指向这个对象;否则将u置为空
u.reset(nullptr)  

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

技术分享图片
 1 #include <iostream>
 2 #include <memory>
 3 #include <string>
 4 #include <vector>
 5 
 6 int main()
 7 {
 8 
 9     std::unique_ptr<int> p(new int(1024));
10     std::cout << *p << std::endl;
11     return 0;
12 }
View Code

  由于一个unique_ptr拥有它指向的对象,因此unique_ptr不支持普通的拷贝或赋值操作。可以通过release和reset将指针的所有权从一个(非const)unique_ptr转移给另一个unique_ptr。

   调用release会切断unique_ptr和它原来管理的对象间的联系。release返回的指针通常被用来初始化另一个智能指针或给另一个智能指针赋值。如果我们不用另一个智能指针来保存release返回的指针,我们的程序就要负责资源的释放:

技术分享图片
 1 #include <iostream>
 2 #include <memory>
 3 #include <string>
 4 #include <vector>
 5 #include <new>
 6 
 7 class Blob
 8 {
 9 public:
10     Blob() :x(0){}
11     Blob(int _x) :x(_x){}
12     ~Blob(){
13         std::cout << x << " ~Blob" << std::endl;
14     }
15     int x;
16 };
17 int main()
18 {
19     std::unique_ptr<Blob> p(new Blob(1024));
20     auto p2 = p.release();
21     std::cout << "-------------" << std::endl;
22     delete p2; // 必须要记得delete
23     return 0;
24 }
View Code

技术分享图片

  reset成员接受一个可选的指针参数,令unique_ptr重新指向给定的指针。如果unique_ptr不为空,它原来指向的对象被释放。

技术分享图片
 1 #include <iostream>
 2 #include <memory>
 3 #include <string>
 4 #include <vector>
 5 #include <new>
 6 
 7 class Blob
 8 {
 9 public:
10     Blob() :x(0){}
11     Blob(int _x) :x(_x){}
12     ~Blob(){
13         std::cout << x << " ~Blob" << std::endl;
14     }
15     int x;
16 };
17 int main()
18 {
19     std::unique_ptr<Blob> p1(new Blob(1));
20     Blob *p2(new Blob(2));
21     p1.reset(p2);
22     std::cout << "--------------" << std::endl;
23     p1.reset();
24     std::cout << "---------------" << std::endl;
25     if (p1 == nullptr)
26         std::cout << "nullptr" << std::endl;
27     return 0;
28 }
View Code

技术分享图片

1)传递unique_ptr参数和返回unique_ptr

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

技术分享图片
 1 #include <iostream>
 2 #include <memory>
 3 #include <string>
 4 #include <vector>
 5 #include <new>
 6 
 7 class Blob
 8 {
 9 public:
10     Blob() :x(0){}
11     Blob(int _x) :x(_x){}
12     ~Blob(){
13         std::cout << x << " ~Blob" << std::endl;
14     }
15     int x;
16 };
17 std::unique_ptr<Blob> func()
18 {
19     std::unique_ptr<Blob> p(new Blob(1));
20 
21     return p;
22 }
23 int main()
24 {
25     std::unique_ptr<Blob> p = func();
26     std::cout << "-------------" << std::endl;
27     return 0;
28 }
View Code

技术分享图片

对于这段代码,编译器知道要返回的对象要被销毁。在此情况下,编译器执行一种特殊的“拷贝”。

2)向unique_ptr传递删除器

  unique_ptr默认情况下用delete释放它指向的对象,我们可以重载一个unique_ptr中默认的删除器。我们必须在尖括号中unique_ptr指向类型之后提供删除类型。在创建或reset一个这种unique_ptr类型的对象时,必须提供一个指定类型的可调用对象(删除器)。

技术分享图片
 1 #include <iostream>
 2 #include <memory>
 3 #include <string>
 4 #include <vector>
 5 #include <stdexcept>
 6 
 7 class Connection //表示一个连接
 8 {
 9 public:
10     Connection() :x(0){}
11     Connection(int _x) :x(_x){}
12     int x;
13 };
14 
15 Connection connect() // 一个连接函数
16 {
17     std::cout << __FUNCTION__ << std::endl;
18     return Connection(1024);
19 }
20 
21 void disconnect(Connection *b) // 一个断开连接的函数
22 {
23     std::cout << __FUNCTION__ << "--" << b->x << std::endl;
24 }
25 
26 void end_connection(Connection *p) // 删除器函数
27 {
28     disconnect(p);
29 }
30 
31 void f()
32 {
33     Connection con = connect(); // 获得一个连接,记住使用完后要关闭它
34     std::unique_ptr<Connection, decltype(end_connection)*> p(&con, end_connection);
35     std::cout << "something do" << std::endl;
36 }
37 
38 int main()
39 {
40 
41     f();
42     return 0;
43 }
View Code

技术分享图片

 

6、weak_ptr

  weak_ptr是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。即使有weak_ptr指向对象,对象还是会被释放,因此,weak_ptr的名字抓住了这种智能指针“弱”共享对象的特点。

  weak_ptr的操作:

操作 说明
weak_ptr<T> w 空weak_ptr可以指向类型为T的对象
weak_ptr<T> w(sp) 与shared_ptr sp指向相同对象的weak_ptr。T必须能转换为sp指向的类型
w=p p可以是一个shared_ptr 或一个weak_ptr。赋值后w与p共享对象
w.reset() 将w置空
w.use_count() 与w共享对象的shared_ptr的数量
w.expired() 若w.use_count()为0,返回true,否则返回false
w.lock() 如果expired为true,返回一个空shared_ptr;否则返回一个指向w的对象的shared_ptr

  由于对象可能不存在,我们不能使用weak_ptr直接访问对象,而必须调用lock。此函数检查weak_ptr指向的对象是否存在。

技术分享图片
 1 #include <iostream>
 2 #include <memory>
 3 #include <string>
 4 
 5 void check(std::weak_ptr<int> w)
 6 {
 7     if (w.lock())
 8     {
 9         std::shared_ptr<int> p = w.lock();
10         std::cout << "exist" << " " << *p << std::endl;
11     }
12     else
13     {
14         std::cout << "nullptr" << std::endl;
15     }
16 }
17 int main()
18 {
19 
20     std::weak_ptr<int> w;
21     check(w);
22     std::shared_ptr<int> p; // 空指针
23     w = p;
24     check(w);
25     int *q(new int(1024));
26     p.reset(q);
27     check(w); // 此时w指向的还是空指针
28     w = p;
29     check(w);
30     return 0;
31 }
View Code

技术分享图片

 

以上是关于动态内存——动态内存与智能指针的主要内容,如果未能解决你的问题,请参考以下文章

动态内存——动态内存与智能指针

动态内存1(动态内存与智能指针)

动态内存与智能指针

12.1 动态内存与智能指针

c++动态内存管理与智能指针

第十二章 动态内存与智能指针