用C++实现设计模式---两个迭代器的传说

本文探讨了设计模式在面向对象系统中的应用,并详细对比了Gamma书中迭代器设计模式与C++标准模板库(STL)中迭代器的实现方式。通过对Builder模式的介绍和迭代器模式的不同实现方式的讨论,展示了面向对象设计与泛型编程的优势。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 简介

         在面向对象的系统中,设计模式是可被复用的数据结构成为了Gamma的《设计模式》书的中心主题。在这篇文章里,我将

解释设计模式的原理,同时我将比较在Gamma的书中描绘的迭代器设计模式和标准模板库中实现的迭代器。

 

设计模式的历史摘要

        在面向对象程序的早期,对象模型几乎被认为开创了代码设计和复用的新时代,随着科学技术的发展,现实比宣传的更复

杂,面向对象的主体是一个强大的工具,但是并不能导致好的软件设计,就像砖和灰并不能导致好的建筑设计一样。

        在面向对象的系统设计中你仅仅理解对象模型是不够的,对象是怎样被创建的?它们是怎样被初始化的?谁拥有对象实例

?如何访问对象实例?谁负责销毁对象?

        可以由设计模式来回答这些问题,设计模式描绘了对象复用的关系,类似的,设计模式是面向对象的程序,算法是结构化

的程序,结构化程序(顺序指令,条件,分支)的基本元素中不包含象链表,快速查询这样的结构的概念,所以他不是很有效的。

        但是明确的说什么是设计模式?

        考虑一个位图类 Bitmap,假设你向要这个 Bitmap 类支持多种图形文件格式(GIF, JPG, BMP, etc).一种方法是你为每一

个文件类型增加一个成员函数:

class Bitmap
{
public:
    void ReadBMP(const char *filename);
    void ReadJPG(const char *filename);
    void ReadGIF(const char *filename);
...
};

        这种方法有许多缺点,第一,随着对大量文件格式的支持,接口变的日益混乱,支持新的文件格式需要修改 Bitmap 类本

身,所以这是比较难而且不受欢迎的,假如你想要 Bitmap 支持一种应用程序文件格式,修改 Bitmap支持这种格式将污染接口。

    一种比较好的解决这种问题的方式是从类中分离文件的部分,这要通过定义第二个类:

class BitmapBuilder
{
public:
    virtual void ReadFromFile(Bitmap *,
        const char *filename) = 0;
};

 

    为了支持新的文件格式,你需要做的只是继承BitampBuilder类,并实现ReadFromFile()函数,JPGBuilder读JPG文件,

BMPBuilder读BMP文件,等等。现在你可以实现新的文件格式以及私有的文件格式而不用去修改 Bitmap接口。

    这种观念,也就是从一个初始化接口分离类的接口的概念,是 Builder 设计模式。

    固然,这种模式在Gamma和其他人附上"Builder"名字之前已经存在。但是,给一个复杂的概念加上如此简单的名字却帮助我们

为对象设计发展了一个强大的词汇。

    此外,研究 Builder的结构帮助我们认识到支持多种文件格式的问题可以通用到这样一类的问题,通过一个模板创建一个新的

词处理文档,或者为过关游戏产生一个随机的迷宫设计。

    为对象结构加上"Builder"这个结构类似于给O(nlog(n))divide-and-conquer查询算法增加一个"quicksort"的名字。每次你

用到它的时候都不用再去解释它。

 

设计模式和C++

    在它们的书里Gamma和其他人,把他们限制到了传统的面向对象的语言中,命名继承和虚函数,这些都是强大的工具,他们并

不是C++程序员所用到的工具,

     为了举例说明传统的面向对象语言和C++语言能达到殊途同归的原理,我将要比较Gamma的书中迭代器的设计模式的描述和标

准模板库提供的迭代器。

 

迭代器设计模式

           研究一个能实现多种容器的(列表,向量,二进制数,等等),容器从一个抽象的容器类继承。

 

template<class Item>
class Container
{
public:
    void AddItem(const Item &item) = 0;
    void RemoveItem (const Item &item) = 0;
};


    列表,向量,队列,树,等等都是从容器类继承,并且实现了Additem()和RemoveItem()方法。

    如果你想让客户端访问这些容器类的元素,但是你并不知道使用的是哪种容器,迭代器设计模式就象书中描述的一样,用继承

和虚函数提供一种解决这种问题的途径。方法是用第二种对象类型,去提供对容器中元素的访问。

    抽象的迭代器类通过定义客户端接口来访问容器的元素。

  

