STL学习笔记(二)

STL的基本观念就是将数据和操作分离,数据都在容器(Container)里,操作都在算法(Algorithm)里,而连接数据和操作的桥梁就在迭代器(Iterator)里。所以容器、算法、迭代器也是STL中最重要的组成部分。
STL是泛型编程的优秀案例。

容器

1. 序列式容器:vector、list、deque(string、array也可认为是)
string和vector差不多,去别就是它的元素都是字符;
而array不是STL的容器,但是可以使用算法,这个我们后面再说。

2. 关联式容器:map、set、multimap、multiset
set其实就是特殊的map(实值==键值);
multimap可以当做字典使用;
map可以作为关联式数组:索引可以是任意型别的;
map和multimap相比由于键值是唯一的,所以插入数据的时候不仅可以插入make_pair,也可以将键值作为下标插入,而multimap只能插入pair(make_pair):

map<int, string> mp;
mp["1"] = "haha";
mp.insert(make_pair(2, "xixi"));

multimap<int, string> mmp;
mmp.insert(make_pair(0, "houhou"));

3. 容器配接器:stack、queue、priority queue
容器配接器是由前面的基本容器做出来的。
priority queue是在queue的基础上增加了元素的优先级,遵循一个原则:下一个元素一定是优先级最高的。

迭代器

迭代器的出现充分的体现了STL的泛型编程思想:接口相同、型别不同。
如果某个容器为空,那么begin()==end()
除了iterator、reverse_iterator之外还有const_iterator,注意:这里的const_iterator相当于const* p,而不是* const p,所以迭代器是可以++操作的,但是不能修改(*const_iterator)的值。
迭代器有两种类型:
(1)双向迭代器:
这个是我们常用的迭代器,list、set、map、multiset、multimap中定义的迭代器都是这个,使用的时候:

while (ite != list.end())

(2)随机存储迭代器:
vector、deque、string提供的迭代器都是这个。随机存储迭代器可以有<和>的功能:

while (ite < list.end())

随机存储迭代器有算术的能力,所以可以有:ite+1 这种形式的出现。

算法

STL中的算法体现的是泛型编程思想,而不是面向对象思想,因为面向对象思想是将数据和操作融为一体,即将算法操作作为成员。可是STL中将算法与容器等剥离出来,作为全局函数使用,这样算法只需做出一份儿,大大降低了程序代码的体积。但是这样也有缺点,首先就是不直观;其次就是有些数据和算法不兼容。所以我们要深入学习STL,趋利避害!
所有算法处理的区间都是半开区间!
compose_f_gx_hx是个灵巧的辅助仿函数,我们后面再说。

迭代器之配接器

三种配接器:
1. insert iterator(安插迭代器):
2. stream iterator(流迭代器):
3. reverse iterator(逆向迭代器):

Manipulatiing Algorithms(更易型算法)

remove()删除元素之后会使用后面的继续覆盖删除的元素,可是整体的size()和end()都不变,所以我们要用新的end()记住remove()的返回值,并且删除新的end()和旧的end()直接这些元素。distance()函数可以返回两个迭代器直接的距离。
例如,我们想要删除vtr向量中值为3的元素:

vtr.erase(vtr.remove(vtr.begin(), vtr.end(), 3), vtr.end());

在关联式容器中我们不能使用remove()来删除元素,因为这样会破坏他们已有的某种排序方式,但是我们可以通过他们的成员函数erase()来删除元素。
另外,比如list删除元素,那么我们用他的成员remove()效率就要远远好过算法remove(),因为算法remove()是将前一个删除,后一个放在前一个的位置上,这就违背的list的初衷(更像是vector了)。

判断式

判断式的返回值为bool。
一元判断式(unary predicates),实例如下:

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

bool IsPrime(int num)   //判断一个数是否是质数
{
    num = abs(num);
    if (num == 0 || num == 1)
        return true;
    int bj = 0;
    for (bj = num/2; num%bj != 0; --bj){}

    return bj == 1;
}

int main()
{
    vector<int> vtr;
    for (int i = 15; i < 20; i++)
    {
        vtr.push_back(i);
    }
    vector<int>::const_iterator ite = vtr.begin();
    ite = find_if(vtr.begin(), vtr.end(), IsPrime);    //find_if()返回第一个质数的迭代器,如果没有则返回end()
    if (ite != vtr.end())
        cout << *ite << endl;
    system("pause");
    return 0;
}

