C++ STL容器的选择

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++ STL容器的选择相关的知识,希望对你有一定的参考价值。

RT,有什么参照吗,比如我现在有一个结构体,我不想用链表,想用容器代替,用哪种容器好,各个容器的优缺点是什么,全面点,各位大湿有时间就多写点,给个好一点的网址也行(主要讲容器怎么选择的),谢谢了

参考技术A 一些常见的容器选择问题
? 你是否需要在容器的任意位置插入新元素?
如果需要,就选择序列容器;关联容器是不行的。
? 你是否关心容器中的元素是如何排序的?
如果不关心,则哈希容器是一个可行的选择方案;否则,你要避免哈希容器。
? 你选择的容器必须是标准C++的一部分吗?
如果必须是,就排除了哈希容器、slist和rope。
? 你需要哪种类型的迭代器?
如果它们必须是随机访问迭代器,则对容器的选择就被限定为vector、deque和string。或许你也可以考虑rope。如果要求使用双向迭代器,那么你必须避免slist以及哈希容器的一个常见实现。
? 当发生元素的插入或删除操作时,避免移动容器中原来的元素是否很重要?
如果是,就要避免连续内存的容器。
? 容器中的数据的布局是否需要和C兼容?
如果需要兼容,就只能选择vector容器。
? 元素的查找速度是否关键的考虑因素?
如果是,就要考虑哈希容器,排序的vector,和标准关联容器 --- 或许这就是优先顺序。
? 如果容器内部使用了引用计数技术(reference counting),你是否介意?
如果是,就要避免使用string,因为许多string的实现都是用了引用计数。Rope也需要避免,因为权威的rope实现是基于引用计数的。当然,你需要某种表示字符串的方法,可以考虑vector<char>。
? 对插入和删除操作,你需要事务语义(transactional semantics)吗?也就是说在插入和删除操作失败时,你需要回滚的能力吗?
如果需要,你就要使用基于节点的容器。如果对多个元素的插入操作需要事务语义,则你需要选择list,因为在标准容器中,只有list对多个元素的插入操作提供了事务语义。
? 你需要使迭代器、指针和引用变为无效的次数最少吗?
如果是这样,就要使用基于节点的容器,因为对这类容器的插入和删除操作从来不会使迭代器、指针和引用变为无效(除非它们指向了一个你正在删除的元素)。而针对连续内存容器的插入和删除操作一般会使指向该容器的迭代器、指针和引用变为无效。
? 如果序列容器的迭代器是随机访问迭代器类型,而且只要没有删除操作发生,且插入操作只发生在容器的末尾,则指向数据的指针和引用就不会变为无效,这样的容器是否对你有帮助?
这是非常特殊的情况,但如果你面对的情形正是如此,则deque是你所希望的容器。(有意思的是,当插入操作仅在容器末尾发生时,deque的迭代器有可能变为无效。deque是唯一的迭代器可能会变为无效而指针和引用不会变为无效的STL标准容器。)
转自CSDN
参考技术B 在STL容器中有一些数据结构基本的类型,如链表,队列,堆栈等等。容器类型的选择和不用容器做,是差不多的。用系统的容器只是减少了你使用时的代码量。这个就是他比较显著的一个优点,用容器做可以省去很多写这些结构体的时间。

一些关于广泛使用的C++标准库STL的思考

在这里插入图片描述

from Effective STL

1、接纳typedef

我们可以通过自由的对容器和迭代器类型使用typedef,因此,不要这么写:

class test{...};
vector<test> vw;
test besttest;
... // 给besttest一个值
vector<test>::iterator i = // 寻找和bestWidget相等的Widget
find(vw.begin(), vw.end(), besttest);

要这么写:

class test { ... };
typedef vector<test> testContainer;
typedef testContainer::iterator TSIterator;
testContainer cw;
test besttest;
...
TSIterator i = find(cw.begin(), cw.end(), besttest);

就算typedef对于提高运行速度可能没有什么实质性的帮助,但是它对于撰写你的代码是会有帮助的,你总不会喜欢一直重复的去写:

unordered_map<str,str>

吧?

