引言

Program into Your Language, Not in It——《代码大全》。如何深入一门语言去编程?我认为有三步:熟悉它;知道它的局限性;扩展它。如何熟悉?不必说,自然是看书看资料,多用多写。如何知晓其局限性?这步我们只能通过对比了,任何事物都有其自身的局限性,没有任何东西是完美的(除了上帝哈)。在这里,我用C#与C++做对比,尝试勾勒出C#与C++一些观念上的不同。如何扩展?这点我正在尝试

C++的STL

STL包含六大组件:容器(Containers)、迭代器(Iterators)、算法(Algorithms)、仿函数(functors)、配接器(Adapters)、配置器(Allocators)。容器通过配置器取得数据存储空间,算法通过迭代器来存取容器的内容,仿函数协助算法完成不同的操作策略,配接器用来修饰或套接仿函数。这一整套配合,可以使我们完全掌控数据在存储器上的增删查改。(在这里我很想画一张图出来,但是我找了很久,实在找不到好的工具,有没有哪位同学能分享一些好的画示意图之类的工具呢?)

容器

STL中,最常用的容器要算vector、list、map、set这四种了。C#中,对应的容器分别是:List、LinkedList、Dictionary、HashSet。单看容器,其实它只是抽象出了一些逻辑结构,根据不同的逻辑需要,在存储器上反应出不同的物理存储结构。这点C++和C#的抽象没有什么不同,当然,其实现上,很不相同。这点通过代码的书写,就可以略窥一斑。

C++代码如下:

#include <iostream>
#include <vector>
#include <list>
#include <map>
#include <set>
#include <algorithm>
using namespace std; int main()
{
vector<int> vec;
list<int> lst;
map<int,int> mp;
set<int> st; for (int i = 0; i < 10; ++i)
{
vec.push_back(i);
lst.push_back(i);
mp.insert(make_pair(i, i));
st.insert(i);
} cout << "vector: " << endl;
vector<int>::const_iterator iterVec(vec.begin());
while (iterVec != vec.end())
{
cout << *iterVec << endl;
++iterVec;
} cout << "\nlist: " << endl;
list<int>::const_iterator iterLst(lst.begin());
while (iterLst != lst.end())
{
cout << *iterLst << endl;
++iterLst;
} cout << "\nmap: " << endl;
map<int, int>::const_iterator iterMap(mp.begin());
while (iterMap != mp.end())
{
cout << "Key = " << iterMap->first
<< "Value = " << iterMap->second
<< endl;
++iterMap;
} cout << "\nset: " << endl;
set<int>::const_iterator iterSet(st.begin());
while (iterSet != st.end())
{
cout << *iterSet << endl;
++iterSet;
} }

C#代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Collections;
using System.Net.Sockets;
using System.Net; namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
List<String> list = new List<String>();
LinkedList<String> linkedList = new LinkedList<String>();
Dictionary<Int32, String> dic = new Dictionary<Int32, String>();
HashSet<String> set = new HashSet<String>(); for (int i = 0; i < 10; ++i )
{
list.Add(i.ToString());
linkedList.AddLast(i.ToString());
dic.Add(i, i.ToString());
set.Add(i.ToString());
} Console.WriteLine("List: ");
foreach (var item in list)
{
Console.WriteLine(item);
} Console.WriteLine("\nLinkedList: ");
foreach (var item in linkedList)
{
Console.WriteLine(item);
} Console.WriteLine("\nDictionary: ");
foreach (var item in dic)
{
Console.WriteLine("Key = {0}, Value = {1}", item.Key, item.Value);
} Console.WriteLine("\nHashSet: ");
foreach (var item in set)
{
Console.WriteLine(item);
}
}
}
}

C++并没有内置的foreach语句(貌似新的标准中有?),所以它通过迭代器来帮助它来完成迭代。而C#就非常方便了,在语法级别完成了这个功能。从写法上我们可以看到,c++的迭代器看上去是一个指针,是一个可以做自增操作的指针。c#迭代出的每个item则是当前存放的数据。

迭代器

STL中的迭代器有五种:输入迭代器(Input Iterator)、输出迭代器(Output Iterator)、前向迭代器(Forward Iterator)、双向迭代器(Bidirectional Iterator)、随机存取迭代器(Random Access Iterator)。C#中,没有相对应的迭代器概念。毕竟迭代器就是一个智能指针,而C#却不支持指针(unsafe另算哈)。

