Effective STL 学习笔记

秋招期间,一直在继续补习C++,最近开始看Effective STL, 了解了不少STL的使用技巧和注意事项,现在将学习成果笔记记录一遍,以后有时间再把Effective C++重看一遍。

基础

Item1 慎重选择容器

  • C++中的容器分为几大类?
  • 什么情景下该使用什么样的容器?

Item2 不要尝试写独立于容器的代码

看这章的第一感:有具体例子吗,那STL的泛型算法算是怎么回事?

不要试图编写多个容器都适用的程序代码,原因:

  • 一些特定的功能只存在于特定的容器,比如push_front, pop_front只能在deque里用
  • 即使是同名功能,对不用容器实现的机制也大不一样,比如vector和list的insert()和erase()。(试图写对这两个容器都通用的insert()和erase()的同学别想了)
  • 只能使用参与容器的功能交集,比如写对vector,deque 和 list都适用的代码,那么就不能用operator[](list不支持),push_front()pop_front()(vector不支持),不能用reverse或者capacity。。。等等,这会使得代码编写和维护极为艰难。
  • 不同容器的迭代器类型不一致,迭代器,指针和引用失效规则也不一致,还以上述的vector,list和deque为例唯一兼容的方法,就是让每次insert和erase都导致迭代器,指针和引用失效(这还怎么玩?)

综上,不要幻想写出对各种容器都兼容的代码。

Item3 使得容器里对象的拷贝操作轻量而正确

主要说明了STL容器的对象移动多为拷贝为主(想想出书的时候还没有c++11的移动语义), 典型例子就是vector的push_back,以及用容器初始化另一个容器,留心。

Item4:用empty来代替检查size()是否为0

c.empty()代替c.size()==0,原因:

无论对什么容器,c.empty()都是O(1)时间,而c.size() == 0可能是O(N)时间,取决于实现。以list为例,实现了O(1)的size函数就不能使用区间版本的splice函数,反之实现区间版本的splice只能使用O(N)的size函数, 取决于list的实现,使用不同平台的STL时留心。

Item5: 尽量使用区间成员函数代替它们的单元素兄弟

**引入:**为什么建议用区间成员函数代替单元素函数:

​ 例子:使得vector v1和v2的后半部分医院:v1.assign vs 手写循环

#include<bits/stdc++.h>
using namespace std;
int main(){
    vector<int> v1{1,2,3,4,5,6};
    vector<int> v2{4,5,6,7,8,9};
    v1.assign(v2.begin() + v2.size() / 2, v2.end());
    for(int i : v1){
        cout << i << " ";
    }

    // v1.clear();
    // for (vector<int>::const_iterator ci = v2.begin() + v2.size() / 2; ci != v2.end(); ++ci)
    // v1.push_back(*ci);
}

结论:用区间成员函数使得代码更少, 更清晰(理由一);

**开门:**说明用区间成员函数之理由二(效率)

#include<bits/stdc++.h>
using namespace std;
int main(){
    int numValues = 5;
    int data[numValues] = {1,2,3,4,5}; // 假设numValues在
    // 其他地方定义
    vector<int> v;
    //区间成员函数
    // v.insert(v.begin(), data, data + numValues); // 把data中的int插入v前部

    //对比1
    // vector<int>::iterator insertLoc(v.begin());
    // for (int i = 0; i < numValues; ++i) {
    //     insertLoc = v.insert(insertLoc, data[i]);
    //     ++insertLoc;
    // }
    //对比2
    copy(data, data + numValues, inserter(v, v.begin()));


    for(int i : v){
        cout << i << " ";
    }
}

​ 例:将int数组头插到vector

  • 显式循环+迭代插入 -> 需考虑迭代器无效问题

    • copy ->同样底层是显式循环

