本篇接《C#与C++相比较之STL篇》,主要探索C++STL的两个组件:算法和仿函数,以及C#的linq和拉姆达表达式、委托。

STL的算法与仿函数

算法是个庞大的主题,STL包含了超过100个算法,仅仅记住算法的名字就已经很蛋疼了。所以我在这将范围缩小一点:主要讨论STL算法中的遍历、排序、查找这几类算法。

遍历算法常用的有for_each、transform、copy这三种。for_each接受一项操作,如果该操作的参数是用引用方法传递,则它可以变动其遍历的元素,反之不能。transform是一个变动性算法,它运用某项操作,该操作返回被改动后的参数。它比for_each更灵活,能同时对三个容器的元素进行操作。copy算法正向遍历给定区间,将区间内的元素分别复制到另一个指定容器的迭代器指向的元素空间,这个算法需要注意目的容器的大小要大于等于源容器的大小。下面写出这三种算法的一般写法:

#include <iostream>
#include <vector>
#include <list>
#include <map>
#include <set>
#include <algorithm>
#include <iterator>
#include <string>
using namespace std; template <typename Container>
void PrintElements(Container c)
{
typedef typename iterator_traits<Container::iterator>::value_type type; //提取出其
typedef Container::value_type type; //获得类型 cout << endl; ostream_iterator<type> os(cout, "\t");
copy(c.begin(), c.end(), os);
} template <typename T>
void PrintElement(T value)
{
cout << value << "\t";
}
template <typename T>
T DoubleParameter(T value)
{
return 2 * value;
} int main()
{
vector<int> vecSource;
list<int> lstDest;
for (int i = 0; i < 10; ++i)
{
vecSource.push_back(i);
} for_each(vecSource.begin(), vecSource.end(), PrintElement<int>); // 确保lstDest有足够的空间
lstDest.resize(vecSource.size()); transform(vecSource.begin(), vecSource.end(), lstDest.begin(), DoubleParameter<int>); PrintElements(lstDest);
}

排序算法以sort为代表,sort默认以操作符<来进行排序,如果要指明排序规则,就必须自定义一个排序方法。示例代码如下:

#include <iostream>
#include <vector>
#include <list>
#include <map>
#include <set>
#include <algorithm>
#include <iterator>
#include <string>
#include <functional>
#include <ctime>
using namespace std; template <typename Container>
void PrintElements(Container c)
{
typedef Container::value_type type; //获得类型 cout << endl; ostream_iterator<type> os(cout, "\t");
copy(c.begin(), c.end(), os);
} int main()
{
srand((unsigned) time(NULL));
vector<int> vecSource;
for (int i = 0; i < 50; ++i)
{
vecSource.push_back( rand()% 10000);
} cout << "排序之前:" ; PrintElements(vecSource); sort(vecSource.begin(), vecSource.end()); cout << endl << "默认排序之后:" ;
PrintElements(vecSource); // 在这里,我们不用默认的less<int>排序, 改成greater<int>排序
sort(vecSource.begin(), vecSource.end(), greater<int>()); cout << endl << "反向排序之后:" ;
PrintElements(vecSource);
}

查找算法以find为例。我们知道,在不同的元素排放顺序上,有不同的查找对策。比如,对于已经排好序的元素列表,使用二分法查找,是最快的。如果是未排序的列表,那么只能一个个遍历了。STL中,有些容器在内部是已经排好序的,比如map、set等。如果要查找一个元素,最好要先查看该容器是否已经提供了查找算法,如果没有提供,再用通用的find算法。代码示例如下:

#include <iostream>
#include <vector>
#include <list>
#include <map>
#include <set>
#include <algorithm>
#include <iterator>
#include <string>
#include <functional>
#include <ctime>
using namespace std; int main()
{
srand((unsigned) time(NULL));
vector<int> vecSource;
set<int> st;
int element = 0;
for (int i = 0; i < 500; ++i)
{
element = rand()% 1000;
vecSource.push_back( element);
st.insert(element);
} vector<int>::const_iterator iterVec = find(vecSource.begin(), vecSource.end(), 111);
if (iterVec != vecSource.end())
{
cout << "found " << *iterVec << endl;
}
else
{
cout << "not found in vector" << endl;
}
set<int>::const_iterator iterSt = st.find(222);
if (iterSt != st.end())
{
cout << "found " << *iterSt << endl;
}
else
{
cout << "not found in set" << endl;
}
}

