本篇接《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. XMPP——Smack[5]文件传输及离线消息的获取

    三天时间,赶在最后一下午实现了文件的传输,本来需要实现离线文件的发送的,一直没想好怎么弄,找openfire的离线文件插件没找到,后来想出一种方法,起服务器时起了一个系统用户,一直在线,当用户发送离线 ...

  2. cocos2d-x中本地推送消息

    作者:HU 转载请注明,原文链接:http://www.cnblogs.com/xioapingguo/p/4038277.html  IOS下很简单: 添加一条推送 void PushNotific ...

  3. 使用纯代码定义UICollectionView和自定义UICollectionViewCell

    1.自定义UICollectionView 2.实现<UICollectionViewDataSource,UICollectionViewDelegateFlowLayout,UICollec ...

  4. 关于Mysql Can't connect to mysql server on localhost(10061)的问题解决

    这个问题很烦,试了网上很多朋友的方法,还是不行,大家都知道卸载mysql再想装就不那么容易了(虽然我卸载安装无数次都成功了),好了,不废话了, 如果出现这种问题,不要急,找到mysql的安装包  例如 ...

  5. Lotus分析

    一 Lotus的任务 Lotus是一个消息通知服务,topic和subscription是多对多的关系.后面我加了一个发送自定义邮件和自定义短信的功能. 产品里面有个监控报警和通知列表.监控报警里创建 ...

  6. 【47】请使用traits classes表现类型信息

    1.考虑下面的需求,对迭代器移动d个单位.因为不同类型的迭代器,能力不同,有的迭代器(vector,deque内置迭代器)可以一步到位移动到指定位置,有的迭代器(list内置迭代器)必须一步一步移动, ...

  7. Codeforces Codeforces Round #319 (Div. 2) C. Vasya and Petya's Game 数学

    C. Vasya and Petya's Game Time Limit: 1 Sec Memory Limit: 256 MB 题目连接 http://codeforces.com/contest/ ...

  8. 图像的稀疏表示——ScSPM和LLC的总结

    前言 上一篇提到了SPM.这篇博客打算把ScSPM和LLC一起总结了.ScSPM和LLC其实都是对SPM的改进.这些技术,都是对特征的描述.它们既没有创造出新的特征(都是提取SIFT,HOG, RGB ...

  9. node.js在windows下的学习笔记(10)---URL模块

    1.parse函数的作用是解析url,返回一个json格式的数组 url.parse('http://www.zjut.edu.cn'); { protocol: 'http:', slashes: ...

  10. There is no Action mapped for action name XXX. - [unknown location]

    今天被这个问题费了不少时间,原因是缺少了 struts2-json-plugin-2.3.1.2.jar 包 当然,有时候也可能是缺少其他包, 把这个包添加到lib文件夹后还要刷新,clean一下,因 ...