显式循环的冲击:三种税(以单元素insert为例)

  • 不必要的函数调用(n个元素调用n次insert)
  • 每次insert造成的移动开销(每个元素移动n * numsvalues次,对于int等原生类型只需调用memmove,对于自定义class还要加上调用n * numvalue次拷贝构造函数/赋值运算符的开销,对比区间成员函数能预先计算移动位置,所有元素都只移动一次,累计n次,开销更少,但实现条件是参数迭代器必须为前向迭代器及其后继)
  • 不必要的内存匹配
    • 插入满的vector n个元素造成最多 l o g 2 N log_2N log2N次扩容(对比区间函数可一次计算出新内存)
    • list插入时重复设置prev和next指针(假设n个节点插入B与A之间,则A->prev多赋值n-1次,插入节点->next多赋值n-1次,供2*(n - 1)次多余赋值)

总结:

​ 使用区间成员函数代替单元素函数的理由:代码更少,效率更高;

​ 哪些成员函数支持区间:构造,insert, erase

Item6:警惕C++最令人恼怒的解析

C++里的一条通用规则——几乎任何东西都可能被分析成函数声明。

#include<iostream>
#include<fstream>
#include<list>
#include<iterator>
using namespace std;

//如下都是合法的函数声明
int f(double d);
int f(double (d)); // 同上;d左右的括号被忽略
int f(double); // 同上;参数名被省略

int g(double (*pf)()); // g带有一个指向函数的指针作为参数
int g(double pf()); // 同上;pf其实是一个指针
int g(double ()); // 同上;参数名省略

int main(){
    ifstream dataFile("ints.dat");
    // 警告!这完成的并不是像你想象的那样

    //C++会把data的第二个参数当成函数指针
    // list<int> data(istream_iterator<int>(dataFile),istream_iterator<int>()); 
    
    //解决方法1
    list<int> data((istream_iterator<int>(dataFile)),istream_iterator<int>()); 
    
    //解决方法2
    // istream_iterator<int> dataBegin(dataFile);
    // istream_iterator<int> dataEnd;
    // list<int> data(dataBegin, dataEnd);

    for(auto it = data.begin(); it != data.end(); it++){
        cout << *it << " ";
    }
    
}

vector和string

Item13:尽量使用vector和string来代替动态分配的数组

Question:

string和vector在实现上有哪些不同?

为什么引用计数会有多线程的性能问题?

如果你有只支持int数组和char数组的CAPI,怎么给它们传入string和vector?

A1:

相比vector,很多string实现采用了引用计数机制来减少不必要的拷贝和动态分配,但代价是在多线程环境下要承受保证线程安全性带来的性能下降问题,解决方法有:寻找string实现中是否有关闭引用计数的宏选项; 直接实现一个string;采用vector<char>, 代价是无法使用string提供的各类成员函数。

A2:

参考shared_ptr的例子,shared_ptr的引用计数是个原子类型,多线程下不会有读写冲突问题,但是指向对象的指针不是,指针访问和拷贝对象都不是线程安全的,可能会导致下列问题:

线程A使得对象a1指向对象foo,欲让对象a2也指向foo,这时候线程B抢先一步把a1改为指向bar, foo引用计数为0被销毁,这时候线程A操作,a2指向一个空对象。。。。

因此多线程下使用shared_ptr一般要考虑并发控制。

参考:https://www.cnblogs.com/solstice/archive/2013/01/28/2879366.html

A3:参考Item16

Item14 使用reverse来避免不必要的重新分配

#include<bits/stdc++.h>
using namespace std;

int main(){
    vector<int> v;
    //不用reserve, 这个代码将导致log_2{1000}次(约10次)扩容

    v.reserve(1000);
    for(int i = 0; i < 1000; i++){
        v[i] = 2;
    }
    cout << v.size() << " " << v.capacity() << endl;    //warning:size不变!
    for(int i = 0; i < 1000; i++){
        v.push_back(v[i]);
    }
    cout << v.size() << " " << v.capacity() << endl;    //正常做法

}

条款15:小心string实现的多样性

Question:

在你的机器sizeof(string)的结果是多少?

面试官如果要你讲string的实现,怎么办?

