1.Valarray:
头文件:valarray,用于处理数值类。它是一个模板类,可用以处理不同的数据类型,模板特性意味着在声明对象时必须指定具体的数据类型。如下:
Valarray<int> q_values;
Valarray<double> weights;
了解下使用其构造函数和其他类方法的例子:
Double gpa[5]={3.1,3.5,3.8,2.9,3.3};
Valarray<double> v1; //一个数组0个元素
Valarray<int> v2(8); //一个数组8个int元素
Valarray<int> v3(10,8); //一个数组8个int元素,都初始化为10
Valarray<double> v4(gpa,4);//一个数组4个元素,初始化为gpa数组的前四个。
C++11也可以使用列表初始化:
Valarrray<int> v5={20,32,17,9};
一些方法:operator[]():访问各个元素,size(),sum(),max(),min().
若设计一个包含string姓名和valarray<double>分数的student类,那么不能从这两个共有类去继承,因为这不是简单的is-a关系,student不是姓名也不是分数,而是student有姓名和分数,关系是has-a,c++用组合(包含)创建一个包含其他对象的类。类声明:
Class student
{
Private:
String name;
Vaiarray<double> scores;
};
这意味着student类的成员函数可以使用string和valarray<double>类的公有接口来访问和修改name,scores对象,但在类外不可这么做。这就叫student类获得了其成员对象的实现但没有继承接口。在student表中,可用student对象调用string方法name.size(),以及valarray方法scores.sum()。
注意:使用公有继承时类可以继承接口,可能还有实现。获得接口是is-a关系的一部分,而使用组合,类可以获得实现,但不能获得接口。不继承接口是has-a的组成部分。
在上述类设计时,使用explicit Student(int n):name(“Nully”),scores(n){} 这涉及到了之前提到的转换函数的概念。
使用explicit关闭隐式转换的原因是在这里把一个int值转化成类对象是无意义的。
Student doh(“Homer”,10);
Doh=5;
如果没有explicit限定词,那么此时int类型的5将调用上述构造函数成为一个Student对象,它有默认name是Nully,因此赋值操作将利用这个临时对象替换原来的doh值。而使用了explicit关键字后,则认为上述赋值过程是错的。
*注意:c++使用explicit防止单参数构造函数的隐式转换。在编译阶段出现错误优于在运行阶段出现错误!
当初始化列表包含多个项目的时候,这些项目被初始化的顺序为它们被声明的顺序,而不是它们在初始化列表中的顺序。
在类的内部,可以使用被包含对象的接口来进行操作,比如Student中的scores是valarray对象,那么可在student类内部使用scores对象调用valarray的方法。即Student对象调用student方法,而student方法再通过被包含的其他类对象调用valarray类方法。
正如:
Ostream &operator<<(ostream &os ,const Student &stu)//必须是student的友元函数,否则无法访问name成员。
{
Os<<”scores for”<<stu.name<<”:\n”;
}
Stu.name是一个string对象,所以它将调用operator<<(ostream &,const string &)。
2.私有继承
使用私有继承,基类的公有成员和保护成员都将成为派生类的私有成员。这意味着基类方法将不会成为派生对象共有接口的一部分。(即将不能在类外直接使用类对象调用这些方法)。使用私有继承,类将继承实现。即私有继承与包含关系类似:获得实现,但不获得接口。
Class Student:private std::string,private std::valarray<double>
{
Public:
…
};
使用多个基类的继承是多重继承,新Student类不需要私有数据,因为两个基类已经提供了所需的所有数据成员,包含版本提供了连个被显示命名的对象成员,而私有继承提供了两个无名称的子对象成员。这是两种方法的重要区别之一。
此时没有了命名好的成员对象,那么在撰写类构造函数等方法的时候就有别于一般方法,将使用类名代替类对象进行列表初始化语法:
Student(const char *str,double *pd,int n):std::string(str),ArrayDb(pd,n){}//使用类名而非成员名来实现构造函数。
使用私有继承时,只能在派生类的方法中使用基类的方法。如果希望类工具可以公用,就重新在派生类中实现一个公有方法调用继承来的私有方法即可。另包含关系在类中使用对象来调用相应的方法,私有继承使用类名和作用域解析符来调用基类的方法。如:
Double Student::Average() const
{
If(ArrayDb::size()>0)
Return ArrayDb::sum()/ArrayDb::size();
Else
Return 0;
}
访问基类对象:有时候派生类需要使用到基类对象,比如返回基类的string对象或引用,当使用私有继承时候该对象没有名称,那么使用强制类型转换,由于Student类是由string类派生而来,因此可以通过强制类型转换将Student对象转换为string对象,利用*this获取调用当前方法的对象,再施加一个强制指针转换就可以实现,如下:
const string &Student::Name() const
{
Return (const string &) *this;
}
//通过Student对象返回继承而来的string对象。
访问基类的友元函数:友元不属于类,因此无法使用类名限定函数名来访问基类的友元函数,可通过显示地转换成基类来调用正确的函数。如下:
Ostream &operator<<(ostream &os,const Student &stu)
{
Os<<”Scores for ”<<(const string &)stu<<”\n”;//通过显示转换使得stu转换成为const string类型,从而调用基类的operator <<(ostream &,const string &)。
}
注意:引用stu不会自动转换为string引用,根本原因在于在私有继承中,未进行显式类型转换的派生类引用或者指针,无法赋值给基类的引用和指针。在这个例子中即使是公有继承,也必须使用显示类型转换,否则将导致递归调用,stu传进去,里面还是针对stu的重载接着调用。另一个原因是该类是多重继承,编译器无法确定应该转换成哪个基类方法,如果连个基类都提供了函数operator<<。
注意:通常尽可能使用包含来建立has-a关系,如果新类需要访问原有类的保护成员,或者需要重新定义虚函数,则应该使用私有继承。
*保护继承:基类的公有成员和保护成员都将变成派生类的保护成员。和私有继承一样,基类的接口在派生类中可用,但在继承层次结构之外是不可用的。私有和保护重要的区别是:使用私有继承,第三代类将不能使用基类的接口,因为基类的公有方法在派生类中将变成私有方法,这意味着无法在类外继续使用所有的基类方法。保护继承时,基类的公有方法在第二代中将变成保护方法,第三代派生类还可以使用它们。
注意:此处回忆一下权限问题,对于某个类而言,private/protected/public在类中都是可见的,可以在类内部随便使用,但是在类的外部,也就是对象调用的时候,private/protected是不能直接调用的,而public方法可以用。即在类外,只有public成员可见。
在继承中的权限控制如下图:
基类 |
private |
Protected |
public |
private |
protected |
public |
private |
protected |
public |
继承方式 |
private |
protected |
Public | ||||||
类内部可见性 |
不可见 |
private |
private |
不可见 |
protected |
protected |
不可见 |
protected |
public |
类外部可见性 |
不可见 |
不可见 |
不可见 |
不可见 |
不可见 |
不可见 |
不可见 |
不可见 |
可见 |
(1)内部访问:由派生类中新增的成员函数对基类继承来的成员的访问。
(2)外部访问:在派生类外部,通过派生类的对象对从基类继承来的成员的访问。
但也有例外的从操作可以改变访问权限:
1.使用保护/私有继承时,基类的公有成员将成为保护成员或私有成员。假设要让基类的方法在派生类外面(派生类对象)可用,方法之一是定义一个使用该基类方法的派生类方法。如若希望Student类能够使用valarray类的sum方法,但私有继承后在派生类中变为私有方法,类外部可见,此时:
Double Student::sum() const
{
Return std::valarray<double>::sum();//
}
这样student对象也可在类外调用student::sum(),后者将进而调用std::valarray<double>::sum()方法。
2.另一种方法是:将函数调用包装在另一个函数调用中,即使用一个using声明,来指出派生类可以使用特定的基类成员。即使采用的是私有派生。假设希望通过Student类能够使用valarray的方法min和max,那么可在student.h文件中的公有部分加入如下声明:
Using std::valarray<double>::min;
Using std::valarray<double>::max;
使得这两个方法相当于成为student类的公有方法,student类对象可在类外调用该函数。但是注意:using声明只适用于继承关系。
3.多重继承
MI描述的是有多个直接基类的类。它遇到的主要问题是,从两个不同基类继承同名方法,从两个或者更多相关基类那里继承同一个类的多个实例。
注:用一个抽象基类worker派生出singer,waiter两个类,可使用如下存储:
Worker *pw[LIM]={&bob,&bev,&w_temp,&s_temp};//其中bob,w_temp是waiter对象,两外两个是singer对象。如果所用方法为虚方法,那么使用该指针自动调用属于各个类对象对应的方法。
假设:class singerwaiter :public singer,public waiter{…};//公有继承
此时若从singer和waiter中派生出一个singewaiter类,那么将面临的问题是,因为singer和waiter都继承了worker组件,那么singerwaiter将包含两个worker组件。
同时在执行如下语法:
Singerwaiter ed;
Worker *pw=&ed;//模棱两可
通常这种赋值将把基类指针设置成为派生类对象中的基类对象的地址,但ed中包含两个worker对象,有两个地址可供选择,所以必须用类型转换来制定对象:
Worker *pw1=(waiter *)&ed;
Worker *pw2=(singer *)&ed;
这将明确调用的是哪个worker组件。
包含两个worker对象拷贝还会导致其他问题,真正的问题就是:并不需要两个worker的拷贝,c++引入多重继承的时候引入了一种新技术—虚基类。
虚基类:使得从多个类(它们的基类相同)派生出来的对象只继承一个基类对象。通过在继承时类声明中使用关键字virtual,可以使得worker被用作singer和waiter的虚基类。
Class singer:virtual public worker(){…};
Class waiter:public virtual worker(){…};//virtual 和 public 顺序无所谓。
然后可将singerwaiter定义为:
Class singingwaiter:public singer,public waiter{…};
此时singerwaiter只包含一个worker对象副本。
此时新的构造函数规则
使用虚基类时需要对类构造函数采用另一种新的方法。
首先回忆之前知识点:
对于普通的类(非虚基类),在其继承体系内,唯一可以出现在初始化列表中的构造函数是即时基类构造函数(即仅上一级父类),但这些构造函数可能需要将信息传递给最早的基类如:
Class A
{
Int a;
Public:
A(int n=0):a(n){}
};
Class B : public A
{
Int b;
Public:
B(int m=0,int n=0):A(n) ,b(m){}
};
Class C:public B
{
Int c;
Public:
C(int q=0,int m=0,int n=0):B(m,n),c(q){}
};
可看出C类的构造函数只能调用B类的构造函数,而B类的构造函数只能调用A类的构造函数。但如果worker是虚基类,则这种信息传递不起作用,如:
Singerwaiter(const worker & wk, int p=0,int v=singer::other)
: waiter(wk,p),singer(wk,v){}
自动传递信息时将通过两条不同途径waiter和singer将wk传递给worker对象。为避免这种冲突,c++在基类是虚的时候禁止信息通过中间类自动传递给基类。因此上述做法可将p和v传递给waiter和singer去初始化各自的成员,但wk参数中的信息将不会传递给子对象waiter。然后编译器必须在构造派生对象之前构造基类对象组件,所以此时编译器将使用worker的默认构造函数,或者如果有非默认的话那么就显式调用所需的基类构造函数,如:
Singerwaiter(const worker & wk,int p=0,int v=singer::other)
:worker(wk),waiter(wk,p),singer(wk,v){}
此时将显式调用构造函数worker(const worker &),注意,对于虚基类必须这样做,但对于非虚基类是非法的!
即若类有间接虚基类,那么必须直接使用该基类默认构造函数,或者显示调用非默认构造函数。
关于使用哪个方法:
对于单继承来说,若没有在子类中重新定义某个方法,假设是show(),则将使用最近祖先中的定义,而在多重继承中,每个直接祖先都有一个相同的方法show(),所以此时若用singerwaiter对象调用show方法,将会导致歧义。此时当然可以使用作用域::符来解决这个问题,但更好的办法是在singerwaiter中重新定义show()方法。假设希望singerwaiter对象使用singer版本的show(),那么可如下重新定义:
Void singerwaiter::show()
{
Singer::show();
} (后面的例子也来自这儿)
回顾一下:在单继承中,让派生类方法调用基类方法是可以的。例如假设headwriter类是从waiter类派生类而来的,则可以使用下面的定义序列,其中每个派生类调用基类的方法,并附加自己的信息。
Void worker::show() const
{
…;
}
Void waiter::show() const
{
Worker::show();
…;
}
Void headwriter::show() const
{
Waiter::show();
…;
}
然而这种递增方式对于singerwaiter这种有间接虚基类的函数来说是无效的,如上面括号附近的例子的方式就是无效的,因为它忽略了singerwaiter中的waiter组件。若在其中再添加一个waiter::show(),那么又会出现问题,就是worker组件的信息会被打印两次。
这种问题的解决办法其中之一是:
使用模块化方式而不是递增式方式。即提供一个只显示worker自己组件,和一个只显示singer和waiter自己组件的的方法。然后再在singerwaiter类中将各种方法自由组合起来。
如:
Void worker::data() const
{
…;
}
Void waiter::data() const
{
…;
}
Void singer::data() const
{
…;
}
Void singerwaiter::show() const
{
Singer::data();
Waiter::data();
}
虚基类和支配:
注意:使用虚基类将改变c++解析二义性的方式,在使用非虚基类时,如果类从不同的类那里继承了两个或更多的同名成员,则使用该成员名时若没有用类名进行限定将导致二义性。但若使用的是虚基类,则这样做不一定会导致二义性。在这种情况下如果某个名称优先于其他所有名称,则使用它时,即便不用限定符,也不会导致二义性。
优于:派生类中的名称优于直接或者间接祖先类中的相同名称。
Class B
{
Public:
Short q();
…
};
Class c:virtual public B
{
Public:
Long q();
Int omg();
…
};
Class d : public c
{
…
};
Class e : virtual public b
{
Private:
Int omg();
…
};
Class F:public d,public e
{
…
};
类c中的q()定义优先于类B中的q()定义,因为类c是由类b派生而来。因此f中的方法可以使用q()来表示c::q()。另一方面任何omg()定义都不优先于其他omg()定义,因为c和e都不是对方的基类。所以在f中使用非限定的omg()将导致二义性。
虚二义性规则与访问规则无关,也就是说,即使e::omg()是私有的,不能在f类中直接访问,但使用omg()仍将导致二义性。同样即使c::q()是私有的,它也将优先于d::q
(),这种情况下可以在f类中调用b::q(),但如果不限定q(),则将意味着要调用不可访问的c::q()。
MI小结:
首先回顾不使用虚基类的MI,这种形式的MI不会引入新规则。然而如果一个类从两个不同的类那里继承了两个同名的成员,则需要在派生类中使用类限定符类区分它们。即在从gunslinger和pokerplayer派生而来的baddude类中,将分别使用gunslinger::draw()和pokerplayer::draw()来区分从这两个类那里继承的draw方法,否则将导致二义性。
如果一个类通过多种途径继承了非虚基类,则该类从每种途径分别继承非虚基类的一个实例。
使用虚基类的MI,当派生类使用关键字virtual来指示派生时,基类就成为虚基类。
Class marketing :public virtual raeality{…};
主要变化是,从虚基类的一个或者多个实例派生而来的类将只继承了一个基类对象,为实现这种特性,必须满足某种要求:
1.有间接虚基类的派生类包含直接调用间接基类构造函数的构造函数,这对于间接非虚基类来说是非法的。
2.通过优先规则解决二义性。
4.类模板
模板类以下面代码开头:
Template,.<class type>
其中template告诉编译器将要定义一个模板,尖括号的内容相当于函数的参数列表。可以把class关键字看作变量的类型名,该变量接受类型作为其值,type看作该变量的名称。在模板定义中使用泛型名type来标识要存储在栈中的类型。(typename也可以代替class)
此时需要做出一些改变,比如将限定名从stack::改为stack<int>::
将
Typedef unsigned long Item;//这个放在头文件可以每次制定一个类型,但每次都需要重新编译
Bool stack::push(const Item & item)
{
…
}
Template<class type>
Bool stack<type>::push(const type &item)
{
…
}
如果在类声明中定义了方法,那么可以省略模板前缀和类限定符。对类使用模板的时,要注意类模板和成员函数模板都需要重新改写。以上是成员函数模板的撰写。
注意:要知道模板不是类和成员函数定义至关重要。他们是c++编译器指令,说明了如何生成类和成员函数定义。模板的具体实现—如用来处理string对象的栈类—被称为实例化或具体化。不能将模板成员函数放在独立的实现文件中,由于模板不是函数,他们不能单独编译。模板必须与特定的模板实例化请求一起使用。为此最简单的办法就是将所有模板信息放在一个头文件中(包括类声明模板与成员函数模板),并在要使用这些模板的文件中包含该头文件。
仅在程序中包含模板并不能生成模板类,而必须请求实例化。因此需要声明一个类型为模板类的对象,方法是使用具体的类型替换泛型名type。
Stack<int> kernels;
Stack<string> colonels;
上述声明编译器将按照stack<type>模板生成两个独立的类声明和两组独立的类方法。分别用int和string替换模板中的所有的type。注意:必须显示地提供所需的类型,这与常规的函数模板是不同的,因为编译器可以根据函数的参数类型来确定生成那种函数,但是类模板没有这个参数接口
Template <class T>
Void simple(T t){cout<<t<<’\n’;}
Simple(2);
Simple(“two”);
但类模板没有这个机会。
可以将内置类型或类对象用作stack<type>的类型,那么指针是否可以呢?
答案是可以创建指针栈,但如果不对程序进行重大修改将无法很好的工作。以下讲述为何此处的stack模板类不适合用于创建指针栈然后介绍一个指针栈很有用的例子。
版本1:
Stack<char *> st;
然后将string po;
改为 char *po;
这旨在使用char指针而不是string对象来接收键盘输入。这种方法很快就失败了,因为仅仅创建指针,没有创建用于保存输入字符串的空间,即接受不了字符串,程序将通过编译,但是用cin试图将输入保存在某些不合适的内存单元中崩溃。
版本2:
将string po;
替换为 char po【40】;
这为输入的字符串分配了空间,另外po的类型为char*,因此可以被放在栈中,但数组完全与pop()方法的假设相冲突。
Template<class type>
Bool stack<type>::pop(type &item)
{
If(top>0)
{
Item=items[--top];
Return true;
}
Else
Return false;
}
首先引用变量item必须引用某种类型的左值,而不是数组名。其次代码假设可以给item赋值,即使item能够引用数组,也不能为数组名赋值,因此这种办法失败了。
版本3:
将string po替换为char *po=new char[40];
这样的方式为输入的字符串分配了空间,另外po是变量,因此与pop代码兼容。然而这里会遇到基本的问题:只有一个pop变量,该变量总是指向相同的内存单元。确实在每当读取新字符串时,内存的内容都将发生改变,但每次执行压入操作时,加入到栈中的地址都相同。因此对栈执行弹出操作时,得到的地址总是相同的,它总是指向读入的最后一个字符串。具体地说栈并没有保存一个新字符串,因此没有任何用途。因为压入的是指针的地址,而循环读入的时候没有遍历读入,都默认读入到数组名也就是第一个元素地址处。
书中关于字符串使用有一个重要的点:
Template<class type>
Class stack
{
Type *items;
…
};
Stack实现了一个栈模板类。此时在实例化时,假设有一大堆 字符串 需要结合这个栈类进行存储,按照相应规则入栈出栈之时。实例化为这个格式stack<const char *>st (stacksize);此时我们想用const char *指针 来构建一个指针数组好容纳所有的字符串,即
Const char * in[num];此时每一个元素都是字符串。
注意此时type为const char *类型,且items为type类型的指针,那么相当于类中的items变成了const char *items[],即也是指针数组的形式,此时可按照相应的元素进行与栈方法的交互。
另外模板还可接受非类型(表达式)参数。如
Template<class T,int n>
其中int指出n的类型,这种参数(指定特殊类型而不是使用泛型名)成为非类型或表达式参数。
表达式参数有一些类型限制,表达式参数可以是整型,枚举,引用或者指针。因此double m是不合法的,但double *rm是合法的。另外模板代码不能修改参数的值,也不能使用参数的地址。所以在模板类中不能使用++n,和&n的等比到表达式。另外实例化模板时,用做表达式参数的值必须是常量表达式。
与stack中使用的构造函数方法相比,这种改变数组大小的方法有一个优点。构造函数使用的是通过new和delete管理的堆内存,而表达式参数方法使用的是为自动变量维护的栈内存,这样执行速度更快,尤其是在使用了很多小型数组的时候。
表达式参数的方法主要缺点是:每种数组大小都将生成自己的模板,即使是同一种类型的数组,大小不同也会生成不同的模板声明。
Arraytp<double , 12> eggweights;
Arraytp<double , 13> donuts;
但下面的声明只生成一个类声明,并将数组大小信息传递给类的构造函数:
Stack<int> eggs(12);
Stack<int> dunkers(13);
另一个区别是,构造函数方法更通用,这是因为数组大小是作为类成员存储在定义中的,这样可以将以一种尺寸的数组赋给另一种尺寸的数组,也可以创建允许数组大小可变的类。
模板的多功能性:
可以将用于常规类的技术用于模板类。模板类可用作基类,也可用作组件类,还可用作其他模板的类型参数。例如使用数组模板实现栈模板如下:
Template<typename T>
Class array
{
Private:
T entry;
…
};
Template<typename Tp>
Class stack
{
Private:
Array<Tp> ar;
…
};
实例化:
Array< stack<int> > asi;
另一个模板多功能性的例子是可以递归使用模板,例如对于前面的数组模板定义,可以这样使用它
Arraytp<arraytp<int , 5>,10> twodee;
即twodee是一个包含10个元素的数组,其中每个元素都是一个包含5个int元素的数组。与之等价的常规数组声明如下:
Int twodee[10][5];
请注意在模板语法中,维的顺序与等价的二维数组相反。
模板可以包含多个类型参数,假设希望类可以保存两种值,则可以创建并使用pair模板来保存两个不同的值。大概框架如下:
Template<class T1, class T2>
Class pair
{
Private:
T1 a;
T2 b;
Public:
…
};
类模板的另一项新特性是,可以为类型参数提供默认值。
Template<class T1 ,class T2=int> class topo{…};
这样如果省略T2的值将使用int类型来填充模板。
Template<double,double> m1;
Template<double,int> m2;
另需要注意:虽然可以为类模板类型参数提供默认值,但不能为函数模板参数提供默认值。然而可以为非类型参数提供默认值,这对于类模板和函数模板都是适用的。
5模板的具体化
类模板和函数模板很相似,因为可以有隐式实例化,显示实例化和显示具体化,它们统称为显示具体化。模板以泛型的方式描述类,而具体化是使用具体的类型生成类声明。
隐式实例化:
以上用法都是隐式实例化,即它们声明一个或多个对象,指出所需类型,而编译器使用通用模板提供的处方生成具体的类定义。
Arraytp<int,100> stuff;//隐式实例化
编译器在需要对象之前,不会生成类的隐式实例化。
Arraytp<double,30> *pt;一个指针,没有对象被需要
Pt=new arraytp<double,30>; 现在一个对象被需要了
显示实例化:
当使用关键字template并指出所需类型来声明类时,编译器将生成类声明的显示实例化。声明必须位于模板定义所在的名称空间中。例如下面的声明将arraytp<string,100>声明为一个类。
Template class arraytp<string,100>;
在这种情况下虽然没有创建或者提及类对象,编译器也将类声明(包括方法定义)。和隐式实例化一样,也将根据通用模板来生成具体化。
显示具体化:
显示具体化是特定类型(用于替换模板中的泛型)的定义。即有时候可能需要在为特殊类型实例化时对模板进行修改,使其行为不同。在这种情况下可以创建显示具体化。
比如对于某种用于表示排序后数组的类模板,假设模板使用>运算符来对值进行比较,对于数字这管用,如果T表示一种类,则只要定义T::operator()方法这也管用,但如果T是const char *表示的字符串,这将不管用。实际上模板可以正常工作,但是字符串将按照地址的字母顺序进行排序,这要求为类定义使用strcmp(),而不是>来对值进行比较。在这种情况下可以提供一个显示模板具体化,这将采用为具体类型定义的模板,而不是为泛型定义的模板。当具体化模板和通用模板都与实例化请求匹配时,编译器将采用具体化版本。
具体化类模板定义格式如下:
Template<>class classname<specialized-type-name>{…};
要使用新的表示法提供一个专供const char *类型使用的sortedarray模板,可以使用类似于下面的代码。
Template<>class sortedarray<const char *>
在其中的实现将使用strcmp()而不是>来比较数组值。现在当请求const char *类型的sortedarray模板时,编译器将使用上述的专用定义,而不是通用模板定义。
Sortedarray<int> scores;//通用模板
Sortedarray<const char *>dates;//显示具体化
部分具体化:
C++还允许部分具体化,即部分限制模板的通用性。例如部分具体化可以给类型参数之一指定具体的类型。
通用模板:
Template <class T1,class T2> class pair{…};
将T2设置为int的具体化:
Template<class T1> class pair<T1,int>{…};
关键字template后面的<>声明的是没有被具体化的类型参数。因此上述第二个声明将T2具体化为int,但T1保持不变。注意如果指定所有的类型,则<>内将为空,这将导致显示具体化,如
Template<>class pair<int,int>{…};
如果有多个模板可供选择,编译器将使用具体化程度最高的模板,给定上述三个模板情况如下:
Pair<double,double> p1;//使用通用模板
Pair<double,int> p2;//使用pair<T1,int>部分具体化模板
Pair<int,int> p3;//使用pair<int,int>显示具体化模板
也可以通过为指针提供特殊版本来部分具体化现有的模板:
Template<class T>
Class feeb{…};
Template<class T*>
Class feeb{…};
如果提供的类型不是指针,则编译器将使用通用版本,如果提供的是指针,则编译器将使用指针具体化版本。
Feeb<char> fb1;//使用通用模板,T是char
Feeb<char *>fb2;//使用具体化模板,T是char
如果没有部分具体化,则第二个声明将使用通用模板,将T转化为char *类型。如果进行了部分具体化,则第二个声明将使用具体化模板,将T转换为char。
部分具体化特性使得能够设置各种限制,如:
通用模板:
Template<class T1,class T2,class T3> class trio{…};
将T3设置为T2的部分具体化:
Template<class T1,class T2> class Trio<T1,T2,T2>{…};
将T2,T3设置为T1*的部分具体化:
Template<class T1> class trio<T1,T1*,T1*>{…};
给定上述声明,编译器将作出如下选择:
Trio<int,short,char*>t1;//使用通用模板
Trio<int,short> t2;//使用trio<T1,T2,T2>;
Trio<char,char*,char*> t3;//使用Trio<T1,T1*,T1*>;
成员模板:有问题!
将模板用于参数:
模板可以包含类型参数(typename T)和非类型参数(如int n)。模板还可以包含本身就是模板的参数,这种参数是模板新增的特性。如模板头:
Template<template<typename T>class Thing>
Class crab
模板参数是template<typename T> class thing,其中template<typename T> class是类型,thing是参数。这意味着:
如有下面的声明 crab<king> legs;为使上述声明被接受,模板参数king必须是一个模板类,其声明与模板参数thing的声明匹配:这里的legs声明将用king<int> 替换thing<int> 将用king<double> 替换thing<double>。
Template<typename T>
Class king{…};
在crab类模板代码中的私有成员中,声明了两个数据对象:
Thing<int> s1;
Thing<double> s2;
并且在代码中包含如下声明:
Crab<stack> nebula;
因此thing<int> 将被实例化为 stack<int>,而thing<double>将被实例化为stack<double>。总之模板参数thing将被替换为声明crab对象时被用作模板参数的模板类型。
同样也可以混合使用模板参数和常规参数,如crab也可如下打头:
Template<template<typename T> class thing,typename U, typename V>
Class crab
{
Private:
Thing<U> s1;
Thing<V> s2;
};
现在s1和s2可存储的数据类型是泛型,而不是硬编码指定的类型。
模板类和友元:
模板类可以有友元,模板的友元分为3个类:
1.非模板友元
2.约束模板友元 即友元类型取决于类被实例化时的类型
3.非约束模板友元,即友元的所有具体化都是类的每一个具体化的友元。
模板类的非模板友元函数:
在模板类中将一个常规函数声明为友元
Template<class T>
Class hasfriend
{
Public:
Friend void counts();
…
};
上述函数使counts函数成为模板所有实例化的友元。例如它将是类hasfriend<int>和hasfriend<string>的友元。Counts函数不是通过对象调用的,它是友元不是成员函数,也没有对象参数。
那么它如何访问hasfriend对象,它可以访问全局对象,可以使用全局指针访问非全局对象,可以创建自己的对象,可以访问独立于对象的模板类的静态数据成员。
假设要为友元函数提供模板类参数不能friend void report(hasfriend &);因为没有hasfriend这样的对象,而只有特定的具体化,如hasfriend<short>,要提供模板类参数,必须指明具体化。如下:
Template<class T>
Class hasfriend
{
Friend void report(hasfriend<T> &);
}
这样当声明一个特定类型的对象时,将生成的具体化:
Hasfriend<int> hf;
编译器将使用int代替模板参数T,因此友元声明的格式如下:
Class hasfriend<int>
{
Friend void report(hasfreind<int> &);
…
};
即带hasfriend<int>参数的report()将成为hasfriend<int>类的友元,同样,带Hasfriend<double>参数的report()将是report的一个重载版本,它是hasffriend<double>类的友元。注意report()本身并不是模板函数,而只是使用了一个模板作为参数,这意味着必须为要使用的友元定义显示具体化:
Void report(hasfriend<short>&){…};//为short类型的显示具体化
Void report(hasfriend<int> &){…};//为int类型的显示具体化
假设hasfriend模板有一个静态成员ct。这意味着这个类的每一个特定的具体化都将有自己的静态成员。未提供模板类参数的友元函数,将是所有模板类具体化的友元函数:
Template<typename T>
Class hasfriend
{
Private:
…
Static int ct;
Public:
Friend void counts();
Friend void reports(hasfriend<T> &);
};
Template<typename T>
Int hasfriend:<T>::ct=0;
Count方法将是所有hasfriend具体化的友元,它报告两个特定的具体化.(hasfriend<int>::ct hasfriend<double>::ct )的ct的值。定义的时候该程序提供两个report函数,它们分别是某个特定的hasfriend具体化的友元。Void report(hasfriend<int> &lf) ,void report(hasfriend<double> & hf)
模板类的约束模板友元函数:
可以修改前一个示例,使友元函数本身成为模板,具体地说为约束模板友元作准备,要使类的每一个具体化都获得与友元匹配的具体化。这比非模板友元复杂一些,包含以下三步:
首先在类定义的前面声明每个模板函数。
Template<typename T> void counts();
Template<typename T>void report(T &);
然后在函数中再次将模板声明为友元。这些语句根据类模板参数的类型声明具体化。
Template<typename TT>
Class hasfriendT
{
…
Friend void counts<TT>();
Friend void report<>(hasfriendt<TT> &);
};
声明中的<>指出这是模板具体化。对于report<>可以为空,因为可以从函数参数推断出如下模板类型参数:
Hasfriendst<TT>
然而也可以使用
Report<hasfriendT<TT>>(hasfriend<TT> &)
但counts()函数没有参数,因此必须使用模板参数语法(<TT>)来指明其具体化。还需要注意的是TT是hasfriendst的参数类型。
假设声明如下对象:
Hasfriendt<int> squack;
编译器将用int替换TT,并生成下面定义:
Class hasfriendT<int>
{
Friend void counts<int>();
Friend void report<>(hasfriendT<int> &);
};
基于TT的具体化将变成int,基于hasfriend<TT>的具体化将变为hasfriend<int>。因此模板具体化counts() report<hasfriend<int>>()被声明为hasfriendt<int>类的友元。
模板类的非约束模板友元函数:
约束模板友元函数是在类外声明的模板的具体化,int类具体化获得int函数具体化。通过在类内部声明模板,可以创建非约束友元函数,即每个函数具体化都是每个类具体化的友元。对于非约束友元,友元模板类型参数与模板类类型参数是不同的。
Template<typename T>
Class manyfriend
{
Template<typename c,typename d>friend void show2(c&,d&);
};
函数调用show2(hfi1,hfi2)与下面的具体化匹配:
Void show2<manyfriend<int> &,manyfriend<int> &>(manyfriend<int> &c,manyfriend<int> &d);
因为它是所有的manyfriend具体化的友元,所以能够访问所有具体化的item成员,但它只访问了manyfriend<int>对象。
同样show2(hfi1,hfi2)还与以下具体化相匹配:
Void show2<manyfriend<double> &,manyfriend<double> &>(manyfriend<double> &c,manyfriend<double> &d);
它也是所有manyfriend具体化的友元,并访问了manyfriend<int>对象的item成员和manyfriend<double>对象的item成员。
模板别名:
可使用typedef为模板具体化指定别名:
Typedef std::array<double,12> arrd;
Typedef std::array<int,12> ari;
C++11可以使用模板提供一系列别名
Template<typename T>
Using arrtype=std::array<T,12>;
这将arrtype定义为一个模板别名可使用它来指定类型。
Arrtype<double> gallons;
Arrtype<int> days;
Arrtype<std::string> months;
Arrtype<T>表示类型arrtype<T,12>.
C++11允许将该语法用于非模板,此时与typedef等价
Typedef const char *pc1;
Using pc2=const char*;
Typedef const int*(*pa1) [10];
Using pa2=const int *(*)[10];