输出: 17
二元判断式(binary predicates),比如两个参数不支持重载操作符的情况下,就可以派上用场了,实例如下:

#include <iostream>
#include <deque>
#include <algorithm>
#include <string>
using namespace std;

class CAA{
public:
    CAA(string strFirst, string strSencond){
        this->strFirstName = strFirst;
        this->strSecondName = strSencond;
    }
    string GetFirstName(){
        return this->strFirstName;
    }
    string GetSecondName(){
        return this->strSecondName;
    }
private:
    string strFirstName;
    string strSecondName;
};

bool SortPersonName(CAA* a, CAA* b)
{
    return (a->GetSecondName() < b->GetSecondName() || (!(b->GetSecondName() < a->GetSecondName()) && a->GetFirstName() < b->GetFirstName()));
}

int main()
{
    deque<CAA*> dq;

    CAA* a = new CAA("a", "a");
    CAA* b = new CAA("a", "b");
    CAA* c = new CAA("a", "c");
    CAA* d = new CAA("b", "a");
    CAA* e = new CAA("b", "b");
    dq.push_back(a);
    dq.push_back(b);
    dq.push_back(c);
    dq.push_back(d);
    dq.push_back(e);

    sort(dq.begin(), dq.end(), SortPersonName);

    deque<CAA*>::iterator ite = dq.begin();
    while (ite != dq.end())
    {
        cout << (*ite)->GetFirstName() << " " << (*ite)->GetSecondName() << endl;
        ++ite;
    }
    system("pause");
    return 0;
}

输出结果:
这里写图片描述
(不过我这里存在一个问题,就是SortPersonName这个判断式如果直接 return true;就会崩掉。希望各位大神看到之后能给小弟一些指点。)
其实判断式可以用另一个函数代替——仿函数,并且仿函数还有一个优点,他可以作为一种型别直接在容器定义时就给了他们某种准则。

仿函数(functions object)

for_each()算法如下:

namespace std {
    template<class Iterator, class Operation>
    Operation for_each(Iterator act, Iterator end, Operation op) {
        while (act != end) {
            op(*act);
            ++act;
        }
        return op;
    }
}

仿函数较一般函数有以下优点:
1. 他是智能型函数,因为是class,所以有自己的成员,也就有了状态,但是成员的初始化一定要在使用前!编译器和runtime都行。
2. 每个仿函数都有自己的型别,可以利用template实现泛型编程,不同容器都可以通过相同的仿函数来完成诸如:排序、合并或比较等功能。甚至可以仿函数继承。
3. 仿函数通常性能更佳。因为其template的原因,很多细节在编译器就确定了。
下面我们来看一个仿函数实例:

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

class AddValue{ 
public:
    AddValue(int num):nValue(num){
    }
    void operator() (int& object) const{    //仿函数
        object += nValue; 
    }
private:
    int nValue;
};

template<typename T>
void Print(T t)         //打印容器元素
{
    T::iterator ite = t.begin();
    while (ite != t.end())
    {
        cout << *ite << " " ;
        ++ite;
    }
    cout << endl;
}

int main()
{
    vector<int> vtr;
    for (int i = 0; i < 10; i++)
    {
        vtr.push_back(i);
    }
    Print(vtr);

    for_each(vtr.begin(), vtr.end(), AddValue(10));  //每个元素加上10
    Print(vtr);
    for_each(vtr.begin(), vtr.end(), AddValue(*vtr.begin()));  //每个元素加上第一个元素的值
    Print(vtr);

    system("pause");
    return 0;
}

结果:
这里写图片描述
上面的例子中我们看到了仿函数的灵活性。如果是普通函数,传入的变量时static或全局变量时,一个函数只能给一个容器使用了,我们要给两个容器赋不同的参数就只能另外定义一个函数;而仿函数由于其是对象,具有属性,我们可以通过构造函数赋值。

预先定义的仿函数
C++中有一些预先就定义好的仿函数,如:
从小到大:less<>;
从大到小:greater<>;
取反:negate<>();
平方:multiplies<>(),下面看一个实例:

#include <iostream>
#include <set>
#include <deque>
#include <functional>   //仿函数头文件
#include <algorithm>
#include <iterator>     //front_inserter头文件
using namespace std;

