C++范型编程(范式编程)
所谓的泛型编程,指我们在编写程序时,是独立于任何特定类型的,即在编程时,没有数据类型的概念,没有特定的数据类型,数据类型就被当成一个参数
例如设计一个数组类型,这个数组可能只能存放 int 类型的数据,但是我们在使用数组时,不一定总是用 int 数组,可能需要其他类型的数组即,数组是一个通用的概念,不应该局限于某种特定的类型(如 int 类型),如果,设计出来的数组,能存放任意类型的元素,那么这就是泛型。
在 C++中,用模板来实现泛型,或者说 用模板来表示通用的概念。
模板
模板允许我们在定义函数或者类时,将数据类型当做参数,编译器会根据你提供的参数类型自动生成特定的函数或者类。
使用模板的目的就是能够让程序员编写与类型无关的代码。比如编写了一个交换两个整型int 类型的 swap 函数,这个函数就只能实现 int 型,对 double,字符这些类型无法实现,要实现这些类型的交换就要重新编写另一个 swap 函数。使用模板的目的就是要让这程序的实现与类型无关,比如一个 swap 模板函数,即可以实现 int 型,又可以实现 double 型的交换。模板可以应用于函数和类。下面分别介绍。
注意:模板的声明或定义只能在全局,命名空间或类范围内进行。即不能在局部范围,函数内进行,比如不能在 main 函数中声明或定义一个模板。
函数模板
函数模板的定义格式:
template <class 形参名,class 形参名,......>返回类型 函数名(参数列表){函数体}
注意:template <…>与后面的模板函数是一体的,只不过是为了美观将它们分开写,也就是这中间是不能插入任何语句的。
其中 template 和 class 是关键字,当然 class 可以用 typename 代替(一般也是用 typename,因为一般使用 class 来表示类)。<>括号中的参数叫模板形参,模板形参和函数形参很相像,模板形参不能为空。一但声明了模板形参去声明函数形参,即可以在该函数中使用内置类型的地方都可以使用模板形参名。模板形参需要调用该模板函数时提供的模板实参来初始化模板形参,一旦编译器确定了实际的模板实参类型就称他实例化了函数模板的一个实例。比如 swap 的模板函数形式为:
template <class T>
void swap(T& a, T& b)
{
T x;
x = a;
a = b;
b = x;
};
当调用这样的模板函数时类型 T
就会被被调用时的类型所代替,比如
swap(a,b)
其中
a
和
b
是
int
型,这时模板函数
swap
中的形参
T
就会被
int 所代替,模板函数就变为
swap(int &a, int &b)
。而当
swap(c,d)
其中
c
和
d
是
double
类型时,模板函数会被替换为 swap(double &a, double &b)
,这样就实现了函数的实现与类型无关的代码。
int main(int argc, char const *argv[])
{
swap(1,2); //整数交换
swap(2.3,5.7); //浮点数交换
swap(2,3.14); //error,两个类型不同的数交换
return 0;
}
类模板
函数可以有模板,类也可以拥有模板,类的模板格式为:
template < typename 形参名 , typename 形参名 ,…>class 类名{ ... };
类模板和函数模板都是以 template
开始后接模板形参列表组成,模板形参不能为空,一但声明了类模板就可以用类模板的形参名声明类中的成员变量和成员函数,即可以在类中使用内置类型的地方都可以使用模板形参名来声明。比如
template<typename T1,typename T2>
class A
{
private:
T1 a;
T2 b;
public:
A(T1 x,T2 y):a(x),b(y){}
T1 fun()
{
cout << a << endl;
return a;
}
void sun(T1 a1,T2 a2);
};
template<typename T1,typename T2>
void A<T1,T2>::sun(T1 a1,T2 a2)
{
a = a1;
b = a2;
}
如果模板类的成员函数需要在类外定义,则该成员函数必须有模板声(template<typename Type>)且作用域(模板类名)也得指明模板的参数,如下面的 sun 函数。
调用函数模板,只要提供数据(值),编译器会根据用户提供的值,自动推导数据类型再生成对应版本的函数,使用类模板的时候,必须显示的指定模板参数。例:
int main(int argc, char const *argv[])
{
A<int,int> a(3,4); //正确
A<2,4> b(2,4); //错误,类模板<>中不能根据实际数据推导类型
A<int,double> c(3,3.14); //正确
A<int,double> d(3,"swe"); //错误,指定的类型一定要传递相应的数据
return 0;
}
特别注意的是:
无论是函数模板还是类模板,如果模板函数/模板类的声明和定义要分开,
只能分开在同一个文件的不同位置,不能把声明和定义写在不同的文件中,也就是,不能在头
文件中声明模板函数/类,然后去源文件中定义模板函数/类,即模板的声明和定义必须在同一
个文件中,意味着模板实现的功能必须开源。
类模板默认参数
可以为类模板的类型形参提供默认值,但不能为函数模板的类型形参提供默认值。函数模板和类模板都可以为模板的非类型形参提供默认值。
类模板的类型形参默认值形式为:template<class T1, class T2=int> class A{};
为第二个模板类型形参 T2
提供
int
型的默认值。
类模板类型形参默认值和函数的默认参数一样,如果有多个类型形参则从第一个形参设定了默认值之后的所有模板形参都要设定默认值,比如 template<class T1=int, class
T2>class A{};
就是错误的,因为
T1
给出了默认值,而
T2
没有设定。
这个可以对比函数默认参数进行学习,其作用就是在定义模板类对象的时候,可以按照需求来传递类型参数给到模板类。例如:
template<typename T1,typename T2 = int>
class A
{
private:
T1 a;
T2 b;
public:
A(T1 x,T2 y):a(x),b(y){}
};
int main(int argc, char const *argv[])
{
A<int,char> a(3,'3');
A<double> b(3.14,5); //可以只传一个参数
return 0;
}
非类型参数模板
非类型模板形参:
模板的非类型形参也就是内置类型形参
,如:
template<typename T, int a>
class B{};
其中
int a
就是非类型模板参数。
非类型形参在模板定义的内部是常量值,也就是说非类型形参在模板的内部是常量。
例如:
template<typename T, int a>
class B
{
private:
T a;
int buf[a];
};
使用时,必须给非类型参数传递常量值:
B < double , 6 > b ;
此外, 非类型模板的形参只能是整型,指针和引用
,像 double,String, String **这样的
类型是不允许的。
但是
double &,double *,
对象的引用或指针是正确的。
任何局部对象,
局部变量,局部对象的地址,局部变量的地址都不是一个常量表达式,都不能用作非类型
模板形参的实参
。全局指针类型,全局变量,全局对象也不是一个常量表达式,不能用作非类型模板形参的实参。全局变量的地址或引用,全局对象的地址或引用 const 类型变量是常
量表达式,可以用作非类型模板形参的实参
。
sizeof
表达式的结果是一个常量表达式,也能用作非类型模板形参的实参
非类型形参一般不应用于函数模板中,比如有函数模板
template<class T, int a> void
h(T b){}
,若使用
h(2)
调用会出现无法为非类型形参
a
推演出参数的错误,对这种模板函数可以用显示模板实参来解决,如用 h<int, 3>(2)
这样就把非类型形参
a
设置为整数
3
。
非类型模板形参的形参和实参间所允许的转换:
1、允许从数组到指针,从函数到指针的转换。如:template <int *a> class A{}; int b[1];
A<b> m;即数组到指针的转换
2、const 修饰符的转换。如:template<const int *a> class A{}; int b; A<&b>
m; 即从 int *到 const int *的转换。
3、提升转换。如:template<int a> class A{}; const short b=2; A<b> m; 即从 short
到 int 的提升转换
4、整值转换。如:template<unsigned int a> class A{}; A<3> m; 即从 int 到
unsigned int 的转换。
5、常规转换。