c++语言是一种非常丰富的语言,它寻求支持许多不同的编程范式。我们已经看到c++支持两种最流行的编程范式:
在这些课堂笔记中,我将向您介绍c++支持的第三种主要编程范例,函数式编程。首先是正式的定义:
在函数式编程中,函数成为第一类语言元素,可以存储,作为参数传递给其他函数,并作为其他函数的结果返回。
下面是一个相当自然的例子,它体现了函数式编程中的一个关键思想,即将一个函数作为参数传递给另一个函数。
这个例子从我们的朋友快速排序开始,
Void swap(int &a, int &b){int temp = a;A = b;B = temp;} int median(vector<int> & a,int first,int last) {int mid=(first+last)/2;如果(a[first] < a[mid]){如果(a[mid] < a[last])返回mid;if (a[first] < a[last]) return last;否则首先返回;} else {if(a[first] < a[last])返回first;如果(a[last] < a[mid])返回中值;否则返回最后一个;}} int partition(vector<int> & a,int first,int last) {int p=first;交换((第一)、(值(一个,首先,持续1)));Int piv = a[first];(int k = + 1; k <最后;k + +){如果([k] < = piv) {p + +;交换([k], [p]);}} swap(a[p],a[first]);返回p;} void quickSort(vector<int> & a,int first,int last) {if (last - first > 20) {int piv = partition(a,first,last);快速排序(a,第一,piv);快速排序(piv + 1,);} else {selectionSort(a,first,last);}} void quickSort(vector<int>& a) {quickSort(a,0,a.size());}
关于这段代码需要注意的一件重要事情是,它将整数列表按升序排序。算法的这一方面在非常深的层次上被编码到实现中。配分函数的这一部分决定了一个数字应该进入哪一半的配分:
(int k = + 1; k <最后;k + +){如果([k] < = piv) {p + +;交换([k], [p]);}}
这段代码在if语句中使用比较来决定number应该去哪里。如果a[k]比主元小它就往左,否则就往右。
如果我们想创建一个快速排序的版本,将整型向量按降序排序,则必须将If语句中的<=替换为>=。我们还必须重命名几个函数:
int partitiondescent (vector<int> &a,int first,int last) {int p=first;交换((第一)、(值(一个,首先,持续1)));Int piv = a[first];(int k = + 1; k <最后;k + +){如果([k] > = piv) {p + +;交换([k], [p]);}} swap(a[p],a[first]);返回p;} void quicksortdescent (vector<int> &a,int first,int last) {if (last - first > 20) {int piv = partitiondescent (a,first,last);快速排序(a,第一,piv);快速排序(piv + 1,);} else {selectionsortdescent (a,first,last);}}无效quicksortdescent (vector<int>& a) {quicksortdescent (a,0,a.size());}
还要注意,由于quickSort调用selectionSort,我们必须准备一个单独的selectionSort版本,将数据按降序排序。
使快速排序更灵活的一种稍微不那么恼人的方法是对问题赢博体育抽象。在这种方法中,我们隔离了函数中可能从一个版本更改到下一个版本的代码部分。
Bool in_order(int a,int b){返回a < b;} int partition(vector<int> & a,int first,int last) {int p=first;交换((第一)、(值(一个,首先,持续1)));Int piv = a[first];(int k = + 1; k <最后;k + +){如果(! in_order(此外,[k])) {p + +;交换([k], [p]);}} swap(a[p],a[first]);返回p;} void quickSort(vector<int> & a,int first,int last) {if (last - first > 20) {int piv = partition(a,first,last);快速排序(a,第一,piv);快速排序(piv + 1,);} else {selectionSort(a,first,last);}} void quickSort(vector<int>& a) {quickSort(a,0,a.size());}
在这个版本中,我们将比较逻辑隔离在函数in_order中,partition和selectionSort都可以使用该函数来完成它们的工作。要将排序代码从升序更改为降序,只需更改in_order函数的代码。
这种方法的明显缺点是,一旦我们为in_order编写代码,我们就将自己锁定在排序顺序中。更好的方法是将比较函数作为参数传递给算法。这样,就可以传入一个版本的in_order函数来进行升序排序,传入另一个版本来进行降序排序。
最初的C语言实际上可以将函数作为参数传递给其他函数,但是这样做的机制有些笨拙。如果你想在C中把一个函数作为参数传递给另一个函数,你需要传递一个指向你想要使用的函数的指针。下面的代码展示了如何做到这一点。
Bool升序(int a,int b){返回a < b;} bool降序(int a,int b){返回> b;} int partition(vector<int> &,int first, int last, bool (*in_order)(int,int)) {int p=first;交换((第一)、(值(一个,首先,持续1)));Int piv = a[first];(int k = + 1; k <最后;k + +){如果(! in_order(此外,[k])) {p + +;交换([k], [p]);}} swap(a[p],a[first]);返回p;} void quickSort(vector<int> &a, int first, int last, bool (*in_order)(int,int)) {if (last - first > 20) {int piv = partition(a,first,last,in_order);快速排序(a,第一,piv);快速排序(piv + 1,);} else {selectionSort(a,first,last,in_order);}} void quickSort(vector<int>& a,bool (*in_order)(int,int)) {quickSort(a,0,a.size(),in_order);}
在这段代码的不同地方,您将看到采用以下形式的参数
bool (* in_order) (int, int)
这是“in_order是一个指向函数的指针,该函数以两个int型作为参数,然后返回一个bool型”。作为参数传递给一个函数的函数指针可以作为参数传递给另一个函数。最后,当我们需要使用它来比较两个项时,我们调用作为配分函数参数传入的函数。
有了这样的安排,我们就叫
快速排序(V,提升);
将包含整型数的向量V按升序排序。这里我将升序函数作为参数传递给快速排序。在C和c++中,函数名都被解释为指向该函数的指针。由于升序函数是一个以两个int型作为参数然后返回bool型的函数,因此升序函数是一个有效的函数指针,可以作为第二个参数传递给quickSort。
c++提供了另一种方法来简化将函数作为参数传递给其他函数。这种方法是基于使用c++模板函数来简化函数参数设置的任务。下面是上面用模板函数重写的例子。
模板<typename T> bool升序(const T& a,const T& b){返回> b;}模板<typename T> bool降序(const T& a,const T& b){返回a < b;}模板<typename T,typename F> int partition(vector<T> &a, int first, int last, F in_order) {int p=first;交换((第一)、(值(一个,首先,持续1)));Int piv = a[first];(int k = + 1; k <最后;k + +){如果(! in_order(此外,[k])) {p + +;交换([k], [p]);}} swap(a[p],a[first]);返回p;} template <typename T,typename F> void quickSort(vector<T> &a, int first, int last, F in_order) {if (last - first > 20) {int piv = partition(a,first,last,in_order);快速排序(a,第一,piv);快速排序(piv + 1,);} else {selectionSort(a,first,last,in_order);}}模板<typename T,typename F> void quickSort(向量<T>& a,F in_order) {quickSort(a,0,a.size(),in_order);}
在这个版本的代码中,in_order形参看起来就像另一个形参,其类型F被模板占位符替换了。作为额外的好处,我们可以构造一个完全模板化的排序算法,其中被排序的内容的类型被更改为模板参数。
与前面的示例一样,我们通过向快速排序传递适当的比较函数来指定希望对项进行排序的方式。我们用升序排序
快速排序(V,提升);
此时,模板机制开始发挥作用,为我们推导出T和F类型。如果V被声明为int型向量,编译器将推断T是int型,F是指向一个函数的指针,该函数接受两个int型作为形参并返回bool型。
上周我们学习了堆数据结构。堆有两种不同的类型,最大堆和最小堆。为了实现这两种不同的风格,我们可以定义两个单独的类,或者我们可以尝试使用前面示例中的一些想法来实现一个堆类,它可以根据传递给它的比较函数作为最小堆或最大堆的功能。
下面是使用我在上一个示例中开发的模板机制实现堆类的实现。
模板<typename T,typename F>类堆{public: heap(F func): in_order(func) {} bool isEmpty(){返回cells.empty();} T top(){返回cells.front();} void add(const T& item) {cells.push_back(item);Int node = last();while(node != 0 && !in_order(cells[node],cells[parent(node)])) {swapNodes(node,parent(node));Node = parent(节点);}} void remove() {cells[0] = cells.back();cells.pop_back ();Int节点= 0;While (true) {int winner = node;If (left(node)<(int)cells.size()&& !in_order(cells[left(node)],cells[winner])) winner = left(node);If (right(node)<(int) cells.size() && !in_order(cells[right(node)],cells[winner])) winner = right(node);If (winner == node) break;else {swapNodes(node,winner);节点=赢家;}}} private: std::vector<T> cells;F in_order;Int parent(Int n) {return (n-1)/2;} int left(int n){返回2*n+1;} int right(int n){返回2*n+2;} int last(){返回cells.size()-1;}无效swapNodes(int a,int b) {T temp = cells[a];Cells [a] = Cells [b];Cells [b] = temp;}};
在这个例子和最后一个例子之间有一个微妙但重要的区别。在快速排序示例中,我们处理的是过程代码,我们所要担心的是将in_order函数作为参数从一个函数传递到另一个函数。在本例中,in_order函数实际上被存储为一个成员变量,供以后使用。堆类的构造函数将比较函数作为其唯一参数,然后将该函数存储在成员变量in_order中。稍后,add()和remove()方法将调用存储的比较函数来完成它们的工作。
将函数存储为成员变量,然后稍后调用它,这与我们在第一个示例中看到的情况相比只是一个很小的变化。不幸的是,在这个例子的表面之下隐藏着一个更大的问题。当我们开始使用堆类时,这个问题就会显现出来。堆类是一个模板类——当你使用模板类时,你必须声明替换模板中占位符的实际类型。假设现在我们想要建立一个保存int类型的最小堆:
模板<typename T> bool降序(const T& a,const T& b){返回a < b;堆}< int, ? ?> H(降序);
模板堆类有第二个模板参数F,它代表我们希望在堆中使用的比较函数的类型。要正确声明模板堆对象,我们需要为这两个占位符提供具体的类型。T很简单,我们用int表示。F有点难以管理。
管理F的一种方法是定义一个类型,该类型是指向具有正确结构的函数的指针。有三种方法可以做到这一点。第一个方法是基于C语言的typedef语句。
模板<typename T> bool降序(const T& a,const T& b){返回a < b;} typedef bool (*comp)(int,int);堆< int, comp > H(降序);
typedef语句引入了一个新类型comp,它被声明为指向一个函数的指针,该函数接受两个int型作为形参,返回一个bool型。
typedef是一个C语言结构,不能很好地与现代c++概念(如模板)配合使用。c++ 11引入了一种更现代的方式来替代古老的类型定义,别名声明。
模板<typename T> bool降序(const T& a,const T& b){返回a < b;}模板<typename T> using comp = bool (*)(const T&,const T&);堆<int,比较<int> > H(降序<int>);
这里的别名声明从using开始,表示comp类型相当于一个指向函数的指针,该函数以两个T引用作为其形参并返回bool值。作为额外的好处,我们可以将别名声明作为模板,以使比较类型的定义更泛型。
处理C语言类型的最后一个选择是使用c++ 11中引入的新类型演绎工具之一,decltype构造:
Bool降序(int a,int b){返回a < b;} heap<int,decltype(降序)> H(降序);
decltype构造是一种指示编译器为我们找出某些东西的类型的方法。Decltype将具有所需类型的东西作为其参数,并强制编译器推断并使用该类型。
使用我上面演示的技术,我们离在c++中完全实现函数式编程范式越来越近了。现在可以将函数作为参数传递给其他函数,并且可以将函数作为成员变量存储在对象中以供以后使用。
为了更深入地研究函数式编程,我们接下来需要考虑c++中函数对象的概念。
让一个对象像它的函数一样工作的关键是让一个对象有可能复制使函数成为函数的那一点,那就是调用函数的能力。在上面的赢博体育例子中,我们最终都通过调用函数来使用我们传递的函数。例如,在快速排序示例中,我们看到in_order函数用于实现分区函数中的关键步骤。
模板<typename T,typename F> int partition(vector<T> &a, int first, int last, F in_order) {int p=first;交换((第一)、(值(一个,首先,持续1)));Int piv = a[first];for(int k=first+1;k<last;k++) {if(!in_order(piv,a[k])){//这是我们调用in_order p++;交换([k], [p]);}} swap(a[p],a[first]);返回p;}
让不是函数的东西像函数一样工作的关键是使用c++操作符重载的魔力。c++将函数调用视为操作,并使您可以重载函数调用操作符,其名称为operator()。
下面是一个实现operator()的类的例子。
类降序{公共:bool操作符()(int a,int b){返回a < b;}};降维;//声明降序类型的对象std::vector<int> V;快速排序(V, d);//将降序对象传递给V
这看起来并没有对之前的快速排序代码有多大的改进。这种技术真正发挥作用的地方是在堆的例子中。如果您还记得的话,堆示例中的主要困难是声明将要替换F占位符的东西的类型。由于根据定义,类是一种类型,因此可以在任何需要类型名的地方使用类名。这就得到了最小堆的最佳实现:
//声明一个函数对象类型模板<typename T> class降序{public: bool operator()(const T& a,const T& b){返回a < b;}};下行< int > d;//创建一个函数对象//将其传递给堆的构造函数heap<int,降序<int> > H(d);
最后,我们可以利用c++中对象提供的另一个有趣的优势:对象可以初始化自己。如果在对象类型的类中声明成员变量,有时可以跳过成员变量的初始化步骤,而依赖于对象成员变量使用其默认构造函数初始化自身的能力。下面是为了使用这个技巧而重写的堆类:
模板<typename T,类F>类堆{public: heap(){} //不需要在这里初始化in_order…bool isEmpty(){返回cells.empty();} T top(){返回cells.front();} void add(const T& item) {cells.push_back(item);Int node = last();while(node != 0 && !in_order(cells[node],cells[parent(node)])) {swapNodes(node,parent(node));Node = parent(节点);}} void remove() {cells[0] = cells.back();cells.pop_back ();Int节点= 0;While (true) {int winner = node;If (left(node) < (int) cells.size() && !in_order(cells[left(node)],cells[winner])) winner = left(node);If (right(node) < (int) cells.size() && !in_order(cells[right(node)],cells[winner])) winner = right(node);If (winner == node) break;else {swapNodes(node,winner);节点=赢家;}}} private: std::vector<T> cells;F in_order;/ /……我们依赖in_order来初始化自身。Int parent(Int n) {return (n-1)/2;} int left(int n){返回2*n+1;} int right(int n){返回2*n+2;} int last(){返回cells.size()-1;}无效swapNodes(int a,int b) {T temp = cells[a];Cells [a] = Cells [b];Cells [b] = temp;}};
这也简化了堆类的使用:
//声明一个函数对象类型模板<typename T> class降序{public: bool operator()(const T& a,const T& b){返回a < b;}};//在堆类中使用heap<int,降序<int> > H;
这种方法的一个缺点是,它将我们锁定为c类型使用函数对象类型。为了帮助记录这一事实,我在堆类中将F模板参数声明为class类型,而不是更通用的typename。
函数对象是现代c++编程实践的重要组成部分。因此,STL定义了许多有用的函数对象类。这些类都在
heap<int,std::less<int, >;
和一个最大堆
heap<int,std::greater<int> >;