《C++程序设计》课件第11章_第1页
《C++程序设计》课件第11章_第2页
《C++程序设计》课件第11章_第3页
《C++程序设计》课件第11章_第4页
《C++程序设计》课件第11章_第5页
已阅读5页,还剩120页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

第11章模板11.1模板概述11.2模板函数

11.3模板类11.4模板的多态

11.5高级编程本章小结习题

11.1模板概述模板是C++在20世纪90年代引进的一个新概念,原本是为了对容器类(containerclasses)的支持,但是现在模板产生的效果已经远非当初所想象。简单地讲,模板就是一种参数化(parameterized)的类或函数,也就是类的形态(成员、方法、布局等)或者函数的形态(参数、返回值等)可以被参数改变。这里所说的参数,不只是我们传统函数中所说的数值形式的参数,还可以是一种类型(实际上我们更多地会注意到使用类型作为参数,而往往忽略使用数值作为参数的情况)。下面举例说明模板的概念。例如,在C语言中,如果我们要比较两个数的大小,常常会定义两个宏:

#definemin(a,b)((a)>(b)?(b):(a))

#definemax(a,b)((a)>(b)?(a):(b))这样就可以在程序中通过宏展开得到所求的结果:

returnmin(10,4);或

returnmin(5.3,18.6);

这两个宏非常好用,但是在C++中,它们并不像在C中那样受欢迎。宏因为没有类型检查而不安全。例如,如果将代码写为min(a++,b--); ,则结果非你所愿,因此,宏在C++中被inline函数替代。如果将min/max改为函数,这个函数又通常不能处理形参的类型以外的其他类型的参数。例如min()声明为:

intmin(inta,intb);则它显然不能处理float类型的参数,但是原来的宏却可以实现。也可以通过重载不同类型的min()函数来实现。实际上,C++对于这类可以抽象的算法提供了更好的办法,即模板。下面是一个模板函数的例子:

template<classT>constT&min(constT&t1,constT&t2){

returnt1>t2?t2:t1;

}有了模板之后,可以像在C语言中使用min宏一样来使用这个模板。例如:

returnmin(10,4);

returnmin(5.3,18.6)

这样就获得了一个类型安全的而又可以支持任意类型的min函数,它显然比min宏更实用。

当然,上面这个例子只涉及了模板的一个方面,模板的作用远不只是用来替代宏。实际上,模板是泛化编程(GenericProgramming)的基础。所谓泛化编程,就是对抽象的算法的编程。泛化是指一段代码可以广泛地适用于不同的数据类型,例如上面提到的min算法。11.2模板函数11.2.1模板函数的重载和普通函数一样,模板函数也可以进行重载,即相同的模板函数名称可以具有不同的函数定义。因此,当使用函数名称进行函数调用时,C++编译器就必须决定究竟要调用哪个候选函数。本节主要讨论有关模板的重载问题。程序11.1描述了如何重载模板函数。

【程序11.1】

//求两个int类型值中的最小值

inlineintconst&min(intconst&a,intconst&b)

{

returna<b?a:b;

}

//求两个任意类型值中的最小值

template<typenameT>

inlineTconst&min(Tconst&a,Tconst&b)

{

returna<b?a:b;

}

//求三个任意类型值中的最小值

template<typenameT>

inlineTconst&min(Tconst&a,Tconst&b,Tconst&c)

{

returnmin(min(a,b),c);

}

intmain()

{

min(2,13,45); //调用三个参数的模板函数

min(2.0,13.0); //调用min<double>(通过实参演绎)

min('a','b'); //调用min<char>(通过实参演绎)

min(2,13); //调用有两个int类型参数的普通函数

min<>(2,13);//调用min<int>(通过实参演绎)

min<double>(2,13);//调用min<double>(没有实参演绎)

min('a',13.5); //调用int重载的普通函数

return0;

}如程序11.1所示,一个模板函数可以和一个同名的普通函数同时存在,而且模板函数还可以被实例化为这个普通函数。对于模板函数和同名的普通函数,如果其他条件都相同,重载解析时先搜索普通函数,如果找到就解除查找,并匹配找到的函数,而不会从该模板产生出一个实例。第4个调用就是这样的:

min(2,13)//两个int类型参数,与普通函数很匹配如果模板可以产生一个具有更好匹配的函数实例,那么将选择模板。第2次和第3次调用就是例子:

min(2.0,13.0) //调用min<double>(通过实参演绎)

min('a','b'); //调用min<char>(通过实参演绎)还可以显式地指定一个空的模板实参列表,这个语法告诉编译器:只有模板才能匹配这个调用,而且所有的模板参数都应该根据调用实参演绎出来:

min<>(2,13) //调用min<int>(通过实参演绎)模板不允许自动类型转换,但普通函数可以进行自动类型转换,所以最后一个调用将使用普通函数('a',13.5都被转换为int类型):

min('a',13.5) //对于不同类型的参数,只有普通函数允许转换程序11.2是在指针和普通的C字符串重载上面求最小值的模板。

【程序11.2】

