C++基础之泛型算法

标准库并未给每个容器添加大量功能,因此,通过大量泛型算法,来弥补。这些算法大多数独立于任何特定的容器,且是通用的,可用于不同类型的容器和不同的元素。

迭代器使得算法不依赖容器,但是算法依赖于元素的类型操作;例如某些算法需要元素支持"<"运算。

算法永远不会执行容器操作,它们只会运行于迭代器之上,执行迭代器操作;因此,算法不会改变底层容器的大小,算法可能会改变容器中保存的元素的值,也可能移动元素,但是不会直接添加删除元素。

注意:

  1. 大部分算法在algorithm头文件中,但是少部分数值相关的算法在numeric头文件中。下面定义在numeric中的算法,会指出来,没有特别说明的是定义在algorithm中。
  2. 向输出迭代器写入数据的算法一般都假定目标空间容量足够,因此可以将输出迭代器绑定到一个插入迭代器。
  3. 接受第二个输入序列的算法,如果只有beg2则表示它与第一个大小相同。
  4. 带_if的算法接受谓词做参数,带_copy的算法表示会拷贝元素

只读算法:

该类算法只会读取容器的值,而不改变元素。

1.iterator find(iterator::begin,iterator::end,val);

在begin和end之间找值为val的位置,找到了返回值为val的迭代器,如果找不到,则返回尾迭代器;

工作流程:从begin到end找到每一个元素与val比较,相等则返回当前的迭代器,否则找到尾部,返回尾部迭代器(注意这里返回的是传进去的尾部迭代器iterator::end,如果传的不是尾迭代器,则返回的也不会是尾迭代器)。

还可以像如下方式来使用find处理内置数组:

int a[] = {1,2,3,4,5,6,7};

int val = 4;

auto result = find(begin(a),end(a),val);//result是int *类型。

2.iterator::difference_type count(iterator::begin,iterator::end,val);

统计begin到end中val出现的次数,返回次数,是带符号的。

3.accumulate(iterator::begin,iterator::end,val);

它定义在numeric头文件中;求指定范围内元素的和。

注意:val是指求和的初值,它表明了该函数要调用哪个加法运算符和返回类型。具体来说,传入的序列的类型必须与val类型匹配或者能够转换;同时只要支持加法运算符,都可以调用该函数,即可用用该函数拼接string;

string sum = accumulate(v.cbegin(),v,cend(),srting(""));

但是注意,val不能直接传字符串字面值,即下面的做法是错误的。因为字符串字面值,保存的类型是const char*,它不支持加法运算符。

string sum = accumulate(v.cbegin(),v,cend(),"");

4.bool equal(iterator::begin,iterator::end,iterator::begin);

确定两个序列是否保存相同的值,将第一个序列的每个值与第二个序列对应的位置比较,都相同则返回true。

两个序列的类型可以不相同,只要能用==比较即可,因此可以比较vector<string>和vector<const char*>,甚至第二个序列可以是list、deque等与第一个不同的容器;但是,第二个序列必须至少和第一个序列一样长。

int arr[] = {1,2,3};

int brr[] = {1,2,3,4,5,6};

equal(cbegin(arr),cend(arr),cbegin(brr));//返回true

只写算法:

更新序列中的值,,必须注意确保序列原大小至少不能小于算法写入元素的数目。

1.void fill(iterator::begin,iterator::end,val);

将begin到end范围内元素都设置为val,只要注意begin到end是有效的范围,写入就是安全的。

还可以接受通过数量来确定范围的函数:void fill_n(iterator::begin,iterator::size_type,val);

将begin开始的size_type个元素的值赋为val。

可以通过插入迭代器,不提前给容器分配空间,而给容器赋值,它是通过插入迭代器调用push_back()函数;

vector<int>arr;//空向量

auto  it =back_inserter(arr);//返回一个与容器arr绑定的插入迭代器

fill_n(it,10,0);//想arr中添加10个0

2.iterator copy(iterator::begin,iterator::end,iterator::dest);

 将begin到end的范围内元素复制到dest指向的内容之后;返回其递增后的目的位置的迭代器的值

int a1[] = {0,1,2,3,4,5,6,7,8,9};

int a2[sizeof(a1)/sizeof(a1[0])];//分配与a2一样的大小

//ret指向拷贝到a2的尾元素之后的位置

auto ret = copy(begin(a1),end(a1),a2);//把a1的内容拷贝给a2

3.replace(iterator::begin,iterator::end,type src,type dest);

将begin到end之间的元素中值为src全部用dest代替;

如果希望保证原容器的内容不变可以使用

replace_copy(iterator::begin,iterator::end,inserter::insert,type src,type dest);

例如:replace_copy(arr.cbegin(),arr.cend(),back_inserter(ivec),0,42);

上面的arr并没有改变,ivec中不仅包含arr的拷贝,还将其中的0都替换未42。

replace的用法可参考:http://blog.csdn.net/zwj1452267376/article/details/46821527

重拍容器中的元素的算法

1.sort(iterator::begin,iterator::end)

