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

Posted ygeloutingyu

tags:

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

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

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

 

动态内存与智能指针:

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

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

 

shared_ptr 类:

智能指针也是模板:

技术分享图片
 1 #include <iostream>
 2 #include <memory>
 3 #include <list>
 4 using namespace std;
 5 
 6 int main(void){
 7     shared_ptr<string> p1;//shared_ptr,可以指向string
 8     shared_ptr<list<int>> p2;//shared_ptr,可以指向int的list
 9 
10     if(p1 && p1->empty()) *p1 = "hi";//如果p1指向一个空string,解引用p1,将一个新值赋予string
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 共享对象的智能指针数量;可能很慢,主要用于调试

 

make_shared 函数:

最安全的分配和使用动态内存的方法是调用一个名为 make_shared 的标准库函数

技术分享图片
 1 #include <iostream>
 2 #include <memory>
 3 #include <list>
 4 using namespace std;
 5 
 6 int main(void){
 7     //指向一个值为42的int的shared_ptr
 8     shared_ptr<int> p1 = make_shared<int>(42);
 9     cout << *p1 << endl;//42
10     //p2指向一个值为"999"的string
11     shared_ptr<string> p2 = make_shared<string>(3, 9);
12     cout << *p2 << endl;//999
13     //p3指向一个值初始化的int,即0
14     shared_ptr<int> p3 = make_shared<int>();
15     cout << *p3 << endl;//0
16 
17     return 0;
18 }
View Code

注意:使用 make_shared 时,必须指定要创建的对象类型。定义方式与模板类相同,在函数名后跟一个尖括号,在其中给出类型

类似顺序容器的 emplace 成员,make_shared 用其参数来构造给定类型的对象。因此给的参数必须符合对应类型的构造规则

 

当然,用 auto 定义一个对象来保存 make_shared 的结果会简单很多:

技术分享图片
 1 #include <iostream>
 2 #include <memory>
 3 #include <list>
 4 using namespace std;
 5 
 6 int main(void){
 7     //指向一个值为42的int的shared_ptr
 8     auto p1 = make_shared<int>(42);
 9     cout << *p1 << endl;//42
10     //p2指向一个值为"999"的string
11     auto p2 = make_shared<string>(3, 9);
12     cout << *p2 << endl;//999
13     //p3指向一个值初始化的int,即0
14     auto p3 = make_shared<int>();
15     cout << *p3 << endl;//0
16 
17     return 0;
18 }
View Code

 

shared_ptr 的拷贝和赋值:

当进行拷贝和赋值操作时,每个 shared_ptr 都会记录有多少个其它 shared_ptr 指向相同的对象:

技术分享图片
 1 #include <iostream>
 2 #include <memory>
 3 using namespace std;
 4 
 5 int main(void){
 6     auto p = make_shared<int>(42);//p指向的对象此时只有p一个引用者
 7     auto q(p);//p和q指向相同的对象,此对象有两个引用者
 8     cout << p.use_count() << endl;//2
 9     cout << q.use_count() << endl;//2
10 
11     return 0;
12 }
View Code

 

我们可以认为每个 shared_ptr 都有一个关联的计数器,通常称其为引用计数。无论何时我们拷贝一个 shared_ptr,计数器都会递增。如,当用一个 shared_ptr 初始化另一个 shared_ptr,或将它作为参数传递给另一个函数以及作为函数的返回值时,它所关联的计数器都会递增。当我们给 shared_ptr 赋予一个新值或是 shared_ptr 被销毁(如,一个局部的 shared_ptr 离开其作用域)时,计数器都会递减。一旦一个 shared_ptr 的计数器变为 0,它就会自动释放自己所管理的对象:

技术分享图片
 1 #include <iostream>
 2 #include <memory>
 3 using namespace std;
 4 
 5 int main(void){
 6     auto p = make_shared<int>(42);//p指向的对象此时只有p一个引用者
 7     auto q(p);//p和q指向相同的对象,此对象有两个引用者
 8     cout << p.use_count() << endl;//2
 9     cout << q.use_count() << endl;//2
10 
11     q = make_shared<int>(1024);//给q开一个新的内存,q此时不再和p一起指向存储42那块内存
12     cout << p.use_count() << endl;//1
13     cout << q.use_count() << endl;//1
14 
15     p = q;//给p赋值,此时p和q同时指向存储1024的这块内存
16     cout << q.use_count() << endl;//2 递增q指向的对象的引用计数
17     //递减原来指向的对象的引用计数,p原来指向的对象引用计数变为0,自动释放该对象的内存(存储42的这块内存)
18 
19     return 0;
20 }
View Code

 

shared_ptr 自动销毁所管理的对象:

当指向对象的最后一个 shared_ptr 被销毁时,shared_ptr 会自动通过析构函数销毁此对象 。由于在最后一个 shared_ptr 销毁之前内存都不会释放,保证 shared_ptr 在无用之后不再保存就十分·重要了。如果忘记销毁程序不再需要的 shared_ptr,程序仍会正确执行,但会浪费内存。shared_ptr 在无用之后仍然保留的的一种可能情况是,你将 shared_ptr 存放在一个容器中,随后重排了容器,从而不再需要某些元素。这种情况下,你应该确保用 erase 删除那些不再需要的 shared_ptr 元素

 

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

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

1.程序不知道自己需要使用多少对象

2.程序不知道所需对象的准确类型

3.程序需要在多个对象间共享数据

 

我们来分析一波第 3 种情况:

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

技术分享图片
1 vector<string> v1;//空vector
2 {//新作用域
3     vector<string> v2 = {"f", "jf"};
4     v1 = v2;//从v2拷贝元素到v1中
5 }//离开作用域,v2被销毁,其中元素也被销毁
6 //v1中有2个元素,是原来v2中元素的拷贝
View Code

注意:由一个 vector 分配的元素只有当这个 vector 存在时才存在。当一个 vector 被销毁时,其中的元素也都被销毁

 

但某些类分配的资源具有与原对象相独立的生存期。如,假定我们希望定义一个名为 Blod 的类,保存一组元素。与容器不同,我们希望 Blod 对象的不同拷贝之间共享相同的元素。即,当我们拷贝一个 Blod 时,原 Blod 对象及其拷贝应该引用相同的底层元素。

通常,如果两个对象共享底层数据,当某个对象被销毁时,我们不能单方面销毁底层数据:

技术分享图片
1 Blod<string> b1;//空Blod
2 {//新作用域
3     Blod<string> b2 = {"f", "jf"};
4     v1 = v2;//从b2拷贝元素到b1中
5 }//离开作用域,b2被销毁,但其中元素不能销毁
6 //b1指向最初由b2创建的元素
View Code

注意:b1 和 b2 共享相同的元素,当 b2 离开作用域时,这些元素必须保留,因为 b1 仍任在使用它们

 

定义一个管理 string 的 Blod 类,命名为 strBlod:

技术分享图片
  1 #include <iostream>
  2 #include <memory>
  3 #include <vector>
  4 using namespace std;
  5 
  6 class strBlod{
  7 public:
  8     typedef std::vector<std::string>::size_type size_type;
  9     strBlod();
 10     strBlod(std::initializer_list<std::string> il);
 11     ~strBlod();
 12 
 13     size_type size() const {
 14         return data->size();
 15     }
 16 
 17     bool empty() const {
 18         return data->empty();
 19     }
 20 
 21     //添加和删除元素
 22     void push_back(const std::string &t){
 23         data->push_back(t);
 24     }
 25     void pop_back();
 26 
 27     //元素访问
 28     std::string& front();
 29     std::string& front() const;
 30 
 31     std::string& back();
 32     std::string& back() const;
 33 
 34     int get_use_count() const{//得到shared_ptr成员data的引用计数
 35         return data.use_count();
 36     }
 37 
 38 private:
 39     std::shared_ptr<std::vector<std::string>> data;
 40 
 41     //如果data[i]不合法,抛出一个异常
 42     void check(size_type i, const std::string &msg) const;
 43 
 44     std::string& front_display() const;
 45     std::string& back_display() const;
 46     
 47 };
 48 
 49 strBlod::strBlod() : data(make_shared<vector<string>>()) {}//默认构造函数
 50 strBlod::strBlod(initializer_list<string> il) : data(make_shared<vector<string>>(il)) {}
 51 strBlod::~strBlod() {}
 52 
 53 void strBlod::check(size_type i, const string &msg) const {
 54     if(i >= data->size()) throw out_of_range(msg);
 55 }
 56 
 57 string& strBlod::front(){
 58     return front_display();
 59 }
 60 
 61 string& strBlod::front() const{
 62     return front_display();
 63 }
 64 
 65 string& strBlod::front_display() const{
 66     //如果vector为空,check会抛出一个异常
 67     check(0, "front on empty strBlod!");
 68     return data->front();
 69 }
 70 
 71 string& strBlod::back(){
 72     return back_display();
 73 }
 74 
 75 string& strBlod::back() const{
 76     return back_display();
 77 }
 78 
 79 string& strBlod::back_display() const{
 80     check(0, "back on empty strBlod!");
 81     return data->back();
 82 }
 83 
 84 void strBlod::pop_back(){
 85     check(0, "pop_back on empty strBlod!");
 86     data->pop_back();
 87 }
 88 
 89 int main(void){
 90     strBlod b1;//b1执行默认构造
 91     cout << b1.get_use_count() << endl;//1
 92 
 93     {
 94         strBlod b2({"hello", "word", "!"});
 95         cout << b2.get_use_count() << endl;//1
 96         b1 = b2;//b1和b2共享元素
 97         cout << b1.get_use_count() << endl;//2
 98         cout << b1.get_use_count() << endl;//2
 99         // 当我们拷贝、赋值或销毁一个 strBlod 对象时,它的 shared_ptr 成员会被拷贝、赋值或销毁
100     }
101     // string s = b2.front();//错误,离开了b2的作用域,b2被销毁了
102     //b1指向原本由b2创建的元素
103 
104     cout << b1.get_use_count() << endl;//1
105     string s = b1.front();
106     cout << s << endl;//hello
107 
108     return 0;
109 }//离开b1的作用域,b1被销毁
110 //由strBlod构造函数分配的vector已经没有strBlod对象指向它,此时被自动销毁
View Code

注意:当我们拷贝、赋值或销毁一个 strBlod 对象时,它的 shared_ptr 成员会被拷贝、赋值或销毁

如果一个 shared_ptr 的引用计数变为 0,它所指像的对象会被自动销毁。因此,对于由 strBlod 构造函数分配的 vector,当最后一个指向它的 strBlod 对象被销毁时,它也会随之被自动销毁

 

直接管理内存:

使用 new 动态分配和初始化对象:

技术分享图片
 1 #include <iostream>
 2 #include <vector>
 3 using namespace std;
 4 
 5 int main(void){
 6     int *pi = new int;//pi指向一个动态分配的,未初始化的对象
 7     cout << *pi << endl;//默认情况下,动态内存分配的对象是默认初始化的
 8     //即,内置类型或组合类型的对象的值将是未定义的(随机值),而类类型对象将用默认构造函数进行初始化
 9     string *ps = new string;//初始化为空string
10 
11     int *p1 = new int(1024);//构造初始化,p1指向一个值为1024的int对象
12     string *ps1 = new string(3, 9);//ps1指向一个值为"999"的string对象
13 
14     vector<int> *pv1 = new vector<int>{0, 1, 2, 3, 4};//列表初始化
15 
16     //值初始化(类型名后面跟一对括号)
17     string *ps2 = new string;//默认初始化为空string
18     string *ps3 = new string();//值初始化为空string
19     int *p2 = new int();//值初始化为0
20     cout << *p2 << endl;//0
21 
22     //使用auto推断
23     int obj = 1024;
24     int obj1 = 1, obj2 = 2, obj3 = 3;
25     auto p4 = new auto(obj);//p4指向一个与obj类型相同的对象
26     auto p5 = new auto{obj3, obj2, obj1};//c++prime 5th上说这种用法是错误的,只能有单个初始化器,但我用g++11编译通过了
27     //然而并不知道这个推断出来的是什么类型,也不是int*
28     // cout << p5[0] << endl;//错误,p5不是int*类型
29 
30     // 动态分配const对象
31     const int *p6 = new const int(1024);//分配并初始化一个const int
32     // *p6 = 0;//错误,p6指向的对象是const int
33     const string *ps4 = new const string;//分配并默认初始化一个const的空string
34 
35     return 0;
36 }
View Code

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

默认情况下,动态内存分配的对象是默认初始化的,即,内置类型或组合类型的对象的值将是未定义的(随机值),而类类型对象将用默认构造函数进行初始化

对于定义了自己的构造函数的类类型来说,要求值初始化是没有意义的,不管采用什么形式,对象都会通过默认构造函数类初始化

对于内置类型,应该尽量使用值初始化来代替默认初始化,因为其默认初始化是未定义的

如果我们提供了一个括号包围的初始化器,就可以使用 auto 从此初始化器来推断类型。但是由于编译器要用初始化器的类型来推断分配的类型,只有当括号中仅有单一初始化器是才可以使用 auto

动态分配 const 对象时对于没有默认构造函数的类类型,必须显示初始化

 

内存耗尽:

一旦一个程序用完了它所有可用的内存,new 表达式就会失败。默认情况下,如果 new 不能分配所需求的内存空间,会抛出一个类型为 bad_alloc 的异常。我们可以改变使用 new 的方式来阻止它抛出异常:

技术分享图片
1     int *p1 = new int;//如果分配失败,new 抛出std::bad_alloc
2     int *p2 = new (nothrow) int;//如果分配失败,new 返回一个空指针
3     //定位new表达式允许我们像new传递额外的参数
View Code

 

释放动态内存:

指针值和delete:

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

技术分享图片
 1 #include <iostream>
 2 using namespace std;
 3 
 4 int main(void){
 5     int i, *pi1 = &i, *pi2 = nullptr;
 6     double *pd = new double(33), *pd2 = pd;
 7     // delete i;//错误,i不是应该指针
 8     // delete pi1;//能通过编译,但是该行为是未定义的,pi1指向一个局部变量
 9     delete pd;
10     // delete pd2;能通过编译,但是该行为是未定义的,p2指向的内存已经被释放了
11     delete pi2;//释放一个空指针总是没有错误的
12 
13     const int *p = new const int(1024);
14     delete p;//正确,释放一个const对象
15 
16     return 0;
17 }
View Code

注意:通常情况下,编译器不能分辨一个指针指向的是静态还是动态分配的对象。类似的,编译器也不能分辨一个指针所指向的内存是否已经释放了。对于这些 delete 表达式,大多数编译器会编译通过,尽管它们是错误的

 由内置指针管理的动态内存被显示释放前是一直存在的。当一个只想动态内存的指针离开其作用域时其关联的内存不会被自动释放

 

动态内存管理常见错误:

1.忘记 delete 内存,导致内存泄漏

2.使用已经释放掉的对象。通过在释放内存后将指针置为空,有时可以检测出这种错误

3.同一块内存释放多次

 

delete 之后重置指针:

在 delete 之后,指针就变成了空悬指针。可以在 delete 之后将 nullptr 赋予指针,这样就清楚地指出指针不指向任何对象。但是,动态内存可能有多个指针指向相同的内存。在 delete 之后重置指针的方法只对这个指针有效,对其它任何仍指向(已释放的)内存的指针是没有作用的:

技术分享图片
1     int *p(new int(42));
2     auto q = p;
3     delete p;//p和q均无效
4     p = nullptr;//指出p不再绑定到任何对象
View Code

注意:此时 q 已经无效了,但重置 p 对 q 没有任何影响,折意味着 q 并没有被表示为 nullptr

 

shared_ptr 和 new 结合使用:

技术分享图片
 1 #include <iostream>
 2 #include <memory>
 3 using namespace std;
 4 
 5 shared_ptr<int> clone(int p){
 6     // return new int(p);//错误,将一个int*隐式转换为shred_ptr<int>
 7     return shared_ptr<int>(new int(p));//正确,显示地用int*创建shared_ptr<int>
 8 }
 9 
10 int main(void){
11     shared_ptr<double> p1;//shared_ptr可以指向一个double
12     shared_ptr<int> p2(new int(1024));//p2指向一个值为42的int
13     //接受指针参数的智能指针构造函数是explicit的,我们不能将一个
14     //内置指针隐式转化一个智能指针,必须使用直接初始化形式来初始化一个智能指针
15     // shared_ptr<int> p3 = new int(1024);//错误,必须使用直接初始化形式
16 
17     return 0;
18 }
View Code

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

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

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

 

定义和改变 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 的拷贝,且用可调用对象 d 来替代 delete

p.reset()           若 p 是唯一指向其对象的 shared_ptr,reset 会释放此对象。若传递了可选

p.reset(q)         参数指针 q,会令 p 指向 q,否则会将 p 置为空。若还传递了可选参数 d,

p.reset(q, d)        将会调用 d 而不是 delete 来释放 q

 

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

技术分享图片
 1 #include <iostream>
 2 #include <memory>
 3 using namespace std;
 4 
 5 void process(shared_ptr<int> ptr){//值传参,拷贝后引用计数加一
 6 
 7 }//离开ptr作用域,ptr被销毁,其所指对象引用计数减一
 8 
 9 int main(void){
10     shared_ptr<int> p(new int(42));//引用计数为1
11     process(p);//拷贝p会递增它的引用计数,传入process后引用计数为2
12     int i = *p;//正确,引用计数为1
13 
14     int *x(new int(1024));//x是一个普通指针
15     // process(x);//错误,不能将int*隐式转换为shared_ptr<int>
16     process(shared_ptr<int>(x));//创建shared_ptr<int>临时变量时其引用计数为1,
17     //离开创建临时变量的表达式时临时变量会被销毁,传入process后引用计数为1
18     // int j = *p;//未定义的,x时一个空悬指针
19 
20     return 0;
21 }
View Code

注意:当将一个 shared_ptr 绑定到一个普通指针时,我们就将内存的管理交给了这个 shared_ptr。一旦这样做了,我们就不应该再使用内置指针来访问 shared_ptr 所指向的内存了

显示 / 隐式转换时即创建了一个对应的临时变量

离开创建临时变量的表达式时,该表达式中创建的临时变量会被销毁:

技术分享图片
 1 #include <iostream>
 2 #include <memory>
 3 using namespace std;
 4 
 5 class gel{
 6 friend ostream& operator<<(ostream&, const gel&);
 7 
 8 private:
 9     int x, y;
10 
11 public:
12     gel(int a, int b) : x(a), y(a) {}
13     gel(int a) : gel(a, 0) {}
14     gel() : gel(0, 0) {}
15     ~gel() {
16         cout << "~gel" << endl;
17     }
18     
19 };
20 
21 ostream& operator<<(ostream &os, const gel &it){
22     cout << it.x << " " << it.y;
23     return os;
24 }
25 
26 int main(void){
27     gel(1, 2);//创建一个gel临时变量
28 
29     auto g = gel(1, 2);//g离开其作用域时被销毁
30     cout << "===" << endl;
31 
32     gel gg(1, 2);//gg离开其作用域时被销毁
33     /*输出:
34         ~gel
35         ===
36         ~gel
37         ~gel
38     */
39     return 0;
40 }
View Code

 

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

动态内存与智能指针

动态内存和智能指针

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

动态内存与智能指针

动态内存与智能指针

12.1 动态内存与智能指针