#include<iostream>

#include<cstring>

#include<string>

//求两个任意类型值中的最小值

template<typenameT>

inlineTconst&min(Tconst&a,Tconst&b)

{

returna<b?a:b;

}

//求两个指针所指对象中的最小值

template<typenameT>

inlineT*const&min(T*const&a,T*const&b)

{

return*a<*b?a:b;

}

//求两个C字符串中的最小值

inlinecharconst*const&min(charconst*const&a,charconst*const&b)

{

returnstd::strcmp(a,b)<0?a:b;

}

intmain()

{

inta=2;

intb=13;

::min(a,b); //min()求两个int类型中的最小值

std::strings="what’s";

std::stringt="up";

::min(s,t); //min()求两个std::string类型中的最小值

int*p1=&b;

int*p2=&a;

::min(p1,p2);//min()求两个指针所指对象中的最小值

charconst*s1="Lyle";

charconst*s2="Dansy";

::min(s1,s2);//min()求两个C字符串中的最小值

return0;

}注意,在所有重载的实现里,我们都是通过引用来传递实参的。一般而言,在重载模板函数的时候,应该把改变限制在下面两种情况:改变参数的数目或者直接指定模板参数。否则就可能出现错误的结果。例如,对于原来使用的传引用的min模板,用C-string类型进行重载;但对于现在(程序11.3)基于C-string的min函数,是通过传值来传递参数,那么就不能使用三个参数的min版本来对三个C-string求最小值,如下例所示。

【程序11.3】

#include<iostream>

#include<cstring>

#include<string>

//求两个任意类型值中的最小值(通过传引用调用)

template<typenameT>

inlineTconst&min(Tconst&a,Tconst&b)

{

returna<b?a:b;

}

//求两个C字符串中的最小值(通过传值调用)

inlinecharconst*min(charconst*a,charconst*b)

{

returnstd::strcmp(a,b)<0?a:b;

}

//求三个任意类型值中的最小值(通过传引用调用)

template<typenameT>

inlineTconst&min(Tconst&a,Tconst&b,Tconst&c)

{

returnmin(min(a,b),c); //如果min(a,b)使用传值调用,则这里会产生错误

}

intmain()

{

::min(2,13,45); //正确

constchar*s1="what";

constchar*s2="is";

constchar*s3="up";

::min(s1,s2,s3); //错误

return0;

}为什么我们对三个C-strings调用min()函数就出现错误了呢?这是因为语句:

returnmin(min(a,b),c);产生了一个错误。对于C-string而言,这里的min(a,b)产生了一个新的临时局部变量,该变量有可能会被外面的min()函数以传引用的方式返回,而这将导致传回无效的引用。对于复杂的重载解析规则而言,这只是产生非预期行为的代码中的例子之一。例如,当调用重载函数时,调用结果就可能与该重载函数在此时是否可见有关,但也可能无关。事实上,定义一个具有三个参数的min()版本,而且直到该定义处还没有看到一个具有两个int参数的重载min()版本的声明,那么这个具有三个int实参的max()调用将会使用具有两个参数的模板,而不会使用基于int的重载版本min(),如下所示:

//求两个任意类型值中的最小值

template<typenameT>

inlineTconst&min(Tconst&a,Tconst&b)

{

returna<b?a:b;

}

//求三个任意类型值中的最小值

template<typenameT>

inlineTconst&min(Tconst&a,Tconst&b,Tconst&c)

{

returnmin(min(a,b),c); //这里的min(a,b)调用上面模板产生的模板实例,

} //因为基于int类型的重载函数声明得太晚

//求两个int类型值中的最小值

inlineintconst&min(intconst&a,intconst&b)

{

returna<b?a:b;

}应该牢记这条规则:函数的所有重载版本的声明都应该位于该函数被调用的位置之前。11.2.2模板函数的语法本节简述声明模板和定义模板的方法以及模板函数常见的语法方面的问题。template<>是模板的标志,在<>中是模板的参数部分。参数可以是类型,也可以是数值。例如:

template<classT,Tt>

classTemp{

public:

...

voidprint(){cout<<t<<endl;}

private:

Tt_;

};在这个声明中,第一个参数是一个类型,第二个参数是一个数值。这里的数值必须是一个常量。例如针对上面的声明:

Temp<int,10>temp; //合法

inti=10;

Temp<int,i>temp; //不合法

constintj=10;

Temp<int,j>temp; //合法

参数也可以有默认值:

template<classT,classC=char>...

默认值的规则与函数的默认值一样,如果一个参数有默认值,则其后的每个参数都必须有默认值。

参数的名字在整个模板的作用域内有效,类型参数可以作为作用域内变量的类型(例如上例中的Tt_),数值型参数可以参与计算,就像使用一个普通常数一样(例如上例中的cout<<t<<endl)。模板有个值得注意的地方,就是它的声明方式。与普通类一样,有声明为inline的,或者虽然没有声明为inline,但是函数体在类声明中的才是inline函数。包含了模板类的头文件中除了类接口之外,到处充斥着实现代码,对用户来说是不可读的。为了能像传统头文件一样,让用户尽量只看到接口,而不用看到实现方法,一般会将所有的方法实现部分放在一个后缀为 .i或者 .inl的文件中,然后在模板类的头文件中包含这个 .i或者 .inl文件。例如:

//temp.h开始

template<classT>classTemp

{

public:

voidprint();

};

#include“temp.inl”

//temp.h结束

//temp.in1开始

template<classT>voidTemp<T>::print()

{

...

}

//temp.in1结束

通过这样的变通,既满足了语法的要求,也让头文件更加易读。模板函数也是一样。普通的类中,也可以有模板方法,例如:

classA

{

public:

template<classT>voidprint(constT&t){...}

voiddummy();

};

对于模板方法的要求与模板类的方法一样,也需要与类声明出现在一起。而这个类的其他方法,例如dummy()则没有这样的要求。对模板的语法检查有一部分被延迟到使用时刻(类被定义,或者函数被调用),而不是像普通的类或者函数在被编译器读到的时候就会进行语法检查。因此,如果一个模板没有被使用,那么即使它包含了语法的错误,也会被编译器忽略。与语法检查相关的另一个问题是可以在模板中做一些假设。例如:

template<classT>classTemp

{

public:

Temp(constT&t):t(t){}

voidprint(){t.print();}

private:

Tt_;

};

在这个模板中,假设类型T是一个类,并且有一个print()方法(t.print())。在11.1节中的min模板其实也作了同样的假设,即假设T重载了操作符 '>'。因为语法检查被延迟,编译器看到这个模板的时候,并不去关心类型T是否有print()方法,这些假设在模板被使用的时候才被编译器检查。只要定义中给出的类型满足假设,就可以通过编译。11.2.3模板函数的使用

下面是一个返回两个值中最大者的函数模板:

template<typenameT>

inlineTconst&max(Tconst&a,Tconst&b)

{

//如果a<b,返回b,否则返回a

returna<b?b:a;

}

这个模板定义了一个“返回两个值中最大者”的函数族,这两个值是通过函数参数a和b传递给函数模板的;而参数的类型还没有确定,用模板参数T来代替。如例子中所示,模板参数必须用如下形式的语法来声明:

template<comma-separated-list-of-parameters>程序11.4展示了如何使用max()函数模板。

【程序11.4】

#include<iostream>

#include<string>

#include"max.h"

usingstd::cout;

usingstd::endl;

usingstd::string;

intmain()

{

inti=42;

cout<<"max(7,i):"<<::max(7,i)<<endl;

oublef1=3.4;

doublef2=-6.7;

cout<<"max(f1,f2):"<<::max(f1,f2)<<endl;

strings1="mathematics";

strings2="math";

cout<<"max(s1,s2):"<<::max(s1,s2)<<endl;

return0;

}在上面的程序里,max()被调用了3次,而且调用实参每次都不相同:一次用两个int,一次用两个double,一次用两个std::string。每一次都计算出两个实参的最大值,而调用结果是产生如下的程序输出:可以看到max()模板每次调用的前面都有域限定符 ::,这是为了确认调用的是全局名字空间中的max()。由于标准库中也有一个std::max()模板,在某些情况下也可以被使用,因此有时还会产生二义性。通常而言,并不是把模板编译成一个可以处理任何类型的单一实体,而是对于实例化模板参数的每种类型,都从模板产生出一个不同的实体。因此,针对三种类型中的每一种,max()都被编译了一次。例如,max()的第一次调用:

inti=42;

…max(7,i)…使用了以int作为模板参数T的函数模板。因此,它具有调用如下代码的语义:

inlineintconst&max(intconst&a,intconst&b)

{

//如果a<b返回b,否则返回a

returna<b?b:a;

}这种用具体类型代替模板参数的过程叫做实例化(instantiation)。它产生了一个模板的实例。在面向对象的程序设计里,实例和实例化这两个概念通常会被用于不同的场合——但都是针对一个类的具体对象。由于本书叙述的是关于模板的内容,因此在未做特别指定的情况下,我们所说的实例都指的是模板的实例。只要使用函数模板,(编译器)就会自动地引发这样一个实例化过程。因此程序员并不需要额外请求模板的实例化。类似地,max()的其他调用也将为double和std::string实例化max模板,就像具有如下单独声明和实现一样:

constdouble&max(doubleconst&,doubleconst&);

conststd::string&max(std::stringconst&,std::stringconst&);如果试图基于一个不支持模板内部所使用操作的类型实例化一个模板,那么将会导致一个编译期错误,例如:

std::complex<float>c1,c2; //不支持operator<

