C++【STL】 | STL Effective C++

文章目录

    • 1、慎重选择容器类型
    • 2、不要试图编写独立于容器类型的代码
    • 3、确保容器中的对象拷贝正确而高效
    • 4、调用empty而不是检查size是否为0
    • 5、区间成员函数优先于与之对应的单元素成员函数
    • 6、当心C++编译器最烦人的分析机制
    • 7、如果容器中包含了通过new操作创建的指针,切记在容器对象析构前将指针delete掉
    • 8、切勿创建包含auto_ptr的容器对象
    • 9、慎重选择删除元素的方法
    • 10、了解分配子的约定和限制
    • 11、理解自定义分配子的合理用法
    • 12、切勿对STL容器的线程安全性有不切实际的依赖
    • 13、vector和string优先于动态分配的数组
    • 14、使用reserve避免不必要的重新分配
    • 15、注意string实现的多样性
    • 16、了解如何把vector和string数据传给旧的API
    • 17、使用swap技巧除去多余的容量
    • 18、避免适用vector\
    • 19、理解相等和等价的区别
    • 20、为包含指针的关联容器指定比较类型
    • 【21】、总是让比较函数在等值情况下返回false
    • 22、切勿直接修改set或multiset中的键
    • 23、考虑用排序的vector替代关联容器
    • 24、当效率至关重要时,请在map::operator[]与map::insert之间做谨慎做出选择
    • 25、熟悉非标准的散列容器
    • 26、iterator优先于const_iterator、reverse_iterator及const_reverse_iterator
    • 27、使用distance和advance将容器的const_iterator转成iterator
    • 28、正确理解由reverse_iterator的base()成员函数所产生的iterator用法
    • 29、对于逐个字符的输入请考虑使用istreambuf_itreator
    • 30、确保目标区间足够大
    • 31、了解各种与排序有关的选择
    • 32、如果确实需要删除元素,则需要在remove这一类算法之后调用erase
    • 33、对包含指针的容器使用remove这一类算法时要特别小心
    • 34、了解哪些算法要求使用排序的区间作为参数
    • 35、通过mismatch或lexicographical_compare实现简单的忽略大小写的字符串比较
    • 36、理解copy_if算法的正确实现
    • 37、使用accumulate或for_each进行区间统计
    • 38、遵循按值传递的原则来设计函数子类
    • 39、确保判别式是纯函数
    • 40、若一个类是函数子,则应使它配接
    • 41、理解ptr_func、mem_fun和men_fun_ref的来由
    • 42、确保less\与operator<具有相同的语义
    • 43、算法调用优先于手写的循环
    • 44、容器的成员函数优先于同名的算法
    • 45、正确区分count、find、binary_search、lower_bound、upper_bound和equal_range
    • 46、考虑使用函数对象而不是函数作为STL算法的参数
    • 47、避免产生“直写型”代码
    • 48、总是包含正确的头文件
    • 49、学会分析与STL相关的编译器诊断信息
    • 50、熟悉与STL相关的web站点

1、慎重选择容器类型

分类:
	【序列式】:vector、string、deque、list;
	【关联式】:set、multiset、map、multimap;
	【非标准序列式】:slist、rope;
	【非标准关联式】:hash_set、hash_miltiset、hash_map、hash_multimap;
	【标准非STL】:array、bitset、valarray、stack、queue、priority_queue;
	【连续内存容器】:vector、string、deque;
	【基于节点容器】:list、map、set;

选择:【考虑排序情况、迭代器能力、元素布局与C的兼容、查找速度、引用计数、事务语义、算法复杂性……】
	- 若要任意位置插入,序列式容器;
	- 元素是否排序,哈希容器;
	- 随机访问,vector/deque/string;
	- 避免删除或插入时移动容器元素,则选择基于节点容器;
	- 数据布局是否与C兼容,考虑vector;
	- 查找速度,哈希容器、sort的vector、关联容器;
	- 不使用带引用计数的string,vector;
	- 插入删除出现异常需要回滚,选择list;
	- 防止使用swap则其迭代器、指针、引用变得无敌,避免string; 

2、不要试图编写独立于容器类型的代码

虽然各个容器间都有相同的成员函数,但各自都有属于自己的特性,故无法编写能够兼容所有容器的函数;
例如:
	插入方式:序列式采用push_front/push_back,关联式采用insert;
	区间锁定:关联式提供了lower_bound、upper_bound、equal_range;
	删除操作:关联是采用erase-remove,关联式直接使用erase;
	operator:对map提供而对multimap不提供;

typedef应用于容器:
	一般将容器类型,迭代器使用该种方式进行定义,便于后续的使用以及对容器的修改;
	若要想减少在替换容器类型时所需要修改的代码,可将其隐藏到一个类中,并尽量减少通过类接口开放与容器相关的信息;
class A {
    typedef vector<int> vc;
    typedef vc::iterator it;
};

3、确保容器中的对象拷贝正确而高效

当对象存入到容器中,是将指定对象进行拷贝,当对象进行移动时,也是将对象进行拷贝;
【继承关系下拷贝动作会被分割】:
	- 当容器中放入基类的类型是,当接收的对象为子类,则将会丢失部分子类的信息;
	- 为了避免该问题,我们将使用在容器存放该基类的指针类型来解决这一问题;

容器的设计是为了避免创建不必要的对象

当创建n长度的数组,则数组会自动填充数组;
而容器会只会余留空间,当需要时用户在创建即可;
数组一般通过拷贝创建对象,当哟个过户使用默认构造时,才使用该方法创建;
class A {
public:
    A() {}

    A(const A& a) {
        cout << "copy" << endl;
    }

    void pprint() {
        cout << "----" << endl;
    }

    ~A() {}

};

void test() {
    A a[10];
    cout << "test array" << endl;
    for (int i = 0; i < 10; ++i) {
        a->pprint();
    }

    vector<A> aa;
    aa.reserve(10);
    cout << "test container" << endl;
    for(auto i : aa) {
        i.pprint();
    }
}

