The C++ standard library(侯捷/孟岩 译) 06--容器二

6.6 map and multimap(page194)

map/multimap将键值对作为元素进行管理。multimap允许重复的键,而map不允许。
类似其它容器,map/multimap是定义在namespace std中的class template:

namespace std
{
    template ,
        class Allocator = alloctor > >
    class map;

    template ,
        class Allocator = alloctor > >
    class multimap;
}

对于map/multimap的key/value须满足两条件:

1. key/value须具备assignable、copyable。
2. 对排序准则而言,key须是comparable。
6.6.1 map/multimap 能力

元素是key/value pair,其次map可作为关联式数组运用。
map/multimap根据元素key自动对元素排序,如此根据已知key查找性能很好,但根据value查找性能不行。

“自动排序”的特性使得map/multimap有个限制

不能直接改变元素key,因为这会破坏正确次序。

要修改key,必须先移除拥有该key的元素,然后插入新的key/value元素。

从迭代器角度看,key是常数,非常数型态的value可直接修改。

6.6.2 map/multimap operation
  • 生成、复制、销毁


    t6-26.png

定义排序准则的两个方式:

1. 以template参数定义。
  eg:std::map > col1;
该情况下,排序准则是型别的一部分。
    因此系统确保“只有排序准则相同的容器才能合并”。
明确而言,第三参数是排序准则的型别,实际的排序准则是容器产生的对象。
  为生成它,容器构造函数会调用“排序准则型别”的default构造函数。eg见p294。

2. 以构造函数参数定义。
该情况下,可有一个“排序准则型别”并为它指定不同的排序准则。
  即让该型别产生的对象作为一个排序准则。
若执行期才获得该排序准则,且程序需用到不同的排序准则(但数据型别须同),此方式可用。
如执行期指定“key的型别为string"的例子,详见p213。
  • Nonmodifying operation(非变动性操作)


    t6-27.png

元素比较操作只适用于型别相同的容器,即容器的key/value/排序准则都须相同,否则编译错误。
比较操作以”字典顺序“比较。

  • special search operation(特殊查找操作)(page198)


    t6-28.png
  • Assignment(赋值)(page200)


    t6-29.png

    赋值操作的两端容器的型别须相同,尽管排序准则可以不同。若排序准则不同,准则也会被赋值或交换。

  • Iterator Functions And Element Access
    map/multimap不支持元素随机存取,元素存取通常通过迭代器。但例外的是map提供subscript(下标)操作符,可直接存取元素(见p205)。

    t6-30.png

    与所有关联式容器类似,迭代器是双向迭代器。
    且注意元素的key被视为常数,此限制是为了确保不会应为变更元素key而破坏已序元素。
    故不能对map/multimap调用modifying algorithms,若要改变元素key,只有一个方法:以一个”value相同“的新元素替换旧元素,下面是个泛函数(以map为例):

namespace MyLib
{
    template  inline 
        bool replace_key (Cont& c,
                const typename Cont::key_type& old_key,
                const typename Cont::key_type& new_key)
        {
            typename Cong::iterator pos;
            pos = c.find(old_key);

            if (pos != c.end())
            {
                // insert new element with value of old element
                c.insert(typename Cont::value_type(new_key, pos->second));

                // remove old element
                c.erase(pos);
                return true;
            }
            else
            {
                // key not found
                return false;
            }
        }
}

note:map提供了很方便的手法改变元素的key,只需:

// insert new element with value of old element
col1["new_key"] = col1["old_key"];
// remove old element
col1.erase("old_key");
  • Inserting And Rmoving
    t6-31.png

    上图中作者应该笔误了,erase(elem)指的是根据key值删除元素,返回被删除元素的个数。

注,map/multimap内部key被视为常数,插入元素时,要么提供正确型别,要么提供隐式或显式型别转换,有三种方法将value传入map/multimap:

1. 用value_type.
  为避免隐式类型转换,可用value_type传递正确型别。
  value_type是容器本身提供的型别定义。eg:
  std::map col1;
  ...
  col1.insert(std::map::value_type("otto", 22.3) );

