包含对象成员的类
我们通过一个例子来看看这个如何在一个类中包含其他对象成员的类,我们要定义一个学生的类,包含学生的姓名和学生的考试成绩,我们可以使用一个包含两个成员的类来表示它,一个是学生的姓名,另一个就是学生成绩,对于学生的姓名,我们可以使用字符数组,但是这样会限制长度,我们也可以进行动态内存分配,但是这样需要编写更多的代码来支持,我们还可以使用一个开发好的类的对象来表示,例如string类的对象,对于学生分数,也可以使用数组或者动态内存分配,还可以使用标准C++库中查找一个能够表示数据的类
C++标准就提供了这样的一个类,即valarray
这个类用于处理数值,它包含在头文件valarray中,这个类用于处理数值,支持将数组所有元素的值相加以及在数组中找到最大最小值等操作,valarray被定义为模版类,便于能够处理不同的数据,下面是这个类的一些方法
operator[]() :能访问各个元素
size() :返回包含的元素个数
sum() :返回包含元素总和
max() :返回最大的元素
min() :返回最小元素
valarray的用法
valarray<数据类型>数组名(数组容量)
例如double gpa[5] = {1.1 , 2.2, 3.3, 4.4, 5.5};
valarray<double>gpa ,声明一个double类型数组,没有指定大小,为空
valarray<double>gpa(5),数组的大小为5
valarray<double>gpa(2.2, 5),数组大小为5,且都初始化为2.2
我们将上面的例子进行代码实现
#include<iostream> #include<string> #include<valarray> using namespace std; class Student { private: string name; valarray<double>scores; public: //会调用string类构造函数,将字符串赋值给name //scores()就相当于创建了一个空数组 Student():name("NO Student"),scores(){} explicit Student(const string & s):name(s),scores(){} explicit Student(int n):name("NO "),scores(n){} Student(const string &s, int n):name(s),scores(n){} Student(const string &s, const valarray<double> &b):name(s),scores(b){} Student(const string &s, const double *pd, int n):name(s),scores(pd,n){} virtual ~Student(){}; double average() const {if(scores.size()) return scores.sum() / (double)scores.size(); else return 0;} const string & Name() const {return name;} double &operator[](int n){return scores[n];} friend istream &operator>>(istream &is, Student & stu); friend istream &getline(istream &is, Student & stu); friend ostream &operator<<(ostream &os, const Student & stu); }; istream &operator>>(istream &is, Student & stu) { is>>stu.name; return is; } istream & getline(istream &is, Student & stu) { getline(is,stu.name); return is; } ostream &operator<<(ostream &os, const Student & stu) { cout<<"Scores for:"<<stu.name<<" :"<<endl; int lim = stu.scores.size(); if(lim > 0) { for(int i = 0; i < lim ; i++) os<<stu.scores[i]<<" "; } else os<<"NULL\n"; return os; } void set(Student &stu, int n) { cout<<"Please enter the student's name: "; getline(cin,stu); cout<<"please enter 5 quiz scores:\n"; for(int i = 0; i < 5; i++) cin >> stu[i]; while(cin.get() != '\n'); } int main(void) { Student ada[3] = {Student(5),Student(5),Student(5)}; for(int i = 0; i < 3 ; i++) set(ada[i],5); cout<<"\nStudent Lisi : \n"; for(int i = 0; i < 3; i++) cout<<ada[i].Name(); for(int i = 0; i < 3; i++) { cout<<ada[i]<<endl; cout<<"average :"<<ada[i].average()<<endl; } return 0; }
在Student类中,我们使用string类和valarray类的对象,这就意味着Student类的成员函数可以使用string类和valarray类的公有接口来访问和修改Student类私有的name和scores,因为我们的私有成员是string和valarray类成员,因此我们在编写接口函数时就能去调用这两个类的成员函数去访问它们的值,但在类的外面并不能这么做,而是只能通过Student类的公有接口去访问name和scores,对于这种情况,通常就被描述为Student类获得了其成员函数的实现,但是没有继承接口,这就是一个类的对象使用了另外一个类的方法
在这里我们就是定义了Student类,我们就可以通过Student的类成员方法去操作name和scores,而name和scores是另外两个类,即string和valarray类的对象,在这里,我们定义的Student类并不是string类和valarray类的派生类,但是却可以通过这两个类的对象去使用这两个类的方法,这就实现了我们在一个类中使用了别的类的方法,但却不是派生类,这就是一个包含关系,对于包含关系来说,类对象不能自动获得包含对象的接口是一件好事,例如string类中的+运算符,用在Student类中将不同学生姓名或成绩相加并没有意义,这样就不适合公有继承
运行结果如下:
私有继承
上个例子中,我们在一个类的对象使用了另外一个类的方法,实现了包含关系,即has-a,在这里我们可以通过使用私有继承来实现,基类的公有成员和保护成员都会成为派生类的私有成员,这就意味着基类方法不会成为派生类对象公有接口的一部分,但是可以在派生类的成员函数中去使用,使用私有继承,基类的公有方法会成为派生类的公有方法,即派生类会继承基类的接口,但是使用私有继承,基类的公有方法会成为派生类的私有方法,即派生类不继承基类的接口函数
在声明一个派生类时,将基类的继承方法指定为private,称为私有继承,用私有继承的方式建立的派生类称为私有派生类,其基类称为私有基类
class 私有派生类名 : private 私有基类名
{
...
};
当我们声明为私有继承时,就代表原来能被外界引用的成员会隐藏起来,不让外界引用,因此,私有基类的公有成员和保护成员会成为派生类的私有成员
私有基类的私有成员只能被基类的成员函数引用,在基类外不能去访问,因为它们在派生类中是隐藏的,私有派生类如果想使用私有基类中的私有成员,只能通过私有基类的公有接口函数去访问它们
私有基类的公有成员和保护成员在私有派生类中的访问属性相当于派生类中的私有成员,即私有派生类的成员函数只能访问它们,而在私有派生类外不能访问它们,及时是私有对象也不行
使用多个基类的继承被称为多重继承,我们通过改写上一个例子的代码来进行说明,当实现私有继承时,声明就应该为
class Student : private string, private valarray<double>
因为在这里的Student类是两个类派生而来的,即string类和valarray<double>类,注意,在我们我们就不再需要在私有成员中去定义这两个类的对象name和scores了,因为两个基类中已经包含了这两个数据成员,我们上面的包含版本是提供了两个为显示命名的对象成员,而私有继承是提供了两个无名的对象成员,包含将对象作为一个命名的对象添加到类中,而私有继承将对象作为一个未命名的继承对象添加到类中,所以,它们都是获得了实现,但是没有获得接口,并且在内联构造函数中要使用类名,而不是成员名,因为我们新的Student是string和valarray<double>类的私有派生类,而我们派生类使用构造函数时需要先调用基类的构造函数完成基类成员的初始化,因为我们没有显式定义这两个基类的对象,这就需要显式的调用基类的构造函数去完成成员初始化列表,因此只能使用类名而不是对象名,使用私有继承时,只能在派生类中使用基类的方法,在上个例子中,我们是通过对象去调用方法,而在私有继承时,我们需要使用类名和作用域解析运费使得我们能够调用基类中的方法,当我们使用私有继承,要去访问对象时,就需要进行强制类型转换,在我们的例子中,由于Student类是string类派生来的,因为我们需要访问string类对象时,可以将Student类强制转换为string类的对象,这样的结果就是继承来的string对象,对于我们的私有继承,友元函数是不能去调用私有基类中的方法,只有通过强制类型转换后才能实现,也可以先调用私有派生类中的成员函数,再通过私有派生类的成员函数再去调用私有基类的方法
使用私有继承的方法对上面的代码进行改写:
include<iostream>
#include<string>
#include<valarray>
using namespace std;
class Student:private string, private valarray<double>
{
private:
ostream &arr_out(ostream &os) const;
public:
Student():string("NO Student"),valarray<double>(){}
explicit Student(const string & s):string(s),valarray<double>(){}
explicit Student(int n):string("NO "),valarray<double>(n){}
Student(const string &s, int n):string(s),valarray<double>(n){}
Student(const string &s, const valarray<double> &b):string(s),valarray<double>(b){}
Student(const string &s, const double *pd, int n):string(s),valarray(pd,n){}
virtual ~Student(){};
double average() const {if(valarray<double>::size() > 0) return valarray<double>::sum() / valarray<double>::size(); else return 0;}
const string & Name() const {return (const string &)*this;}
double &operator[](int n){return valarray<double>::operator[](n);}
friend istream &operator>>(istream &is, Student & stu);
friend istream &getline(istream &is, Student & stu);
friend ostream &operator<<(ostream &os, const Student & stu);
};
istream &operator>>(istream &is, Student & stu)
{
is>>(string &)stu;
return is;
}
istream & getline(istream &is, Student & stu)
{
getline(is,(string &)stu);
return is;
}
ostream & Student::arr_out(ostream &os) const
{
int lim = valarray<double>::size();
if(lim > 0)
{
for(int i = 0; i < lim ; i++)
os<<valarray<double>::operator[](i)<<" ";
}
else
os<<"NULL\n";
return os;
}
ostream &operator<<(ostream &os, const Student & stu)
{
cout<<"Scores for:"<<(const string &)stu<<" :"<<endl;
stu.arr_out(os);
return os;
}
void set(Student &stu, int n)
{
cout<<"Please enter the student's name: ";
getline(cin,stu);
cout<<"please enter 5 quiz scores:\n";
for(int i = 0; i < 5; i++)
cin >> stu[i];
while(cin.get() != '\n');
}
int main(void)
{
Student ada[3] = {Student(5),Student(5),Student(5)};
for(int i = 0; i < 3 ; i++)
set(ada[i],5);
cout<<"\nStudent Lisi : \n";
for(int i = 0; i < 3; i++)
cout<<ada[i].Name();
for(int i = 0; i < 3; i++)
{
cout<<ada[i]<<endl;
cout<<"average :"<<ada[i].average()<<endl;
}
return 0;
}
注意:因为我们并没有去改变派生类的接口,因此main函数中的测试内容就不需要再去修改
使用包含还是私有继承
由于包含和私有继承都可以建立包含关系,最好使用包含关系,因为包含更易于理解,它在类声明中包含表示被包含类的显式命名对象,代码可以通过名称直接引用这些对象,而继承处理这些问题会更加抽象,尤其是从多个基类继承时,可能必须处理很多问题,如包含同名方法的独立的基类或者共享祖先的独立基类,此外,包含能够包括多个同类的子对象,例如如果某个类需要3个string对象,可以包含3个独立的string成员,而私有继承只能使用一个这样的对象,且没有名称,很难区分
但是,私有继承提供的特性比包含多,例如,假设类包含保护成员,则这样的成员在派生类中是可用的,但是继承层次的结构外不可用,如果使用组合将这样的类包含在另外一个类中,则后者不是派生类,而是在继承层次结构之外,因此不能访问,但是通过继承得到的是派生类,就能够去访问被包含的成员,并且有时候我们需要使用私有继承去定义虚函数,派生类可以定义虚函数,但包含类不能,使用私有继承,重新定义的函数只能在类中使用,不是公有的,因此
我们通常应该使用包含来建立has-a(某个类包含了别的类中的某些方法)关系,如果新类需要访问原有类的保护成员,或者需要重新定义虚函数,则应该使用私有继承
保护继承
保护继承是私有继承的变体,其在列出基类时使用关键字protected,使用保护成员protected可以让派生类能够访问基类的成员,但是其他类或外部函数不能直接访问这些成员,格式如下
class 保护继承名 : protected 类名
{
....
}
在使用保护继承时,基类的公有成员和保护成员都会成为派生类的保护成员,与私有继承一样,基类的接口在派生类中也是可以使用的,但是在继承层次的结果之外不可用,当从派生类中生成另一个类时,使用私有继承,第三代不能使用基类的接口,而使用包含继承时,基类的公有方法在第二带中也变为受保护的,因此在第三代中可以使用
使用using重新定义访问权限
当使用包含派生或者私有派生时,基类的公有成员会成员保护成员或者私有成员,如果我们要让基类的方法在派生类外可以使用,方法之一就是使用该基类方法的派生类方法
在这里重载<<不是类成员函数,我们使用的方法就是重新定义了一个arr_out函数,它属于私有派生类中的成员函数,我们通过调用派生类成员函数,即arr_out函数去访问私有基类中的size函数
另外 一种方法就是将函数调用包装在另一个函数调用中,即使用using声明,来指出派生类可以使用特定的基类成员,即使基类采用的是私有派生,例如
class Student : private string, private valarray<double> {
public:
using valarray<double>::min;
using valarray<double>::max;
....
}
上面的using valarray<double>::min;和using valarray<double>::max;使得valarray<double>中的max和min可用,就像是Student类的公有成员一样,其他的类也可以通过Student类的对象去使用valarray<double>类中的max和min两种方法,这样,我们就可以改写我们的函数,不需要再去使用基类的派生类方法,而是使用using指令
注意:
using 声明只使用成员名——没有圆括号,函数特征标和返回值
多重继承
在我们上面所写的Student类中已经使用了多重继承的方法,即Student类继承了string类和valarray<double>类,使用多重继承可能会带来一些问题
1.从两个不同的基类继承同名的方法,例如string类和valarray<double>中都有size函数,那么调用size时是调用的哪个类呢?
2.从两个或者更多的相关基类那里继承同一个类的多个实例,例如A是一个抽象基类,A派生出B类和C类,B类和C类又共同创建了一个D类,那么A中的一个方法就出现了多次,这就会导致一些问题,我们下面通过一个例子来看看这个问题
我们先定义了一个Worker类,再从Worker类中派生出Singer类和Waiter类,最后再从Singer和Waiter中派生出一个SiningWaiter类,这样就出现了问题
class Worker
{
private:
string name;
long id;
public:
Worker():name("NULL"),id(0){}
Worker(const string &s, long n):name(s),id(n){}
virtual ~Worker(){}
virtual void set()
{
cout<<"enter Worker name: ";
getline(cin,name);
cout<<"enter Worker id: ";
cin>>id;
while(cin.get() != '\n');
}
virtual void show() const
{
cout<<"name: "<<name<<" id: "<<id<<endl;
}
};
class Waiter : public Worker
{
private:
int panache;
public:
Waiter():Worker(),panache(0){}
Waiter(const string &s, long n, int p):Worker(s,n),panache(0){}
Waiter(const Worker & wk, int p = 0):Worker(wk),panache(0){};
virtual void set()
{
Worker::set();
cout<<"enter waiter panache rating : ";
cin>>panache;
while(cin.get() != '\n');
}
virtual void show() const
{
Worker::show();
cout<<"panache : "<<panache<<endl;
}
};
class Singer : public Worker
{
protected:
enum{A,B,C,D,E,F,G};
static const int Vtypes = 7;
private:
int voice;
public:
Singer():Worker(),voice(0){}
Singer(const string & s, long n, int vo = 0):Worker(s,n),voice(vo){}
Singer(const Worker &wk, int vo):Worker(wk),voice(vo){}
virtual void set()
{
Worker::set();
cout<<"please enter a value : 0 ~ 7\n";
cin>>voice;
}
virtual void show() const
{
Worker::show();
cout<<"voice : "<<voice<<" :"<<voice + '0' <<endl;
}
};
在SiningWaiter中包含了多少个Worker?
因为Singer和Waiter都继承于一个Worker组件,因此SiningWaiter会包含两个Worker,而Worker类包含的是姓名和id号,这样就会导致我们的SiningWaiter包含了两个姓名和ID,而我们只需要一个姓名和id就足够了,那应该如何解决呢?
图片来自C++primer plus
C++引入了虚基类的方法
虚基类
虚基类使得从多个类(它们的基类相同)派生出的对象只继承一个基类对象
这样SiningWaiter只会包含一个Worker的副本,从本质来说,继承的Singer类和Waiter类共享了一个Worker类对象,而不是各自引入了Worker对象的副本,创建的过程如下所示
当我们使用虚基类时,需要对构造函数采用一种新的方法,对于非虚基类,可以唯一出现在初始化列表中的构造函数即时基类构造函数,但是这些构造函数需要将信息传递给其基类 ,对于我们的例子,即虚基类,SiningWaiter的构造函数就只能调用Singer类和Waiter类的构造函数,但是Singer类和Waiter类是不会再去调用Worker类的构造函数,否则
就像这样,wk通过两条不同的途径传递给基类Worker,即它为初始化两次,为了解决这个问题,我们需要显式调用基类的构造函数,即如下所示
class SiningWaiter : public Waiter,public Singer
{
public:
SingingWaiter(const Worker &wk, long n,int p = 0, int v =0):Worker(wk,n),Waiter(wk,n,p),Singer(wk,n,v){}
};
注意:对于虚基类,这样是合法的,否则是非法的,因为编译器会自动传递
对于上面的虚基类,我们都定义了show函数,当SiningWaiter没有重新定义show方法,并且尝试通过SiningWaiter对象去调用show时,就会出现二义性,因为在多重继承中,每个祖先都有一个show函数,我们为了防止出现这个问题,可以使用作用域解析运算符,如下所示
SiningWaiter A("a",2002,6,0);
A.Singer::show();
对于更好的方法是,在SiningWaiter也对show进行定义,但是这样也会出现一些问题,即如下
更改SiningWaiter中的show
void SiningWaiter::show() { Singer::show(); }
这样只显示了Singer中的内容
当再次进行更改时
void SiningWaiter::show() { Singer::show(); Waiter::show(); }
这样同时显示了两者的内容,但是这样又会导致重复显示Worker,这样的解决方法是使用模块化的方法,即只提供一个Worker组件的方法和一个只显示Singer组件和Waiter组件的方法,即只显示自己类中独有的部分,最后再进行组合
类模版
C++的类模版为生成通用的类声明提供了一种更好的方法,模版提供参数化类型,即能够将类型名作为参数传递传递给接收方或者建立类或函数
定义模版类
当我们定义模版类时,使用模本定义来替换类的声明,使用模版成员函数替换类成员函数,和模版函数一样,模版类的开头如下
template <class Type>
关键字template告诉编译器,要定义一个模版,尖括号中的内容相当于函数的参数列表,可以将关键字class看做是变量的类型名,该变量接收类型作为它的值,把Type看做是变量的名称,注意,这里使用了class并不代表着class是一个类,只是表明了Type是一个通用的类型说明符,在使用模版时,将会使用实际的类型来替换它
类模版并没有真正的定义一个类,只能创建了一个类的架子,在实际使用过程中,我们需要往这个架子里填充参数,补全内容,让这个架子实例化,例如我们定义了一个栈的模版类
template<class Type>
class Stack
{
private:
Type stack[10];
int top;
public:
Stack();
bool isfull() const;
bool isempty() const;
void push(Type & stack);
Type pop()
};
函数实现:当我们使用模版成员函数替换原有类方法时,需要在每个函数头前进行相同的模版声明,并且需要使用我们定义的泛型名Type,还需要将类限定符从Stack::改为Stack<Type>::,如果我们在函数声明时就将函数的定义给实现了,那么则不需要进行这些修改,即模版声明和类限定符
template<class Type>
Stack<Type>::Stack() : top(-1) {}
template<class Type>
bool Stack<Type>::isfull() const {
return top == 9;
}
template<class Type>
bool Stack<Type>::isempty() const {
return top == -1;
}
template<class Type>
void Stack<Type>::push(Type & item) {
if (!isfull())
{
stack[++top] = item;
}
else
{
std::cout << "Stack is full!" << std::endl;
}
}
template<class Type>
Type Stack<Type>::pop() {
if (!isempty())
{
return stack[top--];
}
else
{
std::cout << "Stack is empty!" << std::endl;
return Type();
}
}
注意:我们上面只是列出来类模版和成员函数模版,这些模版并不是类和成员函数,它们是C++编译器的指令,说明了如何生成类和成员函数定义,模本的具体实现称为实例化或者具体化,不能将模版成员函数放在独立的文件中实现,由于模版不是函数,因此不能单独编译,模版必须与特定的模版实例化请求一起使用,因此,最简单的方法便是将所有的模版信息都存放在一个头文件中,并在要使用这些模版的文件中包含这些头文件
使用模版类
仅在程序中包含模版并不能生成模版类,因此我们必须进行实例化,使用就需要声明一个类型作为模版类的对象,方法就是使用具体的类型来替换泛型名,例如
Stack<int>A; Stack<string>B;
这代表创建了两个栈,一个存储int类型,一个存储string对象,当出现这样的声明后,编译器会按照Stack<Type>模版来生成两个独立的类声明和两组独立的类方法,我们可以通过对象A和B分别去调用两个实例化的类中的公有成员函数
数组模版示例和非类型参数
模版通常是一个容器,因为类型参数的概念使得它非常适合用于将相同的方案存储于不同的类型,为容器模版类提供可重用代码是引入模版类的主要动机,所以我们接下来来看一个例子,更深入学习的模版设计和使用的几个方面,即学习一些表达式参数以及如何使用数组处理继承类
我们想设计一个可以指定一个可以指定大小的简单数组模版,一种方法是在类中使用动态数组已经构造函数来提供参数的数目,一种是使用模版参数来提供常规的数组大小,C++11的array就是使用的第二种方法,我们通过一个例子来看看表达式参数(非类型参数)
在头文件中定义模版类#ifndef _ARRAY_H_ #define _ARRAY_H_ #include<iostream> using namespace std; template<class T, int n > class ArrayTP { private: T ar[n]; public: T & operator[](int i); T & operator[](int i)const; }; template<class T, int n> T & ArrayTP<T,n>::operator[](int i) { return ar[i]; } template<class T, int n> T & ArrayTP<T,n>::operator[](int i) const { return ar[i]; } #endif
在template<class T, int n>中
关键字class指出T为类型参数,int指出n的类型为int,这种参数是非类型参数或者表达式参数,表达式参数可以是整型,枚举,引用或者指针吗,但是不能是实型,也不能是参数的地址,意味着它在编译时必须是已知的、固定的值,在我们实例化模版时,用作表达式参数的值必须是常量表达式
在测试文件中
#include<iostream> #include"array.h" using namespace std; int main() { ArrayTP<int , 10>sum; ArrayTP<double, 10>aves; ArrayTP<ArrayTP<int,5>,10>twodee; int i, j; for(i = 0; i < 10; i++) { sum[i] = 0; for(j = 0; j < 5; j++) { twodee[i][j] = (i+1)*(j+1); sum[i] += twodee[i][j]; } aves[i] = (double)sum[i] / 10.0; } for(i = 0; i < 10 ; i++) { for(j = 0; j < 5; j++) { cout<<twodee[i][j]<<" "; } cout<<" :sum "<< sum[i]<<" , average :"<<aves[i]<<endl; } return 0; }
在这段代码中,我们定义了两个类,
ArrayTP<int , 10>sum; ArrayTP<double, 10>aves;这两条语句会使得编译器定义了名字为ArrayTP<int , 10>与ArrayTP<double,10>的两个类,并且创造了两个类对象
递归使用模版
ArrayTP<ArrayTP<int,5>,10>twodee;这条语句表明,twodee是一个二维数组,这就与twodee[10][5]的用法一样
使用多个类型参数
当我们希望类能够保存多种类型的值,就可以使用多个类型参数,以两个类型参数为例
#include<iostream> #include<string> using namespace std; template<class T1, class T2> class C { private: T1 a; T2 b; public: C(const T1 & t1, const T2 & t2):a(t1),b(t2){} T1 &first(); T2 &second(); }; template<class T1, class T2> T1 &C<T1,T2>::first() { return a; } template<class T1, class T2> T2 &C<T1,T2>::second() { return b; } int main() { //使用构造函数创建类的对象 C<string,int>rating[4] = { C<string,int>("A",1), C<string,int>("B",2), C<string,int>("C",3), C<string,int>("D",4), }; int n = sizeof(rating)/sizeof(C<string,int>); for(int i = 0; i < n; i++) { cout<<rating[i].first()<<endl; } return 0; }
模版具体化
类模版分为三种,即隐式实例化,显式实例化,显示具体化,模拟以泛型的方式来描述类,而具体化是以具体的类型生成类声明
隐式实例化
我们上面写出的例子都是使用的隐式实例化,它们声明一个或者多个对象,指出所需的类型,而编译器使用通用模版提供的处方生成具体的类定义
例如 ArrayTP<int , 10>sum,即当我们对一个类的模版没有创造出对象时,模版类没有真正使用时就不会实例化,也不会去使用内存,当这个模版类真正使用时,才会生成类定义,并根据定义创造对象
显示实例化
当使用关键字template并且指出所需的类型来声明类时,编译器会生成类声明的显式实例化,声明必须位于模版定义的名称空间中,例如 template class ArrayTP<int , 10>,一般放在头文件中,在这种情况下,虽然没有创建类的对象,但是编译器也会生成类声明,也会根据通用模版来生成具体化,这就相当于编译器预先准备好了这个类,我们可以直接进行使用,而隐式实例化是相当于不使用就不准备,当使用到的时候才去创建类
显式具体化
显式具体化是特定类型(替换模版中的泛型)的定义,有时候需要在特殊类型实例化时,对模版进行修改,使得其行为不同,在这种情况下,就可以使用显式具体化,即我们已经有了模版类的定义时,但是觉得这个类的定义有些地方不太好,我们想去重新修改这个定义,就可以使用显式具体化,格式如下
template<>,在<>中的类型是我们要修改的类型,例子如下
template<class T1, class T2> class A { public: void show(); }; template<class T1, class T2> void A<T1, T2>::show() { cout<<"NULL\n"; } //显式实例化 template class A<int, int>; //显式具体化 template<> class A<double,double> { public: void show(); }; void A<double, double>::show() { cout<<"NNNNN\n"; }
部分具体化
可以给类型参数之一指定具体的类型,例子如下
template<class T1> class A<T1,char> { public: void show(); }; template<class T1> void A<T1,char>::show() { cout<<" yyyyy\n"; }
在这里只指定了T2的类型必须为char,而T1的类型不管是什么都可以
成员模版
模版可以用作结构,类或者模版类的成员,我们可以通过一个简单的成员来看看这是如何实现的,这个程序是一个模版类将另一个模版类和模版函数作为成员
template<class T>
class betas
{
private:
// 内部模板类 hold,用于存储值
template<class V>
class hold
{
private:
V val; // 成员变量 val,用于存储值
public:
// 构造函数,初始化 val,默认参数为 0
hold(V v = 0) : val(v) {}
// 显示值的函数
void show() const { cout << val << endl; }
// 返回值的函数
V value() const { return val; }
};
hold<T> q; // 类型为 hold<T> 的成员变量 q,根据 betas 传入的类型 T 而定
hold<int> n; // 类型为 hold<int> 的成员变量 n,类型固定为 hold<int>
public:
// 构造函数,初始化 q 和 n
betas(T t, int i) : q(t), n(i) {}
// 显示 q 和 n 的值
void Show() const { q.show(); n.show(); }
// 模板函数,求和
template<class U>
U add(U u, T t){return (q.value()+n.value()) *u / t;}
};
这种在模板类内部定义模板类和模板函数的方法,使得代码更加灵活和通用,可以根据需要处理不同类型的数据
将模版作为参数
模版可以包含类型参数和非类型参数,例如template<class T, int n>,此外,模版还可以包含本身就是模版的参数,因为模版类本身也就是一种类型,例如
template<template<class T> class V>
template<class T> class V
:这部分定义了模板模板参数V
,它是一个模板类,其中包含一个类型参数T,V是参数
template<>
:这个关键字表明后面是一个模板参数声明
<>
:模板参数列表,用于指定模板类V
接受的参数类型
我们可以通过一个栈的例子来看看模版类作为参数
#include <iostream>
// 定义一个简单的栈类模板
template <typename T>
class Stack {
private:
static const int MAX_SIZE = 100; // 假设最大容量为100
T items[MAX_SIZE]; // 使用数组存储栈元素
int top; // 栈顶索引
public:
Stack() : top(-1) {} // 构造函数
void push(const T& item) { // 入栈操作
if (top < MAX_SIZE - 1) {
items[++top] = item;
} else {
std::cout << "Stack overflow!" << std::endl;
}
}
T pop() { // 出栈操作
if (top >= 0) {
return items[top--];
} else {
std::cout << "Stack underflow!" << std::endl;
return T(); // 返回默认值
}
}
bool isEmpty() const { // 检查栈是否为空
return top == -1;
}
};
// 使用模板类作为参数的示例
template <template<class> class StackType, typename T>
class Container {
private:
StackType<T> stack; // 使用模板类作为参数
public:
void addElement(const T& element) { // 添加元素到栈中
stack.push(element);
}
T removeElement() { // 从栈中移除元素
return stack.pop();
}
bool isEmpty() const { // 检查栈是否为空
return stack.isEmpty();
}
};
int main() {
Container<Stack, int> intStackContainer; // 创建一个容器,使用整型栈
// 向容器中添加元素
intStackContainer.addElement(10);
intStackContainer.addElement(20);
intStackContainer.addElement(30);
// 从容器中移除元素并打印
while (!intStackContainer.isEmpty()) {
std::cout << intStackContainer.removeElement() << std::endl;
}
return 0;
}
这个示例展示了一个 Stack
类,一个 Container
类以及一个使用整型栈的示例, Container
类接受一个模板类作为参数,然后通过创建相应类型的栈来实现其功能
模版类和友元
在模版类中也可以去使用友元函数,模版的友元分为3种,分别是非模版友元,约束友元(友元的类型取决于类被实例化时的类型),非约束友元(友元的所有具体化都是类的每一个具体化的友元),下面分别来学习一下
1.模版类的非模版友元
我们可以在一个模版类中将一个常规函数声明为一个友元
template<class T> class HAS { ... public: friend void cout(); }
这个声明会让cout函数成为模版所有实例化的有元,例如类HAS<int>或者类HAS<char>的有元,cout不是通过函数调用的,也没有对象参数
2.模版类的约束友元模版
我们可以让友元函数本身成为模版,即为约束模版友元做准备,使得类的每个具体化都获得与友元的匹配,步骤如下
1.在类定义前声明每个模版函数
template<class T> void cout();
2.在函数中再次将模版声明为友元,这些语句会根据类模版参数的类型声明具体化
template<template T> class HAS { .... friend void cout<T>(); }
3.为友元提供模版定义
3.模版类的非约束友元模版
板类的非约束模板友元是指一个模板类中声明的模板友元,它可以访问该模板类的私有成员,并且不受模板参数的影响