对容器中begin到end之间的元素排序,也可以通过第三个参数来调整是升序还降序,默认是升序。

它是利用<运算符来比较的,所以容器的元素类型必须支持<运算符,否则就要自己提供,详细的后面讲到。

2.iterator:: end_unique unique(iterator::begin,iterator::end)

消除已序容器相邻的相同元素,并返回一个指向不重复值范围末尾的迭代器。

但是,他并没有真正删除重复元素在返回的迭代器end_unique到end之间元素仍存在,但是我们知道它的值。

void elimDups(vector<string> words){

  //按照字典序排序words,以便查找重复单词

  sort(words,begin(),words.end());

  //将消除前面的重复元素

  auto end_unique = unique(words.begin(),words.end());

  //删除重复单词

  words.erase(end_unique,words.end());

}

谓词

谓词是一个可调用的表达式,其返回结果是一个能用作条件的值。分为一元谓词和二元谓词(一元二元是指的参数个数)。但是算法的输入序列中的元素类型必须能转换为谓词的参数类型。

例如:将单词按照长度排序;

bool isShorter(const string &s1,const string &s2){

  return s1.size() < s2.size();

}

//按照长度由短至长排序

sort(words.begin(),words.end(),isShorter);

lambda表达式

当传入的参数超过两个是就需要使用lambda表达式。现阶段经常使用的可调用对象包括函数、函数指针、重载了函数调用运算符的类以及lambda表达式。

一个lambda表达式表示一个可调用的代码单元。它包含一个返回类型、一个参数列表和一个函数体,但是lambda可以定义在函数内部。

[ capture ] ( params ) mutable exception attribute -> return { body } (1)  
[ capture ] ( params ) -> return { body } (2)  
[ capture ] ( params ) { body } (3)  
[ capture ] { body } (4)  

其中

  • (1) 是完整的 lambda 表达式形式,
  • (2) const 类型的 lambda 表达式,该类型的表达式不能改捕获("capture")列表中的值。
  • (3)省略了返回值类型的 lambda 表达式,但是该 lambda 表达式的返回类型可以按照下列规则推演出来:
    • 如果 lambda 代码块中包含了 return 语句,则该 lambda 表达式的返回类型由 return 语句的返回类型确定。
    • 如果没有 return 语句,则类似 void f(...) 函数。
  • 省略了参数列表,类似于无参函数 f()。

mutable 修饰符说明 lambda 表达式体内的代码可以修改被捕获的变量,并且可以访问被捕获对象的 non-const 方法。