C++【STL】 | STL Effective C++_第1张图片
容器中对象拷贝

当对象存入容器时是指定对象的拷贝(aaa),当保存到容器中,会进一步被拷贝;
void test() {
	A aaa;
	vector<A> aa(10, aaa);
	cout << "test container" << endl;
	for(auto i : aa) {
	    i.pprint();
	}
}

C++【STL】 | STL Effective C++_第2张图片

4、调用empty而不是检查size是否为0

empty:对于所有容器都是常数时间操作;
size:对链表结构实现时,耗费线性时间;

为什么对链表结构不同呢?【参考下图】
当删除或者移动链表中的节点时,只需要断开节点之间的指针即可,即为常数时间的操作;若需要在该操作中记录链表的大小,则需要遍历其中的个数,进而
会增加操作的时间使之成为线性时间操作;但其size()函数则可通过常数时间获取其长度;相反,若在删除或移动等操作中不对size进行记录,则该操作为常
数时间,则size为线性时间;而empty即可直接通过是否右结点来判断,此时empty的性能即比size高;
【empty的性能>=size()】

C++【STL】 | STL Effective C++_第3张图片

5、区间成员函数优先于与之对应的单元素成员函数

- 减少代码量;
- 让代码的意图更加简洁明了;
- 单元素成员函数将会调用更多的内存分配子,更频繁的调用拷贝,更多的冗余操作;

当对于数组类型操作时

- 区间形式只需要调用1次,相比于循环节省了n-1次;
- 频繁地操作,导致调用赋值或拷贝动作;
- 若类似动态增长内存的容器,在当元素成员函数不断插入的时候不断的扩充内存导致效率大大降低,而区间函数即可在一开始即了解内存大小,从而一步扩充;

C++【STL】 | STL Effective C++_第4张图片
C++【STL】 | STL Effective C++_第5张图片

6、当心C++编译器最烦人的分析机制

参数名忽略

// 1、类型参数
int f(double d); ===> int f(double (d));
// ===> 省略参数名
int f(double);

// 2、函数参数
int f(double(*pf)())
// ===> 省略参数名
int f(double ());
ifstream dataFile("ints.dat");
list<int> data(istream_isterator<int>(dataFile), istream_iterator<int>()); 
以上该data由两个参数组成,参数一:istream_isterator类型,参数二为istream_iterator的函数指针;
为了避免代码的移植性一般不使用匿名对象;
ifstream dataFile("ints.dat");
istream_isterator<int> dataBegin(dataFile);
istream_iterator<int> dataEnd;
list<int> data(dataBegin, dataEnd); 

7、如果容器中包含了通过new操作创建的指针,切记在容器对象析构前将指针delete掉

8、切勿创建包含auto_ptr的容器对象

COAP:auto_ptr的容器;
由于COAP的特性,在赋值的过程中会交出所有权,而自身变为nullptr;
例如,当在进行排序或其他操作中,会改变其内容;

9、慎重选择删除元素的方法

当删除连续内存的容器的最好方法即`erase-remove/remove_if`;
list直接使用remove、remove_if;
关联容器删除erase;

一般不使用遍历的方法进行删除,若错误使用会导致迭代器变得无效

// 错误做法,该迭代器使用前缀++,返回其本身,当i被erase后即i为无效值
for(container<int>::iterator i = c.begin(); i!=c.end(); ++i) {
	if(...) c.erase(i);
}
// 正确做法:使用后缀++,返回其副本
for(container<int>::iterator i = c.begin(); i!=end();) {
	if(...) c.erase(i++);
	eles ++i;
}
既然提供了函数可直接删除,为什么需要遍历来进行删除呢?
- 当我们在删除需要增加一部分其他操作,例如将该操作写入日志文件中等;

10、了解分配子的约定和限制

分配子时提供一个内存模型的抽象;C++中,一个类型为T的对象,它的默认分配子为allocator,其提供了两个定义pointer和reference;
如:vecor	⇒ alloc为分配子;
当一个容器包含另一个容器的对象时,其两者分配子应该相同,否则在释放的时候可能出现问题;
为了能够在不同的STL下工作,其分配子不能有任何非静态的数据成员(状态),为了防止影响其行为;

自定义分配子满足的要求

- 该分配子为一个模板,模板参数代表你为它分配内存的对象的类型;
- 提供类型定义pointer(T*)和reference(T&);
- 分配子不能带有非静态数据成员;
- 传给allocate成员函数是要求内存的对象个数,而不是字节数,且返回值为T*;
- 一定要提供嵌套的rebind模板,由于标准容器依赖;

rebind模板

template<typename T>
class allocator{
public:
    template<class U>
    struct rebind {
        typedef allocator<U> other;
    };
};

11、理解自定义分配子的合理用法

用法一

四步曲:分配、构造、析构、释放;
void *mallocShared(size_t byteNeeded);
void *freeShared(void* ptr);

template<class T>
class SharedMemoryAllocator {
public:
    typedef T value_type;
    typedef size_t size_type;
    typedef T* pointer;
    typedef T& reference;

    pointer allocate(size_type numObjs, const void* localityHint = 0) {
        return static_cast<pointer>(mallocShared(numObjs * sizeof(T)));
    }

    void deallocate(pointer ptrToMemory, size_type numObjs) {
        freeShared(ptrToMemory);
    }
    ~SharedMemoryAllocator() {}
};

void test(){
    typedef vector<double, SharedMemoryAllocator<double>> SharedDoubleVec;

    SharedDoubleVec v;
    void *pVecMemory = mallocShared(sizeof(SharedDoubleVec));

    SharedDoubleVec *pv = new (pVecMemory)SharedDoubleVec ;
    pv->~SharedDoubleVec();
    freeShared(pVecMemory);
}

用法二

