string
string一般不被认为是一个c++容器,但是和容器有很多共同点。
string是模板basic_string对char类型的特化,可以认为是一个存放char类型数据的容器,真正的容器与string最大的不同点是里面可以存放任意类型的对象(不止char)。
跟大部分容器一样 string具有下列成员函数 begin得到对象起始点,end得到对象结束点,[begin,end)—begin指向容器内第一个元素 end指向最后一个元素的后面位置,当begin==end容器为空,empty判断容器是否为空,size得到容器的大小,sawp和另外一个容器交换内容
在string的情况下考虑到和C字符串的兼容,当容器为空 end和begin一样一起指向代表字符串结尾的\0字符
容器的开始结束点,容器会记录其状态是否为空,容器大小,容器支持swap直接交换容器内容——这几点是所有容器都有的共同点
string的内存不具大致如下
和vector非常相似 连续内存存放
string和C字符串的不同处:
string负责自动维护字符串的生命周期(引用计数 类似shared_ptr)
string支持字符串的拼接操作符(+,+=)
string支持字符串的查找操作(find,rfind)
string支持从istream安全地读入字符串(getline)
string支持给需要const cahr的接口传递字符串内容 (通过c_str)
string支持到数字的互转(stoi-str2int , to_string—int2string)
等
推荐在代码中尽量使用string来管理字符串。 但是对于对外暴露的接口,情况有一点复杂。 除非已知调用者已经持有string,不建议在接口中使用const string&。
如果函数里不对字符串做复杂处理的话,建议使用const char,可以避免调用者只有C字符串时编译器自动构造string(char可以自动调用string构造,但是使用const cahr则可以阻止char隐式地转换为string string构造和析构的代价都不低)。
反之 如果在函数内要做复杂的字符串处理,需要用到string的成员函数,则考虑下面的策略: 1 如果不修改字符串内容,使用cosnt string&或c++17的string_view作为参数类型。string_view在传入C字符串的情况下也不会引发不必要的内存复制。
2 需要修改字符串内容、但不能影响调用者传入的字符串,使用string作为参数类型(拷贝)
3 需要修改调用者传入的字符串 使用string&作为参数类型(一般不推荐)
vector
c++中的vector相当于java中的ArrayList和python中的list
和string相似,vector的成员在连续内存里存放,同时begin、end、front、back成员函数指向的位置和string一样
vector允许下面的操作(不完全):
可以用中括号[]的下标来访问其成员(同string)
可以使用data来获得指向其内容的裸指针(同string)
可以使用capacity来获得当前分配的存储空间大小,移元素数量计(同string)
可以使用reserve来改变所需的存储空间的大小,成功后capacity()会改变(同string)
可以使用resize来改变其大小,成功后size()会改变(同string)
可以使用pop_back来删除最后一个元素(同string)
可以使用push_back在尾部插入一个元素(同string)
可以使用insert在指定位置插入一个元素(同string)
可以使用erease在指定位置删除一个元素(同string)
可以使用emplace在指定位置构造(拷贝构造 or 移动构造)一个元素
可以使用emplace_back在尾部新构造(拷贝构造 or 移动构造)一个元素
当一个容器存在push_xxx,pop_xxx成员函数存在时,说明容器对指定位置的删除和插入性能较高。vector适合在尾部操作,由它的内存布局决定,只有在尾部插入和删除时,其他元素才会不需要移动,除非内存空间不足导致需要重新分配内存空间。
vector的push_back、insert、reserve、resize等函数可能导致内存重新分配,或者当insert、erease导致元素位置移动时,vector会试图把元素”移动(不是移动构造的那个移动,这里叫拷贝更合适)”新的内存区域。vector通常保证强异常安全性,如果元素类没有提供一个保证不抛异常的移动构造函数(nonexcept修饰),vector在push或insert一个新元素到容器中(容器中的内存中 构造容器元素)会调用拷贝构造。因此,对于拷贝代价较高的自定义元素类型,我们应当定义移动构造函数,并标其为 noexcept,或只在容器中放置对象的智能指针。这就是为什么我之前需要在 smart_ptr 的实现中标上 noexcept 的原因。
为什么一定*noexcept强制移动构造函数不要抛出异常?
移动构造函数抛出异常后,catch处理不可以吗?
答:catch住也没有用了。仔细想一下,我现在要把vector里的两个对象移到一个新的vector,移第一个成功,第二个时有异常,然后vector该怎么办?现在两个vector都废掉了。
为什么拷贝构造函数被允许抛出异常?
答:拷贝不影响旧的容器。即使发生异常,至少老的那个还是好的。这就是异常安全。
class Obj2 {
public:
Obj2()
{
cout << "Obj2()\n";
}
Obj2(const Obj2&)
{
cout << "Obj2(const Obj2&)\n";
}
Obj2(Obj2&&) noexcept
{
cout << "Obj2(Obj2&&)\n";
}
};
int main()
{
vector<Obj2> v2;
v2.reserve(2);
v2.emplace_back();//Obj2() forward中通过带参构造直接构造
v2.emplace_back();//Obj2() forward中通过带参构造直接构造
v2.emplace_back();
//Obj2() forward中通过带参构造直接构造
//Obj2(Obj2&&)
//Obj2(Obj2&&)
}
输出
Obj2()
Obj2()
Obj2()
Obj2(Obj2&&)
Obj2(Obj2&&)
头两个在已有空间上(reserve(2) 预申请了两个obj2的空间)成功构造-输出两个Obj2()。
第三个时发现空间不足(因为之前只预申请2个obj的空间 reserve(2)),系统会请求更大的空间,大小由实现决定(比如两倍)。有了足够的空间后,就会在新空间的第三个的位置构造(第三个obj2)-输出第三个Obj2(),成功之后再把头两个移动过来-输出两个移动构造Obj2(const Obj2&&)!!!!
void push_back( const T& value ); //空间足够的前提下调用Alloc_traits::construct(this->_M_impl, this->_M_impl._M_finish,__x)
static typename std::enable_if<__is_custom_pointer<_Ptr>::value>::type
construct(_Alloc& __a, _Ptr __p, _Args&&... __args)
{
_Base_type::construct(__a, std::__to_address(__p), std::forward<_Args>(__args)...);
}
static auto construct(_Alloc& __a, _Tp* __p, _Args&&... __args)
-> decltype(_S_construct(__a, __p, std::forward<_Args>(__args)...))
{ _S_construct(__a, __p, std::forward<_Args>(__args)...); }
static _Require<__has_construct<_Tp, _Args...>>
_S_construct(_Alloc& __a, _Tp* __p, _Args&&... __args)
{ __a.construct(__p, std::forward<_Args>(__args)...); }
//__a.construct就是下面这个construct
template <class U, class... Args>
void construct (U* p, Args&&... args);
::new ((void*)p) U (forward<Args>(args)...);
_Args&&为万能引用,当实参args为左值 定义其remove reference后的类型为T(T为非引用类型),则根据万能引用规则和引用折叠,认为Args == T&,在之后3次construct中
3次调用forward ,forward后的是个左值(因为是cast为引用 所以forward中的cast不会调用构造函数),在p空间上inplacement new调用U的拷贝构造函数,在容器内存空间内拷贝构造一个新对象。———总的来说push_back中传入一个左值就只调用一次拷贝构造 将传入的实参在容器内存空间内拷贝一份
**当实参为args为右值调用的为下面这个版本push_back,实际调用的就是emplace_back**
void push_back( T&& value );//实际上时调用 emplace_back(std::move(value))
template< class... Args >
reference emplace_back( Args&& __args );
//下面讲emplace_back
//空间足够的前提下调用
_Alloc_traits::construct(this->_M_impl, this->_M_impl._M_finish,std::forward<_Args>(__args)...);
//将 emplace_back() 和 push_back() 中区别最大的程序拎出来看:
_Alloc_traits::construct(this->_M_impl, this->_M_impl._M_finish,
std::forward<_Args>(__args)...); // emplace_back()
_Alloc_traits::construct(this->_M_impl, this->_M_impl._M_finish,
__x); // push_back()
//forward中
template<typename _Tp>
constexpr _Tp &&forward(typename std::remove_reference<_Tp>::type &__t) noexcept {
return static_cast<_Tp &&>(__t);
}
static_cast —-会根据模板类型调用_Tp的拷贝构造,移动构造 or 参数构造,或者引用类型转换(不调构造) 来完成对应的类型转换
emplaceback中传入了一个左值args 定义其remove reference后的类型为T(T为非引用类型),则根据万能引用规则和引用折叠,认为Args == T&,forward后的是个左值(因为是cast为引用 所以返回的是左值对象args==x本身),在之后3次construct中3次调用forward ,forward后的是个左值(因为是cast为引用 所以forward中的cast不会调用构造函数),在p空间上inplacement new调用U的拷贝构造函数,在容器内存空间内拷贝构造一个新对象。——emplace_back中传入了一个左值的操作和push_back一样 最终都是调用一次拷贝构造 将传入的实参在容器内存空间内拷贝一份
emplace_back中传入了一个右值args 定义其remove reference后的类型为T(T为非引用类型),则根据万能引用规则和引用折叠,认为Args == T,forward
,在之后3次construct中3次调用forward,forward中调用static_cast
emplace_back中传入了带参构造函数所需要的参数args,forward中的static_cast
push_back()需要传入一个对象x,push_back中会调用construct—implacement new 用传入的对象x在vector已有的内存上拷贝构造对象
emplace_back()可以只传入构造对象的参数(emplace_back(a,b,c)),emplace_back中会调用construct,construct中的forward中的static_cast会根据传入的参数类型调用拷贝构造,移动构造(前提_Tp定义了nonexcept移动构造),参数构造(emplace_back中只传入了用于带参构造的参数)—参数构造是比较大的特点 直接在容器内构造对象,不需要构造一个实参后再传入再在容器内存空间内再构造一次
c++11开始提供的emplace_xxx系列函数是为了提升容器的性能而设计的。对于vector而言emplace_back()和push_back(Obj2()),结果是一样的。但使用push_back会额外生成临时对象,多一次(移动或拷贝)构造和析构。如果有定义移动的情况,性能损失小,如果对象没有实现移动的话,性能差异可能比较大。
(侯捷stl讲解)
现代处理器的局部性原则使得对先序内存的访问速度比不连续内存要快的多。vector的连续内存使用是它的一大优势所在,当不知道该用什么容器时,缺省就使用vector。vector的一个主要缺陷是大小增长时导致的元素移动(有移动构造是移动 没有就是拷贝),所以尽早使用vecotr函数为vector保留所需内存,这在vector预期会增长很大时能带来很大的性能提升。
deque
deque的意思是double-ended queue,双端队列。
用于满足的 容器不仅可以从尾部自由地添加和删除元素,也可以从头部自由地添加和删除元素 的需求
deque和vector相比有以下区别
deque提供push_front、emplace_front、pop_front成员函数
deque不提供data、capacity、reserve成员函数
deque的布局如下
如果只从头尾两个位置对deque进行增删操作的话,容器里的对象永远不需要移动
容器里的元素只是部分连续的(所以没办法提供data成员函数)
由于元素的存储大部分仍然连续 它的遍历性能也还是比较高的
由于每一段存储大小相等,deque支持使用下标访问容器元素
我们使用的是一位索引d[i],—实际会做d[i/chunk_size][i%chunk_size] —- i为元素计数从0开始 第一个维度相当于对左边的那个连续索引存储 的索引,第二个维度是对右边(一行)真正的元素索引区的索引 chunk_size 为一行的大小
list
list在c++中为双向链表,与vector相比,它优化了在容器中间的插入和删除
list 提供高效的O(1)复杂度的任意位置的插入和删除操作
list不提供下标[]访问元素—因为list节点并不是连续内存空间存储的
list提供push_front、emplace_front、pop_front成员函数(和deque相同)
list不提供data、capacity、reserve成员函数(和deque相同)
list布局如下
虽然 list 提供了任意位置插入新元素的灵活性,但由于每个元素的内存空间都是单独分配、不连续,它的遍历性能比 vector 和 deque 都要低。这在很大程度上抵消了它在插入和删除操作时不需要移动元素的理论性能优势。如果你不太需要遍历容器、又需要在中间频繁插入或删除元素,可以考虑使用 list。
需要注意的一点是,某些标准算法
sort,reverse,merge,remove,remove_if,merge,unique list都有自己的实现,可以用list.xxx()来调用
forward_list
从c++11开始的单向链表叫forward_list
其内存布局如下
对于单向链表来说insert在某个位置之前插入元素,是个不是很好实现的事情,因为单向链表没有记录上个节点的地址,forward_list还是提供了insert_after的实现。
相比list,forward_list还少了下面这些成员函数
back() —- 单向链表只计头指针 front
size
push_back
emplace_back
pop_back
为什么在有了双向链表后还需要一个单向链表?
在元素数量较小的情况下,单向链表能节约的内存是非常可观的(一个节点少一个后向指针),在链表不长的情况下,不能反向查找也不是一个大问题。
queue
队列(FIFO 先入先出) 依赖于现有容器(deque)实现,并不是从头开始完整实现,被成为容器适配器
与deque比 有如下改变:
不能用下标访问[]元素
没有begin() end()成员函数
用 emplace 替代了 emplace_back,用 push 替代了 push_back,用 pop 替代了 pop_front;没有其他的 push
queue内存结构如下
因为 queue 不提供 begin 和 end 方法,无法无损从头到尾遍历。只能全部push pop一遍才能遍历完一次容器,如果需要遍历就不要选择queue
stack
栈(LIFO 后入先出) 依赖于现有容器(deque)实现,并不是从头开始完整实现,被成为容器适配器
它接口设计的概念上和vector更相似,与vector相比有如下改变
不能用下标[]访问元素
没有begin、end成员函数
back变成了top 没有front
用 emplace 替代了 emplace_back,用 push 替代了 push_back,用 pop 替代了 pop_back;没有其他的 push…、pop_…、emplace…、insert、erase 函数
一般图形标识会将stack标识成一个竖起的vector
stack 跟我们前面讨论内存管理时的栈有一个区别:在这里下面是低地址,向上则地址增大;而我们讨论内存管理时,高地址在下面,向上则地址减小,方向正好相反。
为什么 stack(或 queue)的 pop 函数返回类型为 void,而不是直接返回容器的 top(或 front)成员?
stack(queue)和vector一样为保证强异常安全性,如果元素类型没有提供一个保证不抛异常的移动构造函数, 通常会使用拷贝构造函数,而pop作用是释放元素,c++98还没有移动构造的概念,所以如果返回成员,必须要调用拷贝构造函数,这时分配空间可能出错(释放的节点中含指针有堆上内存 先pop了这个节点释放了内存,再将节点拷贝返回 其指针上的内存是已被释放的野指针),导致构造失败,要抛出异常,所以没必要返回成员。