class CPrint{
public:
    template<typename T>
    void operator() (T t) {     //输出仿函数
        cout << t << " ";
    }
};

template<typename T>
void Print(T t)     //打印函数模板
{
    for_each(t.begin(), t.end(), CPrint());
    cout << endl;
}

int main()
{
    set<int, greater<int>> st;   //定义一个从大到小排列的容器
    deque<int> dq;
    for (int i = 0; i < 10; i++)
        st.insert(i);
    Print(st);

    transform(st.begin(), st.end(), front_inserter(dq), bind2nd(multiplies<int>(), 10));  //将 st 元素乘10放入 dq 中,注意这里 transform 第三个参数必须是插入型迭代器,因为我们没有给 dq 初始化任何空间,dq 的空间不足以将 st 的元素放入时就会崩掉!下面这行就是错误的!
    //transform(st.begin(), st.end(), dq.begin(), bind2nd(multiplies<int>(), 10));  //这个会崩
    Print(dq);

    replace_if(dq.begin(), dq.end(), bind2nd(equal_to<int>(), 40), 44);     //将 dq 中等于40d的元素替换为 44
    Print(dq);

    dq.erase(remove_if(dq.begin(), dq.end(), bind2nd(less<int>(), 30)), dq.end());   //删除小于 dq 中所有小于30的元素
    Print(dq);

    system("pause");
    return 0;
}

结果:
这里写图片描述
上面的案例中用到了几个预先定义的仿函数,分别是:
greater<>、less<>()、equal_to<>()和multiplies<>()。
transform()和copy()区别:
transform()可以将源容器的元素进行类似multiplies()操作之后再传入目的容器,而copy()只是复制过去不能进行修改操作(他只有三个参数)。但是他们都对目的容器的大小有要求:必须大于等于源容器。而我们只有两种解决方式:(1)初始化有足够大小的容器;(2)使用插入型迭代器。
另外bind2nd()函数值得注意:当我们的仿函数需要传递参数的时候,就要用到这个bind2nd()函数了,他的第二个参数作为仿函数参数传进去(普通函数也可以)。但是,STL中只支持绑定一个参数,如果多余一个参数就不行了。
另外还有一种仿函数是:men_fun_ref 和 men_fun。使用方法如下:

for_each(st.begin(), st.end(), men_fun_ref(&CAA::show));

他的作用是调用容器中所有元素的成员函数 show,当然前提是所有的成员都是 CAA类的对象或CAA派生类的对象。而 men_fun 和 men_fun_ref 除了类型不同以外,使用方法完全相同。men_fun 适用于容器中都是类型对象的指针时,而 men_fun_ref 适用于容器中都是对象时

关联式容器中,定义时必须确定排序方式,而缺省值是:operator <,由 less<>()调用的。

容器中的元素

我们在将元素放入容器时,可以有三种方式:
1. 拷贝方式 (value语义):元素都是拷贝了一份儿放入容器中,在对元素进行修改操作是也是修改的拷贝的那一份儿,并没有修改原来的元素值;
2. 指针方式:比较危险,因为元素可能已经没了,所以最好使用智能型指针在中间加一些判断(可是不能使用 auto_ptr,因为 auto_ptr 在进行拷贝或赋值操作的时候就会发生转移,这里不明白可以看我的《STL学习笔记(一)》一篇中有关 auto_ptr的介绍);
3. 引用方式 (reference语义):因为 C++ 中只支持 value 语义,所以我们自己实现智能型指针,我们在后面会了解一种:通过“引用计数”智能型指针实现STL的reference语义的方式。

STL使用安全性

我们这里的安全性借用数据库的一个概念:commit or rollback(即提交或回滚)。
想要获得完全 commit or rollback 保障,使用如下三种方式
1. 使用 list,但是不能使用他自身的 sort() 和 unique() 函数;
2. 使用任何关联式容器,但是不要进行安插太多元素的操作;
3. 自己动手封装一个函数,下面的例子可以将任何容器插入一个元素:

template<class T, class ont, class Iter>
void Insert(Cont& t, Iter& pos, const T& value) {
    Cont temp(t);
    temp.insert(pos, value);
    t.swap(temp);
}

但是第三种方式也不是完美的,因为如果容器的“比较准则”发出异常时,swap() 就会抛出异常,也就不能实现完全 commit or rollback 了。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值