1. 初识泛型算法
泛型:可以作用于不同类型的元素和多种容器类型
算法:实现了一些经典算法诸如排序和搜索的接口。
泛型算法运行于迭代器之上,不会执行容器的操作。由此得出假定:泛型算法永远不会改变底层容器的大小。
输入范围:标准库算法都对一个范围内的元素进行操作,该范围称为输入范围。如果算法接受输入范围作为参数,通常用前两个参数表示范围,即第一个元素和尾后元素的迭代器。
大多数算法存储在algorithm头文件内,numeric中定义了一组数值泛型算法。
1.1 只读算法
只会读取输入范围中的元素,而不会改变元素。
1.1.1 find
返回找到的元素的迭代器。如果没有找到,返回第二个参数。输入范围是一个左闭右开的区间。
1.1.2 accumulate
前两个是输入范围,第三个是初始累加值。用第三个值对应的类型保存累加结果。所以如下方式是不行的:
1.1.3 equal
将第一个序列的每个元素和第二个序列对应的元素比较。如果全相等,返回true,否则返回false。
本算法接收三个参数,前两个表示输入范围,第三个表示第二个序列的起始位置。
equal不要求类型相同,只要两个序列的元素能比较即可。同时equal假定第二个序列指定起始位置后剩下的长度至少等于第一个序列的长度。如果第二个序列剩下的长度小于第一个序列,会出现访问不存在元素的错误。
1.2 写容器元素的算法
1.2.1 fill_n
第一个参数是待赋值序列的起始位置,第二个参数是待赋值序列的长度,第三个参数是将要赋予的值。
同样的,这里假定所指向的序列存在。即容器非空,且空间够大。
1.2.2 back_inserter
是一个定义在interator头文件中的函数,以容器的引用作为参数,返回与该容器绑定的插入迭代器。
当用此迭代器赋值时,赋值运算符会调用push_back将值插入容器内。
fill_n对迭代器赋值的行为会被转换为push_back。
1.2.3 拷贝算法
copy接受三个参数,前两个表示要拷贝的范围,第三个为目标的起始位置,依次用拷贝元素值替换目标容器的值。同比较equal一样,要求目标的起始位置后剩下的元素长度至少要和拷贝的内容长度相等。
copy返回拷贝完成后目标容器的迭代器的值。在上述例子中,copy返回指向a2的尾后的位置。
很多算法提供一个“拷贝”版本,即不会真正对原序列进行修改,而是创建一个新序列保存结果。
1.3 重排容器元素
排序:sort
重拍:unique(把重复的单词放后边,并未真正删除重复)
泛型算法不能执行容器操作。所以真正的删除需要程序员自己调用erase完成。
2. 定制操作
很多算法涉及元素的比较,通过使用元素类型的<
或者==
运算符。但是有的情况下可能该元素类型没有定义<
以及 ==
,或者想要的不是默认的<
与 ==
的含义,这时候需要自己定义默认的运算符。
2.1 谓词
谓词是一个表达式,接收参数,返回可用作条件的值。接受谓词的算法对序列中的元素调用谓词。
谓词分为一元(接收一个参数)和二元(接受两个参数)。
谓词需要能在输入序列的所有值上定义一个一致的序。
stable_sort会维持相等元素原来的相对顺序。这里的相等是指执行stable_sort时是不是相等。
2.2 lambda表达式
给定一个元素为字符串的容器,需要找出长度大于某个特定值的元素数?
可以先按照长度排序,接下来就是找出第一个长度大于该特定值的元素。
可以使用find_if
函数,find_if
的第三个参数是谓词,find_if
对序列的每一个元素调用该谓词,返回第一个使谓词为0的元素,如不存在这样的元素,返回尾迭代器。
这样看的话,只需要编写那个谓词,接收一个字符串和长度,然后完成长度判断。然而find_if
规定谓词只能接受一个参数,以便可以使用序列的元素调用它。所以长度特定值无法传入。
问题所在:传统的谓词只能接收一个或者两个参数,不够灵活。
可调用对象:可以对其使用调用运算符的对象。如对象e,可以使用e(args),那么e是可调用对象。我们可以向任何算法传递任何类别的可调用对象。
可调用对象一共四种:函数,函数指针,重载了函数调用符的类,Lambda表达式。其时还有bind创建的对象,但事实上那个也是函数。
lambda表达式可以理解为未命名的内联函数。形式如下:
定义时可以忽略参数列表和返回类型,但是另外两个不能缺。
忽略括号和参数列表等价于指定一个空参数列表。
忽略返回类型,lambda根据函数体的代码推断出返回类型:①如果函数体只有return语句,返回类型则根据返回的表达式类型推断。②如果函数体除了return语句还有其他语句,则返回void。
lambda不能有默认实参。所以,lambda的实参数目永远和形参相等。
lambda示例:
可以在lambda中提供一个以逗号分隔的名字列表,在lamdba中可以使用这些名字,这就解决了使用谓词时参数数量限制的问题:
未包含在名字列表中的名字不能使用:
一个完整的例子:
for_each前两个参数描述了输入范围,然后对范围内的每一个元素,调用后面的lambda表达式。
2.3 lambda捕获和返回
使用lambda时,会产生一个lambda生成的新类型以及该类型的一个对象。该lambda所捕获的变量会在对象内有对应的数据成员,并且该数据成员在创建对象时被初始化。
类似参数传递,变量捕获的方式可以是值或者引用。
2.3.1 值捕获
与传参类似,能用值捕获的前提是该变量可以拷贝。
与传参不同,被捕获的变量值是在lambda创建时拷贝,而不是调用时拷贝。在创建lambda之后,调用lambda之前修改变量不会影响lambda捕获的值。
2.3.2 引用捕获
在参数列表中加上&表明是引用捕获。
引用捕获是必要的。对一些不可拷贝的对象(流对象等等)捕获的唯一方式是捕获其引用。
当从一个函数返回lambda时,需要保证其不包含引用捕获。因为等到函数返回时,它所试图捕获的变量早已不存在了。
2.3.3 隐式捕获
可以让编译器推断捕获列表,使用&
告诉编译器采用引用捕获方式,使用=
告诉编译器采用值捕获方式。
如果希望对部分变量采用值捕获,另一部分采用引用捕获。可以混合使用隐式捕获和显示捕获。
使用示例:
this不能用引用传递,因为this是个const指针,传递它的引用没有意义,指针的值(指向的对象)才是关注的内容。如果需要捕捉this指针,明确地捕捉所有变量(不要使用隐式捕捉)。
2.3.4 可变lambda
一般情况下,值传递方式捕获的变量,不会在lambda内改变它的值(因为意义不大,只是个数据副本),如果希望改变捕获的变量,需要在参数列表后加上mutable关键字,
上述只是对于值捕获的变量,引用捕获的变量能否修改只能取决于是不是常量引用。
2.4 参数绑定
lambda用于只在一两个地方使用的简单操作,如果需要多个地方使用同样的操作,或者操作本身很复杂,最好选择函数。
选择函数就会有传递参数数目的问题,前面说过,谓词一般只能接受一个或者两个参数。
可以使用bind
标准库函数解决这个问题,它定义在头文件functional
中,bind
函数被看作一个通用函数适配器。
arg_list
中的参数可能包含形如_n
的占位符,n
表示生成的可调用对象的参数的位置。_1为newCallable的第一个参数,依此类推。
bind
使用示例:
只有一个占位符,可判断check6
接收一个参数,通过check6
将接受的参数传给_1,然后把整个arg_list
传给check_size
。
传给g的第一个参数给_1,第二个参数给_2,然后把a,b,_2,c,_1按顺序传给f。
解决find_if
只接受一元谓词的问题:
对find_if
而言,它的第三个参数是只接受一个参数的可调用对象。实际上,所接收的参数通过该可调用对象传递给更下层的具有两个参数的函数。
正常情况下,不是占位符的参数被拷贝到bind返回的可调用对象内。可使用头文件fucntional
中的函数ref
或者cref
完成bind
的引用传递:
3. 再探迭代器
3.1 插入迭代器
一种容器适配器,接受一个容器,生成一个可插入元素的迭代器。当通过插入迭代器进行赋值时,迭代器调用容器的操作给容器指定位置插入一个元素。
这三种插入迭代器都必须在底层容器能支持对应操作时才可定义。
front_inserter
会把元素顺序倒过来。
3.2 iostream迭代器
虽然iostream类型不是容器,但是标准库还是给它们定义了迭代器。这些迭代器将流当作元素序列进行处理。流迭代器不能递减,只能往前移动。
3.2.1 istream迭代器
创建一个流迭代器时,必须指定迭代器要读取的对象类型。
当流迭代器关联的流遇到文件尾或者IO错误,绑定在该流上的流迭代器就变成空迭代器。
可以用之前的泛型算法操作流迭代器。
istream_iterator允许使用懒惰求值。当绑定到一个流时,标准库并不保证立刻从流中读取数据,直到使用迭代器时才真正读取。
3.2.2 ostream迭代器
使用ostream迭代器输出元素
上文使用解引用和后置递增是为了和其他迭代器保持写法上的一致,方便修改与理解。
3.2.3 流迭代器处理类类型
可以对任何定义了>>
的类使用istream迭代器,可以对任何定义了<<
的类使用ostream迭代器。
3.3 反向迭代器
除了forward_list
外,其他容器都支持反向迭代器。反向
使用rbegin
和rend
获得的是首前迭代器和尾迭代器。
向sort传递反向迭代器会导致变成递减序。
使用reverse_iterator
的base
成员函数将反向迭代器转为普通迭代器,通常用于打印反向迭代器表示范围的结果。转换完之后,需要移动一次迭代器,具体往哪边移,取决于转换的是反向迭代器的左侧还是右侧。这是因为反向迭代器左开右闭,普通迭代器左闭右开。如下:
4. 泛型算法结构
4.1 5类迭代器
迭代器按照所能提供的操作分层,除了输出迭代器外,高层迭代器能支持底层迭代器的所有操作。
每个泛型算法对迭代器的层次有最低要求。每个参数提供的迭代器的层次至少满足该最低要求。
迭代器分层,越往下层次越高:
4.2 算法形参模式
参数四种形式:
前两个算法是只接受一个序列作为输入,另一个序列作为输出或者没有另一个序列。
dest表示目的位置的迭代器。
后两个算法接受两个输入范围,[begin,end),[begin2,end2)。如果没提供end2,则默认为从begin2开始到序列结尾。
4.3 算法命名规范
前面提到可以用谓词作为参数替换掉原有的<
和==
运算符。事实上是调用了一个重载版本的函数:
_if
版本的算法:
接受一个元素值的算法有一个不同名的版本,用谓词代替元素值:
_copy
版本的算法:
重排元素的算法将重排后的元素写到另一个位置而不是重写回原序列。
或者同时是_copy
和_if
版本:
5. 链表类型特殊算法
链表类型有通用版本的算法,但是那些算法通过交换序列元素完成重排等操作,事实上链表可以通过修改节点间的链接而不是真的交换值来快速交换元素。
下表的操作有通用版本的算法,但是最好使用成员函数版本的算法:
splice就是把另一个链表某个范围内的元素插入到之前的链表的某个位置。
after的区别在于:从lst2中取范围时和插入位置。
对第一种参数:二者取范围没有不同。after插入p之后,常规版本插入p之前。
对第二种参数:after取得是p2后的元素,常规版本的就是p2所指向的元素。after插入p之后,常规版本插入p之前。
对第三种参数:after版本是左开右开区间,常规版本是左开右闭区间。after插入p之后,常规版本插入p之前。
除第一种都可以在本链表内部完成移动。
使用示例:
splice_after(注意用的before_begin):
splice:
上述的链表特有版本算法会改变底层容器,真正的删除和销毁元素。而通用版本不会删除元素。