输入迭代器,只能一次一个向前读取元素,并且只能读取该元素一次。如果我们复制一份输入迭代器,副本输入迭代器和原来的输入迭代器分别向前读取一个元素,那么他们可能会遍历到不同的值。以istream_iterator为列,代码如下:

#include <iostream>
#include <vector>
#include <list>
#include <map>
#include <set>
#include <algorithm>
#include <iterator>
#include <string>
using namespace std; int main()
{
//按Ctrl+Z结束输入,或者按Ctrl+C取消输入
istream_iterator<string> iterBegin(cin);
istream_iterator<string> iterEnd;
while (iterBegin != iterEnd)
{
cout << *iterBegin << endl;
++iterBegin;
}
}

输出迭代器,与输入迭代器相反,它的作用是将元素值一个个写入,所以只能作为左值。以ostream_iterator为列,代码如下:

#include <iostream>
#include <vector>
#include <list>
#include <map>
#include <set>
#include <algorithm>
#include <iterator>
#include <string>
using namespace std; int main()
{
ostream_iterator<int> iter(cout, "\n");
vector<int> vec ;
for (int i = 0; i < 10; ++i)
{
*iter = i;
}
}

前向迭代器,是输入、输入迭代器的结合,但是却没能用有输入、输入迭代器的全部功能,真心觉得这个迭代器很尴尬。前向迭代器提取值的时候,要确保它是有效的迭代器(比如到了序列尾端),而输出迭代器却不用(输出迭代器不提供比较操作,无需检查是否达到尾端)。我没见过比较有代表性的前向迭代器,所以给不出代码示例(囧…)。

双向迭代器,在前向迭代器的基础上增加了回头遍历的能力。写法上来说,就是提供了自减操作。最合适的列子非链表的迭代器莫属了。如下:

#include <map>
#include <set>
#include <algorithm>
#include <iterator>
#include <string>
using namespace std; int main()
{
list<int> lst;
for (int i = 0; i < 10; ++i)
{
lst.push_back(i);
} list<int>::const_iterator iter(lst.begin());
while (iter != lst.end())
{
cout << *iter << " ";
++iter;
}
cout << endl; while (iter != lst.begin())
{
--iter;
cout << *iter << " ";
}
}

随机迭代器,在双向迭代器的基础上增加了随机存取能力。写法上来说,就是提供了加减法操作,还提供了大小比较操作(除了这个迭代器,其他都没有大小比较,所以一般判断迭代器是否结尾,是用 == 或者 != 来判断)。最合适的列子就是vector的迭代器了。如下:

vector<int> vec;
for (int i = 0; i < 10; ++i)
{
vec.push_back(i);
} vector<int>::const_iterator iter(vec.begin());
cout << *(iter + 4) << endl;

至此,我们对C++迭代器有些基本的了解了。现在让我们探索一下这背后到底是怎么实现的。我们知道C++的STL是依靠模板(Template)来实现的,用C#的词来描述就是泛型(Generic)。一个迭代器,其实是一个类型,一个遵循了一系列潜规则的类型。按照被潜的程度,分成两种:自娱自乐,狼狈为奸。如果只是想自娱自乐的话,那么很简单,只要像下面这样既可:

#include <iostream>
#include <vector>
#include <list>
#include <map>
#include <set>
#include <algorithm>
#include <iterator>
#include <string>
using namespace std; template<typename Item>
struct ListIter; template<typename T>
struct ListItem; //作为存放元素的容器
template <typename T>
struct ListContainer
{
ListContainer() : _front(nullptr)
, _end(nullptr)
, _size(0)
{ } void insert_front(T value)
{
ListItem<T>* newItem = new ListItem<T>(value, _front); if (_front == nullptr)
{
_end = newItem;
} _front = newItem;
} void insert_end(T value)
{
ListItem<T>* newItem = new ListItem<T>(value, nullptr);
if (_end == nullptr)
{
_front = newItem;
_end = newItem;
}
else
{
_end->setNext(newItem);
_end = newItem;
}
} void display(std::ostream &os = std::cout) const
{
ListItem<T>* tmp = _front;
while (tmp != nullptr)
{
os << tmp->value() << " ";
tmp = tmp->next();
}
os << std::endl;
} ListItem<T>* front() const
{
return _front;
} private:
ListItem<T>* _end;
ListItem<T>* _front;
long _size;
}; //每个元素
template<typename T>
struct ListItem
{
ListItem(T val, ListItem<T>* next) : _value(val)
, _next(next)
{
} T value() const
{
return _value;
}
ListItem* next() const
{
return _next;
} void setNext(ListItem<T>* next)
{
_next = next;
}
private:
T _value;
ListItem<T>* _next;
}; //迭代器
template<typename Item>
struct ListIter
{
Item* ptr; ListIter(Item* p = 0) : ptr(p)
{} Item& operator*() const { return *ptr; }
Item* operator->() const { return ptr; } ListIter& operator++()
{
ptr = ptr->next();
return *this;
} ListIter operator++(int)
{
ListIter tmp =*this;
++(*this);
return tmp;
} bool operator==(const ListIter& i) const
{
return ptr == i.ptr;
} bool operator!=(const ListIter& i) const
{
return ptr != i.ptr;
}
}; int main()
{
ListContainer<int> myList; for (int i = 0; i < 10; ++i)
{
myList.insert_front(i);
myList.insert_end(i + 10);
} myList.display(); ListIter<ListItem<int> > begin(myList.front());
ListIter<ListItem<int> > end;
while (begin != end)
{
cout << begin->value() << endl;
++begin;
}
}