以下中Heap1是类型而不是对象;
class Heap1 {
public:
    static void* alloc(size_t numBytes, const void* memoryBlockToBeNear);
    static void dealloc(void* ptr);
};

template<class T, class H>
class SpecificHeapAllocator {
public:
    typedef T value_type;
    typedef size_t size_type;
    typedef T* pointer;
    typedef T& reference;

    pointer allocate(size_type numObjs, const void* localiHint = 0) {
        return static_cast<pointer>(H::alloc(numObjs * sizeof(T), localiHint));
    }

    void deallocate(pointer ptrToMemory, size_type numObjects) {
        H::dealloc(ptrToMemory);
    }
};


void test2() {
    vector<int, SpecificHeapAllocator<int, Heap1>> v;
}

12、切勿对STL容器的线程安全性有不切实际的依赖

在STL中:
- 多个线程对同一个容器读操作是安全的;
- 多个线程对不同容器做写操作是安全的;

如何尝试让容器实现完全线程安全

- 对容器成员函数每次调用,都将其lock直到调用结束;
- 在容器所返回的迭代器的生存期结束前,都lock容器;
- 对于作用容器的算法,应将其lock直到算法结束;

自定义Lock获取资源时初始化

template<class container>
class Lock{
public:
    /* 当该类被构造时自动上锁 */
    Lock(const container& ct) : c(ct) {
        getMutexFor(c);
    }
    /* 离开作用域时自动释放锁 */
    ~Lock() {
        releaseMutexFor(c);
    }
private:
    const container& c;
};


void test() {
    vector<int> v;
    Lock<vector<int>> lock(v);
    // ....
}

13、vector和string优先于动态分配的数组

【当使用new来分配内存时】:
- 确保该内存使用delete释放;
- 确保使用正确形式的delete;
- 确保制备delete一次;
【使用arr没有提供成员函数供用户便捷使用】
【使用string具有引用计数】
【vector和string能够自动调用析构函数释放包含元素的内存】
【string使用了引用计数,消除不必要的内存分配和不必要的拷贝动作】
考虑到以上因素故我们应该使用STL中的vector或string更加便捷

14、使用reserve避免不必要的重新分配

- 该成员函数能够把重新分配的次数减少到最低,减少了重新分配和指针、迭代器、引用失效带来的开销;
- 改变容器的容量;

【如何使用reserve给程序带来好处】
- 由于vector内存是以2倍增长的,且每次增长都将旧内存的拷贝到新内存中在将旧内存析构释放,过程繁琐,效率低;
- 若我们还未改容器插入元素时就将其内存大小分配好,就能够避免后续的此类操作,提高程序效率;
- 当然,如果无法预知后续的使用大小,可先预分配一个较大的内存,当元素全部插入完毕后,在将多余的内存去除;

15、注意string实现的多样性

- string一般会提供引用计数的方法,但也提供将其关闭的方法;
- string对象大小的范围阔以是char*指针的大小的1~7倍;
- 创建一个新的str可能需要0次或1、2次的动态内存分配;
- string对象可能共享,也可能不共享其大小和容量信息;
- string可能支持单个对象的分配子;
- 不同的实现对字符内存的最小分配单位有不同的策略;

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

vector

v[0]指向第一个元素,&v[0]则为第一个元素的指针,vector为连续内存,故直接将改指针传递即可;
一般不适用begin传递,因为begin返回的是迭代器,若执意要使用则传递&*v.begin();
/* 将vector传递给指针 */
void test(int *arr, int size) {
	if(size == 0) return;
    for(int i=0; i<size; ++i) {
        cout << arr[i] << endl;
    }
}

int main() {
    vector<int> vc {1, 3, 4};

    test(&vc[0], vc.size());
    return 0;
}

C++【STL】 | STL Effective C++_第6张图片
string

以上从方式不适用于string由于string不是连续内存;
但string有提供相应的函数将其转换成C串 ==> c_str();
c_str:返回的指针不一定指向字符串数据内部,有可能是其数据的const拷贝;
void test01(const char* str) {
    cout << str << endl;
}

int main() {
    string s;
    test01(s.c_str());

    return 0;
}

17、使用swap技巧除去多余的容量

使用该方法减少内存的浪费;
vector(contestants).swap(contestants);该操作将调用拷贝构造;
***临时vector(contestants)使用拷贝构造函数,其只为拷贝元素分配所需的内存,故临时vector没有多余的内存,在将临时的与原vector
进行替换,将有余量的拷贝到临时vector中,当程序离开当当前作用域时,即自动释放;
- string也适用;
void test(vector<int>& v) {
    vector<int>(v).swap(v);
    cout << v.capacity() << endl;
}

int main() {
    vector<int> vc;
    vc.reserve(100);
    vc.push_back(100);
    vc.push_back(200);
    vc.push_back(300);
    cout << vc.capacity() << endl;
    test(vc);

    return 0;
}

在这里插入图片描述

18、避免适用vector

vector不属于STL容器,其内部不存储bool,存储二进制位,无法使用bool来接收;
可使用deque替代,其内部保存真正的bool类型,相比于vector缺少了reserve和capacity;
可使用bitset不支持插入和删除元素,以及迭代器的使用;

19、理解相等和等价的区别

若有两个值中的任何一个都不在另一个的前面,则该两个值为等价关系;

20、为包含指针的关联容器指定比较类型

我们都知道关联容器会自动提供排序准则将数据进行排序,若我们设置的是指针类型,则它将无法准确的按照我们想要的进行排序,故我们将自定义排序准则;
/* 不要提供比较函数,因为它需要一个类型,类型内部创建一个函数,如下 */
struct StringPtrLess : public
        binary_function<const string*, const string*, bool>{
    bool operator()(const string *p1, const string *p2) const {
        return *p1 < *p2;
    }
};