exception 说明 lambda 表达式是否抛出异常(noexcept),以及抛出何种异常,类似于void f(throw(X, Y)。

attribute 用来声明属性。

另外,capture 指定了在可见域范围内 lambda 表达式的代码内可见得外部变量的列表,具体解释如下:

  • [a,&b] a变量以值的方式呗捕获,b以引用的方式被捕获。
  • [this] 以值的方式捕获 this 指针。
  • [&] 以引用的方式捕获所有的外部自动变量。
  • [=] 以值的方式捕获所有的外部自动变量。
  • [] 不捕获外部的任何变量。

此外,params 指定 lambda 表达式的参数列表,return是返回类型,body是函数体,但是lambda表达式必须使用尾置返回。

lambda表达式不能有默认参数,因此其形参数目一定与实参数目相同。

例如:与isShorter函数相同功能的lambda表达式:

[](const string& s1,const string& s2){return s1.size() < s2.size();}

因此可以用stable_sort来调用上面的lambda表达式:

//按照单词长度稳定排序

stable_sort(words.begin(),words.end(),[](const string& s1,const string& s2){return s1.size() < s2.size();});

使用捕获列表获取其所在函数的局部变量;

例如,捕获局部变量sz(表示大小);[sz](const string &a){return a.size() >= sz;};

使用find_if找到第一个长度不小于sz的元素:auto it = find_if(words.begin(),words.end(),[sz](const string &a){return a.size() >= sz;});

lambda的捕获与返回

当定义一个lambda时,编译器生成了一个与lambda对应的新的类类型(类似Java的匿名内部类)。向一个函数传一个lambda时,同时定义新类型和它的对象;默认情况下,从lambda生成的类都包含一个对应该lambda所捕获的变量的数据成员。

1.lambda的值捕获:与参数不同,值捕获时被捕获的变量的值是在lambda创建时拷贝,而不是调用是拷贝。

2.lambda的引用捕获:当捕获一个引用时,必须确保在lambda执行时,变量是存在的。引用捕获有时候是必要的,例如当我们要捕获一个IO流对象时,就必须捕获它的引用,因为IO流对象不允许拷贝或赋值。

注意:为了减少潜在风险,应该避免捕获指针或引用,保持lambda的变量捕获简单化。

lambda可以隐式捕获,让编译器根据代码推断捕获列表,但是为了指示是值捕获还是引用捕获,应该在捕获列表中写上&(引用捕获)或=(值捕获)。甚至可以在捕获列表中混合使用值捕获和引用捕获,此时第一个元素必须是&或=,它表明默认的捕获方式。

可以使用mutable来改变值捕获得到的变量值。

lambda的返回类型

默认情况下,如果一个lambda体包含return之外的任何语句,则编译器认为此lambda表达式返回void。

例如:transform(vi.begin(),vi.end(),vi.begin(),[](int i){return i < 0 ? -i : i;});//求绝对值的lambda表达式,没有问题。

   transform(vi.begin(),vi.end(),vi.begin(),[](int i){if(i < 0)return -i;else return i;});//编译出错,因为有了if语句,编译器认为lambda返回void。

改成 transform(vi.begin(),vi.end(),vi.begin(),[](int i) -> int {if(i < 0)return -i;else return i;});//使用尾置返回类型

lambda表达式应该用于只有一两个地方使用的简单操作,多个地方使用相同操作,或者操作复杂就应该使用函数。

如果需要捕获变量时也应该使用lambda,但是不需要捕获变量时一般还是使用函数来代替lambda表达式。但是并不是一定没有办法解决在函数中使用局部变量的问题,可以使用bind的标准库函数。

bind函数

bind函数定义在头文件functional中,我们可以将bind函数看作一个通用的函数适配器,它的一般形式如下

auto newFun = bind(oldFun,arg_list);

  1. 参数oldFun是需要bind封装的源函数,
  2. newFun是封装了参数后的old_Fun,
  3. arg_list是一个逗号分割的参数列表,对应oldFun的参数,即当我们调用newFun是,它会调用oldFun并且会把参数列表中的参数传给oldFun
  4. arg_list中会包含_n的名字,此类名字的参数又名”占位符”,因为其占据了newCallable的参数的位置,其中_n中的n表示它占据了new_Fun函数中的第几个参数。 
#include<iostream>
#include<vector>
#include<algorithm>
#include<functional>
using namespace std;

bool check_size(const int x,int sz)
{
    return x > sz;
}

int main(void)
{
    vector<int> v = {1,2,3,4,5,6,7,8,9};

    int n = 5;
    //有bind函数新封装生成的函数,其内部调用了check_size
    auto new_check_size = bind(check_size,std::placeholders::_1,n);//_1对应x,n对应sz

    auto it = find_if(v.begin(),v.end(),new_check_size);

    cout<<"第一个大于n的数为:"<<*it<<endl;

    return 0;
}

_n定义在placeholders的命名空间中,而这个命名空间定义在std中,所以上面使用_1时要加上命名空间。

还可以使用bind重排参数顺序:

  //按照长度由长到短排序

  sort(words.begin(),words.end(),bind(isShorter,_2,_1));

bind函数的参数是拷贝进去的如果想传递IO流对象就不行了,因为IO流对象不能拷贝;此时可以使用标准库中的另一个函数ref(定义在functional中),它返回一个对象包含给定的引用,此对象可以拷贝。(还有cref函数可以生成一个保存const引用的类)

  ostream &print(ostream &os, const string &s,char c){

    return os << s << c;

  }

  for_each(words.begin(),words.end(),bind(print,ref(os),_1, ' '));//通过ref使得os对象可以拷贝获取

function对象

我们知道,在C++中,可调用实体主要包括函数,函数指针,函数引用,可以隐式转换为函数指定的对象,或者实现了opetator()的对象(即C++98中的functor)。C++0x中,新增加了一个std::function对象,std::function对象是对C++中现有的可调用实体的一种类型安全的包裹(我们知道像函数指针这类可调用实体,是类型不安全的)。我们来看几个关于function对象的例子:

#include < functional>  
   
std::function< size_t (const char*) > print_func;  
   
/// normal function -> std::function object  
size_t CPrint(const char*) { ... }  
print_func = CPrint;  
print_func("hello world"):  
   
/// functor -> std::function object  
class CxxPrint  
{  
public:  
    size_t operator()(const char*) { ... }  
};  
CxxPrint p;  
print_func = p;  
print_func("hello world");  

在上面的例子中,我们把一个普通的函数和一个functor赋值给了一个std::function对象,然后我们通过该对象来调用。其它的C++中的可调用实体都可以像上面一样来使用。通过std::function的包裹,我们可以像传递普通的对象一样来传递可调用实体,这样就很好解决了类型安全的问题。了解了std::function的基本用法,下面我们来看一些使用过程中的注意事项:

  • (1)关于可调用实体转换为std::function对象需要遵守以下两条原则:
    a. 转换后的std::function对象的参数能转换为可调用实体的参数
    b. 可高用实体的返回值能转换为std::function对象的(这里注意,所有的可调用实体的返回值都与返回void的std::function对象的返回值兼容)。
  • (2)std::function对象可以refer to满足(1)中条件的任意可调用实体
  • (3)std::function object最大的用处就是在实现函数回调,使用者需要注意,它不能被用来检查相等或者不相等

里面还有些东西没讲清楚,后面再补充,有错的地方欢迎指出。

参考:http://blog.csdn.net/shreck66/article/details/48198357

     http://blog.csdn.net/augusdi/article/details/11771699