可以认为它在写法上取了宏定义对于名称的定义,但是typedef只是其它类型的同义字,所以它提供的的封装是纯的词法(译注:不像#define是在预编译阶段替换的)。

在代码优化阶段你会感谢这个好习惯的。


容器中的拷贝现象

当你向容器中添加一个对象(比如通过insert或push_back等),进入容器的是你指定的对象的拷贝。copy进去,copy出来。这就是STL的方式。

不只是增,删改、排序也是如此。

如果你用一个拷贝过程很昂贵对象填充一个容器,那么一个简单的操作——把对象放进容器也会被证明为是一个性能瓶颈。容器中移动越多的东西,你就会在拷贝上浪费越多的内存和时钟周期。

这些都好理解吧。
接下来这点可就不是很好理解了:

由于继承的存在,拷贝会导致分割。那就是说,如果你以基类对象建立一个容器,而你试图插入派生类对象,那么当对象(通过基类的拷贝构造函数)拷入容器的时候对象的派生部分会被删除。

vector<Widget> vw; 
class SpecialWidget: // SpecialWidget从上面的Widget派生
public Widget {...};
SpecialWidget sw; 
vw.push_back(sw); // sw被当作基类对象拷入vw
 // 当拷贝时它的特殊部分丢失了

像这样的,这个点以前倒是不知道的。(当然,前边讲的拷贝的工作方式,以前也是没有去注意到的)
分割问题暗示了把一个派生类对象插入基类对象的容器几乎总是错的。

那,对于这种拷贝的工作方式,有没有什么好的对抗办法呢?
办法总是有的:

一个使拷贝更高效、正确而且对分割问题免疫的简单的方式是建立指针的容器而不是对象的容器。也就是说,不是建立一个Widget的容器,建立一个Widget*的容器。拷贝指针很快,它总是严密地做你希望的(指针拷贝比特),而且当指针拷贝时没有分割。不幸的是,指针的容器有它们自己STL相关的头疼问题。

至于是什么头疼的问题,后面会提。

这里建议在序列式容器中使用,换到关联式容器中可能就会无序了。


小习惯:使用empty来代替检查size()是否为0

事实上empty的典型实现是一个返回size是否返回0的内联函数。
对于所有的标准容器,empty是一个常数时间的操作,但对于一些list实现,size花费线性时间。

这里我要说一点:如果你现在看不懂,或者看懂了但是不信,别急,这本书捋完我再捋一下《STL源码剖析》,源码之前,了无秘密。

(其实我自己也不信,所以要去捋一下源码)


尽量使用区间成员函数代替循环

这个思想,看标题就能看的差不多了吧,我举个栗子:
以下是两种插入区间的方式:

int data[numValues]; // 假设numValues在
 // 其他地方定义
vector<int> v;
...
v.insert(v.begin(), data, data + numValues); // 把data中的int
 // 插入v前部
vector<int>::iterator insertLoc(v.begin());
for (int i = 0; i < numValues; ++i) {
	insertLoc = v.insert(insertLoc, data[i]);
	++insertLoc;
}

说真的,在没有看到上面那种插入方式的源码之前,我不会妄加判断到底二者效率是否有差。
所以,朋友们,看下去,后面放源码,一切就一目了然了。

此外,这条原则还指出了其他多种区间函数,比如说批量删除、批量赋值等


关于在容器中存放指针

的确,当一个指针的容器被销毁时,会销毁它(那个容器)包含的每个元素,但指针的“析构函数”是无操作!它肯定不会调用delete。

还要我多说吗?最终导致的结果肯定是内存泄漏。

那怎么办?还要怎么办,再容器被销毁之前,来个遍历去回收容器中的指针呗。

void doSomething(){
 ... // 同上
 for_each(vwp.begin(), vwp.end(), DeleteObject<Widget>);
}

但是,这样就安全了吗?

不,只是看起来安全了。但并不是异常安全的。不要吹毛求疵,编程就是要吹毛求疵。

如果在手动分配空间到手动回收空间这之间,系统异常了,那这些内存不是照样遭殃?

这里还有一个比较隐蔽的问题,上面这个问题仔细想想我还是能发现的,但是下面这个问题就比较麻烦了:
如果有人继承了上面这个类呢?

比如,有的人故意从string继承:

class SpecialString: public string { ... };

这是很危险的行为,因为string,就像所有的标准STL容器,缺少虚析构函数,而从没有虚析构函数的类公有继承是一个大的C++禁忌。
(我想我需要去看一下《Effective C++》和《More Effective C++》了,在这篇写完之后,估计会在明晚写完。一直看《C++ Primer Plus》始终是,哎)

来看一段代码:

void doSomething(){
	 deque<SpecialString*> dssp;
	 ...
	 for_each(dssp.begin(), dssp.end(), // 行为未定义!通过没有DeleteObject<string>()); // 虚析构函数的基类
} // 指针来删除派生对象

以下这个解决方法我倒是没看太懂,是在下技术不够了,希望有大佬看懂了在评论区指点指点,万分感谢:

你可以通过编译器推断传给DeleteObject::operator()的指针的类型来消除这个错误(也减少DeleteObject的用户需要的击键次数)。我们需要做的所有的事就是把模板化从DeleteObject移到它的operator():

struct DeleteObject { // 删除这里的
	// 模板化和基类
	template<typename T> // 模板化加在这里
	void operator()(const T* ptr) const { 
		delete ptr; 
	} 
}

编译器知道传给DeleteObject::operator()的指针的类型,所以我们可以让它通过指针的类型自动实例化一个operator()。这种类型演绎下降让我们放弃使DeleteObject可适配的能力(参见条款40)。想想DeleteObject的设计目的,会很难想象那会是一个问题。

使用新版DeleteObject,用于SpecialString的客户代码看起来像这样:

void doSomething(){
	deque<SpecialString*> dssp;
	...
	for_each(dssp.begin(), dssp.end(),DeleteObject()); // 啊!良好定义的行为!
}

直截了当而且类型安全。

以上。以下又是我看得懂的了,这个方法依旧不是异常安全的,那要如何做到异常安全呢?使用智能指针。
关于智能指针,等我后天开始研究了那两本书,下一篇会出。


erase

看个例子:

AssocContainer<int> c;
...
for (AssocContainer<int>::iterator i = c.begin(); i!= c.end(); ++i) { 
 	if (badValue(*i)) 
 		c.erase(i); 
}

看出问题了吗?
首先 if 那边的花括号补上,我们再看。

如果没有意识到问题,或者是不能确定是否有那个问题,那可真的是好了伤疤忘了疼啊。

当容器的一个元素被删时,指向那个元素的所有迭代器都失效了。当c.erase(i)返回时,i已经失效。那对于这个循环是个坏消息,因为在erase返回后,i通过for循环的++i部分自增。

为了避免这个问题,我们必须保证在调用erase之前就得到了c中下一元素的迭代器。最容易的方法是当我们调用时在i上使用后置递增:

AssocContainer<int> c;
...
for (AssocContainer<int>::iterator i = c.begin(); i != c.end();/*nothing*/ ){
 	if (badValue(*i)) {
 		c.erase(i++); // 对于坏的值,把当前的i传给erase,然后作为副作用增加i;
 	}
 	else{ 
 		++i; 	// 对于好的值,只增加i
 	}
}

这种调用erase的解决方法可以工作,因为表达式 i++ 的值是 i 的旧值,但作为副作用,i增加了。因此,我们把 i 的旧值(没增加的)传给erase,但在erase开始执行前 i 已经自增了。


了解你的排序选择

当很多程序员想到排序对象时,只有一个算法出现在脑海:sort。

确实。
我就是、

有时候你不需要完全排序。比如,你想排序以鉴别出20个最好的Widget,剩下的可以保持无序。你需要的是部分排序,有一个算法叫做partial_sort,它能准确地完成它的名字所透露的事情:

bool qualityCompare(const Widget& lhs, const Widget& rhs){
 // 返回lhs的质量是不是比rhs的质量好
}

...

partial_sort(widgets.begin(), widgets.begin() + 20, widgets.end(),qualityCompare);	
 // 把最好的20个元素(按顺序)放在widgets的前端
... // 使用widgets...

如果你需要的只是任意顺序的20个最好的Widget,partial_sort就给了你多于需要的东西。
STL有一个算法精确的完成了你需要的,虽然名字不大可能从你的脑中迸出。它叫做nth_element。

nth_element(widgets.begin(), widgets.begin() + 19, widgets.end(), qualityCompare); 
 // 把最好的20个元素放在widgets前端,但不用担心它们的顺序

这两个算法如果不知道怎么实现的,可以自行出去面壁了。

我觉得,微调一下快排即可。
回头去看它们的源码,看看我们谁猜得准。

partial_sort是不稳定的。nth_element、sort也没有提供稳定性,但是有一个法——stable_sort——它完成了它的名字所透露的。
如果当你排序的时候你需要稳定性,你可能要使用stable_sort。STL并不包含partial_sort和nth_element的稳定版本。

现在谈谈nth_element,这个名字奇怪的算法是个引人注目的多面手。除了能帮你找到区间顶部的n个元素,它也可以用于找到区间的中值或者找到在指定百分点的元素(是我孤陋寡闻了)。

真让我越来越想去看它们的源码了

“但性能怎么样?”,你想知道。这是极好的问题。一般来说,做更多工作的算法比做得少的要花更长时间,而必须稳定排序的算法比忽略稳定性的算法要花更长时间。


remove后接erase

因为remove无法知道它正在操作的容器,所以remove不可能从一个容器中除去元素。这解释了另一个令人沮丧的观点——从一个容器中remove元素不会改变容器中元素的个数(这个我倒是早就知道了)

remove并不“真的”删除东西,因为它做不到。

非常简要地说一下,remove移动指定区间中的元素直到所有“不删除的”元素在区间的开头。

示例:
在这里插入图片描述

vector<int>::iterator newEnd(remove(v.begin(), v.end(), 99));

操作之后:

在这里插入图片描述

如果“不删除的”元素在v中的v.begin()和newEnd之间,“删除的”元素就必须在newEnd和v.end()之间——这好像很合理。事实上不是这样!“删除的”值完全不必再存在于v中了。
remove并没有改变区间中元素的顺序,所以不会把所有“删除的”元素放在结尾,并安排所有“不删除的”值在开头。虽然标准没有要求,但一般来说区间中在新逻辑终点以后的元素仍保持它们的原值。调用完remove后,在我知道的所有实现中,v看起来像这样:

在这里插入图片描述

如果你真的要删除东西的话,你应该在remove后面接上erase。

因为remove本身很方便地返回了区间新逻辑终点的迭代器,这个调用很直截了当:

vector<int> v; // 正如从前
v.erase(remove(v.begin(), v.end(), 99), v.end()); // 真的删除所有
 // 等于99的元素
cout << v.size(); // 现在返回7

此外,提防在指针的容器上使用类似remove的算法


不得不说,这本书不错。
目前我能看下来的有这些,不过我觉得早晚我还要再打开这本书,不知道多少回。
接下来我们进《STL 源码剖析》


from 《STL源码剖析》

容器

vector

咱也不多说,直接上代码,好吧,代码里面说。

#include<iostream>
using namespace std;
#include<memory.h>  

// alloc是SGI STL的空间配置器
template <class T, class Alloc = alloc>
class vector
{
public:
    // vector的嵌套类型定义,typedef用于提供iterator_traits<I>支持
    typedef T value_type;
    typedef value_type* pointer;
    typedef value_type* iterator;
    typedef value_type& reference;
    typedef size_t size_type;
    typedef ptrdiff_t difference_type;
protected:
    // 这个提供STL标准的allocator接口
    typedef simple_alloc <value_type, Alloc> data_allocator;

    iterator start;               // 表示目前使用空间的头
    iterator finish;              // 表示目前使用空间的尾
    iterator end_of_storage;      // 表示实际分配内存空间的尾

    void insert_aux(iterator position, const T& x);

    // 释放分配的内存空间
    void deallocate()
    {
        // 由于使用的是data_allocator进行内存空间的分配,
        // 所以需要同样使用data_allocator::deallocate()进行释放
        // 如果直接释放, 对于data_allocator内部使用内存池的版本
        // 就会发生错误
        if (start)
            data_allocator::deallocate(start, end_of_storage - start);
    }

    void fill_initialize(size_type n, const T& value)
    {
        start = allocate_and_fill(n, value);
        finish = start + n;                         // 设置当前使用内存空间的结束点
        // 构造阶段, 此实作不多分配内存,
        // 所以要设置内存空间结束点和, 已经使用的内存空间结束点相同
        end_of_storage = finish;
    }

public:
    // 获取几种迭代器
    iterator begin() { return start; }	
    iterator end() { return finish; }

    // 返回当前对象个数
    size_type size() const { return size_type(end() - begin()); }	//我想,这里的size()花费的应该是常数时间吧
    size_type max_size() const { return size_type(-1) / sizeof(T); }
    // 返回重新分配内存前最多能存储的对象个数
    size_type capacity() const { return size_type(end_of_storage - begin()); }
    bool empty() const { return begin() == end(); }	//其实这里的empty和size花费相差不会很大吧
    reference operator[](size_type n) { return *(begin() + n); }

    // 本实作中默认构造出的vector不分配内存空间
    vector() : start(0), finish(0), end_of_storage(0) {}

    vector(size_type n, const T& value) { fill_initialize(n, value); }
    vector(int n, const T& value) { fill_initialize(n, value); }
    vector(long n, const T& value) { fill_initialize(n, value); }

    // 需要对象提供默认构造函数
    explicit vector(size_type n) { fill_initialize(n, T()); }

    vector(const vector<T, Alloc>& x)
    {
        start = allocate_and_copy(x.end() - x.begin(), x.begin(), x.end());
        finish = start + (x.end() - x.begin());
        end_of_storage = finish;
    }

	//没有纯虚析构函数哦
    ~vector()
    {
        // 析构对象
        destroy(start, finish);
        // 释放内存
        deallocate();
    }

    vector<T, Alloc>& operator=(const vector<T, Alloc>& x);

    // 提供访问函数
    reference front() { return *begin(); }
    reference back() { return *(end() - 1); }

    
    // 向容器尾追加一个元素, 可能导致内存重新分配
    
    //                          push_back(const T& x)
    //                                   |
    //                                   |---------------- 容量已满?
    //                                   |
    //               ----------------------------
    //           No  |                          |  Yes
    //               |                          |
    //               ↓                          ↓
    //      construct(finish, x);       insert_aux(end(), x);
    //      ++finish;                           |
    //                                          |------ 内存不足, 重新分配
    //                                          |       大小为原来的2倍
    //      new_finish = data_allocator::allocate(len);       <stl_alloc.h>
    //      uninitialized_copy(start, position, new_start);   <stl_uninitialized.h>
    //      construct(new_finish, x);                         <stl_construct.h>
    //      ++new_finish;
    //      uninitialized_copy(position, finish, new_finish); <stl_uninitialized.h>
    

    void push_back(const T& x)
    {
        // 内存满足条件则直接追加元素, 否则需要重新分配内存空间
        if (finish != end_of_storage)
        {
            construct(finish, x);
            ++finish;
        }
        else
            insert_aux(end(), x);
    }


    
    // 在指定位置插入元素
    
    //                   insert(iterator position, const T& x)
    //                                   |
    //                                   |------------ 容量是否足够 && 是否是end()?
    //                                   |
    //               -------------------------------------------
    //            No |                                         | Yes
    //               |                                         |
    //               ↓                                         ↓
    //    insert_aux(position, x);                  construct(finish, x);
    //               |                              ++finish;
    //               |-------- 容量是否够用?
    //               |
    //        --------------------------------------------------
    //    Yes |                                                | No
    //        |                                                |
    //        ↓                                                |
    // construct(finish, *(finish - 1));                       |
    // ++finish;                                               |
    // T x_copy = x;                                           |
    // copy_backward(position, finish - 2, finish - 1);        |
    // *position = x_copy;                                     |
    //                                                         ↓
    // data_allocator::allocate(len);                       <stl_alloc.h>
    // uninitialized_copy(start, position, new_start);      <stl_uninitialized.h>
    // construct(new_finish, x);                            <stl_construct.h>
    // ++new_finish;
    // uninitialized_copy(position, finish, new_finish);    <stl_uninitialized.h>
    // destroy(begin(), end());                             <stl_construct.h>
    // deallocate();
    

    iterator insert(iterator position, const T& x)
    {
        size_type n = position - begin();
        if (finish != end_of_storage && position == end())
        {
            construct(finish, x);
            ++finish;
        }
        else
            insert_aux(position, x);
        return begin() + n;
    }

    iterator insert(iterator position) { return insert(position, T()); }

    void pop_back()
    {
        --finish;
        destroy(finish);
    }

    iterator erase(iterator position)
    {
        if (position + 1 != end())
            copy(position + 1, finish, position);
        --finish;
        destroy(finish);
        return position;
    }


    iterator erase(iterator first, iterator lastC++进阶-2-STL初识(容器算法迭代器等)

C++ STL快速入门

C++ :1STL 的容器概述array容器详解迭代器初步分析

C++ STL 基础及应用 容器

指向特定类型的 STL 容器样式和迭代器 (C++)

小白学习C++ 教程二十一C++ 中的STL容器Arrays和vector