模板类常用于实现容器类,因为类型参数的概念非常适合于把相同的存储方案用于不同的类型。并且C++最初引入模板其实主要的目的之一就是重用容器类的代码。
文章目录
数组模板
写一个可指定数组大小的简单数组模板,用两种方法实现数组大小的可变,给出了两种方法的对比,也通过这两个方法穿插介绍了新的知识:类模板的非类型参数。
一,构造函数方法:使用动态数组和构造函数提供元素数目(使用堆内存,更通用)
上篇文章的Stack模板类用于指针栈就是这么做的。
-
构造函数方法的缺点:用new和delete管理堆内存,即使用的是动态的堆内存在实现栈。
-
构造函数方法的优点:
- 不同数组大小都只生成一个类。即
Stack<int> egg(10);
和Stack<int> eggs(30);
会只生成一个独立的类声明和类定义。 - 更加通用。因为数组的大小是作为类成员存储在类定义中的。而不是硬编码(表达式参数方法)。所以我们就可以把一种尺寸的数组赋给另一种尺寸的数组。(???我没懂怎么做到)
二,表达式参数方法:用模板参数提供常规数组的大小(使用自动内存,速度更快)
C++11新增的array类(模板类)就这么做的。
- 表达式参数方法的优点:创建的栈是用的自动内存,即使用的都是自动变量。这种方法速度比new,delete的堆内存更快。
- 表达式参数方法的缺点:每种数组大小都将生成自己的类。即
ArrayTP<double, 10> egg
和ArrayTP<double, 11>
会生成两个独立的类声明和类定义。
代码
//arraytp.h
#ifndef ARRAYTP_H_
#define ARRAYTP_H_
#include <iostream>
#include <cstdlib>
template <typename Type, int n>//使用模板参数提供数组长度;
//关键字typename指出Type是类型参数,被用作泛型名
//而n是非类型参数,也被称为表达式参数
class ArrayTP
{
private:
Type ar[n];
public:
ArrayTP(){};//不写也行
explicit ArrayTP(const Type & t); //把数组的所有元素都初始化为t的值
virtual Type & operator[](int i);
virtual Type operator[](int i) const;
};
template <typename T, int n>
ArrayTP<T, n>::ArrayTP(const T & t)
{
int i;
for (i = 0; i < n; ++i)
ar[i] = t;
}
template<typename T, int n>
T & ArrayTP<T, n>::operator[](int i)
{
//首先判断输入是否合法
if (i < 0 || i >= n)
{
std::cerr << "Error in array limits: " << i << " is out of range!\n";
std::exit(EXIT_FAILURE);
}
else
return ar[i];
}
template <typename T, int n>
T ArrayTP<T, n>::operator[](int i) const
{
//首先判断输入是否合法
if (i < 0 || i >= n)
{
std::cerr << "Error in array limits: " << i << " is out of range!\n";
std::exit(EXIT_FAILURE);
}
else
return ar[i];
}
#endif
非类型参数(表达式参数)
这一次的模板头template <typename T, int n>
和之前template <typename T>
不一样,之前只有用关键字class或者typename声明的类型参数,即泛型名。这一次还有int n。这个n就是非类型参数non-type parameter,也叫做表达式参数expression parameter。
如果在外部程序这样声明:ArrayTP<double, 10> apple;
,那么编译器就会根据模板类ArrayTP的代码,定义一个名为ArrayTP<double, 10>
的类(用double代替T,用10代替n),并创建一个ArrayTP<double, 10>
类的对象apple
。
-
表达式参数只能是整型,枚举,引用或者指针。
不可以是float,double等类型。比如:double m
不合法,但是double *n
合法,double &n
也合法。 -
模板代码并不可以修改非类型参数的值。也不可以使用参数的地址。
所以n++
,&n
一类的表达式是不可以出现在ArrayTP模板的。 -
实例化模板时,用作表达式参数的值必须是常量表达式。
把常规类的技术用于模板类(体现了模板的多功能性,高级的同时有点眩晕)
这部分内容真是太牛逼了······
高级到让人眩晕在C++模板的知识海洋中······
下面说几个把常规类的技术用于模板类的例子。牛逼还是模板类牛逼,高级多面手
把模板类用作基类或者组件类
竟然可以继承一个模板类,即一个通用说明,并不是实际存在的类,大概是继承时说明类型然后才生成一个实实在在的具体的实例类用作基类吧。组件类也是一样。编译器会先根据类型信息实例化一个类出来,才把他作为组件类做后面的事情。
template <typename Type>
class Array
{
private:
Type entry;
···
};
template <typename T>
class GrowArray: public Array<T> // 把Array<T>模板类作为基类
{···};
把模板类用作组件类
template <typename Tp>
class Stack
{
Array<Tp> ar; //把模板类Array<Tp>用作模板类Stack<Tp>的组件,即成员,即代码重用方法中的包含法
};
把模板类用作其他模板类的类型参数
···高,实在是高
比如,用数组模板实现栈模板,或者用数组模板来构造数组(即数组元素是基于栈模板的栈)
Array <Stack<int>> apple; //把模板类Stack<Tp>的示例类Stack<int>作为类型参数传给模板类Array<Type>
递归使用模板
比如,可以用来创建二维数组
ArrayTP <ArrayTP <int, 5>, 10> apple;
apple是一个有10个元素的数组,每个元素都是一个有5个int元素的数组,类似于常规数组声明:int apple[10][5];
示例 (测试上面的ArrayTP类)
ArrayTP类代码
//arraytp.h
#ifndef ARRAYTP_H_
#define ARRAYTP_H_
#include <iostream>
#include <cstdlib>
template <typename Type, int n>//使用模板参数提供数组长度;
//关键字typename指出Type是类型参数,被用作泛型名
//而n是非类型参数,也被称为表达式参数
class ArrayTP
{
private:
Type ar[n];
public:
ArrayTP(){};//不写也行
explicit ArrayTP(const Type & t); //把数组的所有元素都初始化为t的值
virtual Type & operator[](int i);
virtual Type operator[](int i) const;
};
template <typename T, int n>
ArrayTP<T, n>::ArrayTP(const T & t)
{
int i;
for (i = 0; i < n; ++i)
ar[i] = t;
}
template<typename T, int n>
T & ArrayTP<T, n>::operator[](int i)
{
//首先判断输入是否合法
if (i < 0 || i >= n)
{
std::cerr << "Error in array limits: " << i << " is out of range!\n";
std::exit(EXIT_FAILURE);
}
else
return ar[i];
}
template <typename T, int n>
T ArrayTP<T, n>::operator[](int i) const
{
//首先判断输入是否合法
if (i < 0 || i >= n)
{
std::cerr << "Error in array limits: " << i << " is out of range!\n";
std::exit(EXIT_FAILURE);
}
else
return ar[i];
}
#endif
#include <iostream>
#include "arraytp.h"
int main()
{
using std::cout;
ArrayTP <int, 10> sums;
ArrayTP <double, 10> aves;
ArrayTP <ArrayTP <int, 5>, 10> twodee;
int i, j;
for (i = 0; i < 10; ++i)
{
sums[i] = 0;
for (j = 0; j <5; ++j)
{
twodee[i][j] = (i + 1) * (j + 1);
sums[i] += twodee[i][j];
}
aves[i] = (double) sums[i] / 5;
}
for (i = 0; i < 10; ++i)
{
for (j = 0; j < 5; ++j)
{
cout.width(2);
cout << twodee[i][j] << ' ';
}
cout << ": sum = ";
cout.width(3);
cout << sums[i] << ", average = " << aves[i] << std::endl;
}
cout << "Done!\n";
return 0;
}
输出
1 2 3 4 5 : sum = 15, average = 3
2 4 6 8 10 : sum = 30, average = 6
3 6 9 12 15 : sum = 45, average = 9
4 8 12 16 20 : sum = 60, average = 12
5 10 15 20 25 : sum = 75, average = 15
6 12 18 24 30 : sum = 90, average = 18
7 14 21 28 35 : sum = 105, average = 21
8 16 24 32 40 : sum = 120, average = 24
9 18 27 36 45 : sum = 135, average = 27
10 20 30 40 50 : sum = 150, average = 30
Done!
模板类使用多个类型参数
前面的模板都只有一个类型参数,但是之前函数模板是有遇到过两个类型参数的函数模板的,类模板也可以,用于设计和创建需要存储两个不同类型值的模板。比如C++的标准模板库就提供了一个叫做pair的模板。下面我们也来写一个类似于pair的类模板。
代码
//pairs.h -- a pair template
#include <iostream>
#include <string>
template <typename T1, typename T2>
class Pair
{
private:
T1 a;
T2 b;
public:
Pair(){}
Pair(const T1 & aval, const T2 & bval) : a(aval), b(bval){}
T1 & first();
T2 & second();
T1 first() const {return a;}
T2 second() const {return b;}
};
template <typename T1, typename T2>
T1 & Pair<T1, T2>::first()
{
return a;
}
template <typename T1, typename T2>
T2 & Pair<T1, T2>::second()
{
return b;
}
#include <iostream>
#include "pairs.h"
int main()
{
using std::cout;
Pair<std::string, int> ratings[4] =
{
Pair<std::string, int>("The Purpled Duck", 5),//用类名Pair<std::string, int>调用构造函数
Pair<std::string, int>("Jaquie's Frisco", 4),
Pair<std::string, int>("Cafe Souffle", 5),
Pair<std::string, int>("Bertie's Eats", 3)
};
int joints = sizeof(ratings) / sizeof(Pair<std::string, int>);
cout << "Rating: Eatery:\n";
for (int i = 0; i < joints; ++i)
{
cout << ratings[i].second() << " \t "
<< ratings[i].first() << '\n';
}
ratings[3].second() = 6;
ratings[3].first() = "Bertie's Fat Eats";
cout << "Revised version:\n";
cout << ratings[3].second() << " \t "
<< ratings[3].first() << '\n';
cout << "Done!\n";
return 0;
}
输出
Rating: Eatery:
5 The Purpled Duck
4 Jaquie's Frisco
5 Cafe Souffle
3 Bertie's Eats
Revised version:
6 Bertie's Fat Eats
Done!
问题
- 数组内部竟然用了分号。。
学到
- 利用返回引用的不安全性,通过类的公有接口修改类对象的私有变量
终于明白了这两个函数的区别了:
T1 & first(){return a;}
T1 first() const {return a;}
以及
T2 & second(){return b;}
T2 second() const {return b;}
一个返回引用的非const函数,一个返回值的const函数。
之前好几次都遇到这种问题,就是返回引用的不能是const,返回值的是const,一直没有想通,直到这里遇到了这两句代码:
ratings[3].second() = 6;
ratings[3].first() = "Bertie's Fat Eats";
允许赋值给函数!!!终于打开了我尘封的关于返回值为引用的记忆。我忘记了,这种看似怪异的赋值正是通过把返回值类型设置为T &而不是const T &实现的!参看此文的小节:把返回类型设置为const引用,以阻止合法却错误的赋值,如accumulate(dup, five) = four;。并且正是因为这种赋值会改变调用对象,所以一般情况下如果不是很清楚自己真的要改变调用对象,那就返回const T &。
如果这里真的返回const T &, 则就也是const 成员函数了
T1 & first(){return a;}
T2 & second(){return b;}
T1 first() const {return a;}
T2 second() const {return b;}
//等价于
T1 & first(){return a;}
T2 & second(){return b;}
const T1 & first() const {return a;}
const T2 & second() const {return b;}
因此这里是利用了返回引用的这种不安全性,去通过类的公有接口修改类对象的私有变量。
为类型参数提供默认值(STL常把默认类型设置为其他类)
还是上面的示例,只是把类声明前面的模板头中的尖括号中写上默认参数的值,则主程序中无需写第二个类型参数的值就默认是int,除非你不想用int才需要写第二个类型参数
template <typename T1, typename T2 = int>
class Pair
Pair<std::string> ratings[4] =
{
Pair<std::string>("The Purpled Duck", 5),
Pair<std::string>("Jaquie's Frisco", 4),
Pair<std::string>("Cafe Souffle", 5),
Pair<std::string>("Bertie's Eats", 3)
};
我们已经知道,默认参数是很香的,在构造函数中的体现最为明显,默认参数可以使得一个构造函数充当着几个构造函数的功能。还可以少打字,精简代码。所以C++的标准模板库STL经常使用默认参数这个特性,但是STL中一般是把默认类型设置为其他的类,而不是简单的某个数据类型。
类模板的具体化(specialization,生成具体类声明和类定义)
和函数模板一样,类模板也是用泛型的方式描述类的属性和行为,所以也必须把类型具体化才能生成可以编译和运行的实例代码。
具体化主要说的就是类型的具体化,清晰化,明了化。以便于编译器可以生成特定类型的该类代码。
所以泛型编程就是写函数模板和类模板,没有使用具体类型,而是用一个泛型名暂替类型名,具体化的时候在做一个类型替换就生成了有效代码。刚开始接触C++时觉得泛型编程特别牛逼,特别玄妙高深难懂,不知道到底说的是啥,十分好奇,现在终于窥得一隅,十分开心。原来思想很简单,但真的很有用,对于代码重用非常有帮助。
之前函数模板就说了好几个具体化:显式实例化,隐式实例化,显式具体化等。我有点忘记了。刚好现在重温一下。
模板还有个分类,分为通用模板(即使用泛型定义的模板)和具体化模板(为个别特定类型修改模板定义)。
使用通用模板
隐式实例化 implicit instantiation(需要生成对象时才执行)
目前用的都是隐式实例化,在声明对象的时候指出类型。但是编译器只在需要生成对象时才执行隐式实例化(即生成具体类定义)
ArrayTP<int, 100> stuff;//编译器使用通用模板生成ArrayTP<int, 100>类的具体的类定义,并根据定义创建对象
ArrayTP<double, 20> * pt;//指针,还不需要对象,所以这句代码并不会让编译器去执行隐式实例化!
pt = new ArrayTP<double, 20>;//需要创建对象,编译器执行隐式实例化,再根据具体类定义创建对象
显式实例化 explicit instantiation(用关键字template声明类)
隐式实例化不需要使用任何关键字,直接在使用时用所需类型替换类型参数即可。但是要让编译器生成具体类定义,必须是需要生成对象。
而显式实例化利用一个关键字template去声明那个类(用所需类型替换类型参数),就可以保证这句代码就能让编译器立马生成具体化定义,立竿见影。
template class ArrayTP<std::string, 100>;//立刻生成ArrayTP<std::string, 100>类
使用具体化模板(某些特定类型需要对模板进行修改)
特殊类型有点像特殊关系户,别的类型都用通用模板就好好地,这些特定类型使用通用模板的代码却不能实现想要的效果,需要单独编写模板定义,为每一个特殊类型定制一个模板,这就是具体化模板。
显式具体化 explicit specialization
举个例子就很好理解,比如创建一个类模板用于表示排序后的数组:
template <typename T>
class SortedArray
{···};
一般T是int,double等类型,都可以使用<, >等关系运算符来排序,但是如果T是const char *
或者string
,则字符串的排序不可以用关系运算符,因为那样虽然代码不会报错,但是实际上比较的是字符串们的地址,因为字符串本身是指针,就是一个地址。比较字符串的顺序应该用strcmp
函数,这时候就不能再用通用模板去实例化,而要使用为具体类型专门定义的具体化模板来实例化。
template <> //尖括号里只需啊哟声明没有被指定具体类型的泛型
//由于显式具体化的所有类型参数都会被指定,所以是空括号;可以看下面的部分具体化
class SortedArray<const char *>
{···};//用strcmp函数比较排序
如果具体化模板和通用模板都和实例化请求匹配,编译器会使用具体化版本。
SortedArray<int> egg;//隐式实例化,使用通用模板
SortedArray<const char *> apple;//显式具体化,使用具体化模板
部分具体化 partial specialization(部分限制模板的通用性)
方法
- 给类型参数之一指定具体类型(不是默认参数哦)
//general template通用模板
template <class T1, class T2>
class Pair{···};
//部分具体化模板
template <class T1>//这里只需要声明没有被具体化的类型参数
class Pair<T1, int>{···};//这里必须用尖括号写明类型了,之前不用的,由于部分具体化就必须写了
//显式具体化模板
template <>
class Pair<int, int>{···};
所以显式具体化模板也就是完全具体化,如果相对于部分具体化来说的话
- 为指针提供特殊版本
//通用模板
template <class T>
class Feeb{···};
//指针部分具体化模板
template <class T *>
class Feeb{···};
Feeb<char> fb1;//使用通用版本,T是char
Feeb<char *> fb2;//使用T*具体化版本, T是char
如果没有用指针写部分具体化模板,则T就是char *
类型,使用通用模板。但是一般涉及到指针代码肯定有所不同,比如可能要用new和delete管理堆内存等,所以专门为指针类型写个部分具体化。
好处(灵活!)
部分具体化可以使得代码非常灵活:
//general template
template <class T1, class T2, class T3>
class Trio{···};
//specialization with T3 set to T2
template <class T1, class T2>
class Trio<T1, T2, T2>{···};
//specialization with T3 and T2 set to T1 *
template <class T1>
class Trio<T1, T1 *, T1 *>{···};
Trio<int, short, char *> t1;//通用模板
Trio<int, short> t2;//Trio<T1, T2, T2>
Trio<int, int *, int *> t3; //Trio<T1, T1 *, T1 *>
如果有多个模板可供选择(匹配),则编译器选择具体化程度最高的一个
Pair<double, double> p1;//Pair<T1, T2>,通用模板
Pair<double, int> p2;//Pair<T1, int>,部分具体化模板
Pair<int, int> p3;//Pair<int, int>,显式具体化模板