上述代码中,我们完全依赖自己的双手,通过重载*、->、 ++、==、!=等操作符,实现了自己的行为上类似迭代器的迭代器。但是我们仅能自娱自乐而已,不能融入STL的大家庭。我们无法复用STL原有的轮子,也无法将我们的轮子完美的放进STL(只需重载一下全局的!=操作符,可以使用STL的find)。我们为了实现这个迭代器,将容器的元素类型(ListItem)暴露了,而且还暴露了ListItem的内部实现细节(重载++操作符,用到了ptr->next()),明显不科学啊!所以一般迭代器都是相应的容器的设计者实现的,内嵌在容器中。

如果想让我们的迭代器能融入到STL中,那么,我们就必须为我们的迭代器实现五个“接口”,一个表示迭代器的类型iterator_category,一个表示值类型value_type,一个表示两个迭代器之间的距离类型difference_type,一个表示迭代器的指针pointer,一个表示迭代器的解引用reference。这五个“接口”,就是STL关于迭代器的潜规则。比如一个定义良好的iterator_category可以帮助我们的迭代器在使用distance(),advance()之类的函数时,有更高的效率。为了帮助我们定义自己的迭代器,STL有一个结构,只要我们继承即可,在VS中输入iterator然后转到定义,即可看到下图:

 

下面让我们来定义一个可以与STL“狼狈为奸”的迭代器。

#include <iostream>
#include <vector> using namespace std; template <typename T, typename container = ostream>
struct Our_OutputIterator : public iterator<output_iterator_tag,
T,
ptrdiff_t,
T*,
T&>
{
Our_OutputIterator(container& os) : _os(&os)
{ } Our_OutputIterator<T, container>& operator*()
{
return *this;
} Our_OutputIterator<T, container>& operator=(const T& _Val)
{
*_os << _Val << " " ;
return *this;
} Our_OutputIterator<T, container>& operator++()
{
return *this;
} Our_OutputIterator<T, container>& operator++(int)
{
return *this;
} container* _os;
}; int main()
{
Our_OutputIterator<int> os(cout);
vector<int> vec;
for (int i = 0; i < 10; ++i)
{
vec.push_back(i);
vec.push_back(i + 100);
}
copy(vec.begin(), vec.end(), os);
}

看起来好像没什么区别?其实这里面的区别大了。

STL迭代器的潜规则

让我们先从C#的接口谈起,相信大家对接口这个概念都不陌生。能被foreach遍历的类型,必须继承了IEnumerable这个接口。能够做比较运算的类型,必须继承了IComparable接口。接口,是个非常强的概念。它与类的虚函数相比,最大的不同就是:继承该接口后,必须要实现接口中的方法,而虚函数则不必。有了这层语法上的限制,那么我们在C#中定义我们的泛型方法时,就可以强制一些规定,便于我们操作传进来的泛型实参。比如我们要定义一个排序算法。既然是排序,首先就要求元素能够被比较,如果不能比较,那就只能呵呵了…下面贴代码。

public void FunnyQuickSort<T>(IList<T> list, Int32 right, Int32 left) where T : IComparable<T>
{
Int32 start = right, end = left; if (start >= end)
{
return;
} T @base = list[start]; while (start != end)
{
while (start < end && list[end].CompareTo(@base) >= 0)
{
end--;
} list[start] = list[end]; while (start < end && list[start].CompareTo(@base) <= 0)
{
start++;
}
list[end] = list[start];
} list[start] = @base;
FunnyQuickSort(list, right, start - 1);
FunnyQuickSort(list, start + 1, left);
}

