概述
一、C++的STL
1. 智能指针
1.1. auto_ptr
(无论什么情况下都不要使用,C++98标准,到了C++11标注引入了shared_ptr,unique_ptr,,weak_ptr)
1.2. shared_ptr
shared_ptr和auto_ptr最大的区别就是,shared_ptr解决了指针间共享对象所有权的问题,也就是auto_ptr中的赋值的奇怪问题。所以满足了容器的要求,可以用于容器中。而auto_ptr显然禁止共享对象所有权,不可以用于容器中。
(1)默认构造函数,函数参数为变量地址
(2)拷贝构造函数,函数参数为引用
(3)重载函数operator*
(4)重载函数operator->
(5)重载函数operator=,使之可以进行隐性转换操作,注:实际C++源码中是没有这个的,构造函数用关键字explicit声明,在定义对象时必须显示调用初始化式,不能使用赋值操作符进行隐式转换。
(6)空函数判断
(7)引用次数统计函数
注意:使用shared_ptr也要引用头文件#include<memory>,如下使用简单的源码实现
#ifndef _SHARED_PTR_H
#define _SHARED_PTR_H
/*
一个模板T* ptr,指向实际的对象
一个引用次数
重载operator*和operator->,使得能像指针一样使用share_ptr
重载copy constructer,使其引用次数加一
重载operator=,如果原来的shared_ptr已经有对象
*/
template<typename T>
class shared_ptr
{
public:
shared_ptr(T* p) : count(new int(1)),_ptr(p);//默认的构造函数,必须自己显示的开辟内存
shared_ptr(shared_ptr<T>& p):count(&(++p.count)),_ptr(p.ptr);//拷贝构造函数,属于强制转换,显式
T& operator*();//
T* operator->();
shared_ptr<T> & operator=(shared_ptr<T>& p);//重载等号,保证保存同为shared_ptr的指针能够互转,等号计数器减1,右边计数器加1.
~shared_ptr();
bool empty();//检查是否指向同一个空T
int GetCount();
private:
int *count;//引用计数器
T* _ptr;//每创建一个对象,则有一个指针指向一个shared_ptr的类型
};
#include"_shared_ptr.h"
template<typename T>
shared_ptr<T>::shared_ptr(T* p=nullptr):count(new int(1)),_ptr(p)//默认的构造函数,显式开辟内存
{
if(_ptr){
count = new int(1);//如果初始化不为空,则计数器为1
}else{
count = new int(0);//如果初始化为空,则计数器为0
}
}
template<typename T>
shared_ptr<T>::shared_ptr(shared_ptr<T>&p):count(&(++p.count)),_ptr(p.ptr)//拷贝构造函数,属于强制转换,显式
{
if(this != p)
{
this->_ptr = p._ptr;
this->count = p.count;
*(this->count)++;
}
}
template<typename T>
T& shared_ptr<T>::operator*(){
return *(this->_ptr);
}
template<typename T>
T* shared_ptr<T>::operator->(){
return this->_ptr;
}
template<typename T>
shared_ptr<T>& shared_ptr<T>::operator=(shared_ptr<T>& p){
//重载等号,保证保存同为shared_ptr的指针能相互转化,等号左边计数器减1,右边计数器加1.
++*(p.count);//等式右边引用次数加1,左边引用次数减1
if(this->_ptr && 0 == --*this->count) //当左边引用次数为0
{
delete count;
delete _ptr;
}
this->count = p.count;
this->_ptr = p._ptr;
return *this;
}
template<typename T>
bool shared_ptr<T>::empty()//检查是否有一个指向空T,当为空的时候,count必须为0
{
return _ptr == nullptr;
}
template<typename T>
int shared_ptr<T>::GetCount()
{
return *count;
}
1.3. unique_ptr
unique_ptr的构造函数与auto_ptr一样,构造函数采用explicit声明,防止复制/拷贝时不必要的类型转换,在定义对象时必须显示调用初始化式,不能使用赋值操作符进行隐式转换。注:此代码未包含自定义删除器
成员函数:
(1)get函数:获取内部对象的指针,由于已经重载了()方法,因此和直接使用对象是一样的。
(2)release函数:放弃内部对象的所有权,将内部指针置空,此指针需要手动释放。
(3)reset函数:销毁内部对象并接受新的对象的所有权。
(4)默认构造函数,函数参数为变量地址
(5)拷贝构造函数,函数参数为引用
(6)重载函数operator*
(7)重载函数operator->
(8)重载函数operator=
#ifndef UNIQUE_PTR_H
#define UNIQUE_PTR_H
template<typename T>
class _unique_ptr
{
public:
_unique_ptr(T* p = nullptr) :_ptr(p);
//默认构造函数
_unique_ptr(_unique_ptr<T>& p) :_ptr(p._ptr); //拷贝构造函数
T& operator*();
T* operator->();
_unique_ptr<T>& operator=(_unique_ptr<T>& p); //赋值操作符重载
T* get();
T* release();
void reset(T* p);
private:
T * _ptr;
};
#endif
#include"unique_ptr.h"
template<typename T>
_unique_ptr<T>::_unique_ptr(T* p)
{
_ptr = p;
}
template<typename T>
_unique_ptr<T>::_unique_ptr(_unique_ptr<T>& p)
{
_ptr = p.release();
}
template<typename T>
T& _unique_ptr<T>::operator*()
{
return *(this->_ptr);
}
template<typename T>
T* _unique_ptr<T>::operator->()
{
return this->_ptr;
}
template<typename T>
_unique_ptr<T>& _unique_ptr<T>::operator=(_unique_ptr<T>& p)
{
if (p.get() != this->get())
{
delete _ptr;
}
_ptr = p._ptr;
}
template<typename T>
T* _unique_ptr<T>::get()
{
return this->_ptr;
}
template<typename T>
T* _unique_ptr<T>::release()
{
T* tmp = _ptr;
delete _ptr;
return tmp;
}
template<typename T>
void _unique_ptr<T>::reset(T* p)
{
if (p != _ptr)
{
delete _ptr;
}
_ptr = p;
}
4、weak_ptr
weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 进行该对象的内存管理的是那个强引用的 shared_ptr。 weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。定义在 memory 文件中(非memory.h), 命名空间为 std.
总结:
封装普通指针使其表现的像普通指针一样。超过类的作用域,将会自己调用析构函数,自动释放资源
auto_ptr:不可以用于容器,不建议使用,不支持复制和赋值,如果进行了赋值和复制操作,并不报错
unique_ptr:不支持复制和赋值,直接赋值会报错,同一时刻对象仅能拥有一个unique_ptr指向自身
shared_ptr:解决指针间对象共享所有权的问题,auto_ptr是独享,允许多个指针指向同一个对象。基于引用计数,不要一个原始指针初始化多个shared_ptr,避免循环使用避免内存泄露。
对象初始化时应用数为1,指向对象成为另一个对象副本,引用数加一,析构减一。
weak_ptr:不控制对象生命周期的智能指针,只提供了管理对象的访问手段,用于协助shared_ptr的工作,用于观测资源的使用情况。use_count()可以一观察资源的应用数。
2. 容器
STL有什么基本组成
STL主要由:以下几部分组成:
容器,迭代器,仿函数算法,分配器,配接器
他们之间的关系:分配器给容器分配存储空间,算法通过迭代器获取容器中的内容,仿函数可以协助算法完成各种操作,配接器用来套接适配仿函数
2.1 vector
底层为数组,支持随机访问,节点大小是动态的,支持下标访问。随机存取效率很高(O(1)),插入效率不高。
扩容原理:以原大小的两倍配置一份新空间(看编译器的,vs是1.5,gcc是2),如果要插入的值的数量超过当前容量的两倍,是直接扩容到能容纳所有元素的空间大小,而不会拘泥于多少倍,将原空间数据拷贝过来,会导致迭代器失效。
2.2 list
底层为双向链表,内存空间不连续,只能通过指针访问数据,插入删除高效,随机存取非常没有效率。适用于对象需要大量删除插入操作的环境。
list的iterator不支持+,+=,<操作
list.sort():合并前两个元素,合并后两个元素,两个子序列合并,序列大小变化顺序为2->4->8->16…完成排序(O(log(n)))
2.3 deque
双向队列,一个中央控制器+多个缓冲区,支持首尾快速增删,支持随机访问。
2.4 stack
底层为deque/list,封闭头部,不使用vector作为底层的原因是vector扩容耗时。
2.5 queue & priority_queue
queue:底层为deque/list,封闭头部,不使用vector作为底层的原因是vector扩容耗时。
priority_queue:优先队列,以vector为底层,以heap为处理规则管理底层实现。
2.6 红黑树为底层的容器
有序 | 元素可重复 | |
---|---|---|
set | 是 | 否 |
multiset | 是 | 是 |
map | 是 | 否 |
multimap | 是 | 是 |
插入删除,查找时间复杂度为O(log(n))
作为关联容器,set不同于map的地方在于,set的key就是value。
2.7 红黑树
红黑树的特点:
根节点是黑色,叶节点是黑色的null节点,任意节点到其叶节点经过的黑色节点数量是相同的。
去除掉黑色null节点,红黑树是一层黑一层红。
红黑树是近平衡的二叉搜索树,和平衡二叉搜索树相比,红黑树的平衡没有那么平衡,插入后可以保证调整三次之间可以使树达到平衡条件
2.8 hash表为底层的容器
有序 | 元素可重复 | |
---|---|---|
hash_set | 否 | 否 |
hash_multiset | 否 | 是 |
hash_map | 否 | 否 |
hash_multimap | 否 | 是 |
查询时间复杂度O(1),均无序
hash表冲突解决
1、扩大每个桶的容量
2、闭地址:桶后增加溢出链
3、开地址;寻找下一个有空间的桶
3. 容器失效
序列性容器::(vector和list和deque)
erase迭代器不仅使所指向被删元素的迭代器失效,而且使被删元素之后的所有迭代器失效,所以不能使用erase(iter++)的方式,但是erase的返回值为下一个有效的迭代器。
所以正确方法为::
for( iter = c.begin(); iter != c.end(); )
iter = c.erase(iter);
关联性容器::(map和set比较常用)
erase迭代器只是被删元素的迭代器失效,但是返回值为void,所以要采用erase(iter++)的方式删除迭代器,
所以正确方法为::
for( iter = c.begin(); iter != c.end(); )
c.erase(iter++);
删除:Erase
结论:在STL里,我们不能以指针来看待迭代器,指针是与内存绑定的,而迭代器是与容器里的元素绑定的,删除了之后,该迭代器就失效了,在对其重新赋值之前,不能再访问此迭代器。
1.对于序列容器vector,deque来说,使用erase(itertor)后,后边的每个元素的迭代器都会失效,但是后边每个元素都会往前移动一个位置,但是erase会返回下一个有效的迭代器;
2.对于关联容器map set来说,使用了erase(iterator)后,当前元素的迭代器失效,但是其结构是红黑树,删除当前元素的,不会影响到下一个元素的迭代器,所以在调用erase之前,记录下一个元素的迭代器即可。
3.对于list来说,它使用了不连续分配的内存,并且它的erase方法也会返回下一个有效的iterator,因此上面两种正确的方法都可以使用。
插入:Insert
插入一旦让capacity变化之后,所有的迭代器都失效了!capacity发生变化,容器内部做的不仅仅是增加capacity这么简单,因为容器所在内存后面可能没有足够的内存让我们使用,所以,容器要重新开辟一段足够大的内存来存储容器里的元素,当前内存会被释放。这样一来,迭代器自然失效了。
C++ primer 总结:迭代器失效
(1)增加元素到容器后
- 对于vector和string,如果容器内存被重新分配,iterators,pointers,references失效;如果没有重新分配,那么插入点之前的iterator有效,插入点之后的iterator失效;
- 对于deque,如果插入点位于除front和back的其它位置,iterators,pointers,references失效;当我们插入元素到front和back时,deque的迭代器失效,但reference和pointers有效;
- 对于list和forward_list,所有的iterator,pointer和refercnce有效。
(2)从容器中移除元素后
- 对于vector和string,插入点之前的iterators,spointers,references有效;off-the-end迭代器总是失效的;
- 对于deque,如果插入点位于除front和back的其它位置,iterators,pointers,references失效;当我们插入元素到front和back时,off-the-end失效,其他的iterators,pointers,references有效;
- 对于list和forward_list,所有的iterator,pointer和refercnce有效。
(3)在循环中refresh迭代器
- 当处理vector,string,deque时,当在一个循环中可能增加或移除元素时,要考虑到迭代器可能会失效的问题。我们一定要refresh迭代器。
-
int arr[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; deque<int> v(arr,arr+sizeof(arr)/sizeof(*arr)); for (auto it = v.begin(); it != v.end(); ) { if ((*it) & 1) { it = v.insert(it, *it); it += 2; } else it = v.erase(it); }
- 至于it+=2,很容易解释,insert之后,it指向新增加的元素,+2之后,it指向下一个要处理的元素。
(4)在循环不变式中不要store off-the-end迭代器
这个很容易理解了,增加或移除元素之后,off-the-end失效了,不store的话,每次从end()函数中取的都是最新的off-the-end,自然不会失效。
为什么需要迭代器:
Iterator类的访问方式就是把不同集合类的访问逻辑抽象出来,使得不用暴露集合内部的结构而达到循环遍历集合的效果。
STL里resize和reserve的区别
resize()会构造出新的元素,而reserve()只是为元素预留出空间而已。reserve()只是预留空间,而不会构造新的元素出来
STL的allocaotr
STL的分配器用于封装STL容器在内存管理上的底层细节。
在C++中,其内存配置和释放如下:
new运算分两个阶段:(1) 调用::operator new配置内存;(2)调用对象构造函数构造对象内容
delete运算分两个阶段:(1) 调用对象希构函数;(2)掉员工::operator delete释放内存
为了精密分工,STL allocator将两个阶段操作区分开来:
内存配置有alloc::allocate()负责,
内存释放由alloc::deallocate()负责;
对象构造由::construct()负责,
对象析构由::destroy()负责。
同时为了提升内存管理的效率,减少申请小内存造成的内存碎片问题,SGI STL采用了两级配置器,当分配的空间大小超过128B时,会使用第一级空间配置器;当分配的空间大小小于128B时,将使用第二级空间配置器。
第一级空间配置器直接使用malloc()、realloc()、free()函数进行内存空间的分配和释放,
第二级空间配置器采用了内存池技术,通过空闲链表来管理内存。
二、关于内存泄露
2.1 段错误
段错误通常发生在访问非法内存地址的时候,具体来说分为以下几种情况:
- 使用野指针
- 试图修改字符串常量的内容
strcpy()函数
1,检查指针有效性;
2,返回目的指针des;
3,源字符串的末尾 '