void test() {
    typedef set<string*, StringPtrLess> sset;
    string s1 = "Java", s2 = "C++", s3="python";
    sset ss {&s1, &s2, &s3};
    set<string*>  s {&s1, &s2, &s3};

    cout << "my set" << endl;
    for(auto i: ss) {
        cout << *i << " ";
    }
    cout << endl;
    cout << "set" << endl;
    for(auto i:s) {
        cout << *i << " ";
    }
    cout << endl;
}

在这里插入图片描述

【21】、总是让比较函数在等值情况下返回false

任何一个定义了严格弱序化的函数必须对相同值的两个拷贝返回false;
比较函数的返回值时按照函数定义的排序顺序,一个值是否在另一个之前,且相等值不会有前后顺序关系,故对于相等值返回false;
否则将会破坏容器;

22、切勿直接修改set或multiset中的键

在map和multimap中,键是const,是不允许修改的;
而set和multiset不是const说明还是可以修改的:
	- 但修改的时候需要注意不能修改到键的部分,否则会破坏容器的有序性;
	- 当然不同的编译器对它采取的措施并不一样,有些或许可以修改,有些或许禁止,可能会导致降低移植性;
以下测试中,使用强制类型转换,解决该上述问题:
const_cast成功将其修改非键部分;
static_cast没有将其修改成功;
	由于该静态转换只是作用于临时对象上,该临时对象是(*i)的拷贝,只是修改临时对象,故结果没有改变
class Employee {
public:
    Employee(int i) : m_id(i) {}
    Employee(int i, string n, string m) : m_id(1), m_name(n), m_title(m) {}
    const string& getTitle() const { return m_title; }
    void setTitle(const string& title) { m_title=title; }
    const int getId() const { return m_id; }
    const string& getName() const { return m_name; }
    void setName(const string& name) { m_name = name; }
private:
    string m_name;
    string m_title;
    int m_id;
};
/* 键为id */
struct IDNumLess : public binary_function<Employee, Employee, bool> {
    bool operator()(const Employee& lhs, const Employee& rhs) const {
        return lhs.getId() < rhs.getId();
    }
};

void test() {
    typedef set<Employee, IDNumLess> mySet;
    mySet s;
    s.insert(Employee(1, "1", "1"));
    auto i = s.find(1);
    if(i != s.end()) {
        //i->setName("one"); // 'this' argument to member function 'setName' has type 'const Employee', but function is not marked const
        //const_cast(*i).setName("one");       // ret: one
        //static_cast(*i).setName("one");     // ret: 1
    }
    cout << i->getName() << endl;
}

int main() {
    test();

    return 0;
}

如何安全修改关联容器种的元素

- 先查找到该元素;
- 为将要修改的元素做一份拷贝,且不要将其修改为const;
- 修改该拷贝;
- 将该元素在容器种删除;
- 将新值插入到容器内;
该方法将不会受移植性的限制;
void modify() {
    typedef set<Employee, IDNumLess> mySet;
    mySet s;
    s.insert(Employee(1, "1", "1"));
    auto i = s.find(1);
    if( i != s.end()) {
        Employee e(*i);
        e.setTitle("one");
        s.erase(i++);   // 此处递增保持迭代器的有效性
        s.insert(i, e);
    }
}

23、考虑用排序的vector替代关联容器

虽然说关联容器的查找性能很好,但如果散列函数选择得不适合等因素性能将会显著降低;
关联式容器一般的经历以下三个阶段:
- 设置阶段:插入数据;
- 查找阶段:查询数据;
- 重组阶段:删除数据,在插入新数据;
以上操作相比于已排序的vector可能性能较低;
对于已排序的vector能够使用binary_search、lower_bound、equal_range等;

vector在哪些地方胜于关联容器

【大小】:关联容器是以红黑树为基础,其需要内部数据结构需要有左、右、父节点,至少比vector多出3个指针;
	当数据量大时,将会影响系统速度;
【速率】:排序的vector查找速度可能比关联容器更快;

如何使用vector替代关联容器

若使用vector代替map则需要使用pair,且需要自定义比较函数;
比较函数需要提供两个,一个用于排序,一个用于查找,或两个查找,为了兼容根据键还是pair来查找;
typedef pair<string, int> Data;
class DataCompare {
public:
    /* 用于排序 */
    bool operator()(const Data& lhs, const Data& rhs) const {
        return keyLess(lhs.first, rhs.first);
    }
    /* 用于查找 */
    bool operator()(const Data& lhs, const Data::first_type& k) const {
        return keyLess(lhs.first, k);
    }
    
    /* 第二种查找 */
    bool operator()(const Data::first_type& k, const Data& rhs) const {
        return keyLess(k, rhs.first);
    }
private:
    bool keyLess(const Data::first_type& k1, const Data::first_type& k2)const {
        return k1 < k2;
    }
};
void test() {
    vector<Data> vd;
    sort(vd.begin(), vd.end());
    string s("Hello");
    // 二分查找
    if(binary_search(vd.begin(), vd.end(), s, DataCompare())) {
        // ...
    }
    // 查找
    auto i = lower_bound(vd.begin(), vd.end(), DataCompare());
    if(i != vd.end() && !DataCompare()(s, *i)) {
        // ...
    }
    // 判断区间查找
    auto range = equal_range(vd.begin(), vd.end(), s, DataCompare());
    if(range.first != range.second){
        // ...
    }
}

24、当效率至关重要时,请在map::operator[]与map::insert之间做谨慎做出选择

map中的[]操作符提供了更新和插入的功能;
	- 当map中存在该对象将返回一个已有值对象的引用,
	- 当没有时将会默认构造新对象,作为当前索引的值,在将其做为引用返回,在赋值;【开销,临时对象的创建、析构、赋值】
若在插入的操作中使用insert,将会提高程序的效率,节省上述的开销;
template<typename MapType, typename KeyArgType,
        typename ValueArgType>