通过用where这个方法,规定元素T的类型必须是可比较的,来限制用户程序员传入的类型。使用接口约束,不紧能方便我们在方法中做基于一定限制的逻辑操作,还能在需要的时候确定方法的返回类型以及一些类型的限定信息。可能后面这两个优点在C#中还不怎么明显,如果见识到STL为了这么简单的操作饶了多么大的弯,我们就能深刻体会到这种好处了。假设我们有这么个需求:需要返回一个两个迭代器(迭代器一个在前一个在后,能形成半开区间)间元素中的最大值。大家会怎么写这个方法?用C#,方法大概是这样:

我们可以返回一个T或者IComparable<T>。这是由于传进来的值的类型已经确定了是T。这是与C++最大的不同。在C++中,如果为通用的STL迭代器写一个算法,大概如下:

这时候,我们应该返回什么类型?我们甚至不知道这个迭代器指向的是什么类型!我们知道指向的值可以用*begin来表示,但是我们要怎么让编译器知道?这里可没有C#的委托限制,无法用委托来确定类型。大家想到怎么确定迭代器指向的类型了吗?没错,是利用函数模板的参数推到机制。我们要在里面再嵌入一层函数,即可得到迭代器指向的元素的类型。最后看起来代码像这样:

现在还剩一个问题了,最上层的Max应该返回什么类型?有两个方法可以确定:我们再为Max加一个泛型参数指明返回类型;再在模板中加一个插件。前一种很简单,不过不是很优雅,略过不谈。如何在模板中加插件?还记得我们前面所说的潜规则么?我们定义了五个“接口”,其中有一个是表示值类型的value_type。答案就在这!我们通过一个第三方的提取工具:iterator_traits,来获得返回的值类型。代码如下:

将迭代器的类型传入iterator_traits中,提取出定义该迭代器的时候定义的元素类型。这下所有的问题都解决了。是不是很优雅?当然,不要跟C#比。下面我们测试一下我们的代码:

#include <iostream>
#include <vector> using namespace std; template<typename inputIter>
typename iterator_traits<inputIter>::value_type Max(inputIter begin, inputIter end)
{
typedef typename iterator_traits<inputIter>::value_type type;
type val = *begin;
while (begin != end)
{
if (*begin > val)
{
val = *begin;
}
++begin;
} return val;
} int main()
{
vector<int> vec;
for (int i = 0; i < 10; ++i)
{
vec.push_back(i);
} vec.push_back(100);
vec.push_back(1); cout << Max(vec.begin(), vec.end()) << endl;
}

我们看到,由于C++少了接口这个语法级的概念,实现一个这么简单的方法,都要绕这么大一个弯!而且在调试代码的时候,模板出错的报错提示,是出了名的多!一个小问题可以引起大段的错误提示。其实上面的代码很容易出错,如果迭代器指向的类型无法做逻辑比较怎么办?比如将一个map的迭代器传进来,大家可以试一试!而C#从语法层面上将这些弊端都规避掉了。如果不符合接口限制,将会有优雅的提示信息。返回类型可以直接返回接口类型。我真心感觉吊炸天!如果不与C++比较,我是无法知道C#为我做了这么多工作!想到STL是上世纪的杰作,我很佩服当时为了解决这些问题而探索出的traits方法。而C#作为后来者,明显吸收了很多C++的精华。

通过容器和迭代器这两个组件,我们可以看到STL的构思之巧妙,通过一系列的潜规则,来实现了通用的目的。我们也看到了C#的方便之处。到目前的比较为止,C#的表现非常不错。但是,C#会一直这么拽吗?有句话说的好:“你不拽我们还可以做朋友…”。以我现在还和C#是朋友的现状来看……

我本来想以一篇来概括STL的,写了快10小时,发现还仅是写到第二个组件!现在只剩下一句话:欲知后事如何,请听下回分解!

总结

C#作为后来者,在语法层面上规避了很多STL遇到的问题。而STL的构思之妙,略窥一二。