A: 这个要看具体的STL实现,string的实现有很多,有的可能一个本身是一个结构体指向一块存放字符的连续内存(如实现A D),有的是一个指针指向一块存放了字符串相关信息的内存(如实现B C),但不管什么实现,string都会有如下三个属性:size, capacity,存放字符串元素的value,差异在于sizeof(string)的大小可能是char*的一到七倍,动态分配内存的次数(实现ABC一次,实现B两次),是否采用引用计数等。

image-20221112152036789

Item16 如何将vector和string的数据传给遗留的API

question:

如何将string传给接受char*参数的CAPI?

如何通过C API给vector填充元素,CAPI接受int数组呢,char呢?

#include<bits/stdc++.h>
using namespace std;
void fillArray(int *pArr, int n, int val){
    for(int i = 0; i < n; i++)
        pArr[i] = val;
}
void fillString(char *pStr, int n, char val){
    for(int i = 0; i < n; i++)
        pStr[i] = val;
}

int main(){
    int n = 10;
    vector<int> v(5);
    string str = "Hello World!";
    fillArray((int*)&v[0], v.size(), 9);
    fillString((char*)&str[0], str.size(),'A');
    
    for(int i : v)
        cout << i << " ";
    cout << endl;
    cout << str << " ";
}

Item17 使用“交换技巧”来休整过剩容量

Question: 当一个vector容器过剩时,如何调整其大小?

#include<iostream>
#include<vector>
using namespace std;

int main(){
    vector<int> vec;
    vec.reserve(10000);
    for(int i = 0; i < 5; i++){
        vec.push_back(i);
    }
    cout << vec.size() << " " << vec.capacity() << endl;
    //方法一 swap 拷贝构造得到一个size==capacity的临时vector和vec交换, 原vec随后被销毁
    vector<int>(vec).swap(vec);
    //C++11 新增shrink_to_fit
    // vec.shrink_to_fit();
    cout << vec.size() << " " << vec.capacity() << endl;

}

Item18 避免使用vector<bool>

STL专门对vector<bool>做了特化,导致其行为和常规的vector容器有很大差异,首先是bool的存储方式,为了节省空间,bool在vector<bool>中并不是按1字节来存储的,而是按1比特存储的!这就意味着在vector<bool>一个字节能够容纳8个bool, 由于无法对一个比特取地址值,因此下面的代码将报错:

vector<bool> test{true, false, true};
bool *pb = &test[0];    //error: taking address of rvalue

当下标引用test的元素时,实际上是返回一个名为std::_Bit_reference的结构体, 即为比特的"引用", C++中就采用了这种结构体来实现对vector<bool>中某一元素(某一比特位)的引用,而这个结构体主要的两个成员各占了8字节,故sizeof其成员的大小将是16字节:

struct _Bit_reference
  {
    _Bit_type * _M_p;
    _Bit_type _M_mask;
  }
  ...
  
int main(){
    vector<bool> test (3,0);
    cout << "Sizeof test[0]: " << sizeof(test[0]) << endl; //16
    cout << "Sizeof test[1]: " << sizeof(test[1]) << endl; //16
    cout << "Sizeof test[2]: " << sizeof(test[2]) << endl; //16
}

由于调用下标返回时,要格外注意,如果未指定左值类型,则返回是对比特位的"引用",因此实际从vector<bool>元素取下标时若修改左值也会影响到元素本身:

int main(){
    vector<bool> c{ false, true, false, true, false }; 
    bool b = c[0]; //指定了是bool类型,调用拷贝赋值函数
    auto d = c[0]; //未指定类型,返回引用
    d = true;   //影响c
    b = false;  //不影响
    for(auto i:c)
        cout<<i<<" ";   //1 1 0 1 0
    cout<<endl;
}

最后一问:为什么vector<bool>的大小是40字节?

个人并没有找到详细的原因,推测vector<bool>中sizeof统计的成员除了start, finish, end_of_storage这三个迭代器成员外,再加上之前提到的Bit_reference结构体就总计24 + 16 = 40字节。