typename MapType::iterator AddOrUpdate(MapType& m, const KeyArgType& k, 
                                       const ValueArgType& v) {
    /* 先确定在什么位置 */
    typename MapType::iterator lb = m.lower_bound(k);
    /* 是否存在 */
    if(lb != m.end() && !(m.key_comp()(k, lb->first))) {
        lb->second = v;
        return lb;
    }
    else{   // 不存在,则插入
        typedef typename MapType::value_type MVT;
        return m.insert(lb, MVT(k, v));
    }
}

25、熟悉非标准的散列容器

hash_set、hash_multiset、hash_map、hash_multimap

26、iterator优先于const_iterator、reverse_iterator及const_reverse_iterator

STL中提供4中迭代器iterator、const_iterator、reverse_iterator、const_reverse_iterator;
迭代器间存在->即可通过隐式转换,也通过base()转换;
但无法通过const_iterator转换到iterator,也无法通过const_reverse_iterator转换到reverse_iterator;

在插入或删除操作中,const_iterator一般不适用;
尽量使用iterator不适用const或reverse;

C++【STL】 | STL Effective C++_第7张图片

void test() {
    typedef deque<int> Intq;
    typedef Intq::iterator Iter;
    typedef Intq::const_iterator ConstIter;
    
    Iter i;
    ConstIter ci;
    /* 当编译器不通过时,尝试ci==i,若与const,则将iterator转换为const,可使用static_cast转换 */
    if(i == ci) {}
}

27、使用distance和advance将容器的const_iterator转成iterator

当只有const_iterator时,如何插入通过迭代器插入新数据???
我们创建一个与const_iterator指向同一位置的iterator;
	- 先创建一个新的iterator指向容器起点;
	- 在通过与const_iterator的距离移动相同的偏移量;
上述过程可能需要线性时间的代价,且需要访问const_iterator容器;

distance获取偏移量,advance移动距离

typedef deque<int> Intq;
typedef Intq::iterator Iter;
typedef Intq::const_iterator CIter;

void test() {
    Intq d;
    CIter ci;
    
    Iter i(d.begin());
    advance(i, distance<CIter>(i, ci));
}

28、正确理解由reverse_iterator的base()成员函数所产生的iterator用法

insert

若在reverse_iterator插入元素,则将其转化为base,在插入;

void test() {
    vector<int> v;
    v.reserve(5);
    for (int i = 1; i <= 5; ++i) {
        v.push_back(i);
    }
    vector<int>::reverse_iterator ri = (vector<int>::reverse_iterator)find(v.begin(), v.end(), 3);
    vector<int>::iterator i(ri.base());
    cout << "ri:" << *ri << " r:" << *i << endl;
    v.insert(i, 10);
    for(auto x : v) {
        cout << x << " ";
    }
    cout << endl;
}

在这里插入图片描述
erase

由于不能对函数返回的指针进行修改,故将先递增在进行base转换;
(--ri.base()) ==>((++ri).base)
注意ri和ri.base()不等价,则需要通过移动;
void test() {
    vector<int> v;
    v.reserve(5);
    for (int i = 1; i <= 5; ++i) {
        v.push_back(i);
    }
    vector<int>::reverse_iterator ri = (vector<int>::reverse_iterator)find(v.begin(), v.end(), 3);
    v.erase((++ri).base());
    for(auto x : v) {
        cout << x << " ";
    }
    cout << endl;
}

在这里插入图片描述

29、对于逐个字符的输入请考虑使用istreambuf_itreator

istream_iterator通过>>来读取,当读取到空白字符时会默认跳过;
若需要保留空白,则需要通过设置标志位skipws即可;
ifstream inputFile("xxx.txt");
inputFile.unset(ios::skipws);
string fileData(istream_itreator<char>(inputFile), istream_itreator<char>());
对于上述操作每次都需要设置标志位,STL中提供另一个方法istreambuf_iterator,用法和istream_iterator类似;
速度相比于istream_iterator提高了些许;

30、确保目标区间足够大

虽然STL能够正确管理它的存储空间,但使用某些算法时,容易忽略出现在无效对象使用赋值行为;
注意reverse()只是事先分配好空间,但并不对空间进行初始化;
resieze()会将其进行初始化操作;
/** 由于ret中没有对象时,对其赋值无效,若使用resize则会给对象都进行初始化,即可 */
int op_increase (int i) { return ++i; }

void test() {
    vector<int> values {1, 3, 4, 6};
    vector<int> ret;
    //ret.resize(4);

    transform(values.begin(), values.end(), ret.begin(), op_increase);

    for(auto i : ret) {
        cout << i << endl;
    }
}

改进

使用back_inserter它将调用push_back();
void test() {
    vector<int> values {1, 3, 4, 6};
    vector<int> ret;

    transform(values.begin(), values.end(), back_inserter(ret), op_increase);

    for(auto i : ret) {
        cout << i << endl;
    }
}

31、了解各种与排序有关的选择

- STL提供partial_sort,当你只需要top前几的时候使用该函数即可,不需要对整个数组进行排序;
- nth_element提供和上述类似的功能,但它只挑取前几的元素,于前部,不对该区块进行排序;
	- 可以用来找到一个区间的中间值,或百分比上的值;
- stable_sort稳定的算法,排序后等价的元素相对位置不会发生变化;
以上算法以及sort都需要随机访问迭代器,只能应用于vector、string、deque、array;

- partition将所有满足条件的元素放置区间前部,由于完全排序需要大量的交换和比较,为了减少这些开销;
	- 将满足条件的元素放置前部至返回的迭代器区间,至结尾放置与条件不相符合的元素;
- stable_paritition提供稳定的版本;
partition只需由双向迭代器即可;

而对于list它由自己的成员函数能够进行排序,若需要使用以上的排序算法,则将其拷贝至其他容器即可;
算法效率根据时间、空间排序:
partition - stable_parition - nth_element -  partial_sort - sort - stable_sort;
在选择想要的算法前,需要先明确好实现功能;

