目录
本部分转载自https://blog.youkuaiyun.com/haluoluo211/article/details/80560066
STL为什么需要空间配置器
程序中我们经常动态申请,释放内存,这会带来如下两个问题:
-
问题1:就出现了内存碎片问题。(包括外碎片问题)
-
问题2:一直在因为小块内存而进行内存申请,调用malloc,系统调用产生性能问题
STL空间配置器alloc实现的原理
大部分容器都会默认使用std::alloc作为空间配置器,同时也会由自己特定的空间配置器,如list会特有为一个一个节点配置内存的配置器lost_node_allocator。
这两个问题解释清楚之后,就来谈STL空间配置器的实现细节了实现策略
if 用户申请空间 > 128:
调用一级空间配置器
else:
调用二级空间配置器
大致实现为:
二级空间配置器由内存池以及伙伴系统:自由链表组成
一级空间配置器直接封装malloc,free进行处理,增加了C++中的set_handler机制(这里其实也就是个略显牵强的装饰/适配模式了),增加内存分配时客户端可选处理机制。
二级空间配置器:
第二层配置器有一个内存池,用一个union obj数组free_list来存储内存的地址,数组的每一个元素都指向一个obj链表,也就是内存链表。数组从小到大表示负责8b,16b,24b,...,120b,128b内存请求。当请求的内存为n时,会将请求上条至2的指数大小,并从数组相应位置获取内存。例如如果请求为20b,则请求会上调至24b 。 先看下union obj这个定义:
union obj {
union obj * free_list_link;//指向下一个内存的地址
char client_data[1]; //内存的首地址
};
成员以及接口定义如下:
class alloc{
private:
enum EAlign{ ALIGN = 8};//小型区块的上调边界
enum EMaxBytes{ MAXBYTES = 128};//小型区块的上限,超过的区块由malloc分配
//free-lists的个数
enum ENFreeLists{ NFREELISTS = (EMaxBytes::MAXBYTES / EAlign::ALIGN)};
enum ENObjs{ NOBJS = 20};//每次增加的节点数
private:
//free-lists的节点构造
union obj{
union obj *next;
char client[1];
};
static obj *free_list[ENFreeLists::NFREELISTS];
private:
static char *start_free;//内存池起始位置
static char *end_free;//内存池结束位置
static size_t heap_size;//
private:
//将bytes上调至8的倍数,例如 (1, 3, 7) -> 8, (9, 13) -> 16
static size_t ROUND_UP(size_t bytes){
return ((bytes + EAlign::ALIGN - 1) & ~(EAlign::ALIGN - 1));
}
//根据区块大小,决定使用第n号free-list,n从0开始计算
//例如 (1, 3, 7) -> 0, (9, 13) -> 1
static size_t FREELIST_INDEX(size_t bytes){
return (((bytes)+EAlign::ALIGN - 1) / EAlign::ALIGN - 1);
}
//返回一个大小为n的对象,并可能加入大小为n的其他区块到free-list
static void *refill(size_t n);
//配置一大块空间,可容纳nobjs个大小为size的区块
//如果配置nobjs个区块有所不便,nobjs可能会降低
static char *chunk_alloc(size_t size, size_t& nobjs, int print_count=1);
public:
static void *allocate(size_t bytes);//分配内存
static void deallocate(void *ptr, size_t bytes);//内存回收
// 回收旧内存,分配新内存
static void *reallocate(void *ptr, size_t old_sz, size_t new_sz);
};
首先空间配置会将<128字节的大小转化为8的倍数,分析如下:
// 将任意的整数调整为8的倍数
void test_or_align(){
int a =(5 + 8 - 1) & ~(8 - 1);
int b =(9 + 8 - 1) & ~(8 - 1);
int c =(13 + 8 - 1) & ~(8 - 1);
int d =(17 + 8 - 1) & ~(8 - 1);
cout<<a<<endl; // 8 (5 -》 8)
cout<<b<<endl; // 16 (9 -》 16)
cout<<c<<endl; // 16 (13 -》 16)
cout<<d<<endl; // 24 (17 -》 24)
}
8的倍数转化为free_list的index,分析如下:
void test_freelist_index(){
size_t bytes = 5;
size_t idx = (((bytes) + EAlign::ALIGN - 1) / EAlign::ALIGN - 1);
std::cout<<idx<<endl; //0
bytes = 13;
idx = (((bytes) + EAlign::ALIGN - 1) / EAlign::ALIGN - 1);
std::cout<<idx<<endl; // 1
}
初始化:指针数组全部为空:
char *alloc::start_free = 0;
char *alloc::end_free = 0;
size_t alloc::heap_size = 0;
alloc::obj *alloc::free_list[alloc::ENFreeLists::NFREELISTS] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
};
vector缺省参数使用alloc作为空间配置器,并据此另外定义了一个data_allocator,为的是更方便以元素大小为配置单位:
同理,string也会在他的类中typedef simple_alloc<value_type,Alloc> data_allocator; 因此他们用的是同一个空间配置器,使用的是同一块内存。
template <class T,class Alloc=alloc>
class vector{
protected:
typedef simple_alloc<value_type,Alloc> data_allocator;
...
};
当然,用户也可以定制自己的allocator,只要实现allocator模板所定义的接口方法即可,然后通过将自定义的allocator作为模板参数传递给STL容器,创建一个使用自定义allocator的STL容器对象,如:
stl::vector<int, UserDefinedAllocator> array;
data_allocator::allocate(n)表示配置n个元素空间
vector提供很多构造函数,其中一个允许我们指定空间大小以及初值:
//构造函数,允许指定vector大小n和初值value
vector(size_type n,const T& value) {fill_initialize(n,value);}
//填充并予以初始化
void fill_initialize(suze_type n,const T& value)
{
start=allocate_and_fill(n,value);
finish=start+n;
end_of_storage=finish;
}
//配置而后填充
iterator allocate_and_fill(size_type n,const T& x)
{
iterator result=data_allocator::allocate(n);//配置n个元素空间
uninitialized_fill_n(result,n,x);
return result;
}
注意:uninitialized_fill_n()会根据第一参数的型别特性决定使用算法fill_n()或反复调用constructor()来完成任务。
当我们以push_back()将新元素插入于vector尾端时,该函数首先检查是否还有备用空间,如果有就直接在备用空间上构造函数,并调整迭代器finish,使vector变大。如果没有备用空间,就扩充空间(重新配置,移动数据,释放原空间)
void push_back(const T& x)
{
if(finish!=end_of_storage)
{
//还有备用空间
construct(finish,x);//全局函数
++finish;
}
else
{
insert_aux(end(),x);//vector member function
}
}
重点来了:
template <class T,class Alloc>
void vector<T,Alloc>::insert_aux(iterator position,const T& x)
{
if(finish=!=end_of_storage){ //还有备用空间
//在备用空间起始处构造一个元素,并以vector最后一个元素值为其初值
construct(finish,*(finish-1));
//调整水位
++finish;
T x_copy=x;
copy_backward(position,finish-2,finish-1);
*position=x_copy;
}
else{ // 已无备用空间
const size_type old_size=size();
const size_type len=old_size!=0?2*ols_size:1;
//以上原则,如果原大小为0,则配置1(个元素)
//如果原大小不为0,则配置原大小的两倍
//前半段用来放置元数据,后半段用来放置新数据
iterator new_start=data_allocator::allocate(len);//实际配置
iterator new_finish=new_start;
try{
//将原vector的内容拷贝到新的vector
new_finish=uninitialized_copy(start,position,new_start);
//为新元素设定初值x
construct(new_finish,x);
//调整水位
++new_finish;
//将安插点的原内容也拷贝过来
new_finish=uninitialized_copy(position,finish,new_finish);
}
catch(...){
destroy(new_start,new_finish);
data_allocator::deallocate(new_start,len);
throw;
}
//析构并释放原vector
destory(begin(),end());
deallocate();
//调整迭代器,指向新vector
start=new_start;
finish=new_finish;
end_of_storage=new_start+len;
}
}
迭代器失效:
注意,所谓动态增加大小,并不是在原空间之后接续新空间(因为无法保证原空间之后尚有可供配置的空间),而是以原大小的两倍另外配置一块较大空间,然后将原内容拷贝过来,然后才开始在原内容之后构造新元素,并释放原空间。因此,对vector的任何操作,一旦引起空间重新配置,指向原vector的所有迭代器就都失效了。
若是erase了vector中的某个元素,则此元素后面的迭代器都会失效。
例1:erase导致迭代器失效:
vector<int>::iterator it = res.begin();
for (; it != res.end();it++) {
if (*it == 3) {
res.erase(it);
}
else {
cout << *it << " ";
}
}
执行这段程序,发现抛出异常“_mycont是nullptr”,即erase删除一个元素之后又去*it 他,会出现异常;
正确写法:
vector<int>::iterator it = res.begin();
for (; it != res.end();) {
if (*it == 3) {
it = res.erase(it);
}
else {
cout << *it << " ";
++it;
}
}
利用erase函数的返回值来取得下一个元素的迭代器并赋值给it。
问个问题。。用vector的erase(it)删除元素后,为啥迭代器it就失效了 erase只是调用了copy函数把it指向的元素下一个~末尾元素往前覆盖了呀,it不还是指向那个地方的内存吗
erase源码:
iterator erase(iterator position){//清除某位置上的元素
if(position+1 !=end)
{
copy(position+1,finish,position);//后续元素往前移动
}
--finish;
destroy(finish);
return position;
}
但是我看erase的源码,好像也没有对我们传进去的迭代器做什么操作呀
就返回了
思考了一下,it指向的对象和it+1指向的对象是不同的,不能把it看做普通的指针,只是指向内存空间,而应该把他和对象绑定起来,因为删除一个对象的时候会调用它的析构函数,如果只是简单的将it指向下一个对象的话,它调不到新的析构函数了。
我们在构造每个元素的时候,都会同时配置他的迭代器(我++应指向哪个迭代器,我--应该指向哪个迭代器)。那么,在我们erase这个元素的时候,如果没有将it再用erase()的返回值赋值的话,it就是一个被析构后的迭代器(类似野指针),我们要再it++后去解引用的话,就相当于访问了野指针,这就是迭代器失效的原理(我个人理解呀-_-)
迭代器的详细介绍:
-
背景:指针可以用来遍历存储空间连续的数据结构,但是对于存储空间费连续的,就需要寻找一个行为类似指针的类,来对非数组的数据结构进行遍历。
定义:迭代器是一种检查容器内元素并遍历元素的数据类型。
迭代器提供对一个容器中的对象的访问方法,并且定义了容器中对象的范围。
迭代器(Iterator)是指针(pointer)的泛化,它允许程序员用相同的方式处理不同的数据结构(容器)。
(1)迭代器类似于C语言里面的指针类型,它提供了对对象的间接访问。
(2)指针是C语言中的知识点,迭代器是C++中的知识点。指针较灵活,迭代器功能较丰富。
(3)迭代器提供一个对容器对象或者string对象的访问方法,并定义了容器范围。 -
迭代器和指针的区别:
容器和string有迭代器类型同时拥有返回迭代器的成员。如:容器有成员begin和end,其中begin成员复制返回指向第一个元素的迭代器,而end成员返回指向容器尾元素的下一个位置的迭代器,也就是说end指示的是一个不存在的元素,所以end返回的是尾后迭代器。 -
容器迭代器的使用
每种容器类型都定义了自己的迭代器类型,如vector:vector< int>:: iterator iter;//定义一个名为iter的变量,数据类型是由vector< int>定义的iterator 类型。简单说就是容器类定义了自己的iterator类型,用于访问容器内的元素。每个容器定义了一种名为iterator的类型,这种类型支持迭代器的各种行为。
STL的中心思想在于:将数据容器(containers) 和算法(algorithms) 分开,彼此独立设计,最后再以一帖胶着剂将他们撮合在一起。 容器和算法的泛型化从技术角度来看并不困难,c++的 class template 和 function template 可分别达成目标, 那么最主要的问题就是如何设计出两者之间的良好胶着剂,即迭代器。
根据STL中的分类,iterator包括:
Input Iterator:
输入迭代器用于从一个对象中不断读出元素。
除了迭代器基本操作,还需要支持:
- 比较两个迭代器是否指向相同元素
i == j
和i != j
- 访问元素的成员
i->m
- 后缀自增操作
i++
输入迭代器不需要保证自增操作之后,之前的迭代器依然有意义。典型的例子是用于读取流文件的迭代器:每次自增之后,都无法回复到原来的位置。也包括随机数生成器的迭代器:每次自增之后,随机数生成器的状态都发生了变化。
Output Iterator:
输出迭代器用于向一个对象不断添加元素。
除了迭代器基本操作,还需要支持:
- 修改元素
*i = v
- 后缀自增操作
i++
输出迭代器也不保证自增之后,之前的迭代器有意义,并且它还不保证修改元素之后访问元素有意义。典型的例子是用于写入流文件的迭代器和向容器中插入元素的 插入迭代器 (insert iterator)。
Forward Iterator:
前向迭代器用于访问一个容器中的元素。
因此,它必须提供输入迭代器的所有操作,如果它用于访问一个非 const 容器,还需要支持输出迭代器的所有操作。
在此基础上,它需要保证自增之后,其他的迭代器仍然有意义(比输入迭代器要求更高)。它也需要保证可以不受限制地修改和访问当前元素(比输出迭代器要求更高)。
典型的例子包括各种容器的迭代器,比如 vector、list、map 的迭代器。
Bidirectional Iterator:
双向迭代器首先是一个前向迭代器,除此之外,它还需要支持自减操作 --i
和 i--
。
一个线性存储元素的容器应该提供双向迭代器,例如 vector 和 list。
Random Access Iterator:
随机访问迭代器是所有迭代器种类中最强大的,它除了需要支持前向迭代器的所有操作,还支持加上任意偏移量并得到新的迭代器,即 i + n
,其中 n
可以是正数也可以是负数,分别表示向前或向后随机访问。
它需要支持的完整操作包括:
- 加上或减去一个偏移量
i + n
和i - n
- 自加或自减一个偏移量
i += n
和i -= n
- 计算两个迭代器的距离
i - j
- 使用下标形式的加上一个偏移量
i[n]
,其效果等价于*(i + n)
- 比较两个迭代器的先后顺序
a < b
a <= b
a > b
a >= b
我们可以发现,随机访问迭代器在功能上已经等价于指针。
一般来说,只有 vector 这类线性且连续存储元素的容器才会提供随机访问迭代器。
这五类迭代器的从属关系,如下图所示,其中箭头A→B表示,A是B的强化类型,这也说明了如果一个算法要求B,那么A也可以应用于其中。
input output
\ /
forward
|
bidirectional
|
random access
vector 和deque提供的是RandomAccessIterator,list提供的是BidirectionalIterator,set和map提供的 iterators是 ForwardIterator,关于STL中iterator迭代器的操作如下:
说明:每种迭代器均可进行包括表中前一种迭代器可进行的操作。
迭代器操作 说明
(1)所有迭代器
p++ 后置自增迭代器
++p 前置自增迭代器
(2)输入迭代器
*p 复引用迭代器,作为右值
p=p1 将一个迭代器赋给另一个迭代器
p==p1 比较迭代器的相等性
p!=p1 比较迭代器的不等性
(3)输出迭代器
*p 复引用迭代器,作为左值
p=p1 将一个迭代器赋给另一个迭代器
(4)正向迭代器
提供输入输出迭代器的所有功能
(5)双向迭代器
--p 前置自减迭代器
p-- 后置自减迭代器
(6)随机迭代器
p+=i 将迭代器递增i位
p-=i 将迭代器递减i位
p+i 在p位加i位后的迭代器
p-i 在p位减i位后的迭代器
p[i] 返回p位元素偏离i位的元素引用
p<p1 如果迭代器p的位置在p1前,返回true,否则返回false
p<=p1 p的位置在p1的前面或同一位置时返回true,否则返回false
p>p1 如果迭代器p的位置在p1后,返回true,否则返回false
p>=p1 p的位置在p1的后面或同一位置时返回true,否则返回false
只有顺序容器和关联容器支持迭代器遍历,各容器支持的迭代器的类别如下:
容器 支持的迭代器类别 容器 支持的迭代器类别 容器 支持的迭代器类别
vector 随机访问 deque 随机访问 list 双向
set 双向 multiset 双向 map 双向
multimap 双向 stack 不支持 queue 不支持
priority_queue 不支持
对于一个迭代器类型 T,我们会关心和它相关的如下信息:
- 它是哪种类型的迭代器;
- 它指向的数据类型是什么;
- 这个数据类型的引用类型是什么;
- 这个数据类型的指针类型是什么;
- 两个迭代器的距离用什么类型表示。
这些信息通过迭代器类的内部类型定义来呈现,因此,一个完整的迭代器应该包含如下定义:
#include <iterator>
class ArrayIterator {
public:
typedef std::random_access_iterator_tag iterator_category; // 迭代器类型
typedef int value_type; // 数据类型
typedef int& reference_type; // 数据类型的引用
typedef int* pointer_type; // 数据类型的指针
typedef std::ptrdiff_t difference_type; // 距离类型
// 迭代器的实现
};
https://www.cnblogs.com/lakeone/p/5599047.html
map的排序问题
大家都知道map是stl里面的一个模板类,现在我们来看下map的定义:
template < class Key, class T, class Compare = less<Key>,
class Allocator = allocator<pair<const Key,T> > > class map;
它有四个参数,其中我们比较熟悉的有两个: Key 和 Value。第四个是 Allocator,用来定义存储分配模型的,此处我们不作介绍。
现在我们重点看下第三个参数: class Compare = less<Key>
这也是一个class类型的,而且提供了默认值 less<Key>。 less是stl里面的一个函数对象,那么什么是函数对象呢?
所谓的函数对象:即调用操作符的类,其对象常称为函数对象(function object),它们是行为类似函数的对象。表现出一个函数的特征,就是通过“对象名+(参数列表)”的方式使用一个 类,其实质是对operator()操作符的重载。
现在我们来看一下less的实现:
template <class T> struct less : binary_function <T,T,bool> {
bool operator() (const T& x, const T& y) const
{return x<y;}
};
它是一个带模板的struct,里面仅仅对()运算符进行了重载,实现很简单,但用起来很方便,这就是函数对象的优点所在。stl中还为四则运算等常见运算定义了这样的函数对象,与less相对的还有greater:
template <class T> struct greater : binary_function <T,T,bool> {
bool operator() (const T& x, const T& y) const
{return x>y;}
};
map这里指定less作为其默认比较函数(对象),所以我们通常如果不自己指定Compare,map中键值对就会按照Key的less顺序进行组织存储,因此我们就看到了上面代码输出结果是按照学生姓名的字典顺序输出的,即string的less序列。
map<string, int, greater<string> > name_score_map;
现在知道如何为map指定Compare类了,如果我们想自己写一个compare的类,让map按照我们想要的顺序来存储,比如,按照学生姓名的长短排序进行存储,那该怎么做呢?
其实很简单,只要我们自己写一个函数对象,实现想要的逻辑,定义map的时候把Compare指定为我们自己编写的这个就ok啦
struct CmpByKeyLength {
bool operator()(const string& k1, const string& k2) {
return k1.length() < k2.length();
}
};
是不是很简单!这里我们不用把它定义为模板,直接指定它的参数为string类型就可以了。
int main() {
map<string, int, CmpByKeyLength> name_score_map;
name_score_map["LiMin"] = 90;
name_score_map["ZiLinMi"] = 79;
name_score_map["BoB"] = 92;
name_score_map.insert(make_pair("Bing",99));
name_score_map.insert(make_pair("Albert",86));
for (map<string, int>::iterator iter = name_score_map.begin();
iter != name_score_map.end();
++iter) {
cout << *iter << endl;
}
return 0;
}
【运行结果】
二、Map按Value排序
在第一部分中,我们借助map提供的参数接口,为它指定相应Compare类,就可以实现对map按Key排序,是在创建map并不断的向其中添加元素的过程中就会完成排序。
现在我们想要从map中得到学生按成绩的从低到高的次序输出,该如何实现呢?换句话说,该如何实现Map的按Value排序呢?
第一反应是利用stl中提供的sort算法实现,这个想法是好的,不幸的是,sort算法有个限制,利用sort算法只能对序列容器进行排序,就是线性的(如vector,list,deque)。map也是一个集合容器,它里面存储的元素是pair,但是它不是线性存储的(前面提过,像红黑树),所以利用sort不能直接和map结合进行排序。
虽然不能直接用sort对map进行排序,那么我们可不可以迂回一下,把map中的元素放到序列容器(如vector)中,然后再对这些元素进行排序呢?这个想法看似是可行的。要对序列容器中的元素进行排序,也有个必要条件:就是容器中的元素必须是可比较的,也就是实现了<操作的。那么我们现在就来看下map中的元素满足这个条件么?
我们知道map中的元素类型为pair,具体定义如下:
template <class T1, class T2> struct pair
{
typedef T1 first_type;
typedef T2 second_type;
T1 first;
T2 second;
pair() : first(T1()), second(T2()) {}
pair(const T1& x, const T2& y) : first(x), second(y) {}
template <class U, class V>
pair (const pair<U,V> &p) : first(p.first), second(p.second) { }
}
pair也是一个模板类,这样就实现了良好的通用性。它仅有两个数据成员first 和 second,即 key 和 value,而且
在 <utility>头文件中,还为pair重载了 < 运算符, 具体实现如下:
template<class _T1, class _T2>
inline bool
operator<(const pair<_T1, _T2>& __x, const pair<_T1, _T2>& __y)
{ return __x.first < __y.first
|| (!(__y.first < __x.first) && __x.second < __y.second); }
重点看下其实现:
__x.first < __y.first || (!(__y.first < __x.first) && __x.second < __y.second)
这个less在两种情况下返回true,第一种情况:__x.first < __y.first 这个好理解,就是比较key,如果__x的key 小于 __y的key 则返回true。
第二种情况有点费解: !(__y.first < __x.first) && __x.second < __y.second
当然由于||运算具有短路作用,即当前面的条件不满足是,才进行第二种情况的判断 。
第一种情况__x.first < __y.first 不成立,即__x.first >= __y.first 成立,在这个条件下,我们来分析下 !(__y.first < __x.first) && __x.second < __y.second,
首先!(__y.first < __x.first) ,看清楚,这里是y的key不小于x的key ,结合前提条件,__x.first < __y.first 不成立,即x的key不小于y的key
即: !(__y.first < __x.first) && !(__x.first < __y.first ) 等价于 __x.first == __y.first ,也就是说,第二种情况是在key相等的情况下,比较两者的value(second)。
这里比较令人费解的地方就是,为什么不直接写 __x.first == __y.first 呢? 这么写看似费解,但其实也不无道理:前面讲过,作为map的key必须实现<操作符的重载,但是并不保证==符也被重载了,如果key没有提供==,那么 ,__x.first == __y.first 这样写就错了。由此可见,stl中的代码是相当严谨的,值得我们好好研读。
现在我们知道了pair类重载了<符,但是它并不是按照value进行比较的,而是先对key进行比较,key相等时候才对value进行比较。显然不能满足我们按value进行排序的要求。
而且,既然pair已经重载了<符,而且我们不能修改其实现,又不能在外部重复实现重载<符。
typedef pair<string, int> PAIR;
bool operator< (const PAIR& lhs, const PAIR& rhs) {
return lhs.second < rhs.second;
}
如果pair类本身没有重载<符,那么我们按照上面的代码重载<符,是可以实现对pair的按value比较的。现在这样做不行了,甚至会出错(编译器不同,严格的就报错)。
那么我们如何实现对pair按value进行比较呢? 第一种:是最原始的方法,写一个比较函数; 第二种:刚才用到了,写一个函数对象。这两种方式实现起来都比较简单。
typedef pair<string, int> PAIR;
bool cmp_by_value(const PAIR& lhs, const PAIR& rhs) {
return lhs.second < rhs.second;
}
struct CmpByValue {
bool operator()(const PAIR& lhs, const PAIR& rhs) {
return lhs.second < rhs.second;
}
};
这样,使用sort结合我们自定义的比较函数或者函数对象,就能实现在vector中依据map的value值来进行排序。
#include <iostream>
#include <vector>
#include<map>
#include<string>
#include<algorithm>
using namespace std;
typedef pair<string, int> PAIR;
bool cmp_by_value(const PAIR& lhs, const PAIR& rhs) {
return lhs.second < rhs.second;
}
struct CmpByValue {
bool operator()(const PAIR& lhs, const PAIR& rhs) {
return lhs.second < rhs.second;
}
};
int main() {
map<string, int> name_score_map;
name_score_map["LiMin"] = 90;
name_score_map["ZiLinMi"] = 79;
name_score_map["BoB"] = 92;
name_score_map.insert(make_pair("Bing", 99));
name_score_map.insert(make_pair("Albert", 86));
//把map中元素转存到vector中
vector<PAIR> name_score_vec(name_score_map.begin(), name_score_map.end());
sort(name_score_vec.begin(), name_score_vec.end(), CmpByValue());
// sort(name_score_vec.begin(), name_score_vec.end(), cmp_by_value);
for (int i = 0; i < name_score_vec.size(); ++i) {
cout << name_score_vec[i].first << " " << name_score_vec[i].second << endl;
}
system("pause");
return 0;
}
输出结果:
函数对象
即重载函数调用操作符的类,其对象常称为函数对象(function object),即它们是行为类似函数的对象。又称仿函数。
函数对象(function object)是一个程序设计的对象允许被当作普通函数来调用。
函数对象与函数指针相比,有两个优点:第一是编译器可以内联执行函数对象的调用;第二是函数对象内部可以保持状态。
函数式程序设计语言还支持闭包,例如,first-class函数支持在其创建时用到的函数外定义的变量的值保持下来,成为一个函数闭包。
https://www.cnblogs.com/gis-user/p/5086218.html
在学习C++的时候对这个函数对象还没什么感觉,但是在这次学习Boost.Thread的时候才发现,函数对象的重要性以及方便性。在传统的C线程中,会有一个void*参数用于给线程函数传递参数,但是Boost.Thread去是直接构造线程对象,除了一个函数名之外没有其它的参数,那么如果使用传统的方式(直接将函数名称传入)就只能执行无参数的函数了,所以这里使用了函数对象来实现参数的传递。
(一)函数对象
再来回顾一下什么是函数对象,就是一个重载'()'运算符 -- 可以理解为重载了函数调用运算子() --的类的对象。这样就可以直接使用‘对象名( 参数 )’的方式,这跟调用函数一样,所以称谓函数对象。看例子:
例子1:
使得map按照从大到小排序:
先写一个排序的函数对象:
template <class T>
struct greater:binary_function <T,T,bool>{
bool operator() (const T& x, const T& y) const
{return x>y;}
};
//使用时:
map<string, int, greater<string> > m; //greater<string> 具体化模板类类型
//--------------------------------------------------
//直接写比较函数的方法:
bool mycom(pair<string, int>& a, pair<string, int>& b){
return a.first >= b.first;
}
map<string, int, mycom > m;
例子2:
#include <iostream>
#include <string>
class Printer{
public:
explicit Printer(){};
void operator()(const std::string & str)const{
std::cout<<str<<std::endl;
}
};
int main(int atgc,char * argv[]){
Printer p;
p("hello world!"); //这样调用
return 0;
}
现在来说说函数对象有哪些好处:
(1)函数对象有自己的状态,即它可以携带自己的成员函数,而且这个函数对象在多次调用的过程中它的那些状态是共享的,而函数则不能做到这点(除非定义函数内部的静态变量或者全局变量)。
假设我需要一个数字产生器,我给定一个初始的数字,然后每次调用它都会给出下一个数字。
class SuccessiveNumGen
{
public:
SuccessiveNumGen(int origin = 0):m_origin(origin){}
int operator()(){
return m_origin++;
}
private:
int m_origin;
};
int main(int argc,char * argv[]){
std::vector<int> dest;
generate_n(back_inserter(dest),10,SuccessiveNumGen(3));
for(int i=0;i<10;i++){
std::cout<<dest[i]<<std::endl;
}
return 0;
}
注:此处用到了STL的算法,generate_n会调用SuccessiveNumGen函数十次,back_inserter会将SuccessiveNumGen函数的返回值插入到dest中。
执行结果为,每次比上次多1
(2)函数对象有自己的类型,而普通函数则没有。在使用STL的容器时可以将函数对象的类型传递给容器作为参数来实例化相应的模板,从而来定制自己的算法,如排序算法。
函数对象应用实例2:在sort算法中的应用
STL 中的排序模板 sort 能将区间从小到大排序。sort 算法有两个版本。第一个版本的原型如下:
template <class_Randlt>
void sort(_Randlt first, _RandIt last);
该模板可以用来将区间 [first, last) 中的元素从小到大排序,要求 first、last 是随机访问迭代器。元素比较大小是用<
进行的。如果表达式a<b
的值为 true,则 a 排在 b 前面;如果a<b
的值为 false,则 b 未必排在 a 前面,还要看b<a
是否成立,成立的话 b 才排在 a 前面。要使用这个版本的 sort 算法,待排序的对象必须能用<
运算符进行比较。不行的话要重载
sort 算法第二个版本的原型如下:
template <class_Randlt, class Pred>
void sort(_Randlt first, _RandIt last, Pred op);
这个版本和第一个版本的差别在于,元素 a、b 比较大小是通过表达式op(a, b)
进行的。如果该表达式的值为 true,则 a 比 b 小;如果该表达式的值为 false,也不能认为 b 比 a 小,还要看op(b, a)
的值。总之,op 定义了元素比较大小的规则。使用 sort 算法的例子我们上面提到了,此处略去。
STL 中的函数对象类模板
STL 中有一些函数对象类模板,如表 1 所示。
表1:STL 中的函数对象类模板
函数对象类模板 | 成员函数 T operator ( const T & x, const T & y) 的功能 |
---|---|
plus <T> | return x + y; |
minus < > | return x - y; |
multiplies <T> | return x * y; |
divides <T> | return x / y; |
modulus <T> | return x % y; |
成员函数 bool operator( const T & x, const T & y) 的功能 | |
equal_to <T> | return x == y; |
not_equal_to <T> | return x! = y; |
greater <T> | return x > y; |
less <T> | return x < y; |
greater_equal <T> | return x > = y; |
less_equal <T> | return x <= y; |
logical_and <T> | return x && y; |
logical_or <T> | return x || y; |
成员函数 T operator( const T & x) 的功能 | |
negate <T> | return - x; |
成员函数 bool operator( const T & x) 的功能 | |
logical_not <T> | return ! x; |
例如,如果要求两个 double 型变量 x、y 的乘积,可以写:
multiplies<double> () (x, y)
less 是 STL 中最常用的函数对象类模板,其定义如下:
template <class_Tp>
struct less
{
bool operator() (const_Tp & __x, const_Tp & __y) const
{ return __x < __y; }
};
要判断两个 int 变量 x、y 中 x 是否比 y 小,可以写:
if( less<int>()(x, y) ) { ... }
https://blog.youkuaiyun.com/qq_37233607/article/details/79051075#commentBox
explicit
C++提供了关键字explicit,可以阻止不应该允许的经过转换构造函数进行的隐式转换的发生。声明为explicit的构造函数不能在隐式转换中使用。
普通构造函数能够被隐式调用。而explicit构造函数只能被显式调用。
看下面例子:
#include <iostream>
using namespace std;
class A
{
public:
A(int i = 5)
{
m_a = i;
}
private:
int m_a;
};
int main()
{
A s;
//我们会发现,我们没有重载'='运算符,但是去可以把内置的int类型赋值给了对象A.
s = 10;
//实际上,10被隐式转换成了下面的形式,所以才能这样.
//s = A temp(10);
system("pause");
return 0;
}
我们发现成员变量的值被修改了.
由此可知:当类构造函数的参数只有一个的时候,或者所有参数都有默认值的情况下,类A的对象时可以直接被对应的内置类型隐式转换后去赋值的,这样会造成错误,所以接下来会体现出explicit这个关键词的作用.
#include <iostream>
using namespace std;
class A
{
public:
//这里用explicit关键词来修饰类构造函数.
explicit A(int i = 5, int j = 10)
{
m_a = i;
m_b = j;
}
private:
int m_a;
int m_b;
};
int main()
{
A s;
//这样直接赋值,会被提示错误,因为explicit抑制隐式转换的进行
s = 10;//这样会报错!!!
//当然显示转换还是可以的.
s = A(20);
system("pause");
return 0;
}
https://www.cnblogs.com/zhizhan/p/4876093.html
stl之string
String是C++中的重要类型,程序员在C++面试中经常会遇到关于String的细节问题,甚至要求当场实现这个类。只是由于时间关系,可能只要求实现构造函数、析构函数、拷贝构造函数等关键部分。
String的实现涉及很多C++的基础知识、内存控制及异常处理等问题,仔细研究起来非常复杂,本文主要做一个简单的总结和归纳。
一 整体框架
面试时由于时间关系,面试官一般不会要求很详尽的String的功能,一般是要求实现构造函数、拷贝构造函数、赋值函数、析构函数这几个非常重要的部分。因为String里涉及动态内存的管理,默认的拷贝构造函数在运行时只会进行浅拷贝,即只复制内存区域的指针,会造成两个对象指向同一块内存区域的现象。如果一个对象销毁或改变了该内存区域,会造成另一个对象运行或者逻辑上出错。这时就要求程序员自己实现这些函数进行深复制,即不止复制指针,需要连同内存的内容一起复制,即新堆上新开辟内存空间来存放。
除了以上四个必须的函数,这里还实现了一些附加的内容。
- 若干个运算符重载,这里的几个是常见的运算符,可以加深对String的认识和运算符重载的理解。
- 两个常用的函数,包括取字符串长度和取C类型的字符串。
- 两个处理输入输出的运算符重载,为了使用的方便,这里把这两个运算符定义为友元函数。
整体的类的框架如下所示。
class String
{
public:
String(const char *str = NULL); //通用构造函数
String(const String &str); //拷贝构造函数
~String(); //析构函数
String operator+(const String &str) const; //重载+
String& operator=(const String &str); //重载=
String& operator+=(const String &str); //重载+=
bool operator==(const String &str) const; //重载==
char& operator[](int n) const; //重载[]
size_t size() const; //获取长度
const char* c_str() const; //获取C字符串
friend istream& operator>>(istream &is, String &str);//输入
friend ostream& operator<<(ostream &os, String &str);//输出
private:
char *data; //字符串
size_t length; //长度
};
注意,类的成员函数中,有一些是加了const修饰的,表示这个函数不会对类的成员进行任何修改。一些函数的输入参数也加了const修饰,表示该函数不会对改变这个参数的值。
二 具体实现
下面逐个进行成员函数的实现。
同样构造函数适用一个字符串数组进行String的初始化,默认的字符串数组为空。这里的函数定义中不需要再定义参数的默认值,因为在类中已经声明过了。
另外,适用C函数strlen的时候需要注意字符串参数是否为空,对空指针调用strlen会引发内存错误。
String::String(const char *str)//通用构造函数
{
if (!str)
{
length = 0;
data = new char[1];
*data = '\0';
}
else
{
length = strlen(str);
data = new char[length + 1];
strcpy(data, str);
}
}
拷贝构造函数需要进行深拷贝。
String::String(const String &str)//拷贝构造函数
{
length = str.size();
data = new char[length + 1];
strcpy(data, str.c_str());
}
析构函数需要进行内存的释放及长度的归零。
String::~String()//析构函数
{
delete []data;
length = 0;
}
重载字符串连接运算,这个运算会返回一个新的字符串。
String String::operator+(const String &str) const//重载+
{
String newString;
newString.length = length + str.size();
newString.data = new char[newString.length + 1];
strcpy(newString.data, data);
strcat(newString.data, str.data);
return newString;
}
重载字符串赋值运算,这个运算会改变原有字符串的值,为了避免内存泄露,这里释放了原先申请的内存再重新申请一块适当大小的内存存放新的字符串。
String& String::operator=(const String &str)//重载=
{
if (this == &str) return *this;
delete []data;
length = str.length;
data = new char[length + 1];
strcpy(data, str.c_str());
return *this;
}
重载字符串+=操作,总体上是以上两个操作的结合。
String& String::operator+=(const String &str)//重载+=
{
length += str.length;
char *newData = new char[length + 1];
strcpy(newData, data);
strcat(newData, str.data);
delete []data;
data = newData;
return *this;
}
重载相等关系运算,这里定义为内联函数加快运行速度。
inline bool String::operator==(const String &str) const//重载==
{
if (length != str.length) return false;
return strcmp(data, str.data) ? false : true;
}
重载字符串索引运算符,进行了一个简单的错误处理,当长度太大时自动读取最后一个字符。
inline char& String::operator[](int n) const//重载[]
{
if (n >= length) return data[length-1]; //错误处理
else return data[n];
}
重载两个读取私有成员的函数,分别读取长度和C字符串。
inline size_t String::size() const//获取长度
{
return length;
}
重载输入运算符,先申请一块足够大的内存用来存放输入字符串,再进行新字符串的生成。这是一个比较简单朴素的实现,网上很多直接is>>str.data的方法是错误的,因为不能确定str.data的大小和即将输入的字符串的大小关系。
istream& operator>>(istream &is, String &str)//输入
{
char tem[1000]; //简单的申请一块内存
is >> tem;
str.length = strlen(tem);
str.data = new char[str.length + 1];
strcpy(str.data, tem);
return is;
}
重载输出运算符,只需简单地输出字符串的内容即可。注意为了实现形如cout<<a<<b的连续输出,这里需要返回输出流。上面的输入也是类似。
ostream& operator<<(ostream &os, String &str)//输出
{
os << str.data;
return os;
}
inline const char* String::c_str() const//获取C字符串
{
return data;
}
deque
转自https://blog.youkuaiyun.com/y1196645376/article/details/52938474
deque看似和vector很相似,但是 deque 能高效的在首位进行元素的插入删除;并且 deque 也支持随机访问;说明 deque 的内部内存实现要比vector复杂得多。事实上 deque 采用的是动态内存块的策略,块的内部是一段连续的内存,但是块与块之间物理内存不一定连续。
deque的元素数据采用分块的线性结构进行存储,如图所示。deque分成若干线性存储块,称为deque块。块的大小一般为512个字节,元素的数据类型所占用的字节数,决定了每个deque块可容纳的元素个数。
所有的deque块使用一个Map(一个连续空间类似array,而非stl的map容器)进行管理,每个Map数据项记录各个deque块的首地址。Map是deque的中心部件,将先于deque块,依照deque元素的个数计算出deque块数,作为Map块的数据项数,创建出Map块。以后,每创建一个deque块,都将deque块的首地址存入Map的相应数据项中。
在Map和deque块的结构之下,deque使用了两个迭代器M_start和M_finish,对首个deque块和末deque块进行控制访问。迭代器iterator共有4个变量域,包括M_first、M_last、M_cur和M_node。M_node存放当前deque块的Map数据项地址,M_first和M_last分别存放该deque块的首尾元素的地址(M_last实际存放的是deque块的末尾字节的地址),M_cur则存放当前访问的deque双端队列的元素地址。
我们下面就来总结一下,各类容器发生引用(指针)失效的情况:
因此,想实现连续分配vector内存空间的方法:hash套数组或者链表套数组。
其他:
stack
stack没有迭代器,也不能遍历,因为stack所有元素必须符合先进后出的条件。
stack的默认底层实现是deque(封闭了头端开口),像这种以底部容器完成其所有工作,具有“修改某物接口,形成另一种风貌”的性质者,成为adapter(配接器或适配器)。
同时,list也可以作为stack的底层实现。
queue
queue是先进先出的容器,也不允许遍历,没有迭代器。
queue的默认底层实现是deque(封闭了底端出口和前端的入口),同时,list也可以作为底层实现。
priority_queue
priority_queue是一个拥有权值概念的queue,没有迭代器,只允许push和pop,默认情况下由max_heap实现,即大根堆。其中max_heap是一个以vector表现的完全二叉树。因此priority_queue也是一个adapter。
以上均为序列式容器。
下面开始介绍关联式容器。
set
set底层实现为红黑树。删除节点不会导致其他节点的迭代器失效(出了他自身)。查找时间复杂度logn
set有去重的功能。
map
map底层实现也是红黑树,只是树的节点是一个个的pair,同时拥有key和value。按照key进行排序的(若想按照value排序,应该将pair放进一个vector中,并自己写一个排序函数或者函数对象传给sort算法)。
这里提一下multiset和multimap:
他们的区别就是允许有重复键值,因此他们的插入操作才用的是底层RB-tree的insert_equal()而非insert_unique()。
其余用法没有太大差别。
hashtable
hashtable就是哈希表,类似字典的东西。没有自动排序的功能。
解决hash碰撞的方式:线性探测、二次探测、拉链法、再哈希法