max(c1,c2); //编译时出错于是,可以得出一个结论:模板被编译了两次,分别发生在实例化之前和实例化期间。在实例化之前,先检查模板代码本身,查看语法是否正确,在这里会发现错误的语法,如遗漏分号等;在实例化期间,检查模板代码,查看是否所有的调用都有效,在这里会发现无效的调用,如该实例化类型不支持某些函数调用等。这给实际的模板处理带来了一个很重要的问题:当使用函数模板,并且引发模板实例化的时候,编译器(在某个时刻)需要查看模板的定义。这就不同于普通函数中编译和链接之间的区别,因为对于普通函数而言,只需要该函数的声明(即不需要定义),就可以顺利地通过编译。11.3模板类11.3.1模板类的创建及使用模板能直接支持通用型程序设计,即采用类型作为参数的程序设计。C++中的模板机制能在定义函数和类时以类型作为参数。容器类就是一个具有这种特性的典型例子。它通常被用于管理某种特定类型的元素,使用模板类即可实现容器类,而不需要确定容器中元素的类型。本节介绍一下简单地使用Stack作为模板类的例子。与函数模板一样,在同一个头文件中声明和定义模板类Stack<>如下:

#include<list>

#include<stdexcept>

#include<iostream>

usingnamespacestd;

template<classT>

classStack

{

private:

list<T>container; //对象集合

public:

voidpush(Tconst&); //压栈

voidpop();

//出栈

Ttop()const; //返回栈顶对象

boolempty()const; //判断栈是否为空

};

template<typenameT>

voidStack<T>::push(Tconst&obj)

{

container.push_back(obj); //把obj追加到容器末尾

}

template<typenameT>

voidStack<T>::pop()

{

if(container.empty())

{

throwout_of_range("Stack<>::pop():emptystack");

}

container.pop_back(); //删除容器末尾对象

}

template<typenameT>

TStack<T>::top()const

{

if(elems.empty()) {

throwstd::out_of_range("Stack<>::top():emptystack");

}

returncontainer.back(); //返回容器末尾对象

}

template<typenameT>

voidStack<T>::empty()

{

container.empty(); //判断容器是否为空

}上面的程序中,我们使用标准库的模板类list<>来实现模板类Stack<>。模板类的声明和函数模板的声明很相似:在声明之前,先声明作为类型参数的标识符,然后继续使用T作为类型参数的标识符。如下所示:

template<typenameT>

classStack

{

};这里,我们可以使用关键字typename来代替class:

template<typenameT>

classStack

{

};在模板类的内部,T可以像其他任何类型一样,用于声明成员变量和成员函数的返回值类型。下面的例子中,声明list的元素类型为T类型的,声明push()是一个接收常量T引用类型参数的成员函数,声明top()是返回类型为T类型的成员函数:

template<classT>

classStack

{

private:

list<T>container; //容器

public:

Stack();

//构造函数

voidpush(Tconst&);

//压栈

voidpop(); //出栈

Ttop()const; //返回栈顶对象

};这个模板类的类型是Stack<T>,其中T是模板参数。因此,当在声明中需要使用该类的类型时,必须使用Stack<T>。例如,如果要声明自己实现的拷贝构造函数和复制运算符,则应这样编写:

template<classT>

classStack

{

Stack(Stack<T>const&); //拷贝构造函数

Stack<T>&operator=(Stack<T>const&); //赋值运算符

};但是,要使用类名而不是类的类型时,就应该只用Stack。譬如,当指定类的名称﹑类的构造函数﹑析构函数时,就应该使用Stack。为了定义模板类的成员函数,必须指定该成员函数是一个函数模板,而且还需要使用这个模板类的完整类型限定符。因此,类型Stack<T>的成员函数push()的实现如下:

template<classT>

voidStack<T>::push(Tconst&obj)

{

container.push_back(obj); //把对象obj追加到容器末尾

}在上面的例子中,调用了对应list的push_back()方法,它把传入对象附加到该list的末端。为了使用模板类对象,必须显式地指定模板实参。下面的例子展示了如何使用模板类Stack<>:

#include<iostream>

#include<string>

#include<cstdlib>

#include"stack1.h"

usingnamespacestd;

intmain()

{

try {

Stack<int>intStack; //容纳int类型对象的栈

Stack<string>stringStack;//容纳string类型对象的栈

//使用int类型的栈

intStack.push(7);

cout<<intStack.top()<<std::endl;

//使用string类型的栈

stringStack.push("hello");

cout<<stringStack.top()<<endl;

stringStack.pop();

stringStack.pop();

}

catch(std::exceptionconst&ex)

{

cerr<<"Exception:"<<ex.what()<<endl;

eturnEXIT_FAILURE;

//产生异常时,返回错误的程序状态

}

}通过声明类型Stack<int>,在模板类内部就可以用int实例化T。因此,intStack是一个创建自Stack<int>的对象,它的元素存储于list,且类型为int,对于所有被调用的成员函数,都会实例化出基于int类型的函数代码。类似地,如果声明和使用Stack<std :: string>,将会创建一个Stack<string>对象,它的元素存储于list,且类型为string;而对于所有被调用的成员函数,也会实例化出基于string的函数代码。在上面的例子中,缺省构造函数﹑push()和top()都被实例化了一个int版本和一个string版本,而pop()仅被实例化了一个string版本。另一方面,如果模板类中含有静态成员,那么用来实例化的每种类型都会实例化这些静态成员。可以与其他任何类型一样地使用实例化后的模板类类型,只要它支持所调用的操作即可。模板与类继承都可以让代码重用,都是对具体问题的抽象过程。但是它们抽象的侧重点不同,模板侧重于对算法的抽象,也就是说如果在解决一个问题的时候,需要固定的step1、step2…,就可以抽象为模板。而如果一个问题域中有很多相同的操作,但是这些操作并不能组成一个固定的序列,就可以用类继承来解决问题。模板类的运用方式,更多情况是直接使用,而不是作为基类。例如在使用STL提供的模板时,通常直接使用,而不需要从模板库中提供的模板再派生自己的类。但这不是绝对的,也是模板与类继承之间的一点区别。模板虽然也是抽象的,但是它往往不需要通过派生来具体化。11.3.2迭代器的创建及使用

