文章目录
STL之容器
顺序容器
顺序容器实现能按顺序访问的数据结构。
- array 静态的相接数组 (超过时,必须自己搬移数据)
- vector 动态的相接数组
- deque 双端队列
- forward_list 单链表
- list 双链表
vector 线性连续地址空间
地址连续的空间,(普通指针作为其迭代器)
size() 存放元素的实际大小
capacity() 容量大小
超过当时的容量,扩充为两倍.不足时更大
为什么尾部快:finish
- 申请空间
- 复制
- 释放旧空间
在erase元素时,容量大小是不会变的。
- vector 扩容为什么要以1.5倍或者2倍扩容?
有一个曾长因子,空间和时间的权衡。
简单来说, 空间分配的多,平摊时间复杂度低,但浪费空间也多。
具体参见算法导论中,平摊分析那一章关于动态表扩张的分析。
deque 双端队列
与 vector 的区别就是1.允许头部操作 2.由一段一段定量的连续空间构成
索引就是map,分段数组默认512字节
对其排序,可以先复制到 vector 然后sort之后再复制回来
deque 的迭代器
它必须能够指出分段连续空间(亦即缓冲区)在哪里,其次它必须能够判断自己是否已经处于其所在缓冲区的边缘,如果是,一旦前进或后退就必须跳跃至下一个或上一个缓冲区。为了能够正确跳跃,deque必须随时掌握管控中心(map)。所以在迭代器
中需要定义:当前元素的指针,当前元素所在缓冲区的起始指针,当前元素所在缓冲区的尾指针,指向map中指向所在缓区地址的指针,分别为cur, first, last, node
deque 的内部维护了两个start
,fnish
的迭代器,start的cur指针指向第一个元素,finish的cur指针指向最后元素的下一个位置
deque 自己配置了两个专属的空间配置器
重载++,-- 等....
关联容器
关联容器实现能快速查找( O(log n)
复杂度)的数据结构。
项目 | Value | 内部实现都是红黑树 |
---|---|---|
set | 唯一键的集合,按照键排序 | |
map | 键值对的集合,按照键排序,键是唯一的 | |
multiset | 键的集合,按照键排序 | |
multimap | 键值对的集合,按照键排序 |
multimap 是如何解决重复的键值的?
同二叉查找树
如何遍历:
/*
multimap中的三种遍历方法
multimap中如果没有查找到相应元素,则返回的迭代器是依据该元素的排列顺序该键应该插入的位置
如果找不到,则方法一和方法二返回的两个迭代器应该相等
*/
#include <iostream>
#include <map>
#include <string>
#include <utility>
using namespace std;
int main()
{
multimap<string, string> mulMap;
mulMap.insert(make_pair("鲁迅", "朝花夕拾"));
mulMap.insert(make_pair("鲁迅", "阿Q正传"));
mulMap.insert(make_pair("鲁迅", "野草"));
mulMap.insert(make_pair("罗贯中", "三国演义"));
mulMap.insert(make_pair("罗贯中", "隋唐志传"));
mulMap.insert(make_pair("琼瑶", "还珠格格"));
mulMap.insert(make_pair("琼瑶", "情深深雨蒙蒙"));
typedef multimap<string, string>::iterator multiMapItor;
//方法一:推荐
string author("鲁迅");
cout << author << "的书籍有:" << endl;
//equal_range 返回头尾迭代器
pair<multiMapItor, multiMapItor> pos = mulMap.equal_range(author);
while(pos.first != pos.second)
{
cout << pos.first->second << endl;
++pos.first;
}
cout << endl;
//方法二:
author.assign("罗贯中");
cout << author << "的书籍有:" << endl;
multiMapItor beg = mulMap.lower_bound(author);
multiMapItor end = mulMap.upper_bound(author);
while(beg != end)
{
cout << beg->second << endl;
++beg;
}
cout << endl;
//方法三:不推荐
author.assign("琼瑶");
cout << author << "的书籍有:" << endl;
typedef multimap<string, string>::size_type sz_type;
sz_type entries = mulMap.count(author);
multiMapItor itor = mulMap.find(author);
for(sz_type cnt = 0; cnt != entries; ++cnt)
cout << (itor++)->second << endl;
system("pause");
return 0;
}
无序关联容器
无序关联容器提供能快速查找(均摊 O(1) ,最坏情况 O(n) 的复杂度)的无序(哈希)数据结构。
项目 | Value | 内部实现都是散列表 |
---|---|---|
unordered_set | 唯一键的集合,按照键生成散列 | |
unordered_map | 键值对的集合,按照键生成散列,键是唯一的 | |
unordered_multiset | 键的集合,按照键生成散列 | |
unordered_multimap | 键值对的集合,按照键生成散列 |
容器适配器
容器适配器提供顺序容器
的不同接口。
- stack 标准是以deque实现的,但是也可以使用list实现
不允许有遍历行为
没有迭代器
- queue 标准是以deque实现的,但是也可以使用list实现
不允许有遍历行为
没有迭代器
- priority_queue 堆(完全二叉树实现的)
STL之算法:
std::sort的实现
- 为什么堆排序一般快不过快速排序?
- 堆排序数据访问的方式没有快速排序友好。(没有顺序遍历,对CPU缓存不友好)
- 对于同样的数据,在排序过程中,堆排序算法的数据交换次数(建化,VAL树)要多于快速排序。(因为需要建堆,就会打乱有些有序序列)
- 快速排序快得无懈可击吗?
pivot 的选择问题
- 插入排序什么时候快?
插入排序在几乎排好序的序列上很快
插入排序在这种情况下,只需要从头到尾扫描一遍,交换、移动少数元素即可;时间复杂度近乎 Θ(n)。究其原因,堆排或快排按照各自的要求,将已经近似排好序的序列打乱,而后又排序整理,没有用到「几乎已经排好序」的先验知识,所以在这种情况下不如插入排序快就是自然的了。
将三点结合起来,形成内省式排序:
- 在数据量很大时采用正常的快速排序,此时效率为O(logN)。
- 一旦分段后的数据量小于某个阈值,就改用插入排序,因为此时这个分段是基本有序的,这时效率可达O(N)。
- 在递归过程中,如果递归层次过深,分割行为有恶化倾向时,它能够自动侦测出来,使用堆排序来处理,在此情况下,使其效率维持在堆排序的O(N logN),但这又比一开始使用堆排序好。
// sort
template<typename _RandomAccessIterator, typename _Compare>
inline void
__sort(_RandomAccessIterator __first, _RandomAccessIterator __last,
_Compare __comp)
{
if (__first != __last)
{
std::__introsort_loop(__first, __last,
std::__lg(__last - __first) * 2,
__comp); // 内省式排序
std::__final_insertion_sort(__first, __last, __comp);//插入排序,还有更深的优化(还比较重要),有时间再研究!!!
}
}
/// This is a helper function for the sort routine.
template<typename _RandomAccessIterator, typename _Size, typename _Compare>
void
__introsort_loop(_RandomAccessIterator __first,
_RandomAccessIterator __last,
_Size __depth_limit, _Compare __comp)
{
while (__last - __first > int(_S_threshold)) //下一次进来时处理左子序列
{
if (__depth_limit == 0)//允许递归的深度,调用者传递的值为2logN
{
std::__partial_sort(__first, __last, __last, __comp); //堆排序
return;
}
--__depth_limit;
_RandomAccessIterator __cut =
std::__unguarded_partition_pivot(__first, __last, __comp);
//找到pivot:首部、尾部或者中间位置的元素的中间植,
//并根据 pivot 将区间分割为两个子序列
std::__introsort_loop(__cut, __last, __depth_limit, __comp);//快排右子序列
__last = __cut;
}
}
循环前半部分都是用来处理恶化情况
最后的last = cut将终点位置调整到了分割点,那么此时的[first, last)区间就是左子序列了。又因为这是一个循环结构,那么在下一次的循环中,左子序列便得到了处理。只是并未以递归来调用。
我们来比较一下两者的区别,试想,如果一个序列只需要递归两次便可结束,即它可以分成四个子序列。原始的方式需要两个递归函数调用,接着两者各自调用一次,也就是说进行了7次函数调用,如下图左边所示。但是STL这种写法每次划分子序列之后仅对右子序列进行函数调用,左边子序列进行正常的循环调用,如下图右边所示。
两者区别就在于STL节省了接近一半的函数调用,由于每次的函数调用有一定的开销,因此对于数据量非常庞大时,这一半的函数调用可能能够省下相当可观的时间。真是为了效率无所不用其极!!
最小分段阈值 _S_threshold = 16
数据长度小于该阈值时,再使用递归来排序显然不划算,递归的开销相对来说太大。而此时整个区间内部有多个元素个数少于16的子序列,终止快排,调用插入
参考:
http://www.udpwork.com/item/12284.html
STL之空间配置器:
1. 标准空间配置器 std::allocator
SGI定义的标准空间配置器,SGI自己未用,也不建议用户使用
只是简单的对new
和delete
进行了封装
2.特殊的空间配置器 std::alloc
alloc::allocate()
内存申请::construct()
对象构造alloc::deallocate()
内存释放::destory()
对象析构
对象的构造与析构
内存空间的配置与释放
第一层配置器 >128 B __malloc_alloc_template
malloc ,free,remalloc
没有使用C++ new-handler机制,因为没有使用 new
C++ new-handler机制:用户可以自己定义需求无法满足时,调用的函数.也就是在丢出std::bad_alloc之前,会先调用你制定的函数.
allocate() -> malloc() ->失败->oom_malloc()->
reallocate()-> realloc() -> 失败->oom_realloc()->
两者都有内循环不断调用"内存不足处理例程",如果用户没有设置,则丢出`bad_allloc`
这里关于是释放内存的方法有点质疑,因为没有申请成功,何谈释放呐??
template <int inst>
class __malloc_alloc_template {
private:
//malloc调用内存不足时调用函数
static void *oom_malloc(size_t);
//realloc调用内存不足时调用函数
static void *oom_realloc(void *, size_t);
//错误处理函数,类似C++的set_new_handler,默认值为0,如果不设置,则内存分配失败时,返回THROW_BAD_ALLOC
#ifndef __STL_STATIC_TEMPLATE_MEMBER_BUG
static void (* __malloc_alloc_oom_handler)();
#endif
public:
static void * allocate(size_t n)
{
void *result = malloc(n); 第一级配置器直接使用malloc分配内存
if (0 == result) result = oom_malloc(n);//如果分配失败,则调用oom_malloc()
return result;
}
static void deallocate(void *p, size_t /* n */)
{
free(p); //第一级配置器用free回收内存
}
static void * reallocate(void *p, size_t /* old_sz */, size_t new_sz)
{
void * result = realloc(p, new_sz); //第一级配置器用reallocate重分配内存
if (0 == result) result = oom_realloc(p, new_sz);//分配失败,调用oom_realloc分配
return result;
}
// 设置分配错误处理函数,用于在oom_malloc和oom_realloc中使用
static void (* set_malloc_handler(void (*f)()))()
{
void (* old)() = __malloc_alloc_oom_handler;
__malloc_alloc_oom_handler = f;
return(old);
}
}
#ifndef __STL_STATIC_TEMPLATE_MEMBER_BUG
template <int inst>
void (* __malloc_alloc_template<inst>::__malloc_alloc_oom_handler)() = 0;
#endi
template <int inst>
void * __malloc_alloc_template<inst>::oom_malloc(size_t n)
{
void (* my_malloc_handler)();//声明一个函数指针,用于赋值 __malloc_alloc_oom_handler
void *result;//返回的内存指针
for (;;) { // 不断尝试释放内存,分配,再释放,再分配...
my_malloc_handler = __malloc_alloc_oom_handler;
if (0 == my_malloc_handler) { __THROW_BAD_ALLOC; }//为设置处理函数时,抛出错误
(*my_malloc_handler)(); // 调用处理函数
result = malloc(n); // 再重新分配内存。
if (result) return(result);//如果分配成功,返回指针
}
}
template <int inst>
void * __malloc_alloc_template<inst>::oom_realloc(void *p, size_t n)
{
void (* my_malloc_handler)();
void *result;
for (;;) { // 不断尝试释放内存,分配,再释放,再分配...
my_malloc_handler = __malloc_alloc_oom_handler;
if (0 == my_malloc_handler) { __THROW_BAD_ALLOC; }////为设置处理函数时,抛出错误
(*my_malloc_handler)(); // 调用处理函数
result = realloc(p, n); // 再重新分配内存。
if (result) return(result);////如果分配成功,返回指针
}
}
第二层配置器 < 128 B 内存池 __default_alloc_template
内存池管理,整体结构:
chunk
= chunk_alloc
,从内存池取出空间给free_list
自由链表节点定义:
//union 结构,代表指针/实际区块,节省内存
union obj {
union obj * free_list_link;//指向下一个内存的地址
char client_data[1]; //内存的首地址
};
30-> 32
回收就 free_list
连起来就行了
默认配置器:
多态
C++ 的三大特性,封装,继承,多态。封装可以使得代码模块化,继承可以扩展已存在的代码。多态性是允许你将父对象设置成为和一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。简单的说:多态的目的则是为了接口重用,也就是说,不论传递过来的究竟是那个类的对象,函数都能够通过同一个接口调用到适应各自对象的实现方法。
简而言之就是用父类指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数(这里子类就可以有多个啊)
。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议。
C++ 支持两种多态性:编译时多态性,运行时多态性。
- a、编译时多态性(静态多态):通过重载函数实现
- b、运行时多态性(动态多态):通过虚函数实现。
虚函数与虚函数实现机制
占坑。。。。
lamada 表达式
[capture](parameters)->return-type{body}
智能指针
scoped_ptr
scoped_ptr所指向的对象在作用域之外会自动得到析构
Here's an example that uses scoped_ptr.
#include <boost/scoped_ptr.hpp>
#include <iostream>
struct Shoe { ~Shoe() { std::cout << "Buckle my shoe\n"; } };
class MyClass {
boost::scoped_ptr<int> ptr;
public:
MyClass() : ptr(new int) { *ptr = 0; }
int add_one() { return ++*ptr; }
};
int main()
{
boost::scoped_ptr<Shoe> x(new Shoe);
MyClass my_instance;
std::cout << my_instance.add_one() << '\n';
std::cout << my_instance.add_one() << '\n';
}
The example program produces the beginning of a child's nursery rhyme:
1
2
Buckle my shoe
shared_ptr
引用计数(reference counting),也就是说shared_ptr是支持复制的,复制一个shared_ptr的本质是对这个智能指针的引用次数加1,而当这个智能指针的引用次数降低到0的时候,该对象自动被析构
需要特别指出的是,如果shared_ptr所表征的引用关系中出现一个环,那么环上所述对象的引用次数都肯定不可能减为0那么也就不会被删除,为了解决这个问题引入了weak_ptr。
weak_ptr
std::weak_ptr 是一种智能指针,它对被 std::shared_ptr 管理的对象存在非拥有性(“弱”)引用。在访问所引用的对象前必须先转换为 std::shared_ptr。
std::weak_ptr 用来表达临时所有权的概念:当某个对象只有存在时才需要被访问,而且随时可能被他人删除时,可以使用 std::weak_ptr 来跟踪该对象。需要获得临时所有权时,则将其转换为 std::shared_ptr,此时如果原来的 std::shared_ptr 被销毁,则该对象的生命期将被延长至这个临时的 std::shared_ptr 同样被销毁为止。
std::weak_ptr 的另一用法是打断 std::shared_ptr 所管理的对象组成的环状引用。若这种环被孤立(例如无指向环中的外部共享指针),则 shared_ptr 引用计数无法抵达零,而内存被泄露。能令环中的指针之一为弱指针以避免此情况。
unique_ptr
某个时刻只能有一个unique_ptr指向一个给定对象。当unique_ptr被销毁时,它所指向的对象也被销毁。