前言
学习的基本过程有理解→总结→应用这几个步骤.总结的目的大概是概括出大体的一种思路,一些必然和必不然,整理出"概念",并以概念指导应用
引入
尝试做一些和编程有关的概念总结.为了满足那个很朴素的想法:总结出概念,编程的思路就水到渠成地来了.---就好像学了单词就想写出好作文一样,当然谁都知道除非天赋惊人,否则这个过程是很困难的.不管怎么说做一点努力.当中肯定有不严谨的地方,看官不必较真
如何仿照着写程序
起因:学习有时候会遇到这种情况:没有源码理解不清楚,或者有源码又吃不透,但有几个现成的例子给参考.能不能仿照例子把代码写出来. 一半靠猜,一半依葫芦画瓢把问题解决了.
作者最近在看的STL函数符和算法,就差不多是这种情况.尝试从中整理一种思路
回顾STL算法和函数符
简单的说STL算法是非成员函数处理容器数据.函数符是在算法中做形参的函数指针和函数符.函数符分为生成器,一元函数,二元函数,谓词,二元谓词这几类.
一元函数的编写
<C++ Prime Plus> 6th Edition(以下称"本书")P710有个例子:4个参数的transform()函数调用:
//原书代码
transform(gr8.begin(),gr8.end(),out,sqrt);
函数逻辑:把gr8对象所有元素(gr8.begin()和gr8.end()标记值区间),开方(sqrt处理)后,交给迭代器out(类型ostream_iterator<double,char>),输出到屏幕.
问题就是前面提到的:没有transform()的原型,只有个例子,应该怎么看?
sqrt是代入的参数,是一个函数名,因此原型是函数指针,并且是一元函数(书上有写:最后一个参数是一个函数符).
//sqrt原型double sqrt(double a); //a可以接收int类型参数
对应函数指针是:
//伪代码
double (*unary_function)(double val); //只有一个参数的一元函数指针
最关键的一点,也是transform()函数的内在联系:val值已确定,类型为double,从*gr8.begin()开始到*gr8.end()前面的一个值,也就是整个容器内的数据依次传入val.同时返回值类型也已确定,是double型.
如果要仿写代码,现在需求改为gr8的每个值加1,那么先定义一个addOne函数,如下:
//定义被函数指针指向的函数
double addOne(double a){return a+1;
}
//transform函数调用
transform(gr8.begin(),gr8.end(),out,addOne);
但函数指针是在val类型已确定的情况下编写的,适应不了泛型的要求,所以应该开发一个模板类实现的版本,以适应泛型,下面的二元函数有泛型版本.
二元函数的编写
接下来是5个参数的transform()函数
//本书代码
double add(double x,double y){return x+y;}
...
transform(gr8.begin(),gr8.end(),m8.begin(),out,add);
函数逻辑:把gr8对象所有元素(gr8.begin()和gr8.end()标记值区间)和m8.begin()标记开始位置的容器元素(*m8.begin()取值)依次相加(*gr8.begin()和*m8.begin()相加,然后各自移动一个位置,add处理)后,交给迭代器out(类型ostream_iterator<double,char>),输出到屏幕.
对应函数指针是:
//伪代码
double (*binary_function)(double val1,double val2); //两个参数的二元函数指针
已定义好的add代入tranform()函数的形参中,完成函数调用.
==========下面是泛型版本.
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;template<class T>
class myplus { //模板里只一个方法
public:T operator()(const T& t1, const T& t2);
};template<class T> //满足二元函数指针要求的函数原型
T myplus<T>::operator()(const T& t1, const T& t2) { //函数形参传入值来自transofrom()的其他两个参数return t1 + t2;
}
测试代码:
int main(void) {myplus<double> addObject; //生成对象double y = addObject(2.2, 3.4); //函数调用cout << "加法结果是:" << y << endl;double z=myplus<double>() (3.0, 5.0); //匿名对象的函数调用cout << "加法结果是:" << z << endl;vector<double> ar5{ 1.0,2.0,3.0,4.0,5.0 }; //vector对象声明vector<double> br5{ 6.0,7.0,8.0,9.0,10.0 };ostream_iterator<double, char> out(cout, " "); //输出流迭代器声明transform(ar5.begin(), ar5.end(), br5.begin(),out, myplus<double>()); //调用transform()cout << endl;transform(ar5.begin(), ar5.end(), br5.begin(),out, addObject); //调用transform()
}
输出结果:
加法结果是:5.6
加法结果是:8
7 9 11 13 15
7 9 11 13 15
注意:这种写法是否有问题笔者不确定,但是结果没错
再梳理一下应用步骤:
1>transform()函数需要一个二元函数,并且把两个值传给函数形参.----固定的由transform()定义
2>可以根据需要开发一个满足二元函数指针的函数,非泛型版本,形参类型和迭代器对象一致.调用transform()时,把函数名作为实参传入,在transform()最后一个参数,如add.
3>为了更好满足需要,开发一个泛型版本.
1)先声明一个模板类,属性可有可无,根据需要而定.
2)重载operator()().因为是二元函数,所以定义为
T operator()(const T& t1, const T& t2); //二元函数的原型
这也是固定的,不这样写不满足二元函数的要求,不能把他传给transform().
3)传给transform()的时候,写法是"模板类名<容器数据类型>()",这里传进去的是myplus<double>().根据匿名表达式调用模板类方法
double z=myplus<double>() (3.0, 5.0); //二元函数的调用
对比一下其他传函数名的写法
//伪代码
typedef void (*pf)(int a); //函数指针pf定义
void fun(int a); //符合pf的函数fun的原型//另一个函数定义,参数有函数指针pf
void otherfun(pf pff);
otherfun(fun); //传入fun
fun的定义后面有个(int a),去掉括号里面部分,传给otherfun里的函数指针类型时用fun;
myplus模板函数把(3.0,5.0)去掉,传入transoform()函数时,用myplus<double>()
//二元函数的传入
transform(ar5.begin(), ar5.end(), br5.begin(),out, myplus<double>());
transform(ar5.begin(), ar5.end(), br5.begin(),out, addObject);
所以为什么叫函数对象?对象名就是函数名,可以作为函数符的实参传入
myplus函数符对应预定义函数符plus. 其他函数符与之类似
现在可以编写用于transform()的二元函数,分析的目的就是编写自定义函数符
==========以上是二元函数泛型版本编写全过程,一元函数与之类似
谓词的编写
本书P708有个例子,谓词表示的函数指针如下:
//伪代码,谓词表示的函数指针
bool (*pf)(paraType pt);
和前面一样,形参pt的值已确定,整个容器内的数据依次传入pt.list模板有一个将谓词作为参数的remove_if( )成员,该函数将谓词应用于区间中的每个元素,如果谓词返回true,则删除这些元素
bool tooBig(int n){return n>100;}
list<int> scores;
scores.remove_if(tooBig);
注意:这里的谓词是容器方法remove_if的参数,只能由list容器对象调用.他不是STL函数(算法)的参数.
tooBig作为谓词使用,有一个问题:逻辑表达不完整,只能固定与数字100做比较.表达完整逻辑还需要一个参数,泛型版本的tooBig可以解决这个问题.
==========以下泛型版本的谓词
/*已测试,谓词的表达*/
#include<iostream>
#include<list>
#include<algorithm>
using namespace std;template<class T>
class TooBig { T val;
public:TooBig(const T& v); bool operator()(const T& t1);
};template<class T>
TooBig<T>::TooBig(const T& v):val(v){} //构造函数,传入比较数据template<class T> //满足谓词要求的函数原型,当list元素值大于传入值时返回true
bool TooBig<T>::operator()(const T& t1) { //函数形参传入值来自list的元素的值return t1>val;
}
测试代码:
int main(void) {TooBig<double> tg(35.0); //先生成对象bool is_yes = tg(20.0); //函数调用cout << "成立吗?" << is_yes << endl;is_yes = TooBig<double>(25.0)(10.0); //匿名对象调用cout << "成立吗?" << is_yes << endl; list<double> demo1; //生成list<double>对象demo1.push_back(10.0);demo1.push_back(20.0);demo1.push_back(30.0);demo1.push_back(40.0);demo1.push_back(50.0);list<double> demo2(demo1); //生成数据副本demo1.remove_if(tg); //使用函数符tg,删除35.0以上的数据for (auto pr = demo1.begin(); pr != demo1.end(); pr++) { //显示删除后的数据 cout << *pr << endl;}cout << "====================================" << endl;demo2.remove_if(TooBig<double>(25.0)); //使用匿名函数符,删除25.0以上的数据for (auto pr = demo2.begin(); pr != demo2.end(); pr++) { //显示删除后的数据 cout << *pr << endl;}
}
==========以上泛型版本的谓词
函数符的特点
不管是哪一种函数符,都是把容器元素做实参传入.
函数指针的小结:
前面说到过函数指针是"逻辑占位",代表"多段逻辑",从函数符可以看出,他还有"数据和逻辑分离"的作用.
对于外层函数来说,他把数据交给函数指针,规定了返回值类型,以及把函数指针返回的值用于下一个逻辑中.而具体的数据处理,交给函数指针来完成.
小结
在没有函数原型的情况下仿照例子写程序 .仿写程序还是有一点难度的,因为要考虑的因素更多,还要做一些假设,不能保证想得东西完全正确.所以个人认为尽量还是把知识点理解透彻,少走弯路.