迭代器提供对一个容器中的对象的访问方法,并且定义了容器中对象的范围。迭代器就如同一个指针。事实上,C++ 的指针也是一种迭代器。但是,迭代器不仅仅是指针,因此不能认为它们一定具有地址值。例如,一个数组索引也可以认为是一种迭代器。迭代器有各种不同的创建方法。程序可能把迭代器作为一个变量创建。一个STL容器类可能为了使用一个特定类型的数据而创建一个迭代器。作为指针,必须能够使用 * 操作符类获取数据。还可以使用其他数学操作符如 ++。典型的 ++ 操作符用来递增迭代器,以访问容器中的下一个对象。如果迭代器到达了容器中最后一个元素的后面,则迭代器将变成past-the-end值。使用一个past-the-end值的指针来访问对象是非法的,就好像使用NULL作为初始化的指针一样。提示:STL不保证从一个迭代器可以抵达另一个迭代器。例如,当对一个集合中的对象排序时,如果在不同的结构中指定了两个迭代器,而第二个迭代器无法从第一个迭代器抵达,此时程序注定要失败。这是STL灵活性的一个代价。STL不保证检测毫无道理的错误。

1.迭代器的类型对于STL数据结构和算法,可以使用以下五种迭代器:

(1) InputIterators(输入迭代器):提供对数据的只读访问。

(2) OutputIterators(输出迭代器):提供对数据的只写访问。

(3) ForwardIterators(前向迭代器):提供读/写操作,并能向前推进迭代器。

(4) Bidirectionaliterators(双向迭代器):提供读/写操作,并能向前和向后操作。

(5)RandomAccessIterators(随机访问迭代器):提供读/写操作,并能在数据中随机移动。尽管各种不同的STL实现细节方面有所不同,但仍可以将上面的迭代器想象为一种类继承关系。从这个意义上说,下面的迭代是继承自上面的迭代器。由于这种继承关系,可以将一个Forward迭代器作为一个Output或Input迭代器使用。同样,如果一个算法要求是一个Bidirectional迭代器,那么只能使用该种类型和随机访问迭代器。

2.指针迭代器正如下面的小程序显示的,一个指针也是一种迭代器。该程序同样显示了STL的一个主要特性——它不只是能够用于它自己的类类型,而且也能用于任何C或C++类型。Listing1.iterdemo.cpp显示了如何把指针作为迭代器用于STL的find()算法来搜索普通的数组。

Listing1.iterdemo.cpp

#include<iostream.h>

#include<algorithm>

usingnamespacestd;

#defineSIZE100

intiarray[SIZE];

intmain()

{ iarray[20]=50; int*ip=find(iarray,iarray+SIZE,50); if(ip==iarray+SIZE)

cout<<"50notfoundinarray"<<endl;

else cout<<*ip<<"foundinarray"<<endl; return0;

}在引用了I/O流库和STL算法头文件(注意没有 .h后缀)后,该程序告诉编译器使用std名字空间。使用std名字空间的这行是可选的,因为删除该行对于这个小程序来说不会导致名字冲突。程序中定义了尺寸为SIZE的全局数组。由于是全局变量,因此运行时数组自动初始化为零。下面的语句将在索引20位置处的元素设置为50,并使用find()算法来搜索值50:

iarray[20]=50;

int*ip=find(iarray,iarray+SIZE,50);

find()函数接受三个参数,前两个定义了搜索的范围。由于C和C++数组等同于指针,因此表达式iarray指向数组的第一个元素;第二个参数iarray+SIZE等同于past-the-end值,也就是数组中最后一个元素的后面位置;第三个参数是待定位的值,也就是50。find()函数返回和前两个参数相同类型的迭代器,这里是一个指向整数的指针ip。提示:必须记住STL使用模板。因此,STL函数自动根据它们使用的数据类型来构造。为了判断find()是否成功,例子中测试ip和past-the-end值是否相等:

if(ip==iarray+SIZE)...如果表达式为真,则表示在搜索的范围内没有指定的值;否则就是指向一个合法对象的指针,这时可以用下面的语句显示:

cout<<*ip<<"foundinarray"<<endl;测试函数返回值和NULL是否相等是不正确的。不要像下面这样使用:

int*ip=find(iarray,iarray+SIZE,50);

if(ip!=NULL)... //错误当使用STL函数时,只能测试ip是否和past-the-end值相等。尽管在本例中ip是一个C++指针,但其用法也必须符合STL迭代器的规则。3.容器迭代器尽管C++指针也是迭代器,但用得更多的是容器迭代器。容器迭代器的用法和iterdemo.cpp一样,但和将迭代器申明为指针变量不同的是,可以使用容器类方法来获取迭代器对象。两个典型的容器类方法是begin()和end(),它们在大多数容器中表示整个容器范围。其他一些容器还使用rbegin()和rend()方法提供反向迭代器,以按反向顺序指定对象范围。下面的程序创建了一个矢量容器(STL的和数组等价的对象),并使用迭代器在其中搜索。

Listing2.vectdemo.cpp

#include<iostream.h>

#include<algorithm>

#include<vector>

usingnamespacestd;

vector<int>intVector(100);

voidmain()

{

intVector[20]=50;

vector<int>::iteratorintIter= find(intVector.begin(),intVector.end(),50);

if(intIter!=intVector.end())

cout<<"Vectorcontainsvalue"<<*intIter<<endl;

else cout<<"Vectordoesnotcontain50"<<endl;

}可用下面的方法显示搜索到的数据:

cout<<"Vectorcontainsvalue"<<*intIter<<endl;

4.常量迭代器和指针一样,可以给一个迭代器赋值。例如,首先申明一个迭代器:

vector<int>::iteratorfirst;该语句创建了一个vector<int>类的迭代器。下面的语句将该迭代器设置到intVector的第一个对象,并将它指向的对象值设置为123:

first=intVector.begin();*first=123;这种赋值对于大多数容器类都是允许的,除了只读变量。为了防止错误赋值,可以申明迭代器为:

constvector<int>::iteratorresult;

result=find(intVector.begin(),intVector.end(),value);

if(result!=intVector.end())

*result=123;11.4模板的多态11.4.1模板类的继承模板类与普通类一样有基类,也同样可以有派生类。它的基类和派生类既可以是模板类,也可以不是模板类。模板类也具备所有与继承相关的特点,但有一些值得注意的地方。

假设有如下类关系:

template<classT>classA{...};

A<int>aint;

A<double>adouble;则aint和adouble并非A的派生类,甚至可以说根本不存在A这个类,只有A<int>和A<double>这两个类。这两个类没有共同的基类,因此不能通过类A来实现多态。如果希望对这两个类实现多态,正确的类层次应该是:

classAbase{...};

template<classT>classA:publicAbase{...};

A<int>aint;

A<double>adouble;也就是说,在模板类之上增加了一个抽象的基类。注意,这个抽象基类是一个普通类,而非模板。再来看下面的类关系:

template<inti>classA{...};

A<10>a10;

A<5>a5;在这种情况下,模板参数是一个数值,而不是一个类型。尽管如此,a10和a5仍然没有共同的基类。这与用类型作模板参数是一样的。11.4.2模板类多态用C++ 实现多态的常用方法是通过继承和虚函数,但是使用模板同样可以实现多态。使用继承和虚函数来实现多态(动态的多态)存在以下几个设计上的问题:

(1)增加了复杂度;

(2)增加了代码大小以及程序运行时间;

(3)降低了程序的灵活性。使用模板来实现多态(静态的多态)则可以解决上述设计问题。

1.使用继承和虚函数来实现多态使用继承和虚函数实现多态的过程如下:

(1)识别抽象概念。

(2)在抽象基类中将共有方法声明为虚函数。

(3)在各个派生类中实现这些共有方法。例如:

classFile

{

public: virtualvoidOpen()=0;

virtualvoidSave()=0; virtualvoidSaveAs(String&)=0; virtualvoidClose()=0;

};

classTextFile:publicFile

{

public: voidOpen(); voidSave(); voidSaveAs(String&); voidClose();

};

classImageFile:publicFile

{

public: voidClose(); voidSave(); voidSaveAs(String&); voidClose();

};

//通过菜单项打开文件

voidmenu_open_file(File*f_ptr)

{

f_ptr->Open();

...

}可以如下运用上述代码:

File*file_ptr=newTextFile();

menu_open_file(file_ptr); //打开文本文件

...

file_ptr=newImageFile();

menu_open_file(file_ptr);

//打开图片文件总结:

(1)在上面这个实例中,File就是我们识别的抽象概念,在此将其定义为一个抽象基类。

(2) Open、Save、SaveAs、Close是共有方法,并在File类中被声明为纯虚函数。

(3) File抽象概念的具体实现是imagefile和textfile,在此为其提供了具体的实现。

(4) Open、Save、SaveAs、Close等菜单操作将会调用这里的具体实现。

(5)如果用户选择了打开文件菜单项,该菜单项的实现函数将会根据所需打开文件的不同类型调用不同的实现。

2.使用模板来实现多态如果使用模板来实现多态,则不用在基类中声明共有的方法,但是需要在程序中隐式声明它们。下面使用模板来实现上面的实例:

classTextFile

{

voidOpen();

};

classImageFile

{

voidOpen();

};

//使用菜单项来打开文件

template<typenameT>voidmenu_open_file(Tfile)

{

file.open();

}

对上述代码的运用为:

TextFiletxt_file;

ImageFileimg_file;

menu_open_file(txt_file);

//打开文本文件

menu_open_file(img_file);

//打开图片文件总结:

(1)只需定义具体的类,如textfile和imagefile,而不用定义抽象类。

(2)将使用这些类的函数定义为模板,如menu_open_file。

(3)在模板中,不必使用指针及引用。

3.使用动态的多态(利用继承和虚函数)存在的问题

(1)时间及内存使用上的开销较大。使用动态的多态不能很好地实现容器类,因为这种方法既耗时间又耗内存。利用模板则不会有这个问题。C++的标准模板库就是一个很好的例子。下面这个容器类是用继承来实现的。在C++中加入模板之前,这种方法是最常用的。

classcontainer

{

public: virtualvoidadd(); virtualvoidremove(); virtualvoidprint();

};

classlist:container

{ ...

};

classvector:container

{

...

};

voidSort(container&con)

{

...

}

voidSearch(container&con)

{

...

}

(2)使用继承来实现多态会降低程序的灵活性。对基类接口中的共有方法进行增、删、改是一件既费事又容易出错的事情,有时还会引起一系列其他的问题。如果想在接口上有更大的灵活性,更好的方法是使用模板。例如:

classFile

{

public: virtualvoidOpen()=0; virtualvoidSave()=0; virtualvoidSaveAs(String&file_name)=0;

};

classTextFile:publicFile

{

public: voidOpen(); voidSave(); voidSaveAs(String&file_name);

};

classImageFile:publicFile

{

public: voidOpen();

voidSave(); voidSaveAs(String&file_name);

};

classBinaryFile:publicFile

{

public: voidOpen(); voidSave(); voidSaveAs(String&file_name);

};假定想向上面这个文件抽象中添加打印功能,但这里的问题是不能向一个二进制文件中添加打印功能,因为不允许打印一个二进制文件。一种解决方法是使用RTTI(运行时类型信息)。但这种方法也不好,因为它需要对现有的代码做出很多改变。否则,必须将这些类分为两类:可打印的和不可打印的。此时,可使用模板作为解决之道,如下所示:

classTextFile

{

public: voidOpen(); voidSave();

voidSaveAs(String&file_name); voidPrint();

};

classImageFile

{

public: voidOpen(); voidSave(); voidSaveAs(String&file_name); voidPrint();

};

classBinaryFile

{

public: voidOpen(); voidSave(); voidSaveAs(String&file_name); //这里没有提供Print函数

};

template<typenameT>

voidOn_Open(Tfile)

{ file.Open();

}

template<typenameT>

voidOn_Save(Tfile)

{

file.Save();

}

template<typenameT>

voidOn_Print(Tfile)

{

file.Print();

}

TextFiletxt_file;

ImageFileimg_file;

BinaryFilebin_file;

On_Print(txt_file);

On_Print(img_file);

On_Print(bin_file);

//这里编译时将会出错

//对二进制文件的打印将会在编译时出现错误

4.动态的多态与静态的多态的比较动态的多态(基于虚函数的方法)相对于静态的多态(基于模板的方法)有以下优势:

(1)可执行程序的代码相对较小。

(2)不需公布源码。静态的多态(基于模板的方法)相对于动态的多态(基于虚函数的方法)又有以下优势:

(1)类型安全。

(2)执行速度更快。

(3)不必在基类中声明公共接口。

(4)低耦合,因此提高了重用性。总之,这两种方法都各有优劣,总体上用基于模板的方法来实现多态效果更好。通常,可根据可重用性、灵活性以及所需的性能等目标来选择具体的方法。11.5高级编程11.5.1动多态设计(DynamicPolymorphism)起初,C++只是通过继承机制与虚拟函数的结合运用来支持多态。这种背景下的多型设计艺术是:在彼此相关的objecttypes之间确认一套共通能力,并将其声明为某共通基础类别(CommonBaseClass)的一套虚拟函数接口。这种设计最具代表性的例子就是一个管理若干几何形状的程序,这些几何形状可以以某种方式着色(例如在屏幕上着色)。在这样的程序中,我们可以定义一个所谓的抽象基础类别(AbstractBaseClass,ABC)GeoObj,在其中声明适用于几何对象的一些共通操作(operations)和共通属性(properties),每一个针对特定几何对象而设计的具象类别(ConcreteClass)都衍生自GeoObj(见图11.1)。图11.1多型(polymorphism)通过继承(inheritance)来实现程序如下:

#include"coord.h"

//针对几何对象而设计的共通抽象基础类别(CommonAbstractBaseClass)GeoObj

classGeoObj

{

public: //绘制几何对象

virtualvoiddraw()const=0; //传回几何对象的重心(centerofgravity) virtualCoordcenter_of_gravity()const=0; //...

};