STL仿函数的概念很简单,就是重载了运算符()的对象。借助()运算符重载,我们可以在C++中创建具有“状态”的函数。假设,我们要写一个函数,为每个参数加10。那么我们可以这么写:

如果我们要加上11、12之类的呢?总不可能再去专门写一些函数吧。我们可以把要加的数用模板来表示:

这些都要求我们在编译期就要给出想要叠加的值。如果值是一个运行时才能得出的,这可怎么办?别慌,仿函数来帮咱。我们可以这样定义一个仿函数:

我们重新定义了类CDHFunctor的()操作符,该操作符接受一个int类型的参数,返回类型是void。定义看起来非常简单,但是使用起来,却有点晦涩:

   1: #include <iostream>

   2: #include <vector>

   3: #include <list>

   4: #include <map>

   5: #include <set>

   6: #include <algorithm>

   7: #include <iterator>

   8: #include <string>

   9: #include <functional>

  10: #include <ctime>

  11: using namespace std;

  12:  

  13: void FillContainer(vector<int>& c)

  14: {

  15:     for (int i = 0; i < 10; ++i)

  16:     {

  17:         c.push_back(i);

  18:     }

  19: }

  20:  

  21: template <int T>

  22: void add10(int& val)

  23: {

  24:     val += T;

  25: }

  26:  

  27: void add10(int& val)

  28: {

  29:     val +=11;

  30: }

  31:  

  32: template <typename Container>

  33: void PrintElements(Container c)

  34: {

  35:     typedef Container::value_type type; //获得类型

  36:  

  37:     cout << endl;

  38:  

  39:     ostream_iterator<type> os(cout, "\t");

  40:     copy(c.begin(), c.end(), os);

  41: }

  42:  

  43: struct CDHFunctor

  44: {

  45:     CDHFunctor(int val) : mVal(val){}

  46:  

  47:     void operator()(int& v)

  48:     {

  49:         v += mVal;

  50:     }

  51:  

  52: private:

  53:     int mVal;

  54: };

  55:  

  56: int main()

  57: {

  58:     vector<int> vec;

  59:     FillContainer(vec);

  60:     

  61:     for_each(vec.begin(), vec.end(), CDHFunctor(100));

  62:     PrintElements(vec);

  63: }

让我们仔细分析一下第61行代码。for_each是一个遍历算法,它遍历容器vec中所有的元素,用容器中的每个元素作为作为其第三个参数(即CDHFunctor()这个仿函数)的参数,调用之。也就是说,for_each的第三个参数必须是一个以int作为参数的函数。CDHFunctor(100) 这到底是什么意思呢? 让我们把这句代码抠出来理解,C++里,在栈上构造一个对象的写法是这样的:CDHFunctor fun(100),这样,我们就有了一个fun对象,如果我们去掉fun,直接写CDHFunctor(100),表示我们创建了一个临时对象,该对象没有名字。我们可以把它扩展成这样:

现在很容易看出来它是怎么调用的了,中间生成的调用大概是fun(*iter)(iter是vec的迭代器)。正好和我们重载的运算符结构一致。我们再把它抠出来测试一下:

现在,我们可以一览全貌了。我们可以在运行时,动态生成一个对象(即仿函数),并且保存我们需要的状态,然后通过重载的运算符(),将这个对象以函数的形式调用。STL的算法与仿函数搭配起来,威力非常大,而且不失性能!详细请参考《Effective STL》第46条。

LINQ和拉姆达表达式、委托

linq是一个很庞大的话题,在这里,我仅将其与List之类的容器搭配描述。linq提供了一致的语法来操作数据源,只要其对象实现了IEnumerable<T>接口。相关示例我就不给出了。使用linq时需要注意避免捕获昂贵的资源:比如文件。不然很容易就造成了资源泄露或者其他的各种问题。比如下面这段:

static class Extension
{
public static IEnumerable<String> ReadLines(this TextReader reader)
{
String txt = reader.ReadLine();
while (txt != null)
{
yield return txt;
txt = reader.ReadLine();
}
} public static int DefaultParse(this String input, int defaultValue)
{
int answer;
return (int.TryParse(input, out answer)) ? answer : defaultValue;
}
} class Program
{
public static IEnumerable<IEnumerable<int>> ReadNumbersFromStream(TextReader r)
{
var allLines = from line in r.ReadLines()
select line.Split(',');
var matrixOfValues = from line in allLines
select from item in line
select item.DefaultParse(0); return matrixOfValues;
} static void Main(string[] args)
{ string path = Console.ReadLine().Replace("\"", ""); // 在这里,读取文件后, 没有释放资源,造成了资源的泄露
TextReader t = new StreamReader(path);
var rowsOfNumbers = ReadNumbersFromStream(t);
foreach (var line in rowsOfNumbers)
{
foreach (var item in line)
{
Console.Write(item);
}
Console.WriteLine();
}
}

然后有的同学可能就想了,我们用using语句包一下,是不是就没有资源泄露了呢?

包裹起来后,大家可以运行一下,会有个运行时异常:

总而言之,大家要小心闭包引起的对象生命周期延长的问题!

拉姆达表达式,它允许我们很方便的写出一些简短的匿名的函数。使用拉姆达表达式的时候,要千万注意使用上下文中的变量。能不使用上下文中的变量,就不要使用,如果使用,要小心该被引用的对象生命周期,不然会出现一些出乎意料的结果。比如下面:

大家可以运行查看其输出,看看有没有料中。在上面这种情况下,本来i的生命周期是在Test()函数的范围内的,但是由于拉姆达表达式引用到它了,于是它的生命周期延长到Main方法里了,与fun变量的生命周期一致。这仅仅是个值类型,如果是引用类型,大家可以想象:如果Test()是一个经常被调用的函数,而且其返回值被保存起来了,那么其内的引用类型则一直是可达的,就不会被GC回收。到最后结果就是Out of memory了……

算法和仿函数对比LINQ和拉姆达表达式、委托

我们首先来比较一下下面两段代码:

我们看到,同样是查找一个大于5的数,在STL里,要写那么一堆。而C#,简单优雅。lambda就是拽!而且扩展性方面两者也差不多。代码看起来差很多,其实他们的思想是一样的,List的Find方法接受一个委托,签名是返回值为bool类型,一个int类型的参数,也就是所谓的Predicate<T>委托。而STL中,find_if也是接收一个predicate函数,其签名与C#的Predicate委托一样。List的Find方法接收一个Predicate委托,依次用其元素作为参数调用该委托,如果委托的返回值是真,就停止。find_if则接受一个区间,依次用区间内的元素去调用Compare函数,返回真则结束调用。

让我们再看一段C#的代码:

LINQ绝对是划时代的技术,它不仅提供了一致的操作源数据的语法,而且还有lazy load特性,简直吊炸天!

总结

C#中lambda表达式,再配合委托的概念,让我们可以用简短的代码表达很复杂的操作,但是其思想与STL相比没有发生根本性的变化。C#的委托,在使用上与STL中的仿函数很相像。LINQ在思想上,明显已经超过STL了。

由于个人水平有限,写的不是很详尽正确。如果同学们发现了错误,烦请指正。也请有不同看法的同学,能够一起讨论,交流思想。

参考资料

  1. 侯捷.STL源码剖析.武汉:华中科技大学出版社,2013
  2. Nicolai M. Josuttis.C++标准库.侯捷译.武汉:华中科技大学出版社,2011
  3. Bill Wagner.More Effective C#.北京:人民邮电出版社,2009

C#与C++相比较之STL篇(续一)的更多相关文章

  1. C#与C++相比较之STL篇

    引言 Program into Your Language, Not in It--<代码大全>.如何深入一门语言去编程?我认为有三步:熟悉它:知道它的局限性:扩展它.如何熟悉?不必说,自 ...

  2. Python之路【第七篇续】:I/O多路复用

    回顾原生Socket 一.Socket起源: socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,对于文件用[打开][读写][关闭]模式来操作. socket就是该模式的 ...

  3. 各大算法专题-STL篇

    这篇文章着重记录c++中STL的用法.主要粗略的介绍其用法,以知识点的形式呈现其功能,不会深入源码分析其工作原理. 排序和检索. sort(a,a+n),对a[0]往后的n个元素(包括a[0])进行排 ...

  4. 算法专题-STL篇

    这篇文章着重记录c++中STL的用法.主要粗略的介绍其用法,以知识点的形式呈现其功能,不会深入源码分析其工作原理. 排序和检索. sort(a,a+n),对a[0]往后的n个元素(包括a[0])进行排 ...

  5. 深入研究C语言 第一篇(续)

    没有读过第一篇的读者,可以点击这里,阅读深入研究C语言的第一篇. 问题一:如何打印变量的地址? 我们用取地址符&,可以取到变量的偏移地址,用DS可以取到变量的段地址. 1.全局变量: 我们看到 ...

  6. Python之路【第十一篇续】前端之CSS补充

    CSS续 1.标签选择器 为类型标签设置样式例如:<div>.<a>.等标签设置一个样式,代码如下: <style> /*标签选择器,如果启用标签选择器所有指定的标 ...

  7. Redis面试热点之底层实现篇(续)

    0.题外话 接着昨天的[决战西二旗]|Redis面试热点之底层实现篇继续来了解一下ziplist压缩列表这个数据结构. 你可能会抱有疑问:我只是使用Redis的功能并且公司的运维同事都已经搭建好了平台 ...

  8. STL篇--list容器

    list容器: 1.list 容器 的本质就是双向环形链表,最后一个节点刻意做成空节点,符合容器的左闭右开的原则2.list 的迭代器 是一个智能指针,其实就是一个类,通过操作符重载模拟各种操作(++ ...

  9. 深入研究C语言 第二篇(续)

    1. 关于如下的程序,关于结构体的拷贝,拷贝是拷贝到内存中的什么地方? 我们进入debug进行反汇编,单步等操作跟踪查看.发现: 在main中,我们看到call 0266应该对应的是转跳到func处执 ...

随机推荐

  1. 流弊博客集锦(updating)

    1.http://ifeve.com/ 2.淘宝的 code project http://code.taobao.org/ http://blog.csdn.net/tenfyguo/article ...

  2. iOS containsString与rangeOfString

    rangeOfString是在 containsString没出来之前 用于查找字符串中是否包含某字符,iOS <8.0 NSString *str1 = @"can you \n s ...

  3. zoj 3820 Building Fire Stations 树的中心

    Building Fire Stations Time Limit: 1 Sec Memory Limit: 256 MB 题目连接 http://acm.zju.edu.cn/onlinejudge ...

  4. windows环境下搭建ffmpeg开发环境

           ffmpeg是一个开源.跨平台的程序库,能够使用在windows.linux等平台下,本文将简单解说windows环境下ffmpeg开发环境搭建过程,本人使用的操作系统为windows ...

  5. [RxJS] AsyncSubject

    AsyncSubject emit the last value of a sequence only if the sequence completed. This value is then ca ...

  6. 标准I/O库之流和FILE对象

    对于标准I/O库,它们的操作是围绕流(stream)进行的.当用标准I/O库打开或创建一个文件时,我们已使一个流与一个文件相关联. 对于ASCII字符集,一个字符用一个字节表示.对于国际字符集,一个字 ...

  7. PAT 1011

    1011. World Cup Betting (20) With the 2010 FIFA World Cup running, football fans the world over were ...

  8. java 并发官方教程

    http://docs.oracle.com/javase/tutorial/essential/concurrency/index.html Concurrency Computer users t ...

  9. Forms and actions

    Forms and actions Adding new albums We can now code up the functionality to add new albums. There ar ...

  10. 你真的会用 SDWebImage?

    SDWebImage作为目前最受欢迎的图片下载第三方框架,使用率很高.但是你真的会用吗?本文接下来将通过例子分析如何合理使用SDWebImage. 使用场景:自定义的UITableViewCell上有 ...