C++学习笔记

C++学习笔记

(前言:看侯捷视频做的一些笔记。主要分两部分,第一部分为C++ STL相关知识;第二部分为内存管理)

STL

OOP vs GP

OOP(Object-Oriented programming)将数据和方法结合(封装成类)

GP(Generic Progarmming)将数据和方法分离

STL用GP的思想实现,将数据和方法分开分别在容器头文件(vector、list等)和algorithm中实现;容器和算法之间用迭代器沟通

某些容器不支持某些特定的算法,比如list不能用algorithm里的sort,因为list内存中不是紧密排列,无法提供RandomAccessIterator

操作符重载and模板

操作符重载没什么好说的,就是对自定义类型要重载一下 < > =

模板分为:类模板、函数模板、成员模板(不讲)

使用类模板后在定义对象时,需要用<>指定类型;而函数模板不需要,编译器会自动进行实参推导(argument deduction)

模板的核心是泛化(接受所有类型),但如果想处理某些特殊类型,也有特化(Specialization)和偏特化的写法

//	泛化
template<class T> class type1{}
//	特化,如果传入类型为int,作特殊处理
template<> class type1<int>{}
//	偏特化,对模板中部分传入类型作特殊处理
template<class T> class type2<bool, T>{}
template<class T> class type3<T*>{}

在STL中常用于泛型算法针对某些容器作优化,例如上节提到的某些不支持特定算法的容器

分配器

用malloc()分配内存会产生额外开销(存放一些信息,如内存大小,详见内存管理)

allocators调用new(),而new()调用malloc(),会产生很多额外开销