参考:

https://leetcode.cn/circle/discuss/dGSFg1/

关联容器

Item19 了解相等与等价的区别

Question:

如何实现一个去重不分大小写的set?

个人理解:

相等指的是被容器的比较成员如operator==所定义的相等,而等价则是指广泛排序意义上的值相等,类似于js中的===和Java的equal()

#include<bits/stdc++.h>
using namespace std;
struct CIStringCompare{
    //false-0,相等 true-1,不相等 被Linux的哲学坑了 
    bool operator()(const string& lhs, const string& rhs) {
        if(lhs.size() != rhs.size())
            return true;
        for(int i = 0; i < lhs.size(); i++){
            if(tolower(lhs[i]) != tolower(rhs[i]))
                return true;
        }
        return false;
    }
};

int main(){
    set<string,CIStringCompare> set;	//默认set<int> == set<int,less<int>>
    set.insert("persephone");
    set.insert("PERSEPHONE");
    for(string s : set)
        cout << s << " ";
    cout << endl;
    auto it1 = set.find("PERSEPHONE");

    //persephone与PERSEPHONE等价,CIStringCompare(persephone, PERSEPHONE) == false, 故成员函数能查找到
    if(it1 != set.end()){
        cout << "it1: " << *it1 << endl;
    }

    //但persephone与PERSEPHONE不相等,因此泛型算法就找不到了
    auto it2 = find(set.begin(), set.end(), "PERSEPHONE");
    if(it2 != set.end()){
        cout << "it2: " <<  *it2 << endl;
    }

}

Item20 为指针的关联容器指定比较类型

这个不是在写webserver基于set的定时器就做过吗。。。。

class cmp{
    public:
        bool operator()(client_timer *lhs, client_timer *rhs){ return lhs->m_expired < rhs->m_expired; }
};

class sort_timer_lst{
    public:
        sort_timer_lst();
        ~sort_timer_lst();
        void add_timer(client_timer* new_timer);
        void abjust_timer(client_timer* timer);
        void del_timer(client_timer* timer);    //主动清除timer,一般情形是客户端主动断连
        void tick();    //自动清除超时的timer
    #if DEBUG != 1
    private:
    #endif
        std::multiset<client_timer*,cmp> m_timer_lst;
};

好吧,还是有点区别的。

Question:

如何实现一个会根据字符串字典序排序的,装字符串指针的set?

如果set装的指针不一定是string呢,比如int?

扩展:如何用算法取代循环?(item43)

1的错误示范:

#include<bits/stdc++.h>
using namespace std;

int main(){
    set<string*> ssp;
    ssp.insert(new string("Anteater"));
    ssp.insert(new string("Wombat"));
    ssp.insert(new string("Lemur"));
    ssp.insert(new string("Penguin"));

    // 你希望看到这个:“Anteater”,“Lemur”,“Penguin”,“Wombat”, 实际打印的是指针的值
    for (set<string*>::const_iterator i = ssp.begin(); i != ssp.end();  ++i)
        cout << *i << endl; 
    // //上图代码等价于
    // copy(ssp.begin(), ssp.end(),  ostream_iterator<string>(cout, "\n")); //error!

}

正确示范:

struct StringPtrLess{
    bool operator()(const string *ps1, const string *ps2) const{
        return *ps1 < *ps2;
    }
};
int main(){
    set<string*,StringPtrLess> ssp;
    ssp.insert(new string("Anteater"));
    ssp.insert(new string("Wombat"));
    ssp.insert(new string("Lemur"));
    ssp.insert(new string("Penguin"));

    // 你希望看到这个:“Anteater”,“Lemur”,“Penguin”,“Wombat”, 实际打印的是指针的值
    for (set<string*>::const_iterator i = ssp.begin(); i != ssp.end();  ++i)
        cout << **i << endl; 
   //上图代码等价于
    // copy(ssp.begin(), ssp.end(),  ostream_iterator<string>(cout, "\n")); //仍旧无法使用,会打印出指针十六进制值

}