//具象几何类别(concretegeometricclass)Circle

//衍生自GeoObj

classCircle:publicGeoObj

{

public: virtualvoiddraw()const; virtualCoordcenter_of_gravity()const; //...

};

//具象几何类别Line

//衍生自GeoObj

classLine:publicGeoObj

{

public: virtualvoiddraw()const; virtualCoordcenter_of_gravity()const; //...

};

//...建立具象对象(concreteobjects)之后,客户端程序代码可以通过指向基础类别的references或pointers来操纵这些对象,这会启动虚拟函数分派机制(virtualfunctiondispatchmechanism)。当通过一个指向基础类别之子对象(subobject)的references或pointers来调用某虚拟函数时,会调用被指涉(refered)的那个特定具象物件的相应成员函数。以本例而言,具体程序代码大致描绘如下:

#include"dynahier.h"

#include<vector>

//绘制任何GeoObj

voidmyDraw(GeoObjconst&obj)

{ obj.draw(); //根据对象的类型调用draw()

}

//处理两个GeoObjs重心之间的距离

Coorddistance(GeoObjconst&x1,GeoObjconst&x2)

{ Coordc=x1.center_of_gravity()-

x2.center_of_gravity(); returnc.abs(); //传回坐标绝对值

}

//绘出GeoObjs异质群集(heterogeneouscollection)

voiddrawElems(std::vector<GeoObj*>const&elems)

{

for(unsignedi=0;i<elems.size();++i)

{ elems[i]->draw(); //根据对象的类型调用draw()

}

}

intmain()

{ Linel; Circlec,c1,c2;

myDraw(l);//myDraw(GeoObj&)=>Line::draw() myDraw(c);

//myDraw(GeoObj&)=>Circle::draw() distance(c1,c2);/distance(GeoObj&,GeoObj&) distance(l,c);/distance(GeoObj&,GeoObj&) std::vector<GeoObj*>coll; //异质群集

coll.push_back(&l); //插入一个line coll.push_back(&c); //插入一个circle drawElems(coll); //绘出不同的种类

return0;}上述程序的关键性多型接口元素是draw()和center_of_gravity(),两者都是虚函数。程序示范了它们在myDraw()、distance()和drawElems()函数内被使用的情况。由于这三个函数使用共通基础类别GeoObj作为表达手段,因而无法在编译期决定使用哪一个版本的draw()或center_of_gravity()。然而在执行期,调用虚拟函数的那个对象的完整动态类型会被取得,以便对调用语句进行分派(dispatch)。于是,根据几何对象的实际类型,程序得以完成适当操作:如果对一个Line对象调用myDraw(),函数内的obj.draw()就调用Line::draw();对Circle物件调用的则是Circle::draw()。同样道理,对distance()而言,调用的将是与自变量物件相应的那个center_of_gravity()。动态多型最引人注目的特性是处理异质对象群集(heterogeneouscollectionsofobjects)的能力。由drawElems()函数可以看出,elems[i]->draw()能根据目前正被处理的元素类型,调用不同的成员函数。11.5.2静多态设计(StaticPolymorphism)

templates也可以用来实现多型,然而它们并不依赖分解及抽取baseclasses共通行为。在这里,共通性是指应用程序所提供的不同几何形状,必须以共通语法支持其操作(也就是说,相关函数必须同名)。具象类别之间彼此独立定义(见图11.2)。一旦templates被具象类别实例化,便可获得(被赋予)多型的威力。图11.2多型(polymorphism)通过模板(templates)来实现例如前一节中的函数myDraw():

voidmyDraw(GeoObjconst&obj) //GeoObj是抽象基础类别

{

obj.draw();

}可设想改写如下:

template<typenameGeoObj>

voidmyDraw(GeoObjconst&obj) //GeoObj是模板参数

{

obj.draw();

}比较前后两份myDraw()实作码,可以得出一个结论:两者的主要区别在于GeoObj是个模板参数而非一个共通基础类(commonbaseclass)。然而在此表象背后,还有一些根本的区别。比方说,如果使用动态多型,执行期只会有一个myDraw()函数,但如果使用template,则会有不同的函数,如myDraw<Line>()和myDraw<Circle>()。下面使用静态多型机制改写前一节的例子。首先不再使用几何类别阶层体系,而是编写若干个独立的几何类别:

#include"coord.h“

//具象的几何类别Circle

//不衍生自任何类别

classCircle

{

public: voiddraw()const; Coordcenter_of_gravity()const; //...

};

//具象的几何类别Line

//不衍生自任何类别

classLine

{

public: voiddraw()const; Coordcenter_of_gravity()const; //...

};

//...现在,这些classes的应用程序看起来像这样:

#include"statichier.h"

#include<vector>

//绘出任何给定的GeoObj

template<typenameGeoObj>

voidmyDraw(GeoObjconst&obj)

{ obj.draw();

//根据对象的类型调用draw()

}

//处理两个GeoObjs重心之间的距离

template<typenameGeoObj1,typenameGeoObj2>

Coorddistance(GeoObj

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论