• Effective STL~2 vector和string(条款13~18)


    第13条:vector和string优先于动态分配的数组

    使用new来动态分配内存,意味着使用者将承担责任:

    1. 必须确保有人会用delete删除所分配内存。如果没有,new将导致内存泄漏;
    2. 必须确保使用正确delete形式。单个对象删除用delete,数组删除用delete[];
    3. 必须确保只delete一次。如果new一次,delete多次,结果未定义;

    而使用STL顺序容器vector和string,其包含的元素会自动构建、析构,无需额外处理。因此,优先使用vector和string替换动态分配的数组。

    通常,只有一种情况下,用动态分配的数组(new [])取代vector和string是合理的,且只对string适用:在多线程环境中使用了引用计数的string,而避免内存分配和字符拷贝节省的时间还比不上同步控制上的时间。

    如何确认string实现使用了引用计数方式?
    1)查阅库文档;
    2)检查string源码实现。注意string是basic_string的类型定义,wstring是basic_string<wchar_t>的类型定义。

    对于使用引用计数的string在多线程花费较大时间,可以由3种选择:
    1)检查库实现,看是否可能禁用引用计数;
    2)寻找或开发另一个不使用引用计数的string实现;
    3)考虑使用vector而不是string;

    [======]

    第14条:使用reserve来避免不必要的重新分配

    vector和string的内存增长过程,调用类似于realloc的操作:
    1)分配一块大小为当前容量的某个倍数新内存,作为初始内存。大多数实现中,vector、string容量以2倍数增长;
    2)把容器的所有元素从旧内存拷贝到新内存;
    3)析构就内存中的对象;
    4)释放旧内存;

    频繁的扩容,非常耗时,会影响程序效率。如果知道元素的大致数量,从而使用reserve成员函数,强迫容器容量变成指定值,可以有效避免频繁扩容。

    vector<int> vec;
    vec.reserve(1000);
    // 循环过程vec不会发生扩容
    for (size_t i = 0; i < 1000; i++)
    {
           vec.push_back(i);
    }
    

    [======]

    第15条:注意string实现的多样性

    一个string对象大小是多少?即sizeof(string)值是多少?
    有的string实现是4,有的是28。

    cout << sizeof(string) << endl;

    为什么?
    我们先看string实现。几乎每个string都包含如下信息:

    • 字符串大小(size),即所包含的字符个数 ;
    • 用于存该字符串中字符的内存的容量(capacity);
    • 字符串的值(value),即构成该字符串的字符;
    • 它的分配子的一份拷贝(可选,见条款10);

    如果是建立在引用计数基础上的string实现可能还包括:

    • 对值的引用计数;

    4种不同string实现
    1)A实现 sizeof(string) = 28

    默认分配子allocator为指针的4倍空间,自定义分配子可能更大。RefCnt是引用计数。

    2)B实现 sizeof(string) = 4

    “其他”中,包含了一些与多线程环境下同步控制相关的额外数据,实现同步控制的数据大小是指针6倍。RefCnt是引用计数。

    3)C实现 sizeof(string) = 4

    C实现没有对单个对象的分配子支持。X表示不考虑共享问题(一个引用计数值)。

    4)D实现 sizeof(string) = 28

    分两种情况,对应2种数据结构:容量<=15,容量>15。

    对应以下代码:

    string s("Perse");
    

    实现D不会导致任何动态分配,实现A、C将导致一次,B会导致2次。实现B中包含对多线程环境的同步支持。实现C不支持针对单个对象的分配子,意味着只有它可以共享分配子。实现D没有使用引用计数,所有string都不共享数据。

    [======]

    第16条:了解如何把vector和string数据传给旧的API

    对于需要旧式C风格字符串的地方,可以使用c_str成员函数将string传递给const char*

    // C API
    void doSomething(const char* pString);
    
    // 调用
    string s;
    ...
    doSomething(s.c_str());
    

    对于需要旧式C风格T*(数组指针)和size_t(数组大小)的地方,可以使用vector元素的取地址运算符&和vector的size成员函数转换。

    // C API 向pArray[0..arraySize-1]写入, 返回已被写入元素个数
    size_t fillArray(double* pArray, size_t arraySize);
    
    int maxNumDoubles = 10;
    vector<double> vd(maxNumDoubles);
    vd.resize(fillArray(&vd[0], vd.size()));
    

    这项技术只对vector有效,因为只有vector才保住和数组有同样的内存布局。不过,string对象初始化时,可以用vector来进行。

    将除vector和string以为的STL容器把数据传递给C API,可以先把容器元素拷贝到vector,然后再传给C API

    // C API
    void doSomething(const int* pInts, size_t numInts);
    
    set<int> intSet;
    ...
    vector<int> v(intSet.begin(), intSet.end()); // 把set数据拷贝到vector
    if (!v.empty()) doSomething(&v[0], v.size()); // 把数据从vector传给API
    

    [======]

    第17条:使用“swap技巧”除去多余的容量

    shrink to fit(压缩至适当大小)问题
    假设我们向contestants矢量添加一些元素后,又想删除。如果使用条款5介绍的remove+erase成员函数的方法,能从vector删除元素,但无法改变其容量。如果想把它的容量从以前的最大值缩减到当前需要的数量时,该怎么办?这种容量的缩减通常称为“shrink to fit”。

    使用swap实现“shrink to fit”,从contestants矢量中除去多余的容量:

    vector<int> contestants = { 1, 2, 3, 4, 5, 6, 7, 8 };
    // size: 8 capacity: 8
    cout << "size: " << contestants.size() << " capacity: " << contestants.capacity()  << endl;
    // 使用remove + erase成员函数并不能改变vector容量
    contestants.erase(remove(contestants.begin(), contestants.end(), 2));
    
    // size: 7 capacity: 8
    cout << "size: " << contestants.size() << " capacity: " << contestants.capacity()  << endl;
    
    // 使用swap从contestants矢量中除去多余的容量
    vector<int>(contestants).swap(contestants);
    // size: 7 capacity: 7
    cout << "size: " << contestants.size() << " capacity: " << contestants.capacity()  << endl;
    

    vector(contestants)创建一个临时矢量,是contestants的拷贝:由vector的拷贝构造函数来完成。vector拷贝构造函数只为所拷贝的元素分配所需要的内存,因此临时矢量是没有多余容量的。
    然后,用swap操作交换临时矢量和contestants矢量。

    使用swap清除容器

    vector<double> v;
    string s;
    ... // 往v、s中添加、删除数据
    vector<double>().swap(v); // 清除v并把容量变为最小
    string().swap(s); // 清除s并把容量变为最小
    

    注意:string、vector的大小不一定是0,取决于库容器实现。

    [======]

    第18条:避免使用vector

    vector不是一个STL容器,也不存储bool。

    为什么说vector不是一个STL容器?
    因为一个对象要成为STL容器,就必须满足条款23列出的所有条件,其中一个是:如果c是包含对象T的容器,而且支持operator[],那么下面代码必须能被编译:

    T* p = &c[0]; // 用operator[]返回变量的地址初始化一个T*变量
    

    如果你用operator[] 取得了Container中的一个T对象,那么你可以通过它的地址得到一个指向该对象的指针(如上面的&c[0])。
    因此,如果vector是一个容器,那么下面代码可以被编译:

    vector<bool> v;
    bool* pb = &v[0]; // 实际上无法通过编译,因为&v[0]的类型是reference*,而非bool*
    

    但它不能被编译,因为vector并不是一个真的容器,不真正存储bool。为了节省空间,它存储的是bool的紧凑表示:每个bool仅占用1bit空间。而指向单个bit的指针或引用,是被禁止的。

    那如何解决vector::operator[]返回值是T&的问题呢?
    可以使用代理对象(proxy object):

    template<typename Allocator>
    vector<bool, Allocator> {
    public:
        class reference { ... }; // 用来为指向某个bit的引用而产生的代理类
        reference operator[](size_type n); // operator[]返回一个代理
        ...
    };
    

    既然vector应当避免,那么需要vector时,应该使用什么?
    标准库提供了2个选择:
    1)deque
    deque是一个STL容器,而且存储bool。但元素内存不是连续的,因此不能传递给一个期望bool数组的C API。

    2)bitset
    bitset不是标准C++库的一部分。大小(元素个数)编译时确定,因此不支持插入和删除元素。

    标准库为什么会保留vector,而不删除呢?
    因为最初C++标准委员会打算,用vector演示STL如何支持“通过代理来存储其元素的容器”。人们在实现自己的基于代理的容器时,就有现成的参考。不过,要创建一个代理的容器,同时又要满足STL容器的所有要求是不可能的。由于各种原因,他们的尝试被遗留在标准中。

    [======]

  • 相关阅读:
    PHP + JQuery 实现多图上传并预览
    推荐40个专业的CMS下载
    10 个有用的 PHP 代码
    2012年最值得关注最具颠覆性的10个创业公司
    在线生成 CSS3 的工具
    循环显示/隐藏图片
    100个推荐的图片/内容滑动条
    jQuery 下拉菜单插件
    20个网站测试工具
    20个免费在线生成网站
  • 原文地址:https://www.cnblogs.com/fortunely/p/15707313.html
Copyright © 2020-2023  润新知