C++ 中比较简单的容器

2019-12-19  本文已影响0人  play_robot
一、string

虽然string一般不被认为是C++的容器,但是它和容器具有很多相同的特点。因此先说一下string类。string类是模板basic_string对于char类型的特化,可以将string类看作是一个只存放char类型数据的容器。和大部分容器类似,string具有下列成员函数:

string的内存布局如下图所示:


和普通C字符串不同:

因此推荐在代码中尽量使用string来管理字符串。但是对于向外暴露的接口,除非确定调用者持有string,一般不建议在接口中使用const string&。如果函数里不对字符串做复杂处理,推荐使用const char*,这样可以避免在调用者只有C字符串时编译器自动构造string,额外的构造和析构代价并不低。如果实现较为复杂,希望使用string成员函数的话,应该考虑下面的策略:

二、vector

vector基本是最常用的容器,实际应用中把它当成动态数组,相当于Java的ArrayList和Python的list。
和string相似, vector里的成员在内存里连续存放,同时beginendfrontback成员函数指向的位置和string也是一样的:


除了容器类的共同点,vector允许下面的操作:

vector适合在尾部操作,这是由其内存布局决定的,只有在尾部插入或删除时,其他元素才不需要移动,除非内存空间不足导致需要重新分配内存。

push_backinsertreserveresize等函数导致内存重新分配时,或当inserterase导致元素位置移动时,vector会试图把元素移动到新的内存区域。vector 通常保证强异常安全性,如果元素没有提供一个保证不抛异常的移动构造函数,vector通常会使用拷贝构造函数。(因为一旦在移动过程中抛异常,原先容器中的对象将处于不完整状态。)因此,对于拷贝代价较高的自定义元素类型,我们应当定义移动构造函数,并标记为noexcept,或只在容器中放置对象的智能指针

#include <iostream>
#include <vector>

using namespace std;

class Obj1 {
public:
  Obj1()
  {
    cout << "Obj1()\n";
  }
  Obj1(const Obj1&)
  {
    cout << "Obj1(const Obj1&)\n";
  }
  Obj1(Obj1&&)
  {
    cout << "Obj1(Obj1&&)\n";
  }
};

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<Obj1> v1;
  v1.reserve(2);
  v1.emplace_back();
  v1.emplace_back();
  v1.emplace_back();

  vector<Obj2> v2;
  v2.reserve(2);
  v2.emplace_back();
  v2.emplace_back();
  v2.emplace_back();
}

上面代码运行后会输出:

Obj1()
Obj1()
Obj1()
Obj1(const Obj1&)
Obj1(const Obj1&)
Obj2()
Obj2()
Obj2()
Obj2(Obj2&&)
Obj2(Obj2&&)

由于Obj2的移动构造函数加了noexcept修饰,因此vector会选择移动对象。

emplace...系列函数是为了提升容器性能而设计的。如果将上面的第三个v1.emplace_back()改为v1.push_back(Obj1()),输出会变为:

Obj1()
Obj1()
Obj1()
Obj1(Obj1&&)
Obj1(const Obj1&)
Obj1(const Obj1&)

在C++ 11之前,使用push_back的方式会额外生成临时对象,在本例中会多一次移动或拷贝构造和一次析构。(如果没有提供移动构造函数,那么拷贝构造会被调用)

vector一个优点在于它使用连续内存存储数据,CPU访问速度会很快速。它的一个主要缺陷在于大小增长时会导致元素的移动,因此如果可以的话,应该尽早使用reserve函数为vector事先分配好所需内存,这样在vector大小增长时可带来性能提升。

三、deque

deque全称为double-ended queue,即双端队列。它主要用于从尾部头部自由添加和删除元素。和vector相比,区别包括:

可以看出:

四、list

list在C++里代表双向链表。和vector相比,它优化了在容器中间的插入和删除操作。

list内存布局一般如下图:


list每个元素的内存空间都是单独分配,不连续,因此它的遍历性能比vector和deque都要低,在很大程度上抵消了它在插入和删除元素操作时不需要移动元素的理论性能优势。应用中如果不太需要遍历容器,需要在中间频繁插入或删除元素,则可以考虑使用list。

最后,由于某些标准算法在list上会导致问题,list提供了成员函数作为代替,包括:

五、forward_list

forward_list(前向链表)是C++ 11中的单向链表。它的内存布局如下:

大部分C++容器都支持insert函数,语义是在指定位置之前插入一个元素。但是forward_list是单向链表,无法获取某个节点之前的元素位置,因此标准库提供了一个insert_after作为代替。此外,和list相比,它还缺少以下成员函数:

六、queue

队列queue是一个先进先出(FIFO)数据结构,缺省用deque来实现。它的接口跟deque比,有如下变化:

七、stack

栈stack是后进先出(LIFO)数据结构,stack底层默认也用deque实现。相比vector,它的特点如下:

思考

为什么stack或queue的pop函数返回类型为void,而不是直接返回容器的top或front成员?

接口是在c++ 98时设计好的,当时返回对象只能是拷贝,可能发生异常,一旦发生异常,由于对象已经弹出,容器则会处于不完整状态,因此当时做法是返回void。现在,C++ 11有了移动语义,在具有不抛异常的移动构造函数情况下,可以直接返回对象。

上一篇下一篇

猜你喜欢

热点阅读