34. 内存资源及管理
内存被分成五个区:栈、堆、静态存储区、常量区、代码区。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D6oQfAxQ-1628760080125)(file:///D:/写文章-CSDN博客_files/89263655e6e048caa4615ec16accb0eb.png)]
栈:存放函数的参数和局部变量,编译器自动分配和释放。栈属于计算机系统的数据结构,进栈出栈有相应的计算机指令支持,而且分配专门的寄存器存储栈的地址,效率高,内存空间是连续的,但栈的内存空间有限。
堆:需要程序员手动分配和释放(new,delete),属于动态分配方式。内存空间几乎没有限制,内存空间不连续,因此会产生内存碎片。操作系统有一个记录空间内存的链表,当收到内存申请时遍历链表,找到第一个空间大于申请空间的堆节点,将该节点分配给程序,并将该节点从链表中删除。一般,系统会在该内存空间的首地址处记录本次分配的内存大小,用于delete释放该内存空间。
全局/静态存储区:
全局变量,静态变量分配到该区,到程序结束时由系统自动释放,包括DATA段(全局初始化区)与BSS段(全局未初始化段Block Started by Symbol)。
未初始化的全局变量和静态变量存放在BSS段,在程序执行前BSS段自动清零。BSS节在程序的二进制映象文件中并不存在,即不占用磁盘空间而只在运行时占用内存空间,所以如果全局变量和静态变量未初始化那么其可执行文件要小很多。
常量区:
分为常变量和字符串常量,一经初始化,不可修改。静态存储内的常变量是全局变量,与局部常变量不同,区别在于局部常变量存放于栈,实际可间接通过指针或者引用进行修改,而全局常变量存放于静态常量区则不可以间接修改。字符串常量,一般都放在只读数据段中,不允许修改,字符串常量的名称即为它本身,属于常变量。
代码段(code segment/text segment):
通常是指用来存放程序执行代码的一块内存区域,存放程序体的二进制代码,比如写的函数。这部分区域的大小在程序运行前就已确定,并且内存区域通常属于只读,某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等,但一般都是放在只读数据段中。
34.1 new,delete与malloc,free
均属动态内存分配,1)malloc对开辟的空间大小严格指定,而new只需对象名;2)new为对象分配空间时,调用对象的构造函数,delete调用对象的析构函数。3).malloc/free是库函数而不是运算符,不能把执行构造和析构函数的功能强加于malloc/free.
对于非内部数据类型的对象而言,对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free 是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于 malloc/free。而new是运算符,在编译器控制范围之内,调用new时,从堆中申请内存并为内存调用构造函数.
先析构后再将内存归还系统,delete只调用一次析构函数,用于单个对象释放。而delete[]会调用每个成员的析构. new分配的需用delete释放,用new[]分配的内存用delete[]释放。
示例:
const int MAX_ARRAY_SIZE = 100;
int* numberArray = new int[MAX_ARRAY_SIZE];
… delete[] numberArray;
numberArray = NULL;
34.2 brk,mmap,munmap
操作系统角度,进程分配内存有两种方式,分别由两个系统调用完成:brk和mmap(不考虑共享内存)。malloc/free函数分配释放内存,底层由brk,mmap,munmap系统调用实现。
l brk是将数据段(.data)的最高地址指针_edata往高地址推
l mmap是在进程的虚拟地址空间中找一块空闲的虚拟内存(堆和栈中间,称为文件映射区域的位置)。
两种方式分配的都是虚拟内存,没有分配物理内存。当第一次访问已分配的虚拟地址空间时,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。
34.3 内存泄漏问题
指己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
1)类的构造函数和析构函数中new和delete没有配套
2)在释放对象数组时没有使用delete[],使用了delete
3)没有将基类的析构函数定义为虚函数,当基类指针指向子类对象时,如果基类的析构函数不是virtual,那子类的析构将不被调用,子类的资源没有正确释放,造成内存泄露
4)没有正确的清除嵌套的对象指针
5)指向对象的指针数组不等同于对象数组,数组中存放的是指向对象的指针,不仅要释放每个对象的空间,还要释放每个指针的空间,delete []p只释放了每个指针,但并没有释放对象的空间,正确的做法是通过一个循环,将每个对象释放,然后再把指针释放。
6)缺少拷贝构造函数
两次释放相同内存是一种错误做法,同时可能会造成堆的崩溃。按值传递会调用拷贝构造函数,引用传递不会。如果没定义拷贝构造,那编译器就会调用默认的拷贝构造函数,会逐个成员拷贝来复制数据成员,复制指针被定义为将一个变量的地址赋给另一个变量。这种隐式的指针复制使得两个对象拥有指向同一个动态分配的内存空间的指针。当释放第一个对象,析构函数就会释放与该对象有关的动态分配的内存空间。而释放第二个对象时,它的析构函数会释放相同内存,这样是错误的。所以,如果一个类里面有指针成员变量,要么显示的写拷贝构造和重载赋值运算符,要么禁用。
7)野指针,指向被释放的或者访问受限内存的指针,造成野指针的原因:
l 指针变量没有被初始化(如果值不定,可以初始化为NULL)
l 指针被free或delete后,没有置为NULL, free和delete只把指针所指向的内存给释放掉,并没有把指针本身干掉,此时指针指向的是“垃圾”内存,释放后的指针应该被置为NULL
l 指针操作超越了变量的作用范围,比如返回指向栈内存的指针就是野指针
………
检测内存泄漏的关键是检查malloc/new和free/delete是否匹配,一些工具也是这个原理。要做到这点,就是利用宏或者钩子,在用户程序与运行库之间加了一层,用于记录内存分配情况。Linux平台下的内存泄漏检测工具有mtrace,valgrind等。
34.4 栈溢出问题
1)函数调用层次过深,每调用一次,函数的参数、局部变量等信息就压一次栈
2)局部变量体积太大
代码中存在死循环或循环产生过多重复的对象实体
解决办法:
l 增加栈内存大小
l 使用堆内存,有多种方法可直接把数组定义改成指针,然后动态申请内存;也可把局部变量变成全局变量,一个偷懒办法是直接在定义前加个static,变成静态变量(实质是全局变量).
34.5 内存对齐原则
非空类的大小与类中非静态成员变量和虚函数表的多少有关,非静态成员变量的大小与编译器内存对齐的设置有关。成员变量在类中的内存存储并不一定连续,它是按照编译器的设置,按照内存块来存储,这个内存块大小的取值,就是内存对齐。
结构体内的成员按自身长度自对齐,所谓自对齐是指该成员的起始地址必须是它自身长度的整数倍。如int只能以0,4,8这类地址开始。结构体总大小为结构体的有效对齐值的整数倍(默认以结构体中最长的成员长度为有效值的整数倍,用#pragrma pack(n)指定时,以n和结构体中最长的成员长度中较小者为其值)。即sizeof值必须是其内部最大成员的整数倍,不足的要补齐。
示例参考:
class A{
char c;
int a;
char d;
}; // 输出:12
cout << sizeof(A) << endl;
class B{
char c;
char d;
int a;
};
cout << sizeof(B) << endl; // 输出:8
内存对齐原因:
l 平台原因(移植):不是所有的硬件平台都能访问任意地址上的任意数据的,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
l 性能原因:经过内存对齐后,CPU的内存访问速度可大大提升。
34.6 ptmalloc、tcmalloc和jemalloc
C++使用较多的内存分配器有ptmalloc、tcmalloc和jemalloc。分配器处于内核和用户程序之间,响应用户的分配请求,向操作系统申请内存,后将内存返回用户程序。为保证高效,一般都会预先分配一块大于用户请求的内存,然后管理这块内存。用户释放掉的内存也不是立即返回给操作系统,分配器会管理这些释放掉的空闲空间以应对用户以后的分配请求。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mKEDMy43-1628760080128)(file:///D:/写文章-CSDN博客_files/3d223c531f0c48a9a518384ffd0947c2.png)]
-
ptmalloc2即当前使用的glibc malloc版本,其内存块采用chunk管理,且将大小相似的chunk用链表管理,一个链表被称为一个bin。前64个bin里,相邻bin内的chunk大小相差8字节,称为small bin,后面的是large bin,large bin里的chunk按先大小,再最近使用的顺序排列,每次分配都找一个最小的能使用的chunk,结构如下所示。
chunk指针指向开始的地址,mem指针指向用户内存块开始的地址。A=1 为非主分区分配,A=0 为主分区分配;M=1为mmap映射区域分配,M=0为heap区域分配。p=0表示前一个chunk为空闲,prev_size才有效。p=1表示前一个chunk正在使用,prev_size无效,p主要用于内存块的合并操作。ptmalloc分配的第一个块总是将p设为1,以防止程序引用到不存在的区域。
注:
l 后分配的内存先释放,因ptmalloc收缩内存是从top chunk开始,如top chunk相邻chunk不能释放,top chunk以下的都无法释放,防止内存泄露
l ptmalloc不适用于管理长生命周期的内存,特别是持续不定期分配和释放长生命周期的内存,这将导致ptmalloc内存暴增
l 多线程分阶段执行的程序不适合用ptmalloc,这种程序的内存更适合用内存池管理
l 尽量减少程序的线程数和避免频繁分配/释放内存。频繁分配会导致锁的竞争,最终导致非主分配区增加,内存碎片增高,并且性能降低
l 防止程序分配过多内存,或由于Glibc内存暴增,导致系统内存耗尽
-
Tcmalloc是Google开源的一个内存管理库,已在chrome、safari等知名软件中运用,可以代替C和C++默认内存分配器,提供更高的扩展效率和更好的并行性支持。tcmalloc特别对多线程做了优化,对于小对象的分配基本上不存在锁竞争,而大对象使用了细粒度、高效的自旋锁。分配给线程的本地缓存,在长时间空闲情况下会被回收,供其他线程使用,这样提高了在多线程下的内存利用率,而这一点ptmalloc2是做不到的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rFYNStCW-1628760080130)(file:///D:/写文章-CSDN博客_files/aed76fb646874d63a82aaf2101ed4fa2.png)]
线程级cache和进程级cache实际就是一个多级的空闲块列表。一个Free List以大小为k bytes倍数的空闲块进行分配,包含n个链表,每个链表存放大小为nk bytes的空闲块。在tcmalloc中,<=32KB的对象被称作小对象,>32KB的是大对象。在小对象中,<=1024bytes的对象以8n bytes分配,1025<size<=32KB的对象以128n bytes大小分配,比如:要分配20bytes则返回的空闲块大小是24bytes的,这样在<=1024的情况下最多浪费7bytes,>1025则浪费127bytes。而大对象是以页大小4KB进行对齐的,最多会浪费4KB - 1 bytes。下图是一个基本的free list的示意图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MoNQdtru-1628760080132)(file:///D:/写文章-CSDN博客_files/16c8eaaefb0e4f028a31dfac718371ff.png)]
对于大内存分配(大于8个分页, 即32K),tcmalloc直接在中央堆里分配。中央堆的内存管理以分页为单位,同样按大小维护了256个空闲空间链表,前255个分别是1个分页、2个分页到255个分页的空闲空间,最后一个是更多分页的小的空间。这里的空间如果不够用,就直接从系统申请。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kNDMuCH3-1628760080133)(file:///D:/写文章-CSDN博客_files/d4cd22bf1f5c4fbf8c3b38b5aa50bd03.png)]
-
Jemalloc由facebook推出,最早的时候是freebsd的libc malloc实现。在firefox、facebook服务器各种组件中有大量使用。Jemalloc支持和tcmalloc类似的线程本地缓存,避免锁的竞争,需要2%的额外开销。Jemalloc 把内存分配分为了三个部分,第一部分类似tcmalloc,是分别以8字节、16字节、64字节等分隔开的small class;第二部分以分页为单位,等差间隔开的large class;然后就是huge class。内存块的管理也通过一种chunk进行,一个chunk的大小是2^k (默认4 MB)。通过这种分配实现常数时间地分配small和large对象,对数时间(红黑树)地查询huge对象的meta
在多线程环境使用tcmalloc和jemalloc效果非常明显。当线程数量固定,不会频繁创建退出的时候,可以使用jemalloc;反之使用tcmalloc可能是更好的选择。
- new,placement new
35.1 new 操作
new operator就是new操作符,假如A是一个类,那么A * a=new A;实际上执行如下3个过程。所能改变的只是第一步行为,如何为对象分配内存,可重载这个函数。
1)调用operator new分配内存,operator new (sizeof(A))
2)调用构造函数生成类对象,A::A(),初始化所分配的内存
3)返回相应指针,指向对象的内存
分配内存这一操作由operator new(size_t)来完成,如果类A重载了operator new,那么将调用A::operator new(size_t),否则调用全局::operator new(size_t),后者由C++默认提供。
示例:
string *pa = new string(“memory managerment”);
编译器生成的伪代码类似如下:
void* memory = operator new(sizeof(string));
call string::string(“memory managerment”) on memory;
string* Pa = static_cast<string*>(memory);
35.2 operator new
operator new的唯一的职责就是分配内存,对构造函数一无所知。相当于malloc,其实它内部实现也是调用了void * malloc(size_t size);函数
void* operator new(size_t sizt) {
return malloc(size);
}
分为三种形式(前2种不调用构造函数,这点区别于new operator):
void* operator new (std::size_t size) throw (std::bad_alloc);
void* operator new (std::size_t size, const std::nothrow_t& nothrow_constant) throw();
void* operator new (std::size_t size, void* ptr) throw();
第一种分配size个字节的存储空间,并将对象类型进行内存对齐。如果成功,返回一个非空的指针指向首地址。失败抛出bad_alloc异常。
第二种在分配失败时不抛出异常,它返回一个NULL指针。
第三种是placement new版本,它本质上是对operator new的重载,定义于#include 中。它不分配内存,调用合适的构造函数在ptr所指的地方构造一个对象,之后返回实参指针ptr。
第一、第二个版本可以被用户重载,定义自己的版本,第三种placement new不可重载。
A* a = new A; //调用第一种
A* a = new(std::nothrow) A; //调用第二种
new §A(); //调用第三种 ,new §A()调用placement new之后,还会在p上调用A::A()。
35.3 placement new
也叫定位new表达式,它用于在给定的内存中初始化对象。一般来说,使用new申请空间时,是从系统的“堆”中分配空间。但在某些特殊情况下,可能需要在已分配的特定内存创建对象,这就是所谓的“定位放置new”(placement new)操作。
如果有这样一个场景,大量申请一块类似的内存空间,然后又释放掉。比如对于客户端请求,每一次上行数据都需为此申请一块内存,当Server处理完请求给客户端下行回复时释放该内存,表面看符合C++内存管理要求,但要知道每一次申请,系统都要在内存中找到一块合适大小的连续空间,这个过程是很慢的(相对),极端情况下,如果系统中有大量的内存碎片且申请的空间很大,甚至可能失败。如此,可使用placement new在指定的内存空间中构造对象。
语法形式不同于普通new操作。例如,一般用如下语句A* p=new A;申请空间,而定位放置new操作使用A* p=new (ptr)A;申请空间,ptr就是指定的内存首地址。
1)用定位放置new操作,既可在栈上,也可在堆上生成对象
2)语句A* p=new (mem) A;定位生成对象,指针p和数组名mem指向同一片存储区。
3)使用语句A *p=new (mem) A;定位生成对象时,会自动调用类A的构造函数,由于对象的空间不会自动释放(对象实际上是借用别人的空间),所以必须显示调用类的析构函数,如本例中的p->~A()。
示例一:堆上生成对象
char* preBuf = new char[5 * sizeof(BT)];
cout << (void*)preBuf << endl;
BT *p = new(preBuf) BT; // placement new
cout << p << endl;
p->SetNumber(10);
cout << p->GetNumber() << endl;
p->~BT();
delete[]preBuf;
示例二:String 构建
string * str = new string(“hello”);等价于以下两句(先调用new操作符(operator new)申请内存,再调用placement new来初始化对象)
void* memory = :: operator new(sizeof(string));
String* str = new(memory) string(“hello”);
注:
如需把对象的内存分配和初始化操作分离,就需用到operator new 和placement new。在STL里面allocator就用到类似的做法,其中allocate负责向系统申请一块指定大小的内存(operator new),而construct负责在这块内存上根据模板类的类型T来调用T的构造函数初始化内存(placement new)。同理deallocat和destory负责释放内存和析构对象。
- 基于范围的循环
在使用基于范围的 for 循环处理数组时,该循环可自动为数组中的每个元素迭代一次。基于范围的 for 循环可以自动知道数组中元素的个数,所以不必使用计数器变量控制其迭代,也不必担心数组下标越界的问题。
for (dataType rangeVariable : array) {
statement;
}
修改数组
将范围变量声明为一个引用,引用变量是其他值的一个别名,任何对于引用变量的修改都将实际作用于别名所代表的值。
for (int &val : numbers){
cout << "Enter an integer value: ";
cin >> val;
}
要使用范围for语句处理多维数组,除了最内层循环外,其他所有循环的控制变量都应该是引用类型。这种循环主要用于各种模板容器类。
如果出于某些目的需要使用元素下标时,基于范围的 for 循环就不能使用了。另外,如果循环控制变量被用于访问两个或两个以上不同数组的元素,那么它也不适合使用。
- 插入迭代器之inserter
inserter(container,iterator)接受两个参数,第一个为要插入的容器,第二个为指向该容器的一个迭代器,返回一个迭代器,返回的迭代器所指的位置与第二个参数所指的位置一致,对返回的迭代器赋值,即是向容器插入一个值,插入的位置与容器的insert函数一致(即第二个参数所指元素的前一位置)。
对返回的迭代器赋值的形式
list ilist={1,2,3};
auto iter = inserter(ilist,ilist.end());
*iter = 4;
iter = 5;
for (const auto &item : ilist)
cout << item << " ";
// 1 2 3 4 5
常用第一种形式的赋值语句,其实两个赋值语句是等价的。对于插入迭代器 iter来说 ++iter,iter++,*iter都是等价于iter;
list ilist={3,4,5};
auto iter =inserter(ilist,ilist.begin());
*iter=1;
*iter=2;
for (const auto &item : ilist)
cout << item << " ";
//输出是 1 2 3 4 5还是2 1 3 4 5,这里的输出是1 2 3 4 5
对 *iter = 1的操作等价于:
iter=ilist.insert(iter,1);
++iter;
所以,开始的iter所指位置的元素是3(也就是ilist.begin()所指位置),经过*iter=1以后,iter所指位置的元素还是3,(有++iter的操作),所以结果是1 2 3 4 5而不是2 1 3 4 5。
- STL标准模板库
STL包括容器和算法。容器即存放数据的地方(如array, vector),分为序列式容器和关联式容器,序列式容器中的元素不一定有序,但都可被排序,如vector,list,queue,stack,heap, priority-queue。 关联式容器内部结构是一个平衡二叉树,每个元素都有一个键值和一个实值(如map,set,hashtable,hash_set)。算法有排序、复制等,以及各个容器特定算法。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X1fkopT9-1628760080133)(file:///D:/写文章-CSDN博客_files/51a63019b1cd4abb8487cad94fc9865a.png)]
每种容器类型都定义了自己的迭代器类型,定义了命名为begin和end的函数,用于返回迭代器。迭代器是容器的精髓,它提供了一种方法使得它能够按照顺序访问某个容器所含的各个元素,但无需暴露该容器的内部结构,它将容器和算法分开,让二者独立设计。
38.1 vector
vector与数组类似,拥有一段连续的内存空间。可以快速的访问随机的元素,快速的在末尾插入元素。因为内存空间是连续的,所以在进行插入和删除操作时,会造成内存块的拷贝,时间复杂度为o(n)。在序列中间随机的插入、删除元素慢。
vector能很好的支持随机存取,因此vector::iterator支持“+”,“+=”,“<”等操作符。vector::iterator和list::iterator都重载了“++”运算符。
- operator[]和at
由于at会做边界检查,如越界会抛出异常,应用可以try catch这个异常,应用还能继续运行,使用at时应使用try catch包裹住。
使用operator[]时一定要先检查一下是否越界。
- vector扩容
vector是一个动态增长的数组,里面有一个指针指向一片连续的空间。调用push_back方法,当空间装不下的时候,一般会以原大小的两倍申请一片更大的空间,将原来的数据拷贝过去,并释放原来的旧空间。当删除的时候空间并不会被释放,只是清空了里面的数据。而array是静态空间,一旦配置就不能改变大小。vector、map等集合的clear并不释放内存,如果需要释放,可以和一个空的集合进行swap。
vector可以使用reserve预留空间避免反复增长/缩减大小重新分配内存以提高性能,size返回当前vector大小,capacity返回vector在分配新的存储空前之前能存储的元素总数
示例:容器的大小确定方法:
vector v; // 未初始化, size()和capacity()为0
cout<<v.size()<<endl<<v.capacity()<<endl;
38.2 list
双向链表实现,因此内存空间可以是不连续。只能通过指针访问数据,list的随机存取非常没有效率,时间复杂度为o(n), 访问随机元素没有vector快; 但由于链表的特点,能高效地进行插入和删除, 随机地插入元素要比vector快。对每个元素分配空间,不存在空间不够,重新分配的情况。
list的内存空间可以是不连续,不支持随机访问,因此list::iterator不支持“+”、“+=”、“<”等。
38.3 deque
deque(double-ended queue)双端队列容器是由一段一段的定量连续空间构成,小片间用链表相连,可以向两端发展,因此不论在尾部或头部安插元素都十分迅速(常数时间)。在中间部分或随机的插入、删除元素比较费时,因需移动其它元素。deque 容器中存储元素并不能保证所有元素都存储到连续的内存空间中。
deque支持push_front、pop_front、push_back、pop_back
deque没有 capacity() 函数,而 vector 有
deque有 push_front() 和 pop_front()函数,而vector没有
deque没有data()函数,而vector有
deque相对于vector功能更强大,而性能相对较差。
- 内存模型
deque模板中需要map,start,finish三个数据来管理整个内存空间。其一,map是指针数组,里面成员是分配空间Node的地址,明白如何动态分配二维数组,那么这个map就很容易理解;其二,迭代器,迭代器里面含有4个成员,连续空间开始地址(first),结束地址(last),空间中当前元素的地址(cur)及连续空间地址在map中的位置(node).
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Mq6h8fiZ-1628760080134)(file:///D:/写文章-CSDN博客_files/432366f112b54fcfbe8189643893646b.png)]
- 创建deque容器
l 创建一个空 deque 容器: std::deque d;
l 创建一个具有10个元素(默认0)的deque容器
std::deque d(10);
l 创建一个包含10个元素(值为 5)的deque容器
std::deque d(10, 5) //
l 拷贝创建一个新的 deque 容器:
std::deque d1(5);std::deque d2(d1);
l 拷贝其他类型容器中指定区域内的元素,创建一个新容器
//拷贝普通数组,创建deque容器
int a[] = { 1,2,3,4,5 };
std::dequed(a, a + 5);
//适用于所有类型的容器
std::array<int, 5>arr{ 11,12,13,14,15 };
std::dequed(arr.begin()+2, arr.end());//拷贝arr容器中的{13,14,15}
- deque的排序
#include
sort(deq.begin(), deq.end()); // 采用从小到大的排序
// 如果从大到小排序,可采用先排序后反转方式
// reverse(deq.begin(), deq.end());
// 也可自定义从大到小的比较器来改变排序方式
bool Comp(const int& a, const int& b) {
return a > b;
}
sort(deq.begin(), deq.end(), Comp);
38.4 queue
queue的声明queue<int,deque > s,是受限的队列,存储空间由第二个参数的容器确定,第一个参数在没有第二个参数的情况下,决定存储空间类型,第二个参数存在的情况下,第一个类型参数无效,默认deque。因此,queue s1这样声明的队列存储空间就是默认的。另外,queue第二个参数可以选择的容器类型也包括list,其余的如果声明,操作受限。对于任何需要用 FIFO 准则处理的序列来说,使用 queue 容器适配器都是好的选择。
queue 容器及其一些基本操作
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HKcZJyF6-1628760080134)(file:///D:/写文章-CSDN博客_files/80d44eb39bd441bab211f533b5f1c6bc.png)]
Container需满足顺序容器的条件,且必须支持front、back、push_back、pop_front、empty() 和 size()操作,标准容器中有deque和list满足。也就是说标准容器中deque和list可以封装成queue。
std::queue<std::string, std::liststd::string>words;
38.5 priority_queue
优先队列具有队列的所有特性,包括基本操作,只是在这基础上添加了内部的一个排序,它本质是一个堆实现。与queue不同在于可以自定义其中数据的优先级, 让优先级高的排在队列前,优先出队。
priority_queue<Type, Container, Functional>
Container必须用数组实现的容器,如vector,deque等,不能用list,STL里默认用vector, 而dequeue功能虽更强大,但性能相对于vector较差,考虑到包装在优先队列后,dequeue功能并不能很好发挥,所以一般选择vector容器。Functional是比较的方式(默认是less),当需用自定义的数据类型时才需要传入这三个参数,使用基本数据类型时,只需要传入数据类型,默认是大顶堆。
示例:
priority_queue a; //大顶堆,等同priority_queue<int, vector, less > a;
priority_queue<int, vector, greater > c; //小顶堆
priority_queue b;
自定义类型
#include
#include
using namespace std;
//方法1
struct tmp1 { //运算符重载 <
int x;
tmp1(int a) {x = a;}
bool operator<(const tmp1& a) const {
return x < a.x; //大顶堆
}
};
//方法2
struct tmp2 {
bool operator() (tmp1 a, tmp1 b) {
return a.x < b.x; //大顶堆
}
};
int main() {
tmp1 a(1);
tmp1 b(2);
tmp1 c(3);
priority_queue<tmp1> d;
d.push(b);
d.push(c);
d.push(a);
while (!d.empty()) {
cout << d.top().x << '\n';
d.pop();
}
cout << endl;
priority_queue<tmp1, vector<tmp1>, tmp2> f;
f.push(c);
f.push(b);
f.push(a);
while (!f.empty()) {
cout << f.top().x << '\n';
f.pop();
}
}
38.6 stack
stack容器适配器中的数据是以LIFO方式组织,是先进后出栈。只能访问stack顶部的元素,在移除顶部元素后,才能访问下方的元素。stack的默认存储空间也是deque,其他的声明和queue差不多,可以使用的容器类型包括deque、vector、list。
stack<typename T, typename Container=deque>
std::stackstd::string,std::liststd::string fruit;
创建堆栈时,不能在初始化列表中用对象来初始化,但可用另一个容器来初始化,只要堆栈的底层容器类型和这个容器的类型相同。例如:
std::list values {1.414, 3.14159265, 2.71828};
std::stack<double,std::list> my_stack (values);
stack 模板定义了拷贝构造函数,因而可复制现有的stack容器:
std::stack<double,std::list>copy_stack {my_stack}
stack容器应用
譬如:编辑器中的 undo机制就是用堆栈来记录连续的变化。撤销操作可以取消最后一个操作,这也是发生在堆栈顶部的操作。编译器使用堆栈来解析算术表达式,当然也可以用堆栈来记录 C++ 代码的函数调用。
…. …
38.7 map,set
提供键值对的数据管理,内部实现了一个红黑树,元素有序,红黑树的每一个节点都代表着map的一个元素。因此,对于map进行的查找,删除,添加等一系列的操作都相当于是对红黑树进行的操作(O(logN)时间复杂度,效率高)。map中的元素是按二叉搜索树存储,使用中序遍历可将键值按照从小到大遍历出来。但空间占用率高。
- unordered_map
和map类似,存储key-value对,支持通过key快速索引到value,不同的是unordered_map不根据key进行排序,内部无序。底层是一个防冗余的哈希表,存储时根据key的hash值判断元素是否相同,查找的时间复杂度可达到O(1)。对于查找问题,unordered_map会更加高效一些,遇到查找问题常会考虑一下用unordered_map。哈希表的建立会相对比较耗费时间。 - map和set
map为一对一映射,key不重复。set内部元素唯一,遍历时已排序,查找也较快。map和set的底层实现主要通过红黑树来实现,红黑树是一种特殊的二叉查找树(二叉排序树和非严格意义上的二叉平衡树)。
1)每个节点或是黑色,或是红色,根节点是黑色;
2)每个叶子节点(NIL)是黑色。 [注:这里叶子节点是指为空(NIL或NULL)的叶子节点]
3)如果一个节点是红色的,则它的子节点必须是黑色的;
4)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
特性3)4)决定了没有一条路径会比其他路径长出2倍,因此红黑树是接近平衡的二叉树。
38.8 遍历删除
在遍历vector,list,map时进行删除,调用erase方法删除元素,会返回下一个元素的迭代器。
for(vector::iterator it=d.begin();it!=d.end()😉 {
if(*it==3) {
it=d.erase(it);
} else {
it++;
}
}
38.9 对比与选择
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QT5X9luN-1628760080135)(file:///D:/写文章-CSDN博客_files/cdcbc20a5d08491898238b69ed765b07.png)]
容器的选择原则:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PyDc5Dam-1628760080135)(file:///D:/写文章-CSDN博客_files/e82d26f966bf495cb035dac0cca67542.png)]
- 异常、表达式
启用异常,代码上就会有膨胀(约5-15%);
异常路径上的性能损失较大,比错误码大得多(异常发生率应当小于 1%);
异常可能导致错误发生点和错误处理变得不明显;
需避免异常的问题: 如硬实时系统(时间开销)或某些嵌入式系统(空间开销)
39.1 不使用异常的后果
l 不能在构造函数中表示出错
规避方法:使用可永远成功的默认构造,然后再调用返回出错码的init方法
l 不能重载可能出错的运算符
规避方法:换用返回出错码的函数,用出参来返回对象
l 不能使用标准库(接口大部分按使用异常的方式设计的)
规避方法 1:假装内存分配永远成功;一旦失败就崩溃呗
在有虚拟内存的计算机上内存分配操作几乎永不失败
规避方法 2:不使用标准库,换用自己的定制库
39.2 异常安全性保证
l 不抛异常保证:操作一定成功
通常标为 noexcept,尤其是移动构造函数和 swap 函数
l 强异常安全保证:要么成功,要么都不变
关键操作通常提供这级保证,如标准库的 vector::push_bac
l 基本异常安全保证:保证数据处于正常状态,没有资源泄漏
标准库至少提供这一级的异常安全保证,如map里一次插入多项数据
l 无异常安全保证:啥都不保证
异常不安全的代码……
39.3 声明为noexcept
声明为noexcept的函数表示不会抛出异常或抛出的异常不会被截获并处理。默认构造函数、析构函数、swap 函数,move操作符都不应该抛出异常。
-
如果函数不会抛出异常,声明为noexcept可让编译器大程度的优化函数,如减少执行路径,提高错误退出 的效率。建议系统关键路径可以声明为 noexcept
extern “C” double sqrt(double) noexcept; // 永远不会抛出异
-
vector等STL容器,为保证接口健壮性,如果保存元素的move运算符未声明为noexcept,则在容器扩张搬移元素时不会使用move机制,而使用copy机制,带来性能损失风险
// std::vector 的 move 操作需要声明 noexcept
class Foo1 {
public:
Foo1(Foo1&& other); // no noexcept ? move constructor | copy constructor
};
std::vector a1;
a1.push_back(Foo1());
a1.push_back(Foo1()); // 触发容器扩张,搬移已有元素时调用copy constructor
39.4 throw 表达式参考
a) class exceptionType { };
throw exceptionType();
b) enum mathErr { overflow, underflow, zeroDivide };
throw zeroDivide;
- 模板,全特化,偏特化
模板使能快速建立具有类型安全的类库集合和函数集合,实现类型不同但行为相同的代码复用,它的实现方便了大规模的软件开发。
- 可用来创建动态增长和减小的数据结构
2)类型无关,因此具有很高的可复用性
3)在编译时而不是运行时检查数据类型,保证了类型安全
4)平台无关的,具有可移植性
5)可用于基本数据类型