nth_element

void test() { // 1 9 10 13 16 17 22 24
    vector<int> values {1, 13, 24, 16, 10, 9, 17, 22};
    auto begin(values.begin());
    auto end(values.end());
    vector<int>::iterator goalPos = begin + values.size() / 2;
    nth_element(begin, goalPos, end);
    for(auto i : values) {
        cout << i << " ";
    }
    cout << endl;
    cout << "goalPos mid: " << *goalPos << endl;

    vector<int>::size_type goalPosSet= 0.25 * values.size();
    nth_element(begin, begin + goalPosSet, end);
    cout << "in 25% : " << *(begin + goalPosSet) << endl;
}

C++【STL】 | STL Effective C++_第8张图片

partition

void test() { // 1 9 10 13 16 17 22 24
    vector<int> values{1, 13, 24, 16, 10, 9, 17, 22};

    auto it = partition(values.begin(), values.end(),[](int x){return x%2==0 ? true : false; } );

    for(auto i = values.begin(); i != it; ++i) {
        cout << *i << " ";
    }
    cout << endl;

    for(auto i = it; i != values.end(); ++i) {
        cout << *i << " ";
    }
    cout << endl;
}

在这里插入图片描述

32、如果确实需要删除元素,则需要在remove这一类算法之后调用erase

remove不能推断出是什么容器,故删除容器中的元素时,不知道它删除哪个容器的元素,故容器元素数目不会减少;
它将所有要被删除的元素放在尾部,不用删除的元素放在前部;

remove实现
C++【STL】 | STL Effective C++_第9张图片

void test() {
    vector<int> values {1, 10, 24, 16, 10, 9, 10, 22};
    for(auto i: values) {
        cout << i << " ";
    }
    cout << endl;
    remove(values.begin(), values.end(), 10);

    for(auto i : values) {
        cout << i << " ";
    }
    cout << endl;
}

在这里插入图片描述
结合erase

// 由于返回下一个要覆盖的迭代器
void test() {
    vector<int> values {1, 10, 24, 16, 10, 9, 10, 22};
    for(auto i: values) {
        cout << i << " ";
    }
    cout << endl;
    auto it = remove(values.begin(), values.end(), 10);
    values.erase(it, values.end());
    for(auto i : values) {
        cout << i << " ";
    }
    cout << endl;
}

在这里插入图片描述

与之同类的算法还有remove_if和unique;

33、对包含指针的容器使用remove这一类算法时要特别小心

当容器中存放指针类型,在删除时容易造成资源泄漏;
在要remove前,我们应该将其指针置空,然后再删除;
v.erase(remove(begin, end, static_cast(0)), end);
当然,如果使用智能指针即可直接删除;
v.erase(remove_if(begin, end, not1(mem_fun(&class::icVarify))), end);

34、了解哪些算法要求使用排序的区间作为参数

要求区间排序的算法:
binary_search、lower_bound、upper_bound、equal_range、set_union、set_intersection、set_difference、
set_symmetric_difference、merge、inplace_merge、includes
unique、unique_copy不一定需要排序;

binary_search、lower_bound、upper_bound、equal_range此类函数只有在已排序且随机迭代器下才具有对数时间的查找效率
否则需要线性时间;【注意在排序使用的排序准则,在后续的算法中也要使用相同的排序准则】
set_union、set_intersection、set_difference、set_symmetric_difference【集合操作】在已排序的情况下,此类为线性时间的效率;
merge、inplace_merge在已排序的情况下,此类为线性时间的效率;

35、通过mismatch或lexicographical_compare实现简单的忽略大小写的字符串比较

许多字符转换函数的返回值为int,由于在内部进行强制转换为unsigned char;
mismatch的第一参数接收短的字符串;

mismatch

int ciStrCmpImpl(const string& s1, const string& s2);

int ciStringCmp(const string& s1, const string& s2) {
    if(s1.size() <= s2.size()) return ciStrCmpImpl(s1, s2);
    else return -ciStrCmpImpl(s1, s2);
}

int ciStrCmpImpl(const string& s1, const string& s2) {

    auto p = mismatch(s1.begin(), s1.end(), s2.begin(), not2(ptr_fun(ciStringCmp)));
    if(p.first == s1.end()) {
        if(p.second == s2.end()) return 0;
        else return -1;
    }
    return ciStringCmp(*p.first, *p.second);
}

lexicographical_compare

该函数时strcmp的泛化版本,可以与任何类型值一起使用,可接受一个判别式
bool ciCharLess(char c1, char c2) {
    return tolower(static_cast<unsigned char>(c1)) <
            tolower(static_cast<unsigned char>(c2));
}

bool ciStringCmp(const string& s1, const string& s2) {
    return lexicographical_compare(s1.begin(), s1.end(), s2.begin(), s2.end(), ciCharLess);
}

使用cstring函数

int ciStringCmp(const string& s1, const string& s2) {
	return strcmp(s1.c_str(), s2.c_str());
}

36、理解copy_if算法的正确实现

STL中copy算法:
copy、copy_backward、repalce_copy、reverse_copy、replace_copy_if、unique_copy、remove_copy、rotate_copy、
remove_copy_if、paritial_sort_copy、uninitialized_copy;
但STL中弃用了copy_if,那如果我们想要使用该如何实现?
template<typename InputIter, typename OutputIter, typename Predicate>
OutputIter copy_if(InputIter begin, InputIter end, 
                   OutputIter destBegin, Predicate p) {
    while (begin != end) {
        if(p(*begin)) *destBegin++ = *begin;
        ++begin;
    }
    return destBegin;
}

37、使用accumulate或for_each进行区间统计

STL中对于数值的操作算法一般使用:count、count_if、min_element、max_element;
对于区间中的统计处理由accumulate;