现在回答问题2:

#include<bits/stdc++.h>
using namespace std;
struct StringPtrLess{
    bool operator()(const string *ps1, const string *ps2) const{
        return *ps1 < *ps2;
    }
};
struct Dereference {
    template <typename T>
    const T& operator()(const T *ptr) const{
        return *ptr;
    }
};
struct DeferenceLess{
    template <typename PtrType>
        bool operator()(PtrType ptr1, PtrType ptr2) const{
            return *ptr1 < *ptr2;
        }
};
int main(){
    set<string*,DeferenceLess> ssp;
    ssp.insert(new string("Anteater"));
    ssp.insert(new string("Wombat"));
    ssp.insert(new string("Lemur"));
    ssp.insert(new string("Penguin"));

    // 你希望看到这个:“Anteater”,“Lemur”,“Penguin”,“Wombat”, 实际打印的是指针的值
    for (set<string*>::const_iterator i = ssp.begin(); i != ssp.end();  ++i)
        cout << **i << endl; 
   //上图代码等价于
    // transform(ssp.begin(), ssp.end(), ostream_iterator<string>(cout, "\n"), Dereference()); 

}

Item 22 避免原地修改set和multiset的键

Question:

为什么map, set和multimap, multiset的键都不可修改?

答:因为这4个元素内的值都是预先排序好的,修改它们会导致未定义的结果。

map<T>multimap<T>内部的键一定是const T, 但是set和multiset内部的键有可能是T,可以修改,但是我看了我这里的STL实现是不可修改。

#include<iostream>
#include<map>
#include<set>
using namespace std;
struct Employee {
    int _idNumber;
    string _Name;
    string _title;
    Employee(int id, string na, string tit):_idNumber(id),_Name(na),_title(tit){}
};

struct IDNumberLess{ // 参见条款40
    bool operator()(const Employee &lhs,const Employee &rhs) {
        return lhs._idNumber < rhs._idNumber;
    }
};
int main(){
    map<int, string> m;
    // m.begin()->first = 10; // 错误!map键不能改变
    multimap<int, string> mm;
    // mm.begin()->first = 20; // 错误!multimap键也不能改变
    // auto key1 = *m.begin();  //std::pair<const int, std::string> key
    // auto key2 = *mm.begin();  //std::pair<const int, std::string> key

    set<int> s;
    s.insert(10);
    auto it = s.begin(); 
    // *it = 10;    // Error, set键也不能改变!

    set<Employee,IDNumberLess> s2;
    Employee e = Employee(1,"Kanza","manager"); //跟书上说的不一样,改成类修改非排序主键也不行
    s2.insert(e);
    auto it2 = *s2.begin(); 
    // it2->_title = "Employee";  //非排序主键也不可改变!

    //解决方法1 const_cast (不推荐)
    const_cast<Employee&>(it2)._title = "Employee";
    cout << it2._Name << " " << it2._title << endl;

    //解决方法2 copy-erase-insert
    auto it22 = s2.begin();
    Employee e2(*it22);
    s2.erase(it22);
    e2._Name = "Sam";
    s2.insert(e2);
    
    for(auto e : s2){
        cout << e._Name << " " << e._title << endl;
    }

}

Item23 考虑用有序vector代替关联容器

Question:

现有一个有序的vector容器和一个map存储同样多的元素,在vector中二分查找和在map中查找哪个速度更快?

有序vector<pair<K,V>> 和map<K>的区别?

什么情形下,可以用有序vector替换map?

1: vector中二分查找更快,原因:

vector所占用的内存会比map少,因为vector中内存连续,存储一个元素没有指针开销,而在map中存储一个元素有3个指针的开销(left,right,parent);举例32位系统(指针4字节),一页4k,widget类12字节,用vector能保存341个widget, 用map则只能存170个(因为widget+3指针=24字节)