2. 用pair<>
  eg:
  std::map col1;
  ...
  // use implicit conversion:
  col1.insert(std::pair("otto", 22.3) );
  // use no implicit conversion
  col1.insert(std::pair("otto", 22.3) );
第一个insert内的型别并不正确故会被转换为真正的元素型别。
  为了做到这点,insert()成员函数被定义为member template。

3. 用make_pair()
  最方便就是用make_pair()函数。eg:
  std::map col1;
  ...
  col1.insert(std::make_pair("otto", 22.3) );
和用pair一样利用member template进行必要的类型转换。
6.6.3 Associated Arrays(将map视为关联式数组)(page207)

通常关联式容器不提供元素的直接存取,须依靠迭代器,但map是个例外。
Non-const map提供下表操作符,支持元素的直接存取。

t6-32.png

新元素的value有default构造函数构造。所有基本数据类型都提供有default构造函数,以0为初值。

6.6.4 Exception Handling

map/multimap和set/multiset类似。参见page185

6.6.5 map/multimap example
  • 将map作为关联数组(page207)
// 将map作为关联式数组使用,用于反映股票行情。
// - 元素key为股票名称,value为股票价格
// cont/map1.cpp

#include 
#include 
#include 
using namespace std;

int main()
{
    /* Create map / associate array
     * - keys are strings
     * - values are floats
     */
    typedef map StringFloatMap;
    StringFloatMap stocks;  // create empty container

    // insert some elements
    stocks["BASF"] = 369.50;
    stocks["VW"] = 413.50;
    stocks["Daimler"] = 819.00;
    stocks["BMW"] = 834.00;
    stocks["Siemens"] = 842.20;

    // print all elements
    StringFloatMap::iterator pos;
    for (pos = stocks.begin(); pos != stocks.end(); ++pos)
    {
        cout << "stock: " << pos->first << "\t"
            << "price: " << pos->second << endl;
    }
    cout << endl;

    // boom (all prices doubled)
    for (pos = stocks.begin(); pos != stocks.end(); ++pos)
    {
        pos->second *= 2;
    }

    // print all elements
    for (pos = stocks.begin(); pos != stocks.end(); ++pos)
    {
        cout << "stock: " << pos->first << "\t"
            << "price: " << pos->second << endl;
    }
    cout << endl;

    /* rename key from "VW" to "Volkswagen"
     * - only provided by exchanging element
     */
    stocks["Volkswagen"] = stocks["VW"];
    stocks.erase("VW");

    // print all elements
    for (pos = stocks.begin(); pos != stocks.end(); ++pos)
    {
        cout << "stock: " << pos->first << "\t"
            << "price: " << pos->second << endl;
    }   
}

output:
map1.png
  • 将multimap作为字典
// 展示如何将multimap作为一个字典使用
// cont/mmap1.cpp

#include 
#include 
#include 
#include 
using namespace std;

int main()
{
    // define multimap type as string/string dictionary
    typedef multimap StrStrMMap;
    
    // create empty dictionary
    StrStrMMap dict;

    // insert some elements in random order
    dict.insert(make_pair("day", "Tag"));
    dict.insert(make_pair("strange", "fremd"));
    dict.insert(make_pair("car", "Auto"));
    dict.insert(make_pair("smart", "elegant"));
    dict.insert(make_pair("trait", "Merkmal"));
    dict.insert(make_pair("smart", "raffiniert"));
    dict.insert(make_pair("clever", "klug"));
    dict.insert(make_pair("clever", "raffiniert"));

    // print all elements
    StrStrMMap::iterator pos;
    cout.setf(ios::left, ios::adjustfield);
    cout << " " << setw(10) << "english "
        << "german " << endl;
    cout << setfill('-') << setw(20) << " "
        << setfill(' ') << endl;
    for(pos = dict.begin(); pos !=  dict.end(); ++pos)
    {
        cout << " " << setw(10) << pos->first.c_str()
            << pos->second << endl;
    }
    cout << endl;

    // print all value for key "smart"
    string word("smart");
    cout << word << ": " << endl;
    for (pos = dict.lower_bound(word); pos != dict.upper_bound(word); ++pos)
    {
        cout << "   " << pos->second << endl;
    }

    // print all keys for value "raffiniert"
    word = {"raffiniert"};
    cout << word << ": " << endl;
    for (pos = dict.begin(); pos != dict.end(); ++pos)
    {       
        if (pos->second == word )
        {
            cout << "   " << pos->first << endl;
        }
    }
}