VC6中的allocators第二个参数为const void*,传入格式为(int*)0(数值无所谓,主要是指针类型

GNX2.9有自己的alloc(新版本改名为poolalloc加入ext,但已弃用,不知道为什么),用链表,每过一个节点空间加8,省去了一部分malloc(),但仍然需要用malloc()

并不建议直接用alloc申请内存,因为需要记住所申请的内存大小,让容器自己调用就好

容器之间关系

STL中少用继承,而是复合(composition),衍生容器会用底层容器去实现,如set和map底层用红黑树(rb_tree),stack和queue用deque实现

list

list实际上是环状双向链表,end()指向的节点的next会指向begin(),但不放东西(为了满足STL前闭后开)

大部分容器的迭代器(iterator)都是class,需要重载运算符以支持指针操作

在重载后置++时,尽管调用了*this,但并没有用重载过的operator * ,而是调用_list_iterator的拷贝构造函数, *this被解释为其参数

冷知识,前置++返回引用,后置++返回值

GN4.9更新了旧版本一些写的不好的地方,如_list_iterator<T,T&,T*>需要传入三个参数等;并将大小从4改成8(从只有一个指向最后end()节点的指针变为end()中的prev和next指针)

iterator设计原则与Iterator Traits作用

迭代器是连接算法和容器的桥梁,需要向算法给出必要信息

五个迭代器需给出的associated types:iterator_category 迭代器类型(单向?双向?随机寻址?) difference_type 两个迭代器之间距离的类型 value_type 所指向元素的值类型

后两种没被使用:reference 和 pointer

Iterator Traits是迭代器和容器之间的中间层,代替iterator给出信息,目的是兼容普通指针(普通指针不是一个class,没有定义上述5个associateed types);利用模板特化分离出普通指针

vector

vector类大小只有三个指针(begin、end、end_of_storage)

当容器空间用完之后会两倍成长,再把原来的元素拷贝到新位置后删除旧空间;因为insert()也有可能触发申请新空间,所以要额外做多一步,将当前迭代器后面的元素也复制过去

vector空间连续,迭代器相比list更简单,更接近普通指针

deque

对外表现为双向连续队列,但实际实现上不连续;主要分两部分:

一堆vector用于实际存放数据(buffer缓冲区);一个存放指向vector指针的vector充当管理中心,需要向前或向后扩容时就声明一个新的vector,将指针指向它

deque插入元素较普通vector灵活(当然本质上还是个vector,效率比起list还是很低),计算插入位置较靠近队首还是队尾,以减少需要挪动的元素个数

deque的大小为40(用于管理边界的start和finish迭代器,指向管理中心的指针以及size)

deque的迭代器较特殊,除了一般iterator都有的cur指针,还需实现first、last、node(所以大小是16):

cur指向迭代器实际指向的元素(start和finish的cur实际上为begin()和end())

first和last指向该段vector的头和尾,包括尚未分配实际元素的位置

node则指向 事件中心里指向存放当前元素的vector的指针,因此类型是T**

deque对外显示的**连续性主要靠iterator(重载操作符)**来实现:

operator-:两个iterator之间的buffer长度+头尾buffer实际长度

++、–(前置版本调用后置版本):需要检测是否移动到边界

+、-、+=、-=(±-=调用+=):判断是否在同一缓冲区内,若不是就先将node移到正确的buffer

stack和quene

stack和quene复合了一个底层容器(主要是deque,可以用list),各自封闭deque的某些功能

stack和quene都不允许提供迭代器和被遍历(会破坏性质)

rb-tree

红黑树是平衡二叉搜索树(balanced binary search tree),有利于search和insert

rbTree按正确顺寻遍历(DFS)便能得到排序状态,因此以rbTree为底层的set和map可以当作排序数组使用;STL中rbTree的header指向一个红哨兵节点,该节点的左右节点分别指向rbTree中最左和最右的节点,作为容器的begin()和end()返回

不建议用rbTree的iterators直接改变元素值,这样会影响元素的排列顺序;但map的data允许更改,因此并未禁止

rbTree需要传入五个类型参数:Key、Value、KeyOfValue、Compare、Alloc(默认值)

Value由 Key|Data 键值对(map)或Key组成;KeyOfValue和Compare都是仿函数(重载运算符),前者说明如何从Value中取出Key(或者Data);后者说明如何比大小(力扣用的多)

旧版本rbTree大小为9,node_count、header、key_compare(没有数据只有成员函数的结构体默认大小是1,如果有内存对齐则可能是4);新版本G4.9大小变为24,拥有了一个节点(表示颜色的枚举类型、父左右指针)

set、map

set和map都复合一个rbTree,因此会自动用key值排序(默认是升序less)

set的迭代器是底层的const iterator,无法通过迭代器更改其数值;set的key和value类型相同

map的iterator同样不能更改key,但能改data;map的key和data会自动包装成pair

map重载了[],返回对应位置的元素的data,如果不存在,就用默认值创建后返回(用lower_bound实现,该函数返回第一个不小于value的元素)

hashtable

由bucket vector和node两部分组成。bucket vector的大小一般是质数(减小hashcode重复)存入的数据经某种算法算出编号后放到对应的bucket里,bucket存放指向这个node的指针;当元素发生冲突时(有相同的hascode)就放到bucket中上一个node的后面,以链表方式链接(又称Separate Chaining),但搜索效率会变低

一般当node的数量超过buckets vector的大小时,可以认为冲突较多,需要进行rehashing(增大bucket vector大小,再将所有元素重新放进去;一般的做法是直接把旧大小乘2(也有的做法提前算好一堆质数,后一个是前一个的2倍左右)

hashtable和map类似,可以改data不能改key

hashtable参数:Value、Key、HashFcn(算出元素的hashcode)、ExtractKey(提取value里的data,和rbTree的KeyOfValue类似)、EqualKey(如何比大小)

hashtable大小为19(20),三个仿函数结构体,一个size_type、一个vector(12字节);一个hashtable iteratior有两个指针,一根指当前node,一根指当前node所在的bucket

新版本更名为unorder_xxx

迭代器的分类

STL中的算法实际上是template function;为了适应不同种类容器和算法需求,迭代器也需要有多个种类

五种迭代器:random_access_iterator_tag(随机寻址)->bidirectional_iterator_tag(双向)->farward_iterator_tag(单向)->input_iterator_tag【前者继承后者】、output_iterator_tag

Array、Vector、Deque用random access(对外表现连续);List、Set、Map用bidirectional(拥有双向指针,set/map的红黑树实现中有左右和父指针,可等效双向);哈希容器(unorder_xx)要看如何实现separate chain,双向单向都有可能

可以用iterator_traits::iterator_category(萃取类)来判断迭代器的类型:iterator_traits::iterator_category(iterator)

in/output_iterator比较特殊,只有显式声明is/ostream_iterator才能直接使用

例:计算两个迭代器的距离时(调用distance()),如果是random access可以直接相减,如果不是得从头移动到目标位置。效率差异巨大

Type Traits:询问拷贝赋值 函数是否重要(trivial)

算法例子

算法通常有两版本,第二版本接受一个binary_op(binary表示两个操作数)参数,对初值init执行init = binary_op(init,*first);因此binary_op()第一个参数是旧元素,第二个参数是准备运算的新元素

算法会接受一个predicate类型的参数,也就是仿函数适配器

accumulate(累加或累记)当传入第四个参数操作符(函数或仿函数)时,将迭代器范围的内容(第一第二参数)和初值(第三参数)做运算

某些算法在对应的容器中有适配容器的实现;关联式容器对大多数需要遍历的算法都有自己的实现(比如count、find、sort)

带_if后缀的算法接受一个仿函数用作条件比较

binary_search:需要保证容器内已排序;内部调用lower_bound查找边界,而lower_bound内部才是真正的二分查找

仿函数

必须重载()运算符,以用于自定义操作

算法可以接受一个函数或仿函数object作为自定义操作参数(但个人经验不用声明object,传入strut也是可以的)

STL提供的functor都会继承于binary_function以融入STL

可适配条件(adaptable)需要有操作数模板和操作结果模板,也就是继承自unary/binary_function,定义了first_argument_type等

Adapter

内含一个容器/仿函数/迭代器,并此基础上实现新功能

容器适配器:stack和queue就是内含有deque的

仿函数适配器:binder2nd、not1

例:count_if(c.begin(),c.end(), not1(bind2nd(less(),40)) )

(bind2nd是绑定第二个参数,less第一个参数为容器中的值)

bind2nd会记录算式和第二实参,在算法调用时再实际呼叫算式(重载operator())

辅助函数:借助函数模板的实参推导,让使用者不用手写算式类型

typename:判断模板类中传入的参数能否符合要求正常被编译,例如在辅助函数中 typedef typename Operation::first_argument_type xxx;如果传入的类型没有first_argument_type,就无法通过编译

functors adpter可以被别的functor是 adpter再修饰,因此也要继承binary_function

bind2nd和bind1st已经被bind取代

bing可以绑定函数、函数对象、成员函数、数据成员(后两个必须传入一个参数: 对象的地址)

bind绑定的函数参数既可以在bind()时写入,也可以用占位符,在后面调用adapter时再传入,如:void f1(int a,int b); auto a1 = bind(f1,_1,1); fi(10);

内存分配

四个层面(上层调用下层):

C++ Library std::allocator

C++ primitives new(),::operator new()(C++函数 可被重载)new,new[](C++ expreions 不可被重载)

CRT(C run-time library c语言运行库) malloc/free

O.S.API(操作系统的API) HeapAlloc,VirtualAlloc,…

不同标准库的allocator使用接口有些不同,归还时需要写明归还的内存大小,因此很少直接用

new、delete

使用new表达式,会被编译器自动转换为三步:调用::operator new()分配空间(用空指针指向新空间)、将空指针转型成目标类型指针、用新指针调用目标类型构造函数

如果调用::operator new时失败(空间用光),会调用用户自定义的malloc

delete表达式则相反,先调用析构函数,再用operator delete释放内存

不能直接用指针调用构造函数,但可以调用析构函数

array new、array delete

new一个数组,就是重复调用多次new(调用类的默认构造函数,如果目标类无默认构造函数会报错),array delete同理;新申请的一块内存的头部和尾部会存放cookie,记录了整块内存的大小(实际上申请到的内存由于带有header和cookie,且需要填充到16字节的整数倍,会比你申请的空间要大)

如果用了array new没用array delete可能会发生内存泄露:对不含指针的对象(如int)可能影响不大,归还空间时会将一整块空间都回收(有cookie),而数据全部存放在这块空间里,没有泄露(用hj的话讲就是析构函数是trivial的,不调用也没关系);对含指针的对象影响很大,归还空间时只回收了指针的空间,指针所指的空间全没有被回收(析构函数是non-trivial

事实上如果对象含有non-trivial dtor,申请到的空间在头部cookie和header之后会有一个位置存放数组大小(能放多少个对象),如果没有用array delete运行期会报错(亲测关于trivial dtor,只要做了任何操作,比如cout就会变成non-trivial dtor,vs就会报错)

array delete从尾到头调用数组成员的析构函数

placement new(定点new)

允许将object构建在allocated memory(已分配好的内存)中

class *p = new(buf) Class();

标准库中实际上是operato new()的重载版本,增加第二参数为指针(第一参数是空间大小),该版本直接返回指针(因为内存已经分配好了)

placement new并没有分配内存,只是在已有的内存(指针)调用构造函数,可以和array new配合使用,因为array new调用的都是默认构造函数

重载

自定义内存管理的核心是在类中重载operator new()(expression new不可重载);一般不会动全局::operator new()

重载的函数应该是static的,因为new是在创建对象时调用,但成员函数使用时又需要已生成的对象

例:basic_string用new(size_t,extra)申请额外内存

内存池

基本思路:一次性申请一大块内存(array new,造出一堆空对象),用一个static指针(FreeList)指向下一个可用的空间,之后使用时再借用FreeList移动分配新对象的空间,这样对象之间是紧凑的(没有cookie),目的是节省多次调用malloc的时间和空间(cookie)

per-class allocator

在要进行内存管理的类中重写operator new和operator delete作为成员函数,声明static指针;因为借用指针分配空间,因此内存池的行为和单向链表类似

我们暂时先不真正地进行delete(调用free()将内存还给操作系统),而是将他的next指向FreeList所指对象(如果使用embedded pointer此时数据成员已被擦除),再将FreeList指向该对象

嵌入式指针:如果只需要在创建对象和销毁对象时需要用到指针(或类似行为),可以用union将指针和普通数据成员“缝合”在一起,这样就能省下指针空间(数据成员的大小必须大于指针!!!);Union使用时,由于是共用一块空间,一个成员被赋值时会覆盖整块空间,其他成员的信息会丢失。可以使用简化版本,即:struct obj{struct obj* next;};

对象的数据空间只会在被实例化(或者说被实际用到)之前才会表现为指针形态,而当用next辅助FreeList移动后,对象被实际使用,数据成员也会被复制,指针形态被擦除

static allocator

将operator new和operator delete单独写成一个新类,避免重复;需要进行内存管理的类直接复合一个静态的allocator对象,在operator new()中调用Alloc.allocate(),operator delete()中调用Alloc.deallocate()即可

macro of static allocator

将用户类调用static allocator中重复的部分用宏包装一下,如将DECLARE_POOL_ALLOC()define成opeartor new、delete,将类外static成员声明define成IMPLEMENT_POOL_ALLOC(Classname)

[c++小知识:不能把静态成员的初始化放置在类的定义中,但是可以在类的外部通过使用范围解析运算符 :: 来重新声明静态变量从而对它进行初始化]

new handler

当operator new无法申请更多内存时会抛出std::bad_alloc exception,在抛出异常前,程序会调用用户自定义的handler:

typedef void(*new_handler)();
//	相等于把这个函数登记起来,当条件成立时调用该函数
new_handler set_new_handler(new_handler p) throw();

各个c++库的allocator

VC6、BC5、G4.9:类似static allocator,只是在allocate和deallocate里 完成::operator new和::operator delete,无特殊设计;使用时需要传入申请内存的大小(byte)

G2.9:容器使用的分配器不是std::allocator,而是std::alloc(G4.9中改名为_pool_alloc)

G2.9 std::alloc

维护一条大小为16的指针数组(free_list),每块负责一种大小容器的空间申请,大小为(下标+1)*8,例:free_list[0]负责8个字节的容器…;若申请的空间大小不足8的倍数,会被自动填充

管理的空间上限大小为128bytes的容器,大于128bytes的直接 交给::operator new处理

alloc会一次性申请40个对应单位容器大小的连续内存,从中切出一块给客户,其他19块给free_list备用,其余20块放入pool中备用(给用户下一次申请用);如果pool中还有备用空间(可以满足至少一块容器),就优先用pool里面的(不额外申请空间,即使是不够装满20块)

例:用户申请分配的空间为32bytes,alloc申请32 * 20 * 2 + RoundUp(累记申请量除以16),将第一块空间返回给用户,free_list[3]的指针指向第二块空间

RoundUp、和pool会越来越大

pool碎片处理:当pool中剩余内存量小于新申请的单位容器大小时(即装不满一块新的容器),alloc会将剩余内存拨给对应的free_list(如pool剩余80,就交给free_list[9]),再申请新内存

**内存耗尽(无法再申请新内存)**时,会在下一个free_list中(比他大的中最近的)寻找是否有空余的备用空间,有的话就划一块给申请的地方

浪费:内存耗尽时free_list中也许还有很多空余空间,但是使用难度很高(将小区块合并成大区快)且会出现问题;因为RoundUp很大,也许系统能满足当前申请需求 (注意,单次申请总量=容器大z小* 20 * 2 + RoundUp),但只要剩余内存小于单词申请总量,系统就会进入内存耗尽模式;另一方面,在多进程机器上别的free_list的剩余空间可能正在被使用

源码

class中所有的function和member都是static的(方便改成C)

chunk_alloc()负责满足空间申请,pool能满足就用pool里的;不能满足就申请,申请成功(或是向下一个free_list里借)后设置好start_free和end_free(充值到pool里),再递归调用一次

refill()函数负责切割空间,将指针转型成obj*,然后将移动指针(根据单位容器大小),并将上一个对象的free_list_link指向下一个对象

*obj current_obj;
//	转型成char,方便移动指针
current_obj->free_list_link = (obj*)((char*)current_obj + size);
//	跳转到下一块
current_obj = current_obj->free_list_link;

start_free和end_free分别指向pool的头和尾

小知识:写if()的时候可以把变量写在==右边(如if(0 == start_free))防止出现少写一个=,如果把变量放左边,少写一个=编译会通过;但把变量放在右边之后少写=会编译出错(常量不是左值,不能赋值)

malloc/free :VC6内存管理(SBH:small block heap)

VC在程序开始前(调用main()),编译器 会调用一个heap_alloc_base(),里面会交给HeapAlloc(),旧版本会有专门为小区块SBH服务的函数,新版本封装到HeapAlloc()中

有一个_CrtMemBlockHeader的struct,用于管理malloc()所分配的内存。其中有两个指针成员分别指向上一块和下一块分配的空间;这个struct会被自动加到实际使用空间的前面(和cookie一起)

用于填充的内存块都会设定某些初值,以用于表示内存状态(如果用户越界,编译器会给出警告);头尾的cookie记录整块内存块的大小,因为大小必定是16的整数(内存对齐),理论上最低位是0,实际上用最低位表示该内存块的控制权是否在系统手上(给出改1,归还改0)

一个_sbh_pHeader(大概16kb)负责管理1Mb(虚拟地址空间)。含有32个group,每个group管理32kb内存(32 * 32 = 1024);每个group中有六十四对指针,每对指针管理的内存大小以16字节(B)成长(类似pool分配器),第一条16B…最后一条1024B(及以上)

初始状态,32kb分为8个page,全部挂在最后一对指针上;sbh有一条链表(32组 * 64bit)用于管理64对指针的状态,每个bit用0或1表示空闲或占用;与pool allocator相似,如果分配或归还时对应大小的空间被占用了,则启用下一个比当前空间大的位置,一个group用完就用下一个

设计两个上下cookie的 作用之一是归还内存时能顺便将相邻的空闲内存合并成大块空间(以应付需求,避免空间碎片化趋势)通过查看相邻两区块的cookie最低位判断是否空闲,进而合并

free§的过程:首先找p在哪个Header中(Header大小固定,拿p和指向头部_sbh_pHeaderList比较再除以大小得偏移值),再找落在哪个group中(同Header),最后看在哪个freelist(看cookie大小)

分段管理目的:为了便于归还空间给操作系统

Defer(延迟回收):用某个指针指向一个全回收group所属的Header,当出现新的全回收group后才释放该defer group

VC6中有一系列Heap State Reporting Functions,用于追踪内存分配情况

Loki allocator

三个class

SmallObjAllocator复合FixedAllocator、FixedAllocator复合Chunk

Chunk

成员:首地址指针、第一个可用区块编号(最高优先级)、总区块数

FixedAllocator

成员:vector、两个指向Chunk的指针(allocChunk:上一次发生分配的chunk deallocChunk:上一次发生归还的chunk)

SmallObjAllocator

成员:vector、两个指向FixedAllocator的指针及chunkSize和maxObjectSize

Chunk中用偏移量计算出每个小区块的索引(char:相对区块开头的偏移量),并用索引代替指针(本质还是嵌入式) 指向下一个可用的小区块;每个新被归还的区块都会成为最高优先级(取代原来的第一给可用区块编号),因此最高优先级的区块编号和已被占用了的区块编号都不会在空闲索引中出现初始化状态下是123…(没有0,0在最高优先级),随着后续的分配和归还,索引的顺序和位置都会被打乱

FixedAllocator在分配时优先寻找allocChunk指向的chunk有无空闲位置,如果没有则将vector中的Chunk全部遍历一次,如果遍历完整个vector后仍没找到,则push_back一个新的Chunk。vector扩容时有可能引起迭代器失效,需要重设allocChunk(指向新分配的)和deallocChunk(指向末尾的上一个,其实指哪都行)

GNU C++提供的Allocator

种类

_gnu_cxx::new_allocator(以下省略:😃、malloc_allocator(直接用std::malloc和std::free)和bitmap_allocator、pool_allocator、array_allocator、debug_allocator、bitmap_allocator

综合测试(对C++ allocators的速度)

Insertion、多线程insertion和erasure、producer/consumer model

array_allocator

内部复合一个array;固定大小 ;deallocate什么都不做,因此不会回收已给出的内存空间

使用:array_allocator<int, array<int,size>>

debug_allocator

类似cookie,在开头多分配一小块空间以记录数据(hj锐评:没什么用)

bitmap_allocator

继承一个free_list;一次只提供一个元素(? 针对容器)

一开始挖64个block,用完则两倍增长(第二块super block有128个block);在开头用两个int(或者一个unsigned int,大小为64个bit,8个字节)表示64个block的状态,空闲为1,使用为0(当然也 会两倍增长);还有一个int位记录使用中的block的个数;block和bitmap的对应状态相反,block顺序分配,bitmap逆序变化

以上所有统称为一个super block,在开头的开头也会有一个int记录整块super block的大小;用一个模仿vector的自定义数据结构**_mini_vector的数组(称为mem blocks**)管理一个super block,与vector类似,三根指针,两根分别指自己管理那块super block的头和尾,第三根指向全部super block的尾

每个super block只服务于一种类型的容器(?不是很懂)

free list:当一个super block发生全回收时,会被登记到另外一条vector,而_mini_vector中负责管理这一块的会被erase掉。当另外一条vector(称其为free list)大小等于64时,会一次性将free list中所有空间归还给OS;下一次分配的super block大小会在原有需求上减半;free list中的元素按super block的大小决定

当mem block为空(全部挂到free list上了)而用户又发来需求时,会将free list中的第一个super block挂到mem block中

const

设计成员函数时,能加const尽量加const,以免const object调用non-const functions报错

当成员函数的const和non-const版本同时存在时,const object只会调用const版本,non-const object只会调用non-const版本

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值