本篇接《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了。
由于个人水平有限,写的不是很详尽正确。如果同学们发现了错误,烦请指正。也请有不同看法的同学,能够一起讨论,交流思想。
参考资料
- 侯捷.STL源码剖析.武汉:华中科技大学出版社,2013
- Nicolai M. Josuttis.C++标准库.侯捷译.武汉:华中科技大学出版社,2011
- Bill Wagner.More Effective C#.北京:人民邮电出版社,2009