template <class Item>
class Iterator
{
public:
    virtual Item &CurrentItem()=0;
    virtual void GotoFirst()=0;
    virtual void GotoNext()=0;
    virtual bool IsDone()=0;
};

 

        CurrentItem()返回迭代器指向的当前的项,GotoFirst()设置迭代器指向容器的起始位置,GotoNext()让迭代器指向容器

内的下一项,如果迭代器指向了容器内元素的最后一项,IsDOne()返回真。

每个容器的具体的迭代器都是从Iterator继承。ListIterator,VectorIterator,TreeIterator,DequeIterator,等等,每一个都为

他们各自的容器实现了迭代器接口。

      

        既然客户端不用知道他们使用的具体容器,那么他们也不必去知道创建什么种类的迭代器,为了解决这个问题,我们给容

器增加一个CreateIterator()函数。

 

template <class Item>
class Container
{
public:
    Iterator<Item> *CreateIterator() = 0;
    ...
};

 

 

         具体的容器实现CreateIterator()并且返回正确的迭代器类型。

 

template <class Item>
class List : public Container<Item>
{
public:
    Iterator<Item> *CreateIterator()
    {
        return new ListIterator<Item>();
    }
...
};


        那么,我们怎么用这些迭代器呢?一个用来打印容器内的每一个元素的函数看起来就像下面这样.

 

template<class Item>
void PrintElements(Container<Item> *container)
{
    Iterator<Item> *it = container->CreateIterator();
    it->GotoFirst();
    while(!it->IsDone())
    {
        cout << it->CurrentItem() << endl;
        it->GotoNext();
    }
    delete it;
}


       这个方法是很有效的,它使你只需要写一次PrintElements()函数就能在任何的容器内使用,它也有一些缺点,首先,将来

每一个新增加的客户端必须从Container 继承 PrintElements(),第二,PrintElements()使三个虚函数在它内部循环调用。像打印

这种程序可能没太大的影响,但是大量的循环可能就会有问题。

       保护这个实现的普遍特征但同时减轻我们程序的结构和执行的负担会更令人满意的。

 

STL Iterators

 

       STL iterators 为它们的容器提供通用的顺序存取,但同时不用虚函数和继承,怎么样?C++实现了STL这个原理,事实上

,在C里面,迭代器早已经被编译到了语言里,它们叫做指针。

      想一想下面的C++函数:

template <class Item>
void PrintArray(Item *array, size_t len)
{
    Item *it = &array[0];
    while (it!=&array[len])
    {
        cout << *it << endl;
        ++it;
    }
}

 

        这个函数和前部分介绍的PrintElements()函数有相同的功效,"iterator"在数组的开始位置被增加直到延伸到数组的结

尾。只不过这个PrintElements()没有使用虚函数也没有要求用户从任何地方继承。

       仅有的问题是它只为内置的数组工作,这种方法不能为链表或二近制树工作,不是么?

       通常,面向对象的设计将机器底层的概念(就像指针和数组)提升到更高层次,就像容器和迭代器,STL iterator 却用了

相反的方法,STL iterators用重栽操作来实现高级别下就像内置指针一样的行为。++和--操作被用来按顺序移动到下一个或

前一个元素位置,用 *操作来实现数据访问。

        STL的PrintElements()版本看起来就像下面这样:

     

template<class Iterator>
void PrintElements(Iterator first, Iterator last)
{
    while(first != last)
    {
        cout << *first << endl;
        ++first;
    }
}

        这个方法除了支持++,!=和*操作数外跟Iterator是一样的。

        注意我们如何改变Printelements()接口,非常简单,在一个容器内替换途径,我们替换第一个和最后一个Iterator.这个

改变的意思是PrintElements()不仅仅支持STL容器,同样支持内置的C数组。事实上,PrintElements()将支持所有支持++,!=和*的

对象。

 

 

一句话关于模板

        对于在这儿介绍的基于继承的和基于STL版本的PrintElements()都用到模板,不同的是基于存储在容器内的类型和之后介

绍基于Iterator的形式。

        这个不同的结果是一个系统中用一个整型的向量,列表和队列只生成单一版本的PrintElements()而在基于继承的版本中

(Container<int>),将要产生三个版本,在STL的Case 里(三种迭代器类型,每一种一个).

        这是权衡类的尺寸和速度,代码膨胀不是最关心的,现在的系统已经有了几百兆甚至更多的内存。

        真正要关心的是模板,事实上,模板必须被声明为内联的,那写做过C或C++大的项目的人或许知道,在头文件中微小的改

变都能很影响编译时间。

 

 

 

 

 

 

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值