output:
mmap1.png
  • 查找具有特定value的元素(page211)
// 展示 如何使用全局find_if()算法查找特定value对应的元素
// cont/mapfind.cpp

#include 
#include 
#include 
using namespace std;

/* function object to check the value of a map element */
template 
class value_equals
{
    private:
        V value;

    public:
        // constructor (initialize value to compare with)
        value_equals (const V& v) : value(v){}

        // comparson
        bool operator() (pair elem)
        {
            return elem.second == value;
        }
};

int main()
{
    typedef map FloatFloatMap;
    FloatFloatMap col1;
    FloatFloatMap::iterator pos;

    // fill container
    col1[1] = 7;
    col1[2] = 4;
    col1[3] = 2;
    col1[4] = 3;
    col1[5] = 6;
    col1[6] = 1;
    col1[7] = 3;

    // search an element with key 3.0
    pos = col1.find(3.0);   // logarithmic complexity
    if (pos != col1.end())
    {
        cout << pos->first << ": "
            << pos->second << endl;
    }

    // search an element with value 3.0
    pos = find_if(col1.begin(), col1.end(), // liner complexity
            value_equals(3.0) );
    if(pos != col1.end())
    {
        cout << pos->first << ": "
            << pos->second << endl;
    }
}

output:
mapfind.png
6.6.6 综合实例

用map,string于执行期指定排序准则。

/* 本例展现如下技巧:
 *- 使用map
 * - 编写并使用仿函数functor
 * - 执行期间定义排序准则
 * - 忽略大小写情况下比较字符串
 */
// cont/mapcmp.cpp

#include 
#include 
#include 
#include 
#include 
using namespace std;

/* function object to compare strings
 * - allows you to set the comparison criterion at runtime
 * - allows you to compare case insensitive
 */
class RuntimeStringCmp
{
    public:
        // constants for the comparison criterion
        enum cmp_mode {normal, nocase};

    private:
        // actual comparison mode
        const cmp_mode mode;

        //auxiliary function to compare case insentive
        static bool nocase_compare (char c1, char c2)
        {
            return toupper(c1) < toupper(c2);
        }

    public:
        // constructor: initializes the comparison criterion
        RuntimeStringCmp (cmp_mode m = normal) : mode(m){}

        // the comparison
        bool operator() (const string& s1, const string& s2) const
        {
            if (mode == normal)
            {
                return s1 < s2;
            }
            else
            {
                return lexicographical_compare(s1.begin(), s2.end(),
                        s2.begin(), s2.end(), nocase_compare);
            }
        }
};

/* container type:
 * - map with
 * - string keys
 * - string values
 * - the special comparison object type
 */
typedef map StringStringMap;

// function that fills and prints such containers
void fillAndPrint(StringStringMap & col1);

int main()
{
    // create a container with the default comparison criterion
    StringStringMap col11;
    fillAndPrint(col11);

    // create an object for case-insensitive comparisons
    RuntimeStringCmp ignorecase(RuntimeStringCmp::nocase);

    // create a container with the case-insensitive comparisons criterion
    StringStringMap col12(ignorecase);
    fillAndPrint(col12);
}

void fillAndPrint(StringStringMap & col1)
{
    // fill insert elements in random order
    col1["Deutschland"] = "Germany";
    col1["deutsch"] = "German";
    col1["Haken"] = "snag";
    col1["arbeiten"] = "work";
    col1["Hund"] = "dog";
    col1["gehen"] = "go";
    col1["Unternehmen"] = "enterprise";
    col1["unternehmen"] = "undertake";
    col1["gehen"] = "walk";
    col1["Bestatter"] = "undertaker";

    // print elements
    StringStringMap::iterator pos;
    cout.setf(ios::left, ios::adjustfield);
    for(pos = col1.begin(); pos != col1.end(); ++pos)
    {
        cout << setw(15) << pos->first.c_str() << " "
            << pos->second << endl;
    }
    cout << endl;
}