【for_each和accumulate的区别】:
- for_each对每一个元素做一个操作,返回值为一个函数对象,通过该函数对象提取统计信息;
- accumulate对区间进行信息汇总,直接返回统计结果;

accumulate

/* 传入区间起点和终点,以及一个初始值,直接返回汇总后的值 */
template <class InputIterator, class T>
   T accumulate (InputIterator first, InputIterator last, T init);
/* 传入区间起点和终点,以及初始值和一个操作函数,直接返回汇总后的值 */
template <class InputIterator, class T, class BinaryOperation>
   T accumulate (InputIterator first, InputIterator last, T init,
                 BinaryOperation binary_op);
void test() {
    vector<int> vc {1, 3, 4, 5, 6};
    vector<int> vc2 {10, 30, 14, 15, 16};

    auto ret1 = accumulate(vc.begin(), vc.end(), 10);
    cout << "ret1: " << ret1 << endl;
    auto ret2 = accumulate(vc2.begin(), vc2.end(), 100, minus<int>());
    cout << "ret2: " << ret2 << endl;
}

在这里插入图片描述
for_each

/* 传入区间起点和终点,以及一个函数 */
template <class InputIterator, class Function>
   Function for_each (InputIterator first, InputIterator last, Function fn);
struct point {
    int x, y;
    point(int _x, int _y) : x(_x), y(_y) {}
};

class PointAvg : public unary_function<point, void>{
public:
    PointAvg() : xSum(0), ySum(0), numPoint(0) {}

    void operator()(const point& p) {
        ++numPoint;
        xSum += p.x;
        ySum += p.y;
    }

    point result() const {
        int x = xSum / numPoint;
        int y = ySum / numPoint;
        return point(x, y);
    }
private:
    int xSum;
    int ySum;
    size_t numPoint;
};

