C++ Primer 5th 第12章 动态内存 您所在的位置:网站首页 动态数组有什么优点 C++ Primer 5th 第12章 动态内存

C++ Primer 5th 第12章 动态内存

2024-07-18 05:52| 来源: 网络整理| 查看: 265

到本章为止,我们还没有接触过动态内存,接下来我们会学习动态内存的知识。

关于动态内存,第一个疑问是:为什么我们要使用动态内存?它有什么优点?

要了解动态内存,得先了解一个进程在内存中的基本空间结构。进程是被执行起来的程序,程序是未执行的二进制文件。

大概来说,程序包含了三大部分内容:一是程序代码,称为text段,二是程序中定义的初始化了的全局和静态变量(非0初始值),称为data段,三是未初始化的全局和静态变量(或者初始值为0),称为bss段。

进程是执行起来的程序,自然包含上面的三部分内容,另外还会额外增加进两部分内容。一个是堆,另一个是栈。栈用来存储我们在程序中用到的局部变量,而堆则是用于本章所学习的动态内存存储。

栈的效率高,速度快,局部访问,自动管理,空间大小有限。

堆是一个内存池,它也被称为自由存储,堆的空间可以很大,也能全局访问共享,手动管理。

有了这些基本知识,再回到开头的第一个问题:为什么使用动态内存及其优点。因为动态内存是存在堆中的,所以其优点是空间大小可以很大。除了优点之外,更有几个不使用动态内存则无法解决的问题,这也是为什么要使用动态内存的原因:

1.我们不知道程序到底会需要多少空间或者说多少个对象。比如说,程序经常需要处理一批数据,有多少个数据不确定,为了存储这些数据,我们只能根据当时的情况来申请内存空间来进行存储和处理。

2.为了实现面向对象的动态绑定功能。这点将在后面的章节进行学习。

3.程序的多个对象之间进行数据的共享,或者说全局访问共享。典型的例子是接下来即将学习的智能指针。

简而言之,动态内存的关键点在于“动”字上。

 

加上动态内存,在C++总共就有了3种类型的内存:静态内存、动态内存、栈内存。静态内存存储全局变量、静态局部变量、类静态数据成员;栈内存用于存储局部变量、动态内存用于存储手动分配的对象。

 

对于手动通过堆分配的内存来说,我们必须手动管理内存的释放,这是相当有难度的,尤其在C++中那些可能出现异常的地方。由于正确的管理内存很棘手,因此标准库提供了两个“智能指针”来帮助我们动态使用内存。一种是“共享指针”,它允许多个指针指向一个对象,而在没有指针指向对象时自动释放内存;另一种是“独占指针”,某一时刻,它只允许一个指针指向对象。标准库还有一个tiny工具weak_ptr,是一个辅助工具,用于指向“共享指针”指向的对象,但不会引起“共享指针”计数增加。

 

头文件

#include

提供工具

shared_ptr; unique_ptr; weak_ptr;make_shared;

 

shared_ptr和vector一样,是一个模板,在提供了足够信息后,用于生成特定类型的智能指针。用法如下:

shared_ptr *pi; //int类型指针 shared_ptr *ps; //string类型指针 shared_ptr *pv; //vector容器指针

 以上3个指针都是默认初始化的,在智能指针中,默认初始化的指针将会被初始化为nullptr。

智能指针的使用方式与普通指针相同,解引用智能指针返回所指对象,在条件判断中使用智能指针就是检测其是否为空。例如:

if (pv && pv->empty()) { pv.push_back(5); }

 

最安全的分配和使用动态内存的方法是使用make_shared标准库函数,该函数能够代替我们去动态分配内存,并且将对象进行初始化,之后返回一个智能指针给我们使用。同样的,make_shared也是一个模板,虽然他是一个函数模板,但是却不能根据我们传入的参数进行类型推断。原因是该函数的模板参数存在于返回类型之中,导致无法直接推断,因此我们必须手动指定参数类型。其使用方式如下:

shared_ptr ps = make_shared(); auto ps2 = make_shared(10, 'c');

对于智能指针对象ps和ps2的初始化,我们传给make_shared的参数将会用于初始化ps和ps2所指内存中对象的初始化。如果我们没有传递参数给make_shared,那么ps和ps2依然会被初始化,但是是值初始化,也即ps和ps2指向了各自的内存(内存中的对象没有初始化),而不是默认初始化的nullptr;总而言之,make_shared一定会返回一个指向一块内存的智能指针,而不会返回nullptr;

另外,由于make_shared不会返回nullptr,所以我们总是可以用auto关键词来让编译器进行推断,进而得出我们想要的特定类型的智能指针。

 