参考资料

  1. 侯捷.STL源码剖析.武汉:华中科技大学出版社,2013
  2. Nicolai M. Josuttis.C++标准库.侯捷译.武汉:华中科技大学出版社,2011
  3. Jeffrey Richter. CLR via C#.周靖译.北京:清华大学出版社,2011

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

  1. C#与C++相比较之STL篇(续一)

    本篇接<C#与C++相比较之STL篇>,主要探索C++STL的两个组件:算法和仿函数,以及C#的linq和拉姆达表达式.委托. STL的算法与仿函数 算法是个庞大的主题,STL包含了超过1 ...

  2. 各大算法专题-STL篇

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

  3. 算法专题-STL篇

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

  4. STL篇--list容器

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

  5. 第九篇 SQL Server安全透明数据加密

    本篇文章是SQL Server安全系列的第九篇,详细内容请参考原文. Relational databases are used in an amazing variety of applicatio ...

  6. 【译】第九篇 SQL Server安全透明数据加密

    本篇文章是SQL Server安全系列的第九篇,详细内容请参考原文. Relational databases are used in an amazing variety of applicatio ...

  7. [Android Pro] AndroidStudio IDE界面插件开发(进阶篇之Action机制)

    转载请注明出处:[huachao1001的专栏:http://blog.csdn.net/huachao1001/article/details/53883500] 从上一篇<AndroidSt ...

  8. noip模拟12[简单的区间·简单的玄学·简单的填数]

    noip模拟12 solutions 这次考试靠的还是比较好的,但是还是有不好的地方, 为啥嘞??因为我觉得我排列组合好像白学了诶,文化课都忘记了 正难则反!!!!!!!! 害没关系啦,一共拿到了\( ...

  9. 纯JS实现中国行政区域上下联动选择地址

    一.实现目的: 如标题所述,通过JS来实现地址的选取,上一篇博客介绍的方式是通过java读取txt资源文件来实现地址的选择,通过ajax方式访问服务器实现省市区联动.此篇中将介绍如何使用JS实现相同功 ...

随机推荐

  1. Scribefire发CSDN博客

    历史 在非常久非常久曾经,CSDN是支持外部工具来写文章的,但是在还有一个非常久非常久曾经就不行了. 突然看到CSDN有能够用外部工具来写博客了(CSDN的公告),一直以来都纠结这个问题,CSDN的编 ...

  2. zoj 3823 Excavator Contest 构造

    Excavator Contest Time Limit: 1 Sec Memory Limit: 256 MB 题目连接 http://acm.zju.edu.cn/onlinejudge/show ...

  3. Cocos2d-x 3.x 资料整理

     cocos2d-x-3.0rc0新project的分辨率设置和控制台输出信息 http://kome2000.blog.51cto.com/969562/1379704 Cocos2d-x 3. ...

  4. android 删除文件以及递归删除文件夹

    private void deleteDirectory(File file) { if (file.isFile()) { file.delete(); return; } if(file.isDi ...

  5. 接入新浪、腾讯微博和人人网的Android客户端实例 接入新浪、腾讯微博和人人网的Android客户端实例

    做了个Android项目,需要接入新浪微博,实现时也顺带着研究了下腾讯微博和人人网的Android客户端接入,本文就跟大家分享下三者的Android客户端接入方法. 一.实例概述 说白了,接入微博就是 ...

  6. [ACM] 最短路算法整理(bellman_ford , SPFA , floyed , dijkstra 思想,步骤及模板)

    以杭电2544题目为例 最短路 Problem Description 在每年的校赛里,全部进入决赛的同学都会获得一件非常美丽的t-shirt. 可是每当我们的工作人员把上百件的衣服从商店运回到赛场的 ...

  7. WebService 设计总结

    接触过非常多电商的WebService,有种一看就蛋疼的设计,今天要从这个反例说一说 WebService 的设计. [WebMethod] public string QueryOrderDetai ...

  8. [Effective C++ --018]让接口容易被正确使用,不易被误用

    □第一节 什么是接口?什么是接口?百度百科的解释:两个不同系统(或子程序)交接并通过它彼此作用的部分.接口的概念贯穿于整个软件开发过程中,本文讨论的接口概念仅局限于以C++实现的class,funct ...

  9. 信号之sigsuspend函数

    更改进程的信号屏蔽字可以阻塞所选择的信号,或解除对它们的阻塞.使用这种技术可以保护不希望由信号中断的代码临界区.如果希望对一个信号解除阻塞,然后pause等待以前被阻塞的信号发生,则又将如何呢?假定信 ...

  10. 性能测试中用LambdaProbe监控Tomcat Tomcat和Probe的配置

    转载:http://bbs.51testing.com/thread-90047-1-1.html 性能测试中用LambdaProbe监控TomcatLambdaProbe 是一款强大的免费开源工具, ...