void test() {
    vector<point> vp;
    vp.emplace_back(1,2);
    vp.emplace_back(4,5);
    vp.emplace_back(1,3);
    vp.emplace_back(1,9);
    point avg = for_each(vp.begin(), vp.end(), PointAvg()).result();
    cout << avg.x << " " << avg.y << endl;

38、遵循按值传递的原则来设计函数子类

函数传递时,必须通过函数指针进行传递;
在STL中,函数指针是按值传递的;
由于STL中函数按值传递
	- 我们需要让函数对象尽可能地小,以至于不会有大的开销;
	- 函数对象必须是单态的,不得使用虚函数,在以传值的方式会被切割;
	- 函数对象的有点是可以带上状态信息;
***但在实际使用过程中,发现难免无法避免函数小巧,以及不适用到虚函数,该如何解决??
	我们将所需的数据和虚函数从函数子类中分离,放到新类中,且在函数子类中包含一个指针,指向新类;
// 以下的返回值和参数都是按值传递
template<class InputIter, class Function>
Function for_each(InputIter first, InputIter last, Function f);

独立新类,让函数对象内部数据成员成为一个指针

需要注意的是如何处理拷贝构造,由于在STL中函数对象作为参数传递或返回总是被拷贝;
class Widge {
};

template<typename T>
class BPFC;

template<typename T>
class BPFCImpl : public unary_function<T, void> {
private:
    Widge* w;
    int x;
    virtual ~BPFCImpl();
    virtual void operator()(const T& val) const;
    friend class BPFC<T>;
};

/** 新类,将x、w数据成员抽出到新类中 */
template<typename T>
class BPFC: public unary_function<T, void> {
private:
    BPFCImpl<T>* pImpl;

public:
    void operator()(const T& val) const {
        pImpl->operator()(val);
    }
}

39、确保判别式是纯函数

【判别式】:返回一个bool类型,一般用于排序,STL中要求判别式函数必须是纯函数;
	判别式类:为一个函数子类,operator()为一个判别式,其中对该函数必须声明为const,且为纯函数;
	STL中能接受判别式函数的就能够接受判别式类,接受判别式类的就能接受判别式函数;
【纯函数】:返回值仅依赖于其参数的函数,没有状态;

40、若一个类是函数子,则应使它配接

可配接的函数对象提供了(argument_type、first_argument_type、second_argument_type, result_type)必要的类型;
STL中提供4个标准函数配接器(not1, not2, bind1st, bind2nd)当我们自定函数时,且与配接器结合使用需要通过ptr_fun等
函数让我们自定义函数变成可配接;
若要让函数对象变成可配接函数,即通过基类继承即可(unary_function、binary_function);
template<typename T>
class MeetsThreshild : public unary_function<Widget, bool> {
private:
	const T threshold;
public:
	bool operator()(const Widget&) const;
};

struct WidgetNameCmp : public binary_function<Widget, Widget, bool>{
	bool operator()(const Widget& lhs, const Widget& rhs) const;
};

struct WidgetNameCmp : public binary_function<Widget*, Widget*, bool>{
	bool operator()(const Widget* lhs, const Widget* rhs) const;
};

41、理解ptr_func、mem_fun和men_fun_ref的来由

此类函数为了掩盖C++中一个内在语法不一致问题;
由于函数或函数对象在被调用的时候,总是使用非成员函数的语法形式,故在调用成员函数时编译不通过;
mem_fun带一个指向某成员函数的指针参数,并返回一个mem_fun_t的对象(函数子类);
	- mem_fun_t拥有该成员函数的指针,提供了operator()函数;
mem_fun_ref(引用):指针对对象容器的配接器;
另外还提供ptr_fun,该函数不会给程序带来性能损失,只会在阅读的时候会被考虑为何加上该函数进行修饰,若不注重阅读也可
将其加上;
class Widget {
public:
    Widget() {}
    void test() {
        cout << "test" << endl;
    }
    ~Widget() {}
};

void func(Widget *w) {
    cout << "out" << endl;
}

void test() {
    vector<Widget *> vc;
    vc.resize(2);

    for_each(vc.begin(), vc.end(), mem_fun(&Widget::test));
    for_each(vc.begin(), vc.end(), &func);
    //for_each(vc.begin(), vc.end(), &Widget::test);    // error
}

42、确保less与operator<具有相同的语义

当我们要自定义排序准则时,需要创建一个函数子类,命名不能与less同名;
当然也不要对less版本进行特化;
struct MaxSpeedCmp ; public binary_function<Widget, Widget, bool> {
	bool operator()(const Widget& lhs, const Widget& rhs) const  {
		return lhs.maxSpeed() < rhs.maxSpeed();
	}
}

43、算法调用优先于手写的循环

STL为我们提供各种算法,当内部有提供可以满足我们的需求是,我们应使用STL算法;
STL算法优点:
	- 效率,减少了繁琐的操作,例如循环等;
	- 正确性,手写容易出现错误;
	- 可维护性, 代码简洁;
对于deque中数组,基于指针的遍历比基于迭代器的遍历要快,而只有库的内部实现才能使用基于指针的遍历;

STL案例

void test() {
    deque<int> d;
    int arr[] = {1, 3, 4, 5, 6};
    int N = 5;
    /* 此类做法将插入后数据将会逆序,由于每次在begin插入 */
    for(size_t i=0; i<N; ++i) {
        d.insert(d.begin(), arr[i]);
    }
    /* 更正,但仍有问题 */
    auto it = d.begin();
    for(size_t i=0; i<N; ++i) {
        d.insert(it++, arr[i]);
    }
    /* 更正,为了保证it有效递增 */
    for(size_t i=0; i<N; ++i) {
        it = d.insert(it, arr[i]);
        i++;
    }
    
    /* 而使用STL,保证了不易出现错误,上述代码中不小心将会造成插入错误 */
    transform(arr, arr+N, inserter(d, d.begin()), plus<int>());
}

何时使用手写?

当我们的循环简单,使用STL算法来实现,要求混合使用绑定器和配接器或者要求一个单独的函数子类时,使用手写;

44、容器的成员函数优先于同名的算法

成员函数一般速度较快,由于成员函数与容器结合得更加紧密;
虽然其函数名称都一样,但内部所做的事情却大有区别;
例如:
	- set中成员函数以对数时间,而find算法确实以线性时间;
	- list中提供的成员函数无须任何对象拷贝,只是维护好指针;
使用成员函数的好处:
- 其成员函数对该容器具有针对性的操作;
- 在时间效率上,能够获取对数时间的性能而不是线性;
- 对于关联容器可以使用等价性来确定两个值是否相同;
- 使用map等容器可以单独考虑键部分;

45、正确区分count、find、binary_search、lower_bound、upper_bound和equal_range

如果是已序区间,则通过binary_search、lower_bound、upper_bound、equal_range将会有对数的时间效率
若无序,则只能通过count、count_if、find、find_if来查找将获取线性时间效率;

无序区间

我们将使用find和count;
【find】查找区间中的值,当查找第一个这样的值则返回;
【count】是通过区间中这样的值,需要从头遍历到尾;

有序区间

【lower_bound】查找特定值时,不能用end来测试其返回值,应该检查返回对象是否等价于你要查找的值;if(i != vc.end() && *i == w)
【equal_range】:返回一对迭代器,第一个返回lower_bound,第二个返回upper_bound最后一个元素的下一个位置,如果返回两个迭代器相同,则为空;
p = equal_range(vc.begin(), vc.end()); if(p.first != p.second)

使用推荐

STL算法:
	对未排序的区间:
		- find:特定值是否存在,且其第一个值在哪;
		- cout:特定值有多少个;
		- count_if:特定值的前后;
	对已排序:
		- binary_search:特定值是否存在;
		- equal_range:特定值是否存在,且第一个值在哪;
		- lower_bound:第一个不超过特定值的对象在哪;
		- upper_bound:第一个在特定值之后的对象在哪;
		- equal_range:具有特定值的对象多少个,先确定区间,在使用distance;
		- equal_range:具有特定值的对象都在哪;
成员函数:
	对set或map:
		- count:特定值是否存在;
		- find:特定值是否存在且第一个在哪;
		- lower_bound:第一个不超过特定值的对象在哪;
		- upper_bound:第一个特定值后的对象在哪;
		- count:具有特定值的对象有多少;
		- equal_range:具有特定值的对象都在哪;
	对multiset或multimap:
		- find:特定值是否存在;
		- find或lower_bound:特定值是否存在,且第一个值在哪;
		- lower_bound:第一个不超过特定值的对象在哪;
		- upper_bound:第一个特定值后的对象在哪;
		- count:具有特定值的对象有多少;
		- equal_range:具有特定值的对象都在哪;

46、考虑使用函数对象而不是函数作为STL算法的参数

在STL中,将函数对象传递往往比传递实际的函数更加高效;
如果一个函数对象的operator()函数被声明为内联,则该函数体将可以直接被编译器使用;
【函数指针抑制内联机制】当函数作为参数传递时,一般会隐式将其转化为函数指针,而后产生一个间接的函数调用(通过指针),而编译器一般不会试图对通
过函数指针执行的函数调用做内联优化;
函数对象中,C++必须先实例化函数模板和类模板,在调用operator()函数,只是简单函数调用,且负担都在编译期间消化;
template<typename FPTyep>
struct Average: public binary_function<FPType, FPType, FPType> {
	FPType operator()(FPType v1, FPType v2) {
		return average(v1, v2);
	}
}

47、避免产生“直写型”代码

代码阅读次数远大于编写次数,故需要能够让阅读者看懂,易于理解;

48、总是包含正确的头文件

49、学会分析与STL相关的编译器诊断信息

50、熟悉与STL相关的web站点

SGI STL:http://www.sgi.com/tech/stl
STLport:http://www.stlport.org
Boost:http://www.boost.org

参考
侯捷Effective STL

你可能感兴趣的:(STL,C++,STL)