程序说明:
第一部分打印第一个容器的内容,该容器以operator<排序。
第二部分以“不分大小写”模式打印。

注以第二部分少了一个元素,因为在不分大小写情况下“Unternehmen”和"unternehemen"被视为相同的字符串,
  但map不接受重复元素。

output:
mapcmp.png

6.7 其它STL容器(page217)

STL是个框架,除了提供标准容器,还允许使用其它数据结构作为容器,也可自定义特殊容器,但须遵循“开放性封闭”原则:允许扩展,谢绝修改。
使自定义容器“STL化”的三种方法:

1. The invasive approach(侵入性作法)
  直接提供STL所需接口,特别是begin()和end()之类的常用函数。

2. The noninvasive approach(非侵入性作法)
  自定义特殊迭代器,作为算法和特殊容器的接口。
    此作法只需要“遍历容器所有元素”的能力。

3. The wrapper approach(包装法)
  将上述两种方法组合,写一个外套类别 以包装任何数据结构。
6.7.1 String可被视为一种STL容器

C++标准程序库的string类别,是“以侵入法编写STL容器”的一个例子。(详见chapter11 page495)

string可被视为以字符为元素的容器。
因此,标准的string提供了STL容器接口,如begin()和end() ;
  同时为支持迭代器和迭代器配接器,也提供了操作函数如push_back()以支持back inserter。
6.7.2 Array可被视为一种STL容器

array不是类别,故不提供任何成员函数,即只能用非侵入法或包装法。

  • 直接运用数组
采用非侵入法,只需一个对象——普通指针,然后透过STL迭代器接口遍历数组元素。
”行为类似迭代器“的任何东西即一种迭代器,而之战就是一个随机存取迭代器。
eg:
// 展示以array作为STL容器
// cont/array1.cpp

#include 
#include 
#include 
#include 
using namespace std;

int main()
{
    int col1[] = {5, 6, 2, 4, 1, 3};

    // square all elements
    transform (col1, col1+6,    // first source
            col1,       // second source
            col1,       // destiantion
            multiplies() );    // operation

    // sort beginning with the second element
    sort (col1 + 1, col1 + 6);

    // print all elements
    copy (col1, col1 + 6, ostream_iterator(cout, " "));
    cout << endl;
}

note:参数是 区间最后一个元素的下一个位置。

output:
array1.png
  • 一个数组外包装
在数组外包装一层常用的容器接口
eg:
// cont/carray.hpp

#include 

template 
class carray
{
    private:
        T v[thesize];   // fixed-size array of elements of type T

    public:
        // type definitions
        typedef T value_type;
        typedef T* iterator;
        typedef const T* const_iterator;
        typedef T& reference;
        typedef const T& const_reference;
        typedef std::size_t size_type;
        typedef std::ptrdiff_t difference_type;

        // iterator support
        iterator begin()
        {
            return v;
        }
        const_iterator begin() const
        {
            return v;
        }
        iterator end()
        {
            return v + thesize;
        }
        const_iterator end() const
        {
            return v + thesize;
        }

        // direct element access
        reference operator[] (std::size_t i)
        {
            return v[i];
        }
        const_reference operator[] (std::size_t i) const
        {
            return v[i];
        }

        // size is constant
        size_type size() const 
        {
            return thesize;
        }
        size_type max_size() const
        {
            return thesize;
        }

        // conversion to ordinary array
        T* as_array() 
        {
            return v;
        }
};

应用:
// cont/carray1.cpp

#include 
#include 
#include "carray.hpp"
#include "../stl/print.hpp"
using namespace std;

int main()
{
    carray a;

    for (unsigned i = 0; i < a.size(); ++i)
    {
        a[i] = i + 1;
    }

    PRINT_ELEMENTS(a);

    reverse(a.begin(), a.end());
    PRINT_ELEMENTS(a);

    transform (a.begin(), a.end(),  // source
            a.begin(),      // destination
            negate() );    // operation
    PRINT_ELEMENTS(a);
}

output:
carray1.png
6.7.3 Hash Table

hash table可用于群集上,很重要,但未包含于C++标准程序库中。

