《Essential C++》读书笔记(三)

时间:2022-11-25 19:27:26

第三章 泛型编程风格

泛型于容器内的元素。若是基于容器的泛型,一是处理特定容器内的一段元素时,只能是以该段元素新建一容器,然后处理该新建容器。二是处理数组时的缺陷。

3.1~3.2节

书中有一个例子,逐步阐述泛型算法的设计,该过程中我们可以了解到为何是基于元素的泛型。 1.首先设计一函数,在一容器中搜索特定的值,找到时返回指向该值的指针,反之则返回0,书中的代码是这样的:

int* find(const vector<int> &vec,int value){

    for(int ix=0;ix<vec.size();++ix)

        if(vec[ix]==value)

            return &vec[ix];//不过这里有一处错误,应该将那个const去掉,或是在返回型别前加个const

    return 0;

}



注意,此时迭代器还木有出来,所以用下标索引。可能你会有疑问,list不支持下标怎么办?不急,这还不是泛型呢。注意返回的是指针,不能用传值。

2.让该函数不仅可以处理整数,还可以处理其它型别(前提是定义了“==”操作符),由此引出函数模板。

template<typename elemType>

const elemType* find(const vector<elemType> &vec,const elemType &value){

    for(int ix=0;ix<vec.size();++ix)

        if(vec[ix]==value)

            return &vec[ix];

    return 0;

}


3.元素类型的问题解决了,但此时还远远未达到泛型的等级。根据书中的顺序,让我们考虑可以同时处理vector与array的函数。这里书中提出了两个细化了的问题:1.将array的元素传入find(),而非指明array。2.将vector内的元素传入find,而非指明vector。

问题1,此时我们应该来回想一下,数组被传入函数,及数组从函数返回的方式。我们都知道,单独使用数组名时(除sizeof),数组名自动转化为指向数组中第一个元素的指针。

当我们声明一个函数时:

int min(int array[24]){ .....}

意图很明显,想要传递一个数组,不过悲哀的是,对于24这个数字,编译器并不会理会,事实上该声明与以下两种声明方式相同:

int min(int array[ ]){.......}

int min(int *array){.....}

我好久前刚学函数的时候,看到这就很不理解,若是这样的话,那么函数怎么会知道数组的大小呢?渐渐的也就明白了,函数若是接受了那个24,则对于不同的数组都要重写一个代码相同,仅仅是形参不同的函数。

在该函数中,数组名失去了原来所具有的一个什么什么性,也就是说sizeof(array)/sizeof(array【0】),的结果不会是数组中元素的个数,而是依赖于你的计算机,是整型指针所占的内存/整型所占的内存。这样,该函数就不会知道传入数组的大小,这是很揪心的一件事,我们就要通过别的方式确定数组的大小。

第一个是明确指出元素的个数,如:

template<typename elemType>

const elemType* find(const elemType* array,int size,const elemType &value);
这种方法我是不喜欢,而且我相信作者也不会喜欢。

第二个便是指定一个哨兵:

template<typename elemType>

const elemType* find(const elemType* array,const elemType* sentinel,const elemType &value);
这标示了一个范围,现在开始有意思了。

对于第一个解决方法,书中给出了两个实例,一是使用下标运算,二是使用指针。对于代码,就不多说了,不过其两点有意思。

对于数组元素的访问,当取值array[2];时。看看是一个什么样的过程。已经知道数组名是指向第一个元素的指针。当array[2]时,过程实际上是将[ ]内外的数组名于数字相加 array+2 ,然后再取值*(array+2),此时,我突然就明白了为何数组中的元素下标从0开始。而array+2,并不是真的将2加到array的值上。指针的算术运算中会把“指针所指型别”的容量大小考虑进去。例如array指向int型,则array+2==array+2*sizeof(int)。

第二个解决方法是一对标记范围的指针

template<typename elemType>

const elemType* find(const elemType *first,const elemType *last,const elemType &value){

    if(!first||!last)

        return 0;

    for(;first!=last;++first)

        if(*first==value)

            return first;

    return 0;

}


这才是正经的解决之道啊。(注:此时突然就明白为啥c++的循环条件判断中惯用‘!=’而非‘<'了,由primer知,并非所用容器的迭代器都支持<操作符,因为其内存是随即分配的,并不能保证表头的指针就一定小于表尾的,所以‘!=’更通用,而这也决定了end()操作返回的是指向最后元素下一位置的指针)

这时还未提到迭代器,便不会有begin(),end()操作,所以作者采用了一个笨的方法来取vector的begin和end。(对一个空的vector索引时,会引发编译错误)

template<typename elemType>

inline const elemType* begin(const vector<elemType> &vec)

{return vec.empty()?0:&vec[0];}


template<typename elemType>

inline const elemType* end(const vector<elemType> &vec)

{return vec.empty()?0:&vec[vec.size()];}



此刻我所看的思想便是,不管咋样,就是不传容器。

4.这时作者跟进一步,要求函数可以支持各种容器,如list,都知道list内的元素不是连续存放的,所以以上的代码并不符合list,当然有个尤为巧妙的方法可以解决该问题,使得我们不必重载该函数。

书中的原话是“在底层指针的行为之上提供一层抽象化机制,取代程序原本的‘指针直接操作’方式”。

该思想的要点是构造一个类似指针的东西,对于指针的(++,--,==,!=)操作,该机制具相似的运算,可使list运用于上述代码中,迭代器便由此产生。

迭代器的定义本章节并未提到,因为涉及到了类,后面再深究。

将find的代码列出:

template<typename IteratorType,typename elemType>

IteratorType find(IteratorType first,IteratorType last,const elemType &value){

    for(;first!=last;++first)

        if(value==*first)

            return first;

    return last;

}


5.作者说故事尚未结束,我都要哭了!

该函数的弹性还不够,若底部元素所属型别并未提供==运算符,或如果用户希望赋予==不同的意义。

两种解法,一是传入一个函数指针,二是运用function object

用法后面再说。

3.3节 所有容器供通的操作

==,!=,=,empty(),size(),clear(),begin(),end(),insert(),erase() list是双向链表。 各个使用都很简单,了解就好。

3.5使用泛型算法

记得我曾经有个豪迈的打算,背下所用的泛型算法。而我的记忆力证明了我想法的荒唐可笑,太多了,真心记不住。用的时候现查就够了。重要的是想法、思路啊。


先写这些吧,刚刚三分之一,这章笔记有点多。