map内存不连续,存储的元素可能跨了几个内存页面,即使其STL内存分配器实现了内存集中在一个小内存页面,引发的缺页异常频率也会比vector多。

2:

vector中元素类型是pair<k,v>,可修改,map中元素类型是pair<const k, v>,键不可修改;

vector欲有序需自定义实现pair元素的比较函数。

3:

所选用的容器以查找为主,没有或者极少增删时考虑用有序vector,因为有序vector的缺点就是增删元素造成的元素移动开销

Item 24 当关乎效率时应该在map::operator[]和map-insert之间仔细选择

Question:

何时最好使用map::operator[]? 何时最好使用map::insert[]?

能否实现一个添加或更新都高效的函数?(选看)

#include<bits/stdc++.h>
using namespace std;

int main(){
    map<int,string> m;
    // m[1] = "1.50";

    //等价于
    typedef map<int,string> intStrMap;
    // //value_type是一个迭代器属性,这里代表在键值1上构造string, 返回pair
    // pair<intStrMap::iterator,bool> res = m.insert(intStrMap::value_type(1,string()));
    // //为string赋值
    // res.first->second = "3.50";


    /* 可看到上面多了三个不必要的开销:,
        构造临时string的开销
        销毁临时string的开销
        赋值开销
    若要高效插入应该用insert将构造和赋值结合起来 */
    m.insert(intStrMap::value_type(1,"4.50"));
    cout << m[1] << endl;
    //如若进行更新,insert有拷贝开销就不必要了,用operator[]
    m[1] = "3.5";
    cout << m[1] << endl;


}

算法

Item32 如果你真的想删除东西的话就在类似remove的算法后接上erase

vector的remove方法只会交换元素或者移动元素, 并不会真的删除元素,也不会影响迭代器,原理类似于27. 移除元素。

要想真正删除元素,就得在remove和erase配合使用,代码演示如下:

只remove:

#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
int main(){
    vector<int> v; // 建立一个vector<int> 用1-10填充它
    v.reserve(10); // (调用reserve的解释在条款14)
    for (int i = 1; i <= 10; ++i) {
        v.push_back(i);
    }

    cout << v.size() << endl; // 打印10
    v[3] = v[5] = v[9] = 99; // 设置3个元素为99
        
    /* 
        1 2 3 99 5 99 7 8 9 99 
    */
    for(int i = 0; i < v.size(); i++){
        cout << v[i] << " ";
    }
    cout << endl;

    remove(v.begin(), v.end(), 99); // 删除所有等于99的元素

    /* 
        // 仍然是10: remove只交换或者移动元素不会真的删除元素,也不会影响迭代器! 原理可参考27. 移除元素
        1 2 3 5 7 8 9 8 9 99 
    */
    cout << v.size() << endl; 
    for(int i = 0; i < v.size(); i++){
        cout << v[i] << " ";
    }
    cout << endl;
}

remove后erase

#include<iostream>。
#include<vector>
#include<algorithm>
using namespace std;
int main(){
    vector<int> v; // 建立一个vector<int> 用1-10填充它
    v.reserve(10); // (调用reserve的解释在条款14)
    for (int i = 1; i <= 10; ++i) {
        v.push_back(i);
    }

    cout << v.size() << endl; // 打印10
    v[3] = v[5] = v[9] = 99; // 设置3个元素为99
        
    /* 
        1 2 3 99 5 99 7 8 9 99 
    */
    for(int i = 0; i < v.size(); i++){
        cout << v[i] << " ";
    }
    cout << endl;

    /* 
        7
        1 2 3 5 7 8 9 
     */
    //用erase-remove法才能真正删除被remove的元素: remove返回移除后容器的新大小处的迭代器
    v.erase(remove(v.begin(), v.end(), 99), v.end()); 
    cout << v.size() << endl; 
    for(int i = 0; i < v.size(); i++){
        cout << v[i] << " ";
    }
    cout << endl;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值