第10单元 模板初步
文章目录
- 第10单元 模板初步
- 单元导读
- 10.1 模板与泛型编程
- 10.1.1 元编程与泛型编程
- 10.1.2 初识模板
- 10.2 函数模板
- 10.2.1 函数模板
- 10.2.2 函数模版实例化
- 10.3 排序示例与泛型化
- 10.3.1 例子:选择排序
- 10.3.2 将一个函数泛型化
- 10.4 类模板
- 10.4.1 类模板
- 10.4.2 类模版实例化
- 10.5 模板参数、模板类型
- 10.5.1 默认类型与非类型参数
- 10.5.2 模板与继承
- 10.5.3 何时何地使用模板
注:本部分内容主要来自中国大学MOOC北京邮电大学崔毅东的 《C++程序设计》课程。
注:94条 C++程序规范。
单元导读
本单元主要介绍 函数模板 和 类模板 的用法。
- 元编程是写程序来处理程序;泛型编程是写程序来处理很多不同类型的数据。
- C++里面用模板来指定类型参数——也就是可以代表不同类型的一个参数。并由此定义函数模板和类模板。
- 模板有两种实例化方法:显式实例化、隐式实例化。
- 函数模板实例化之后,才是真正的函数;类模版实例化后,才是真正的类。
- 要编写一个函数模板或者类模板,就要首先编写一个非泛型化的函数或者类,然后改造成模板。
这一单元中,理解 模板实例化 是最重要的。在实际编程中,要将函数模板或者类模版的声明和实现都放在同一个头文件中。
10.1 模板与泛型编程
10.1.1 元编程与泛型编程
在下一小节介绍“模板”之前,本小节首先来介绍一下C++几种编程范式中的“元编程和泛型编程”。前面介绍过,C++是一种多模式的编程语言,支持结构化编程、面向对象编程、函数式编程、模板元编程/泛型编程等。下面是一些关于“编程”的概念:
- 编程(Programming):写一个程序去处理数据。
- 元编程(Meta-programming):写个程序去处理程序。
- 泛型编程(Generic Programming):写个程序去处理数据,但是只对数据结构做最小假设,以使该程序能最大化重用来处理广泛的数据类型。
上述来自于stack overflow文章:Generic programming vs. Metaprogramming
上述“元编程”和“泛型编程”的概念上略有不同,但是在C++中这两个概念并没有区别,原因是C++使用 模板 来实现元编程,由编译器在编译期根据模板生成模板实例,也就是程序。C++编译时多态有两种:函数重载、模板多态(泛型编程)。其中C++的泛型编程是借由元编程实现的,也就是由代码模板生成代码,所以C++中“元编程”和“泛型编程”不可区分。代码模板处理的是多类型数据,可以很广泛,但是生成的每一个代码则只能处理一种数据。
10.1.2 初识模板
本小节来介绍“模板/模版”(Template),两种中文写法老师没有查到区别,一般使用“模板”。至于为什么要引入“模板”,可以举个例子:比如现在要实现“求二整数、二双精度浮点数、二字符的最大者”,由于C语言中没有重载函数,所以只能定义三个名称不同的函数;C++中则可以使用重载函数,定义三个名称相同、类型不同的函数:
/*****************************/
//C语言定义三个函数,函数名称不同
int maxInt(int x, int y);
double maxDouble(double x, double y);
char maxChar(char x, char y);
/*****************************/
//C++使用重载函数,函数名称相同
int maxValue(const int &value1, const int &value2) {
if (value1 > value2) return value1;
else return value2;
}
double maxValue(const double &value1, const double &value2){
if (value1 > value2) return value1;
else return value2;
}
char maxValue(const char &value1, const char &value2) {
if (value1 > value2) return value1;
else return value2;
}
虽然上述C++使用重载函数已经非常出色的完成了任务要求,但是逻辑相同的函数写三遍还是很麻烦,那能不能只写一遍代码,就可以同时实现三个函数的功能呢(比如下面这段代码)?可以,这种函数就是“泛型函数”。泛型函数的优点显而易见:节省代码量、易于维护。下一节具体的介绍如何书写具体的“泛型函数”代码,下面仅仅是泛型函数的一个示例:
GenericType maxValue (const GenericType& x, const GenericType& y) {
if (x > y) return x;
else return y;
}
......
int main() {
cout << maxValue(1, 3);
cout << maxValue(1.0, 3.0);
cout << maxValue('a', 'x');
return 0;
}
10.2 函数模板
10.2.1 函数模板
上一小节简单介绍了“模板”的概念,本小节进一步介绍如何具体的书写“函数模板”代码。C++引入了带有泛型的函数模板,如上图10-1所示:
- 模板前缀:所有的函数模板都必须有模板前缀。其中
template
、typename
都是固定的关键字。尖括号里面的内容<typename T>
是“泛型参数的声明”,有两种声明方式:
<typename T>
:描述性强、老师推荐。<class T>
:易与类声明混淆、老师不推荐。
- 模板的“类型参数”类似于函数的“形式参数”。
- 类型参数
T
在函数中至少出现一次,可以作为函数返回值、函数参数、函数局部变量。编码规范:
- 8:用于表示模板类型的名字应该使用一个大写字母,例如
template <typename T>
、template <class C, class D>
。
当然图10-1所演示的仅仅是一个函数类型,当函数需要包含多个参数类型时,可以使用逗号分隔不同的类型参数,注意在模板前缀中每个类型前面都要写一次关键字typename
/class
。比如要定义加法运算的泛型函数,下面给出了具体的代码以及编译器根据模板生成代码的过程:
//模板
template <typename T, typename S>
auto add (T x1, S x2) { //C++14
return (x1 + x2);//编译器会自动进行隐式类型转换成double
}
//主函数
int main () {
auto y = add (1, 2.0);//int, double
return 0;
}
下面使用代码展示如何使用模板:
源文件GenericAdd.cpp:
#include<iostream>
//任务1:编写函数模板T add(T x, T y),并调用之。
template<typename T>
T add(T x, T y) {
return (x + y);
}
//任务2:修改上述函数模板为auto add(T x, S y),并调用之。
template<class T, typename Q>
auto add(T x, Q y) {
return (x + y);
}
int main() {
auto num1 = add(3, 4);
auto num2 = add(3, 4.0);
std::cout << typeid(num1).name() << " : " << num1 << std::endl;
std::cout << typeid(num2).name() << " : " << num2 << std::endl;
}
运行结果:
int : 7
double : 7
10.2.2 函数模版实例化
本节来进一步介绍上一小节中图10-2编译器根据模板生成代码的过程,也就是函数模板实例化(function template instantiation)。函数模板只是蓝图,本身不是类型或函数。编译器扫描代码,遇到模版定义时,并不立即产生代码;只有在程序调用时,确定模板实参后,编译器才会生成实际函数代码,也就是“实例化”。“模板多态”实际上是一种“编译时多态”,也叫做“静态联编”,和“函数重载”是同一个层面上的多态。
函数模板的实例就是一段函数代码。和前几章介绍类的实例化相同,模板实例化也有两种方法:
- 显式实例化(Explicit instantiation):强制某些函数实例化,可出现于程序中模板定义后的任何位置。
- 隐式实例化(Implicit instantiation):编译器查看函数调用,推断模版实参,实现隐式实例化。
代码展示:
#include <iostream> template <typename T> void f(T s){ std::cout << s << '\n'; } /*************显示实例化*************/ template void f<double>(double); //显式实例化1:编译器生成如下代码 //void f(double s) { // std::cout << s << '\n'; //} template void f<>(char); //显式实例化2:等价于f<char>(char),根据形参类型char推导出模板实参T template void f(int); //显式实例化3:等价于f<int>(int),根据形参int推导出模板实参T /*************隐式实例化*************/ int main(){ f<double>(1); //隐式实例化1:调用 f<double>(double) f<>('a'); //隐式实例化2:调用 f<char>(char) f(7); //隐式实例化3:调用 f<int>(int) void (*ptr)(std::string) = f; //隐式实例化4:f<string>(string) //注意上一行是函数指针,函数的名字就相当于一个指针 }
最后看一个小概念“实例函数/实例类”(Instantiated function/class):由函数模板实例化得到的函数叫做“实例函数”,由类模板实例化得到的类叫做“实例类”。上述英文是C++11标准14.7节中给出的,所以注意“实例函数/实例类”的称呼不要写成 “模板函数/模板类”(template function/template class) ,这些概念不严谨,应该只存在“函数模板/类模板”,重点强调在“模板”。
下面使用代码展示“模板实例化”:
源文件Instantiate.cpp:
#include <iostream>
//#include <algorithm> //包括max的标准库
#include <string> //为了使用重载运算符""s
using namespace std::string_literals; //为了使用重载运算符""s
//任务1:函数模板定义
template <typename T>
T max(T x, T y) {
return (x > y ? x : y);
}
//任务2:显式实例化—整数
template <int> int max (int, int);
//任务3:在main()中演示各功能
int main() {
//任务3.1:调用显式实例化的函数
std::cout << "max(1,2): " << max(1, 2) << std::endl;
//任务3.2:浮点数实例化
std::cout << "max(2,9,1.3): " << max(2.9, 1.3) << std::endl;
//任务3.3:字符实例化
std::cout << "max('A','D'): " << max('A', 'D') << std::endl;
//任务3.4:字符串字面量 const char* 实例化
//实际上比较的是const char*指针地址的大小,下面使用生字符串引起来就不需要转义了
std::cout << R"(max("ABC","ABD"): )" << max("ABC", "ABD") << std::endl;
std::cout << R"(max("ABD","ABC"): )" << max("ABD", "ABC") << std::endl;
std::cout << R"(max("123","124"): )" << max("123", "124") << std::endl;
//任务3.5:std::string类型实例化—2023年也会引起名字空间冲突!!
比较的是每个字符的ASCII码,""s 引起来的自动变成std::string类型
//std::cout << R"(max("ABC"s, "ABD"s))" << max("ABC"s, "ABD"s) << std::endl;
//std::cout << R"(max("ABD"s, "ABC"s))" << max("ABD"s, "ABC"s) << std::endl;
//std::cout << R"(max("123"s, "124"s))" << max("123"s, "124"s) << std::endl;
//任务3.6:名字空间冲突问题
//引入头文件<algorithm>会编译报错。
//不要轻易地定义可能和标准库已有函数重名的模板!!
return 0;
}
运行结果:
max(1,2): 2
max(2,9,1.3): 2.9
max('A','D'): D
max("ABC","ABD"): ABC
max("ABD","ABC"): ABC
max("123","124"): 123//说明先写的地址大
10.3 排序示例与泛型化
10.3.1 例子:选择排序
本10.3节以“选择排序”为例,演示如何将函数泛型化。所以本小节首先来介绍“选择排序”的非泛型函数思路。下图给出升序的“选择排序”实现思路:
- 基本思路:每次都找到最小的元素,然后和本次循环最前面的元素进行交换。
- 循环的关键参数:待排序列表中的最小值、待排序列表中的最小值的索引。
- 注意:下面的代码只是一般的排序算法,还没有模板化,也就是还没有针对不同的数据类型做泛型。
下面使用代码编写升序的“选择排序”,注意要选择C++17标准才能正常使用std::array
。另外,由于C++17中引入了std::array<类型, 大小>
数组,所以不仅要编写“C风格原生数组”的排序代码,还要编写“C++风格array数组”的排序代码:
头文件SelectionSort.h
#pragma once
#include<iostream>
#include<array>
//任务1:double数组升序选择排序
void selectionSort(double list[], const std::size_t size);
//任务2:std::array数组升序选择排序——注意模板的声明和实现都要放在头文件中
//这个并没有实现泛型化,只能排序double类型的std::array数组。
template<int N>//注意这个int N是非引用类型参数,不是类型,10.5节会讲
void selectionSort(std::array<double, N>& list) {
constexpr int size = N;
//for循环,每次从list[i]~list[size-1]中找出一个最小的数
for (int i = 0; i < (size - 1); i++) {
//初始化,将list[i]记为最小值,将i记为最小值的索引
double min = list[i];
int index = i;
//用循环,找出list[i+1]~list[size-1]中的最小值和他的下标
for (int j = i + 1; j < size; j++) {
if (min > list[j]) {
min = list[j];
index = j;
}
}
//若list[i]不是最小值,那么交换list[i]<-->list[index]
if (index != i) {
list[index] = list[i];
list[i] = min;
}
}
}
源文件SelectionSort.cpp
#include<iostream>
#include"SelectionSort.h"
//double数组升序选择排序
void selectionSort(double list[], const std::size_t size) {
//for循环,每次从list[i]~list[size-1]中找出一个最小的数
for (int i = 0; i < (size - 1); i++) {
//初始化,将list[i]记为最小值,将i记为最小值的索引
double min = list[i];
int index = i;
//用循环,找出list[i+1]~list[size-1]中的最小值和它的下标
for (int j = i + 1; j < size; j++) {
if (min > list[j]) {
min = list[j];
index = j;
}
}
//若list[i]不是最小值,那么交换list[i]<-->list[index]
if (index != i) {
list[index] = list[i];
list[i] = min;
}
}
}
源文件TestSelectionSort.cpp
#include<iostream>
#include<array>
//#include<iomanip>
#include"SelectionSort.h"
int main() {
//double x[]{ 3.0, 4.0, 7.0, 5.0, 6.0, 1.0, 9.0, 2.0 };
std::array x{ 3.0, 4.0, 7.0, 5.0, 6.0, 1.0, 9.0, 2.0 };
//输出排序前数组所有元素
for (auto i : x) {
std::cout << i << " ";
}
std::cout << std::endl;
//输出排序后数组所有元素
//selectionSort(x, 8);
selectionSort(x);
for (auto i : x) {
std::cout << i << " ";
}
}
运行结果:
3 4 7 5 6 1 9 2
1 2 3 4 5 6 7 9
10.3.2 将一个函数泛型化
于是在上一小节代码的基础上,本小节来将上述排序算法进行泛型化。首先来看看将函数泛型化的一般思路:
- 先设计/编写一个非泛型函数。
- 调试/测试该函数,确保可以正常运行。
- 将上述非泛型函数转换为泛型函数。具体就是将函数处理的数据类型转换成类型参数。注意不是将函数中所有与待处理数据的类型相同的数据类型都换成泛型,而是只转换与待处理数据有关的数据类型,比如下面的代码只需修改
foo的返回值类型
、t
、x
,无需修改s
:int foo(int x, int s) { int t {0}; for(int i=0; i<s; i++){ t ="x;" } return t }
下面使用代码展示如何将上一小节的“选择排序”代码泛型化:
头文件GenericSort.h
#pragma once
#include<iostream>
#include<array>
//任务1:对于原生数组的选择排序进行泛型化
template<typename T>
void selectionSort(T list[], const std::size_t size) {
//for循环,每次从list[i]~list[size-1]中找出一个最小的数
for (int i = 0; i < (size - 1); i++) {
//初始化,将list[i]记为最小值,将i记为最小值的索引
T min = list[i];
int index = i;
//用循环,找出list[i+1]~list[size-1]中的最小值和他的下标
for (int j = i + 1; j < size; j++) {
if (min > list[j]) {
min = list[j];
index = j;
}
}
//若list[i]不是最小值,那么交换list[i]<-->list[index]
if (index != i) {
list[index] = list[i];
list[i] = min;
}
}
}
//任务2:对于std::array数组的选择排序进行泛型化
template<typename T,int N>
void selectionSort(std::array<T, N> &list) {
constexpr int size = N;
//for循环,每次从list[i]~list[size-1]中找出一个最小的数
for (int i = 0; i < (size - 1); i++) {
//初始化,将list[i]记为最小值,将i记为最小值的索引
T min = list[i];
int index = i;
//用循环,找出list[i+1]~list[size-1]中的最小值和他的下标
for (int j = i + 1; j < size; j++) {
if (min > list[j]) {
min = list[j];
index = j;
}
}
//若list[i]不是最小值,那么交换list[i]<-->list[index]
if (index != i) {
list[index] = list[i];
list[i] = min;
}
}
}
源文件GenericSort.cpp
#include<iostream>
#include"GenericSort.h"
源文件TestGenericSort.cpp
#include<iostream>
#include<iomanip>
#include"GenericSort.h"
int main() {
//1. 测试原生数组的泛型化选择排序
//输出排序前原生数组
double x[]{ 3.0, 4.0, 7.0, 5.0, 6.0, 1.0, 9.0, 2.0 };
//char x[]{ 'a', 'f', 'e', 'h', 'j', 'k', 'w', 'l' };
//char x[]{ "djqecs;d" };
for (auto i : x) {
std::cout << std::fixed << std::setprecision(2) << i << " ";
}
std::cout << std::endl;
//输出排序后原生数组
selectionSort(x, 8);
for (auto i : x) {
std::cout <<std::fixed<<std::setprecision(2)<< i << " ";
}
std::cout << std::endl;
//2. 测试原生数组的泛型化选择排序
//输出排序前std::array数组
std::array y{ 3.0, 4.0, 7.0, 5.0, 6.0, 1.0, 9.0, 2.0 };
//std::array y{ "B", "D", "E", "A" };
//std::array y{ 1, 8, 4, 0, 2, 7 };
for (auto i : y) {
std::cout << std::fixed<<std::setprecision(2)<<i << " ";
}
std::cout << std::endl;
//输出排序后std::array数组
selectionSort(y);
for (auto i :y) {
std::cout << std::fixed << std::setprecision(2) << i << " ";
}
return 0;
}
运行结果:
3.00 4.00 7.00 5.00 6.00 1.00 9.00 2.00
1.00 2.00 3.00 4.00 5.00 6.00 7.00 9.00
3.00 4.00 7.00 5.00 6.00 1.00 9.00 2.00
1.00 2.00 3.00 4.00 5.00 6.00 7.00 9.00
10.4 类模板
10.4.1 类模板
既然10.3节介绍了如何将“函数”泛型化并调用,那么本10.4节就来介绍如何将“类”泛型化并实例化。和前面“函数泛型化”的过程类似,“类模板”(Class Template)就是将类中某些类型变为泛型,从而定义一个模板。可以泛型化的类成员包括:
- 数据域成员。
- 函数成员:返回值类型、参数类型、局部变量类型都可以成为泛型。
上图是将一个int型的栈StackOfIntegers
泛型化成Stack
。可以看到,elements
类型以及所有要存取数据的函数类型都进行了泛化;当然size
类型和getSize()
类型也可以泛化成模板类型,但是没有必要。下面的伪代码则针对上图所示,演示了类模板的语法。注意在类外定义成员函数时,类名后面应该加上<类型参数>
。
/**************类的声明*************/
template<typename T>
class Stack {
public:
Stack();
bool empty(); //演示1
T peek(); //演示2
T push(T value);
T pop();
int getSize();
private:
T elements[100];
int size;
};
/***********类成员函数的定义*********/
//返回值不是模板类型
template<typename T>
bool Stack<T>::empty() {
return (size == 0);
}
//返回值是模板类型
template<typename T>
T Stack<T>::peek() {
return elements[size - 1];
}
下面使用代码来展示一下“类模板”,任务如下:
任务1:基于Unit05的StackOfIntegers,将之改造为泛型。
任务2:创建一个字符栈,将一个字符串的内容反转输出。
头文件 Stack.h:
#pragma once
//类声明
template<typename T>
class Stack {
private:
T elements[100];//栈最大容量为100
int size{ 0 };
public:
Stack(); //构造函数
bool empty(); //判断栈是否为空
T peek(); //读取栈顶元素
T push(T value);//压入元素
T pop(); //弹出栈顶元素
int getSize(); //获取当前栈大小
};
//类外定义成员函数,注意类模板的声明和定义要放在一起
template<typename T>
Stack<T>::Stack() {
size = 0;
for (auto&i : elements) {
i = 0;
}
}
template<typename T>
bool Stack<T>::empty() {
return (size == 0 ? true : false);
}
template<typename T>
int Stack<T>::getSize() {
return size;
}
template<typename T>
T Stack<T>::peek() {
return elements[size - 1];
}
template<typename T>
T Stack<T>::pop() {
T temp = elements[size - 1];
elements[size - 1] = 0;
size--;
return temp;
}
template<typename T>
T Stack<T>::push(T value) {
elements[size] = value;
size++;
return value;
}
源文件 GenericStack.cpp
#include<iostream>
#include<string>
#include"Stack.h"
int main() {
Stack<char> c;
std::string s{ "Hello,World!" };
//将字符串压栈
std::cout << "反转前:";
for (auto i : s) {
c.push(i);
std::cout << i;
}
std::cout << std::endl;
//将字符串依次弹出
std::cout << "反转后:";
for (; c.empty() != true;) {//注意这个for循环的控制很奇妙
std::cout << c.pop();
}
}
运行结果:
反转前:Hello,World!
反转后:!dlroW,olleH
编程感想:
- 关于改名。上面的代码有个操作是将
StackOfIntegers
全部改名成Stack
,一个一个改很麻烦。若为函数,可以直接在函数名上右键“快速操作与重构”;但本例中是类名,所以只能在类名右键“重命名”。
10.4.2 类模版实例化
上一小节最后的演示代码中,main()
函数中实例化了类模板Stack<char> c
,本小节就展开讲讲“类模板实例化”,也就是编译器是如何根据模板生成具体的类代码的过程。显然,只要是对类模版进行实例化,编译器就会生成一个类。类的实例化包括也分为“显示实例化”、“隐式实例化”:
- 显式实例化:比如要将上一小节的
Stack
类模板实例化,可以直接定义语句template class Stack<int>;
,就可以将类模板显式实例化为一个处理int
类型的Stack
类。这个新生成的类实例的名称由编译器按规则生成,实际上非常复杂,但是为了叙述方便我们可以假设为IntStack
。- 隐式实例化:并不专门使用
template
、class
关键字进行实例化,而是在定义一个类的对象的时候进行隐式实例化。注意下面的Stack<char>
会生成一个不知道名字的类(假设是CharStack
),而charStack
就是这个类的实例对象://1. 实例化成char型 Stack<char> charStack; // 先实例化一个CharStack类(名字由编译器按规则生成):class CharStack { … char elements[100]; … }; // 然后用 CharStack charStack; 创建一个对象 //2. 实例化成int型 Stack<int> intStack; // 先实例化一个IntStack类:class IntStack { … int elements[100]; … }; // 然后用 IntStack intStack; 创建一个对象 //3. 初始化语句自动推导成int型 Stack intStack{1, 2, 3};//实例化为 Stack<int> // C++17,模板类型参数根据初始化语句自动推导(std::vector就是类模板)
10.5 模板参数、模板类型
10.5.1 默认类型与非类型参数
还记得在10.3节“选择排序”非泛型化时,用了一个template <int N>
来声明double
原生数组的大小,当时说这不是模板类型,这其实是“非类型参数”,本节就来介绍一下这部分内容。非类型参数(Non-type Parameters)在模板前缀中使用,并且在实例化模板时需要指定一个实际的对象作为非类型参数实参。下面的伪代码演示非类型参数:
//示例1:值作为非类型参数
template<typename T, int capacity>
class Stack{
//...
private:
T elements[capacity];
int size;
};
Stack<char, 100> charStack; //定义的时候要写清楚非类型参数的实参
//示例2:类对象作为非类型参数
template<typename T, Color c>
class Label{
//...
};
Color color(0, 0, 255); //先定义好作为非类型参数实参的类对象
Label<char, color> label; //隐式实例化类模板
注意C++中的std::array
就是一个模板类型,在定义时需要写std::array<类型,数组大小>
。比如定义一个大小为10的整型数组,起名叫list
:std::array<int, 8> list
。
除了上一段介绍的“非类型参数”外,还有一个概念是“默认类型”(Default type),也就是类模板中的“类型参数”也可以指定一个默认类型,但注意 只能在“类模板”中使用默认类型,而不能在“函数模板”中使用默认类型! 比如指定泛型类Stack
的默认类型为int
,这样做的好处就是可以很方便的直接隐式生成该类型的类对象:
//指定泛型类的默认类型为int
template<typename T = int>
class Stack{
//...
};
//用默认类型定义一个对象
Stack<> stack;//等价于 Stack<int> stack;
下面基于上一节的stack类模板
代码,展示模板中的“默认类型参数”和“非类型参数”:
任务1:基于stack类模板,展示默认类型参数。
任务2:基于stack类模板,展示非类型参数,以及非类型参数的默认值。
头文件 Stack.h:
#pragma once
//类声明
template<typename T=char, int N=100>
class Stack {
private:
T elements[N];//栈默认最大容量为100
int size{ 0 };
public:
Stack(); //构造函数
bool empty(); //判断栈是否为空
T peek(); //读取栈顶元素
T push(T value);//压入元素
T pop(); //弹出栈顶元素
int getSize(); //获取当前栈大小
};
//类外定义成员函数,注意类模板的声明和定义要放在一起
template<typename T,int N>
Stack<T,N>::Stack() {
size = 0;
for (auto&i : elements) {
i = 0;
}
}
template<typename T, int N>
bool Stack<T, N>::empty() {
return (size == 0 ? true : false);
}
template<typename T, int N>
int Stack<T, N>::getSize() {
return size;
}
template<typename T, int N>
T Stack<T, N>::peek() {
return elements[size - 1];
}
template<typename T, int N>
T Stack<T, N>::pop() {
T temp = elements[size - 1];
elements[size - 1] = 0;
size--;
return temp;
}
template<typename T, int N>
T Stack<T, N>::push(T value) {
elements[size] = value;
size++;
return value;
}
源文件 GenericStack.cpp
#include<iostream>
#include<string>
#include"Stack.h"
//创建一个字符栈,将一个字符串的内容反转输出
int main() {
Stack<> c;
std::string s{ "Hello,World!" };
//将字符串压栈
std::cout << "反转前:";
for (auto i : s) {
c.push(i);
std::cout << i;
}
std::cout << std::endl;
//将字符串依次弹出
std::cout << "反转后:";
for (; c.empty() != true;) {//注意这个for循环的控制很奇妙
std::cout << c.pop();
}
}
运行结果:
反转前:Hello,World!
反转后:!dlroW,olleH
10.5.2 模板与继承
既然有“类模板”的存在,那就很自然的想到“类模板”的继承是什么样子的呢?本小节就来介绍“类模板”的继承原则。注意到模板并不是一个类,于是“模板和继承”就会有一些特殊的原则:
- “普通类”可从“类模板实例”继承,而不能直接从“类模板”继承,如下图左一。
- “模板”可从“普通类”继承,如下图左二。
- “类模板”可从“类模板”继承,如下图右一。
10.5.3 何时何地使用模板
回顾本章,较为详细介绍了“模板”,那么在实际开发时,应该在何时使用模板编程呢?下面列出两种不得不使用模板的情况:
- 使用别人写好的模板库时,如
STL(Standard Template Library)
中的std::array
、std::string
、std::vector
等;或者第三方库Boost
里有很多实验性的代码来测试很多新的机制,该库将模板的技巧用到了极致。- 对不同类型的数据做类似处理(算法、容器、遍历等)。
但需要注意的是,一般只有那些开发标准库的作者才会希望获得“模板大师”的称号,我们在进行一般的应用开发时,还是不要过量使用模板,除非你想:
- 编写出团队中其他人都无法理解的代码。
- 编写出7天后无法理解的代码。
- 代码性能比可维护性更重要。
- 把“模板元编程”列为简历上的一项技能。
- 编写不太可能在许多实际编译器上运行的代码。
虽然上面说在应用开发过程中,不推荐过度使用模板,但是C++并不是一个完全面向对象的编程语言。之前提到,“泛型编程”在C++中广泛使用,经常可以取代“面向对象编程”。几乎整个C++标准库都依赖于泛型编程,比如C++标准库中的sort
函数使用了泛型编程;在C++标准库较少使用继承和运行时多态,异常、字符串和IO流中使用了较多的继承。于是相比于C++,Java则大量使用了继承,所以Java是一个面向对象的编程语言。