我们可以认为每个shared_ptr对象都有一个关联的计数器,通常称为引用计数。当进行一些操作时,计数会根据实际的情况进行增加或减少。例如,拷贝一个智能指针,此时会增加指向对象的指针个数;一个智能指针超出作用域,此时,会减少指向对象的指针个数。当一个计数变为0时,该智能指针会自动释放所指的动态内存。减少计数和销毁动态内存是通过智能指针的析构函数来完成的。

 

动态对象的生存周期同引用类型对象相同,当引用对象超出作用域时,引用对象本身会销毁,但引用对象所引用(或者说所指向)的对象不会销毁。同样的,当指向动态对象的对象(或者说指针)超出作用域之后,指向动态对象的对象销毁和动态对象之间并无关系。实例如下:

int i = 5; { //新作用域 int &ri = i; } //引用对象ri超出作用域 {    int *p = new int{};}

上面代码中引用对象ri超出作用域之后,引用对象ri所引用的对象i并不会销毁。同样的,无名动态对象被指针p所指,当p超出作用域被销毁后,无名对象并不会被销毁。

 

直接管理内存

 在C++中也支持直接手动分配和释放内存,使用new来分配内存,使用delete来释放内存,其中new和delete都是类似于"+"、"/"一样的运算符。

使用new运算符得到是相应类型对象的指针,并且该对象是匿名的,只能使用该对象的指针进行间接访问。当然,我们也可以使用一个引用来绑定到这个动态对象上,从而通过引用来访问对象。

 使用new运算符分配的对象,默认情况下是默认初始化的。对于内置类型,这意味着对象的值是不确定的;对于自定义类型,意味着使用默认构造函数。如果我们想要对动态获得的对象进行初始化,可以使用直接初始化或者列表初始化的方式,当然,也可以使用值初始化的方式。

对于自定义类类型,使用默认初始化,还是值初始化结果都是调用默认构造函数,通常情况下是没有区别的,除非是非常简单的没有定义任何构造函数的类类型会有差异。

对于内置类型,使用默认初始化和值初始化则不相同,默认初始化的值是未定义的,值初始化的值则为0。

 我们还可以使用auto配合new来进行类型推断并初始化,语法如下:

auto p1 = new auto(obj); auto p2 = new auto{obj}; //错误,不允许使用花括号

 该语句的作用是使用obj来推断动态对象的类型,并使用obj来初始化该动态对象,new返回的指针存储于p1中。

动态对象也可以是const类型的,和其他const对象一样,const对象必须初始化。例如:

const int *p1 = new const int(5); const int *p2 = new int(5);

对于p1来说,p1是一个指向const对象的指针。

对于p2来说,p2也是一个指向const对象的指针,然后实际对象并不是const对象。

 

new分配内存也存在失败的情况。需要知道的是,new即使分配内存失败,也不会导致内存泄露。new内部自身会进行一些处理以防止内存泄露。默认情况下,new在妥善处理后,会抛出一个异常来告知内存分配出现问题。当然,如果我们的程序不接受异常或者没有异常处理,则可以在new后使用"(nothrow)"来告知new不要抛出异常,此时,new返回nullptr指针。如果要使用这些特性,需要包含头文件“new”。

动态分配内存使得我们能够手动的申请内存,相应的,我们需要手动的释放申请的内存。方法是使用delete运算符,例如

delete p;

该运算符和一个运算对象组成一个表达式,此表达式的结果是:销毁p指向的对象,并且释放该对象所占用的内存。

该运算符只能对动态申请的内存进行释放,并且只能释放一次。对非动态内存进行释放或者对某一内存多次释放,都是未定义的行为。

另外,由于动态对象的生存周期是我们手动管理的,因此可以在动态对象的生存期间在整个程序中进行共享。

 

如前所述,使用智能指针shared_ptr进行内存管理时,我们如果没有初始化shared_ptr对象,那么它指向一个空指针,除非我们使用make_shared来生成智能指针。

除了使用make_shared来生成有效的智能指针,我们也可以使用new生成的指针来初始化shared_ptr对象从而得到一个有效的智能指针。方法是在定义智能指针对象时,使用new返回的指针来直接初始化。如下:

shared_ptr pi1(new int); shared_ptr pi2 = new int; //错误,shared_ptr的构造函数是explicit的

由于shared_ptr的构造函数是explicit的,因此我们不能使用拷贝构造函数来进行初始化,必须使用直接初始化,即直接匹配构造函数的方式来进行初始化。

还有一个问题是:当我们默认初始化了一个智能指针,该智能指针是指向nullptr的,随后我们自己使用new手动分配了动态内存,如何让默认初始化的智能指针指向我们分配的动态内存?我们可以使用智能指针的reset方法。如下:

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

如果p是唯一一个指向动态对象的智能指针,则reset会释放该动态对象,如果向reset传递了普通的非智能指针q,那么p会新指向q所指的内存,如果还向reset传递了d,那么会使用d来释放q,而不是默认的delete。

 

unique_ptr

一个unique_ptr拥有它所指向的对象,和shared_ptr不同,不同的unique_ptr之间不能“共享”指针指向的内存,当一个unique_ptr被销毁时,unique_ptr所指向的对象也会一并销毁。unique_ptr对象只能通过实例化unique_ptr类得到,没有类似make_shared的方法。

unique_ptr可以默认初始化指向一个空指针,或者使用new返回的指针直接初始化,除此之外,没有其他的初始化方式了。unique_ptr没有拷贝构造和赋值运算的功能。

虽然不能拷贝或者赋值unique_ptr,但是可以通过unique_ptr的release或者reset方法来转移unique_ptr中指针的所有权。例如:

unique_ptr p2(p1.release()); //p1所指对象所有权转移到p2,p1置空 p4.reset(p3.release());      //p3所指对象所有权转移到p4,p4原对象释放

 

weak_ptr

weak_ptr是一种不控制所指对象生存周期的智能指针,也即当weak_ptr析构时,它不会销毁所指对象及释放所指对象的内存。

 

 动态数组

在C++中,new和delete支持一次性分配或释放多个对象的动态内存。对比C语言,可以发现C语言是没有动态分配数组这一说的,C语言是典型的面向过程的编程语言。在动态分配时,C语言解决问题的思路过程是:我们需要申请多少个字节的内存;而C++则是我们需要申请构造一个动态对象还是一组动态对象,并没有C语言中直接意义上的多少字节内存。

C++中动态数组的分配方式是:在欲申请的对象类型后"[ ]"中来指明需要对象的个数;在欲释放的动态数组的指针前面使用"[ ]"来告知delete释放整个数组。例如:

int *p = new int[10]; //申请动态数组 delete [] p;    //释放动态数组

在动态分配数组时,和静态数组不同,动态数组的元素数量可以是个变量,但是该变量的类型必须是整形。 

除了直接使用"[ ]"来分配数组,我们也可以类型别名来用于new表达式分配动态数组,例如:

using int_Arr1 = int [10]; typedef int int_Arr2[15]; auto p1 = new int_Arr1; //p1指向含有10个元素的动态数组 auto p2 = new int_Arr2; //p2指向含有15个元素的动态数组

对于动态分配得到的数组,new返回的是一个单纯的指针类型,而不是数组类型,回忆第三章关于数组的内容,我们知道数组也是一种类型,是一种复合的复杂类型,其类型包括其中元素类型和元素个数。由于对于数组类型来说其类型由元素的个数和类型决定,因此我们可以使用sizeof对数组进行大小计算,也可以使用新的range-for形式for循环来遍历数组,当然也可以使用标准库的begin和end函数来取得其迭代器。但是new [ ]分配得到的数组却不是一个数组类型,而是一个指针类型,因此该动态数组不能像静态数组那样进行迭代器获取行为。

动态数组中的元素默认也是默认初始化的,我们可以进行值初始化,如下:

int *p1 = new int[10](); int *p2 = new int[10] {}; //C++11

我们也可以使用C++11的列表初始化,如下:

int *p = new int[10] {1, 2, 3, 4, 5}; //前5个元素使用给定值初始化,其余元素值初始化

如果我们初始化动态数组时,提供的元素个数多于数组最大容纳个数,则new分配失败,并抛出一个异常。

关于动态数组分配的最后一个问题是:如果动态分配的数组元素个数是0,那么new的行为是什么?答案是new正常返回一个非空指针,但不能对该指针解引用,该指针是一个尾后指针。

 

释放动态数组的语法前面已经做出说明,现在考虑一下动态数组元素销毁的次序。对于动态数组释放,元素销毁的次序是逆序的,即先销毁最后一个元素,然后是倒数第二个,以此类推。

我们在释放动态数组时,必须在指针名前使用"[ ]"以告知编译器销毁的是一个动态数组,而不是单一对象,如果忘记使用这个方括号,那么行为是未定义的,可能造成内存泄露,或者程序崩溃。

令我们惊讶的是,标准库还提供了一个用于管理动态数组的智能指针,该智能指针是unique_ptr。为了使用unique_ptr,我们需要提供一个类型参数供模板实例化一个具体类。我们提供的用于管理动态数组的类型参数是int [ ],如下:

unique_ptr up(new int[10]);up.release()    //自动使用delete []来释放数组

和普通的unique_ptr不同,指向动态数组版本的unique_ptr不支持点和箭头运算符,因为这些解引用操作无意义。另一方面,我们的unique_ptr是指向动态数组的,所以我们可以使用下标索引("[ ]")来访问数组中的元素。

因为unique_ptr同一时刻只能有一个对象拥有动态数组,如果我们需要在多个对象间共享需要怎么做?我们可以使用sharded_ptr,但是我们必须提供一个删除工具,来替代默认delete操作。如下:

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

上面我们提供了一个lambda工具给shared_ptr,使得shared_ptr能够在没有对象使用动态数组时,使用该lambda正确释放动态数组。

由于shared_ptr不支持动态数组管理,因此也就没有提供下标运算符,所以如果我们需要访问动态数组,只能使用shared_ptr中提供的get方法,通过get得到指针之后再继续我们的访问。

 

allocator类

对程序效率要求极高时,new的行为可能会造成一些局限。new运算符在分配动态内存时,无论如何都会对动态对象进行初始化,也即内存的申请和对象的初始化是组合在一起的。如果某些时候需要申请很大空间的动态数组,那么数组中元素的初始化可能就不那么有意义或者说有些开销浪费,因为我们需要在使用内存的时候自己去构造对象。而且对于没有默认构造函数的类来说,我们还必须列表初始化所有的元素。

标准库allocator类提供了一些方法,能使得动态内存的分配和对象初始化分离开来,它存在于头文件中,如下:

#include

allocator类分配的内存是原始未构造的。

类似vector,allocator也是一个模板,需要我们提供类型参数以便allocator类来分配相应类型的内存。allocator类会根据类型参数自动的确定内存的对齐方式。

allocator类的使用示例如下:

allocator ai; //实例化allocator类对象 int *p = ai.allocate(5); //使用类对象的方法来申请动态内存 int *q = p; ai.construct(p, 6); //对申请内存中第一个元素进行构造 cout front(); } const string& StrBlob::back() const { check(0, "back on empty StrBlob"); return data->front(); }

 

练习12.3:StrBlob 需要const 版本的push_back 和 pop_back吗?如果需要,添加进去。否则,解释为什么不需要。

不需要,因为push_back和poo_back是写操作,不能使用const。

 

练习12.4:在我们的 check 函数中,没有检查 i 是否大于0。为什么可以忽略这个检查?

因为size_type是无符号类型,一定大于等于0。

 

练习12.5:我们未编写接受一个 initializer_list explicit 参数的构造函数。讨论这个设计策略的优点和缺点。

 

练习12.6:编写函数,返回一个动态分配的 int 的vector。将此vector 传递给另一个函数,这个函数读取标准输入,将读入的值保存在 vector 元素中。再将vector传递给另一个函数,打印读入的值。记得在恰当的时刻delete vector。

#include #include std::vector* get_vector() { auto p = new std::vector; return p; } void use_vector(std::istream &in, std::vector *p) { int i; while (in >> i) { p->push_back(i); } } void print_vector(std::vector *p) { for (auto b = p->begin(), e = p->end(); b != e; ++b) { std::cout push_back(i); } } void print_vector(std::shared_ptr p) { for (auto b = p->begin(), e = p->end(); b != e; ++b) { std::cout = ret->size()) { throw std::out_of_range(msg); } return ret; } std::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; } class StrBlob { friend class StrBlobPtr; public: typedef std::vector::size_type size_type; StrBlob(); StrBlob(std::initializer_list il); StrBlobPtr begin(); StrBlobPtr end(); size_type size() const { return data->size(); } bool empty() const { return data->empty(); } void push_back(const std::string &t) { data->push_back(t); } void pop_back(); std::string& front(); std::string& back(); private: std::shared_ptr data; void check(size_type i, const std::string &msg) const; }; StrBlobPtr::StrBlobPtr(StrBlob &a, size_t sz = 0): wptr(a.data), curr(sz) { } StrBlobPtr StrBlob::begin() { return StrBlobPtr(*this); } StrBlobPtr StrBlob::end() { auto ret = StrBlobPtr(*this, data->size()); return ret; } int main() { std::cout > s; if (s.size() < 64) { for (int i = 0; i < 64; ++i) { p[i] = s[i]; } } delete [] p; return 0; }

 

练习12.25:给定下面的new表达式,你应该如何释放pa?

int *pa = new int[10]; delete [] pa;

 



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

      专题文章
        CopyRight 2018-2019 实验室设备网 版权所有