本章节主要从以下几个方面做简要分析
- 一开始写一个函数,它可以找出vector内小于10的所有元素,然后函数过于死板,没有弹性。
- 接下来,我为函数加上了一个数值参数,让用户得以指定某个数值,以此和 vector中的元素做比较。
- 接着,我又加上一个新参数,一个函数指针,让用户得以指定比较方式(将类型 equality比较操作符赋予不同的意义)
- 然后,我引入function object的概念,使我们得以将某组行为传给函数,此法比函数指针的做法效率更高,并且简单地检阅了标准库提供的 function object。
- 最后,我将函数以 function template的方式重新实现,为了支持多种容器,我传入一对 iterator,标示出一组元素范围,为了支持多种元素类型,我将元素类型参数化,也将应用于元素上的 比较操作参数化,以便得以同时支持函数指针和 function object两种方式。
下面我们就按照上述步骤,一步一步学习C++泛型编程思想。
1:写一个简单函数:在vector容器内完成给定数值的搜索
// iterative version of find() in Section 3.1
// 给定一个储存整数的 vector,以及一个整数值,如果此值存在于 vector内,我们需要
// 返回一个指针指向改值,反之,则返回0,表示给定的值并不在 vector内
const int* find_ver1(const vector<int>& vec, int value)
{
for (int ix = 0; ix < vec.size(); ++ix)
{
if (vec[ix] == value) {
return &vec[ix];
}
}
return 0;
}
2:想办法让该函数 可以处理其他数据类型
// 在步骤1中,我们发现这个函数只能处理整数类型,下面我们有一个新任务,就是可以处理
// 任何数据类型(前提是该类型定义有 equality相等运算符)
// 其实这个就是:将泛型算法 find() 以 function template的形式呈现。
template <typename elemType>
elemType* find_ver2(const vector<elemType> &vec , const elemType& value)
{
for (int ix = 0; ix < vec.size(); ix++) {
if(vec[ix] == value) {
return &vec[ix];
}
}
return 0;
}
2.1 : find()函数能同时处理 array和 vector容器
- 现在我们仍然碰到这样一个问题:这个函数不能同时处理 array 和 vector容器,可能你脑海首先想到
- 是将此函数重载(overload),一份用来处理 vector 一份用来处理 array。
- 但是,我们并不想这样做。
那么除了通过重载的策略之外,我们还有其他什么策略呢?下面三个步骤或许可以引起你某些思考。
- 将 array的元素传入 find() ,而非指明该 array 。
- 将 vector的元素传入find() ,而不指明 vector。
- find()增加一个参数size ,表明 array 或者 vector容器的大小,或者传入另一个地址,指示读取操作的终点(我们将此值称为 标兵)。
那么函数原型就是:
解法一:直接传入容器大小 size
template <typename elemType>
elemType* find(const elemType* array ,int size , const elemType & value)
解法二:传入另一个地址,指示 array读取操作的终点(我们将此值称为 标兵)
template <typename elemType>
elemType* find(const elemType* array, const elemType* sentinel,const elemType &value);
很显然:上述修改方法:让 array 或者 vector从参数列表中彻底消失了,这解决了我们第一个问题。
另外一个问题就是:由于传递给 find() 的array 是以其第一个元素的指针传入,那么我们应该如何通过特定位置来进行元素的访问了?
下标(subscript)-----》对指针使用下标运算符
下标运算符访问 array每个元素,其实就是将 array的起始地址加上索引值后,产生某个元素的地址,然后该地址被提领(dereference)以返回元素。
注意:在指针的算术运算符中,会把“指针所指的类型”的大小考虑进去。
假设: array所存储的元素是:int 类型, array+2 就是在 array首地址(假设1000)的基础上加2,如果机器的int 长度是 4 byte ,那么这个指针运算符 (array+2) 的答案便是:1008
另一个实现就是:在每次循环迭代中,将 array的值递增 1
for(int ix = 0; ix < size ; ++ix, ++ array)
// 通过下标操作符,来进行指针运算
template<typename elemType>
find_ver3 (const elemType* array, int size, const elemType& value)
{
if(!array || size <1) {
return 0;
}
for(int ix = 0; ix< size; ++ix)
{
// we can apply subscript operator to pointer
if (array[ix] == value)
return &array[ix];
}
// value not found
return 0;
}
// 在每一次的循环中,将 array指针自增
template<typename elemType>
find_ver4(const elemType* array, int size, const elemType& value)
{
if(!array || size <1) return 0;
// ++array increments array by one element
for(int ix = 0; ix< size; ++ix, ++array){
// *array dereference the address
if (*array == value) return array;
}
return 0;
}
2.2 :find()函数的size参数通过指针替代
现在我们再来仔细思考一下:针对 size ,我们是否可以通过另外一个指针来替代了?
答案当然是可以的。这个版本,我们就来实现:通过指针来替代参数 size,扮演标兵的角色。
template <typename elemType>
elemType* find_ver5(const elemType* first, const elemType* last, const elemType& value)
{
if(!array || !last) return 0 ;
// 当 first不等于 last,就把value拿来和 first所指的元素进行比较
// 如果两者相等,便返回 first ,否则将 first递增1,另它指向下一个元素
for(; first != last; ++first)
{
if (*first == value) return first;
}
return 0;
}
总结:
很显然,我们实现了不论数组或者vector 存储的元素类型是什么,我们都可以访问数组中的每个元素。使用方法如下:
int ia[6] = {1,2,3,4,5,6};
string sa[4] = {"pooh", "piglet", "eeyore", "tigger"};
int* pi = find(ia, ia+6, ia[2])
string* ps = find(sa, sa+4, sa[1]);
2.3:find()函数处理:诸如list类型 (元素存储在非连续内存区域中)
很显然前面的2.1/2.2章节都是讲的,find()函数处理诸如 vector,array等元素被存储在连续内存区域中,那么针对 list类型(元素存储在非连续内存区域中),上述指针运算符的规则就不适用了。
题外话:
list也是一个容器,不同的是,list的元素是以一组指针相互链接(linked) :前向(forward)指向下一个(next)元素,后向指针(backward)指针指向上一个(preceding)元素。
思考:
既然底层指针运算符不适用 list类型,那么我们应该如何做,才能达到上述 vector/ array容器的效果了?
解决方案:
解决这个问题的方法是:在底层指针的行为之上提供一层抽象,取代程序原本的“指针直接操作方式”。我们可以把底层指针的处理通通放在此抽象层总,让用户无需面对指针的操作。
抽象层实现
那么如何实现了? 是的,我们需要一组对象可提供如内置运算符(++,* ,==, !=) 的一般运算符,我们可以利用C++类机制来达到目的。
我们先说一下结论:
泛型指针 iterator可以达到这个目的,我们首先来看一下:标准容器的 iterator 。
那么如何取得 iterator呢? 其实每个标准容器都提供一个名为 begin() 的操作函数,它可返回一个 iterator ,指向第一个元素,另一个 end()的操作函数,它返回的 iterator指向最后一个元素的下一个位置。不论如何定义 iterator对象 ,
以下都是对 iterator进行赋值(assign),比较(compare) ,递增(increment),提领(dereference)操作
for (iter = sevc.begin() ; iter != sevc.end(); ++ iter) {
cout << *iter << " ";
}
在说一下设计思想:
不管何种容器(vector或者list) 他们的 iterator迭代器,都会实现上述基本的运算,我们可以大胆猜测,这些基本的运算是通过 iterator 的 inline内联函数实现的,不同的是:
vector : 递增(increment)操作是在目前的地址上直接加上一个元素的大小
list : 递增(increment)操作 是:沿着list指针前进到下一个元素。
总结:
所以如果定义一个iterator ,我们首先要考虑这两个方面:
- 迭代对象(某个容器自身类型)的类型,这是决定如何访问下一个元素关键
- iterator所指的元素类型(容器存类型)这是决定iterator 提领操作的返回值
// 下面我来重新实现 find()函数,让它同时支持这两种形式:一对指针,或是一对指向某种容器的 iterator
template<typename IteratorType, typename elemType>
find_ver6(IteratorType first, IteratorType last, const elemType& value)
{
for(; first != last; ++first){
if(value == *first) return first;
}
// last 是最后一个元素的后面一个
return last;
}
// 现在我们来看看,array , vector, last该如何使用它
const int size = 8;
int ia {size} = {1,1,2,3,5,8,13,21};
// 以ia的 8个元素作为 list 和 vector的初值
vector<int> ivec(ia, ia+size);
list<int> ilist(ia, ia + size);
int *pia = find(ia, ia+size, 13);
if (pia != ia+ size) {
// 找到了 元素 13
.........
}
vector<int>:: iterator it;
it = find(ivec.begin(), ivec.end(), 13);
if (it != ivec.end()){
// 找到了元素13
.......
}
list<int>::iterator iter;
iter = find(ilist.begin(), ilist.end(), 13)
if(iter != ilist.end()){
// 找到了元素13
.......
}
其实优化到 find_ver6() 这一步,find函数本身就已经有了很大的通用性,远远超过我们的想象,但是故事还没结束。
思考:
find()函数实际上是:使用了底部元素所属类型的 equality(相等)运算符,如果底部元素所属类型没有提供这个 相等运算符,或者用户希望这个运算符具备其他的意义,那么这个find()函数的弹性就不够了,如何在这样的场景下增加 find()函数的弹性了?
答案:
- 就是传入一个指针,取代原本固定的相等运算符。
- 或者运用所谓的 function object(一种特殊的 class) 。
我们下一个努力目标就是将刚刚现有的 find() 版本演化为 泛型算法(比如:标准库提供的 find_if() 能够接受函数指针或 function object, 取代底部元素的 equality运算符,大大提升弹性。)
总共有60个泛型算法:
- 搜索算法(search algorithm):find() , count(), adjacent_find(), find_if() , count_if(), binary_search(), find_first_of()。
- 排序算法(sorting)以及次序整理(ordering)算法: merge(),partial_sort(),partition(),random_shuffile(),reverse(),rotate(),sort()。
- 复制(copy)、删除(delete)、替换(substiution)算法:copy(),remove(),remove_if(),replace(),replace_if(),swap(),unique()。
- 关系算法(relational):equal(),includes(),mismatch()。
- 生成(generation)与质变(mutation)算法:fill(),for_each(),generate(),transform()。
- 数值(numeric)算法:accmulate(),adjance_difference(),partial_sum(),inner_product()。
- 集合算法(set):set_union(),set_difference()。
所有容器的共通操作
下列为所有容器类(包括string类)的共通操作
- equality(==)和 inequality(!=) 运算符, 返回 true 或者 false。
- assigment(=)运算符,将某个容器复制给另一个容器。
- empty() 会在容器无任何元素时返回 true,否则返回 false。
- size() 返回容器内目前持有的元素个数。
- clear() 删除所有元素。
- 事实上每个容器都提供了 begin()和end()两个函数,他们分别返回第一个元素和最后一个元素的下一个位置的 iterator。
- begin() 返回一个 iterator指向容器的第一个元素
- end()返回一个 iterator指向容器的最后一个元素的下一个位置
- 通常我们在容器上进行的迭代操作都是始于 begin(),终于end(),所有容器都提供insert()用以插入元素,以及 erase()用以删除元素。
- insert()将单一或某个范围的元素插入容器内
- erase()将容器的单一元素或某个范围内的元素删除。
3:给find()函数加上一个新参数,一个函数指针,让用户得以指定比较方式(将类型 equality比较操作符赋予不同的意义)
具体来说就是:设计一个算法,既可以实现 less_than也可以实现 greater_than比较。
案例背景:
下面我们有一个新任务,用户给予一个整数 vector,我们必须返回一个新的 vector,其中内含原 vector之中所有小于10的所有数值,那么显而易见一个快速但缺乏弹性的解法就是:
vector<int> less_than_10(const vector<int>& vec)
{
vector<int> nvec;
for (int ix = 0; ix< vec.size(); ++ix) {
if (vec[ix] < 10) {
nvec.push_back(vec[ix]);
}
}
return nvec;
}
1:如果用户想要找出所有小于 11 的元素,应该如何做了 ?
我们就给这个函数增加一个参数,用户指定需要比较的数值。
2:如果用户想要指定不同的比较操作,比如:大于,小于,等于,那么如何才能将“比较操作”参数化呢 ?
用函数指针来取代 less_than比较运算符。
即我们可以在问题1的基础上加入 第三个参数 pred,用它来指定一个函数指针(它的参数列表有两个整数,返回值为bool)并将less_than函数用 filter()取代。
3:函数原型
vector<int> filter(const vector<int>& vec , int filter_value, bool (*pred)(int, int ));
站在用户使用的角度考虑,为了方便起见,我们同时定义了许多可传给 filter() 的关系比较函数:
bool less_than(int v1, int v2) {
return v1< v2 ? true : false
}
bool greates_than(int v1, int v2) {
return v1> v2 ? true: false
}
有了上述函数原型后,接下来的工作便是需要去实现filter()函数。
vector<int> filter_ver1(const vector<int>& vec, int filter_value, bool (*pred)(int, int))
{
vector<int> new_vec;
for (int ix= 0; ix < vec.size(); ++ix) {
// 调用pred所指函数
// 比较 vec[ix] 和 filter_value
if (pred(vec[ix], filter_value)) {
new_vec.push_back(vec[ix]);
}
}
return new_vec;
}
4:引入function object的概念(函数对象),来减少函数指针对函数的调用而带来的性能消耗。
4.1: 函数对象 function object替换函数指针
概念:
所谓 function object 是某种class实例对象,这类class对 function call 运算符做了重载操作,如此一来,function object就可以被当成一般函数来使用了。
其实 function object 实现了我们原本可能以独立函数加以定义的事物,但是何必如此了?
其实主要是为了效率,我们可以令 call 运算符称为 inline,从而消除“通过函数指针来调用函数”时需付出的额外代价。
总结:
1:在类中重载定义成员函数 operator() ,这个类就称为函数对象类。
2:可以作为函数的参数(例如传递给标准算法的谓词或比较函数)。
示例:
struct myFunctionObject {
// 重载 operator()操作符,使 myFunctionObject变成函数对象
int operator()(int a) {
return a;
}
} myobject;
int x = myObject(1);
struct Mygreater {
// 重载 operator() 操作符,并实现降序排序,使 Mygreater变成函数对象
bool operator()(const int& x , const int& y) {
return x>y;
}
}mygreater;
vector<int> vec;
// 将 mygreater作为函数参数传递给sort排序函数
sort (vec.begin(), vec.end(), mygreater());
扩展:
在标准库中事先定义 一组 function objcet,主要分为:算术运算(arithmetic) ,关系运算(relational)和逻辑运算(logical)这三大类。以下列表中的type在实际使用时会替换为内置类型或 class类型。
在使用时,需要事先包含头文件 <functional>
6个算术运算 plus<type>,minus<type>,negate<type>,
multiplies<type>,divides<type>,modules<type>
6个关系运算 less<type>,less_equal<type>,greater<type>,
greater_equal<type>,equal_to<type>,not_equal_to<type>
3个逻辑运算 logical_and<type>,logical_or<type>,logic_not<type>
4.2:使用泛型算法 transform()实现数据转换
现在我们在思考一个问题:
我们需要略加变化的方式来显示 Fibonacc 数列: 比如,没给元素和自身相加,和自身相乘,被加到对应的 Pell数列等等。做法之一就是 使用泛型算法 transform() 并搭配 plus<int> 和 multiplus<int> 。
transform()定义
function templat<algorithm> std:: transform
一元操作
template
<
class
InputIterator,
class
OutputIterator,
class
UnaryOperation>
OutputIterator transform(InputIterator first1, InputIterator last1,
OutputIterator result, UnaryOperation op);
将 op操作算法应用于[first1,last1]范围内的每个元素,并将每个操作返回的值存储在从 result开始的范围内。
二元操作
template
<
class
InputIterator1,
class
InputIterator2,class
OutputIterator,
class
BinaryOperation>
OutputIterator transform(InputIterator1 first1, InputIterator1 last1,
InputIterator2 first2, OutputIterator result,
BinaryOperation binary_op);
使用范围[first1, last1]中的每个元素作为第一个参数,并使用范围中从 first2开始的各个参数作为第二个参数来调用 binary_op 。每个调用返回的值将存储在从result开始的范围内。