C++社群已有数种可用的hash table实现版本。
一般而言,程序库会提供四种hash table:hash_set/hash_multiset/hash_map/hash_multimap。
Bjarne Stroustrup的《The C++ Programming Language》3rd 17.6中有hash_map实现。

hash table的运用及设计原理可参考《STL源码剖析》by 侯捷/碁峰2002,5.7的SGI STL。

6.8 实现Reference语义(page222)

要在STL容器中用“reference语义”(不论是因为元素复制代价大或因为 需要不同群集中共享同一个元素),就要用智能型指针,以避免可能的错误。
有一个解决方法:对指针所指的对象采用reference counting智能型指针。

// cont/countptr.hpp

#ifndef COUNTED_PTR_HPP
#define COUNTED_PTR_HPP

/* class for counted reference semantics
 * - deletes the object to which it refers when the last CountedPtr
 *   that refers to it is destoryed
 */
template 
class CountedPtr
{
    private:
        T* ptr; // pointer to the value
        long* count;    // shared number of owners

    public:
        // initialize pointer with existing pointer
        // - requires that the pointer p is a return value of new
        explicit CountedPtr (T* p = 0) : ptr(p), count(new long(1)){}

        // copy pointer (one more owner)
        CountedPtr (const CountedPtr& p) throw() :
                      ptr(p.ptr), count(p.count)
        {
            ++*count;
        }

        // destructor (deletes value if this was the last owner)
        ~CountedPtr() throw()
        {
            dispose();
        }

        // assignment (unshare old and share new value)
        CountedPtr& operator= (const CountedPtr& p) throw()
        {
            if (this != &p)
            {
                dispose();
                ptr = p.ptr;
                count = p.count;
                ++*count;
            }
            return *this;
        }

        // access the value to which the pointer refers
        T& operator*() const throw()
        {
            return *ptr;
        }
        T* operator->() const throw()
        {
            return ptr;
        }

    private:
        void dispose()
        {
            if (--*count == 0)
            {
                delete count;
                delete ptr;
            }
        }
};
#endif  /*COUNTED_PTR_HPP*/

上述class与auto_ptr class有些类似。
但和auto_ptr不同之处是,这种智能型指针被复制后,原指针和新指针都有效;当指向同一对象的最后一个智能型指针被销毁时,所指对象才被删除。
eg:

// cont/refsem1.cpp

#include 
#include 
#include 
#include 
#include "countptr.hpp"
using namespace std;

void printCountedPtr (CountedPtr elem)
{
    cout << *elem << " ";
}

int main()
{
    // array of integers (to share in different containers)
    static int values[] = {3, 5, 9, 1, 6, 4};

    // two different collections
    typedef CountedPtr IntPtr;
    deque coll1;
    list coll2;

    /* insert shared objects into the collections
     * - same order in coll1 coll1
     * - reverse order in coll2 coll2
     */
    for (int i = 0; i < sizeof(values)/sizeof(values[0]); ++i)
    {
        IntPtr ptr(new int(values[i]));
        coll1.push_back(ptr);
        coll2.push_front(ptr);
    }

    // print contents of both collections
    for_each(coll1.begin(), coll1.end(), printCountedPtr);
    cout << endl;
    for_each(coll2.begin(), coll2.end(), printCountedPtr);
    cout << endl << endl;

    /* modify values at different places
     * - square third value in coll1
     * - negate first value in coll1
     * - set first value in coll2 to 0
     */
    *coll1[2] *= *coll1[2];
    (**coll1.begin()) *= -1;
    (**coll2.begin()) = 0;

    // print contents of both collections again
    for_each (coll1.begin(), coll1.end(), printCountedPtr);
    cout << endl;
    for_each (coll2.begin(), coll2.end(), printCountedPtr);
    cout << endl;
}

output:
refsem1.png

note:

若调用一个辅助函数,而它在某处保存了群集内的某个元素,
  那即使群集被销毁或元素全被删除,那个智能指针所指元素依然有效。

6.9 容器的运用时机(page226)
t6-33.png

6.10.6 返回的迭代器(page239)
t6-34.png
6.10.10 STL容器的异常处理(page248)
t6-35.png

chapter6.9及其之后参见 STL源码剖析。

你可能感兴趣的:(The C++ standard library(侯捷/孟岩 译) 06--容器二)