自定义模板入门
1. 引言
C++ 标准库包含许多预打包的模板化数据结构和算法。函数模板和类模板让我们能方便地指定一系列相关的(重载的)函数(即函数模板特化)或一系列相关的类(即类模板特化),这就是泛型编程。函数模板和类模板就像模板,我们可以从中描绘出形状;函数模板特化和类模板特化则像是单独的描绘,它们形状相同,但可能颜色和纹理不同。下面将展示如何创建自定义类模板和操作类模板特化对象的函数模板。
2. 类模板
栈是一种数据结构,我们只能在栈顶插入和取出元素,遵循后进先出(LIFO)原则。我们可以独立于栈中元素的类型来理解栈的概念,但要实例化一个栈,就必须指定数据类型,这为软件复用提供了很好的机会。我们可以泛型地定义一个栈,然后使用这个泛型栈类的特定类型版本。
类模板也被称为参数化类型,因为它们需要一个或多个类型参数来指定如何定制泛型类模板以形成类模板特化。我们只需编写一个类模板定义,当需要特定的特化时,使用简洁的符号,编译器就会生成特化的源代码。例如,一个 Stack 类模板可以成为创建许多 Stack 类模板特化的基础,如 “ Stack of doubles ”、“ Stack of ints ” 等。
2.1 创建类模板 Stack<T>
Stack 类模板的定义看起来像常规的类定义,但有一些关键区别:
- 它以 template<typename T> 开头,其中 T 是类型参数,作为 Stack 元素类型的占位符。类型参数的名称在模板定义内必须唯一,不一定使用 T ,任何有效标识符都可以。
- 在类模板定义中,元素类型通常用 T 表示。当使用类模板创建对象时,编译器会将类型参数替换为指定的类型。
- 这里没有将类模板的接口和实现分离。
// Fig. 23.1: Stack.h
// Stack class template.
#ifndef STACK_H
#define STACK_H
#include <deque>
template< typename T >
class Stack
{
public:
// return the top element of the Stack
T& top()
{
return stack.front();
} // end function template top
// push an element onto the Stack
void push( const T &pushValue )
{
stack.push_front( pushValue );
} // end function template push
// pop an element from the stack
void pop()
{
stack.pop_front();
} // end function template pop
// determine whether Stack is empty
bool isEmpty() const
{
return stack.empty();
} // end function template isEmpty
// return size of Stack
size_t size() const
{
return stack.size();
} // end function template size
private:
std::deque< T > stack; // internal representation of the Stack
}; // end class template Stack
#endif
2.2 Stack<T> 类模板的数据表示
C++ 标准库的预打包栈适配器类可以使用各种容器来存储其元素。由于栈只需要在栈顶进行插入和删除操作,所以可以使用 vector 或 deque 来存储栈的元素。 deque 是标准库栈适配器的默认表示,因为它比 vector 更有效地增长。 vector 是连续的内存块,当满时需要重新分配更大的内存块并复制旧元素;而 deque 通常实现为固定大小的内置数组列表,添加新元素时无需复制现有元素。因此,这里使用 deque 作为 Stack 类的底层容器。
2.3 Stack<T> 类模板的成员函数
类模板的成员函数定义是函数模板,但在类模板体内定义时,不需要以 template 关键字和尖括号中的模板参数开头。这些成员函数使用类模板的模板参数 T 来表示元素类型。 Stack 类模板没有定义自己的构造函数,编译器提供的默认构造函数会调用 deque 的默认构造函数。它提供了以下成员函数:
- top() :返回栈顶元素的引用。
- push(const T &pushValue) :将新元素压入栈顶。
- pop() :移除栈顶元素。
- isEmpty() :如果栈为空返回 true ,否则返回 false 。
- size() :返回栈中元素的数量。
每个成员函数都将其职责委托给 deque 类模板的相应成员函数。
2.4 在类模板定义外部声明类模板的成员函数
成员函数定义也可以出现在类模板定义之外。如果这样做,每个函数定义必须以 template 关键字开头,后跟与类模板相同的一组模板参数。此外,成员函数必须使用类名和作用域解析运算符进行限定。例如,可以在类模板定义之外定义 pop 函数:
template< typename T >
inline void Stack<T>::pop()
{
stack.pop_front();
} // end function template pop
2.5 测试类模板 Stack<T>
下面是一个测试 Stack 类模板的驱动程序:
// Fig. 23.2: fig23_02.cpp
// Stack class template test program.
#include <iostream>
#include "Stack.h" // Stack class template definition
using namespace std;
int main()
{
Stack< double > doubleStack; // create a Stack of double
const size_t doubleStackSize = 5; // stack size
double doubleValue = 1.1; // first value to push
cout << "Pushing elements onto doubleStack\n";
// push 5 doubles onto doubleStack
for ( size_t i = 0; i < doubleStackSize; ++i )
{
doubleStack.push( doubleValue );
cout << doubleValue << ' ';
doubleValue += 1.1;
}
cout << "\n\nPopping elements from doubleStack\n";
// pop elements from doubleStack
while ( !doubleStack.isEmpty() ) // loop while Stack is not empty
{
cout << doubleStack.top() << ' '; // display top element
doubleStack.pop(); // remove top element
}
cout << "\nStack is empty, cannot pop.\n";
Stack< int > intStack; // create a Stack of int
const size_t intStackSize = 10; // stack size
int intValue = 1; // first value to push
cout << "\nPushing elements onto intStack\n";
// push 10 integers onto intStack
for ( size_t i = 0; i < intStackSize; ++i )
{
intStack.push( intValue );
cout << intValue++ << ' ';
}
cout << "\n\nPopping elements from intStack\n";
// pop elements from intStack
while ( !intStack.isEmpty() ) // loop while Stack is not empty
{
cout << intStack.top() << ' '; // display top element
intStack.pop(); // remove top element
}
cout << "\nStack is empty, cannot pop." << endl;
} // end main
这个程序首先实例化一个 Stack<double> 对象 doubleStack ,将一些 double 值压入栈中,然后将它们弹出,验证了栈的后进先出特性。接着实例化一个 Stack<int> 对象 intStack ,进行同样的操作。
3. 操作类模板特化对象的函数模板
观察 Fig. 23.2 中 main 函数的代码, doubleStack 和 intStack 的操作几乎相同,这为使用函数模板提供了机会。
// Fig. 23.3: fig23_03.cpp
// Passing a Stack template object
// to a function template.
#include <iostream>
#include <string>
#include "Stack.h" // Stack class template definition
using namespace std;
// function template to manipulate Stack< T >
template< typename T >
void testStack(
Stack< T > &theStack, // reference to Stack< T >
const T &value, // initial value to push
const T &increment, // increment for subsequent values
size_t size, // number of items to push
const string &stackName ) // name of the Stack< T > object
{
cout << "\nPushing elements onto " << stackName << '\n';
T pushValue = value;
// push element onto Stack
for ( size_t i = 0; i < size; ++i )
{
theStack.push( pushValue ); // push element onto Stack
cout << pushValue << ' ';
pushValue += increment;
}
cout << "\n\nPopping elements from " << stackName << '\n';
// pop elements from Stack
while ( !theStack.isEmpty() ) // loop while Stack is not empty
{
cout << theStack.top() << ' ';
theStack.pop(); // remove top element
}
cout << "\nStack is empty. Cannot pop." << endl;
} // end function template testStack
int main()
{
Stack< double > doubleStack;
const size_t doubleStackSize = 5;
Stack< int > intStack;
const size_t intStackSize = 10;
testStack( doubleStack, 1.1, 1.1, doubleStackSize, "doubleStack" );
testStack( intStack, 1, 1, intStackSize, "intStack" );
return 0;
}
函数模板 testStack 使用 T 来表示 Stack<T> 中存储的数据类型,它接受五个参数:要操作的 Stack<T> 、初始压入值、后续值的增量、要压入的元素数量以及栈对象的名称。 main 函数中实例化了 doubleStack 和 intStack ,并调用 testStack 函数对它们进行操作。编译器会根据传递的栈对象的类型推断 T 的类型。
4. 非类型参数
前面的 Stack 类模板只使用了类型参数,实际上也可以使用非类型模板参数,它们可以有默认参数并被视为常量。例如,C++ 标准的 array 类模板以如下模板声明开头:
template < class T, size_t N >
声明 array< double, 100 > salesFigures; 会创建一个包含 100 个 double 元素的 array 类模板特化,并实例化对象 salesFigures 。 array 类模板封装了一个内置数组,创建 array 类模板特化时,数组的内置数组数据成员具有声明中指定的类型和大小。
5. 模板类型参数的默认参数
类型参数可以指定默认类型参数。例如,C++ 标准的 stack 容器适配器类模板开头如下:
template < class T, class Container = deque< T > >
这表明栈默认使用 deque 来存储类型为 T 的元素。声明 stack< int > values; 会创建一个 stack of ints 类模板特化,并实例化对象 values ,栈的元素存储在 deque<int> 中。
默认类型参数必须是模板类型参数列表中最右边(尾随)的参数。当实例化具有两个或更多默认参数的模板时,如果省略的参数不是最右边的,则其右边的所有类型参数也必须省略。从 C++11 开始,函数模板中的模板类型参数也可以使用默认类型参数。
6. 重载函数模板
函数模板和重载密切相关。当重载函数对不同类型的数据执行相同操作时,可以使用函数模板更紧凑和方便地表达。我们可以编写不同类型参数的函数调用,让编译器生成单独的函数模板特化来处理每个调用。函数模板特化具有相同的名称,编译器使用重载解析来调用合适的函数。
函数模板可以通过以下方式重载:
- 提供具有相同函数名但不同函数参数的其他函数模板。
- 提供具有相同函数名但不同函数参数的非模板函数。如果模板和非模板版本都匹配调用,非模板版本将被使用。
编译器在调用函数时会执行匹配过程,它会查看现有函数和函数模板,以找到函数名和参数类型与调用一致的函数或生成函数模板特化。如果没有匹配项,编译器会发出错误消息;如果有多个匹配项,编译器会尝试确定最佳匹配;如果有多个最佳匹配,调用将是模糊的,编译器也会发出错误消息。
7. 总结
- 模板使我们能够用一段代码指定一系列相关的函数(函数模板特化)或一系列相关的类(类模板特化)。
- 类模板是参数化类型,需要类型参数来定制泛型类模板以形成特化。
- 可以使用非类型参数和默认类型参数。
- 函数模板可以通过多种方式重载。
8. 自我检查练习与答案
8.1 判断题
- a) 错误。
typename和class关键字在模板类型参数中也允许基本类型作为类型参数。 - b) 正确。
- c) 错误。函数模板之间的模板参数名称不必唯一。
- d) 正确。
8.2 填空题
- a) 函数模板特化,类模板特化。
- b)
template,尖括号< and >。 - c) 重载。
- d) 参数化。
- e) 作用域解析。
9. 练习
- 9.1 模板中的运算符重载 :编写一个简单的函数模板
isEqualTo,使用相等运算符==比较两个相同类型的参数,如果相等返回true,否则返回false。在程序中仅使用各种基本类型调用isEqualTo,然后编写一个使用用户定义类类型调用isEqualTo但未重载相等运算符的程序,观察运行结果,再重载相等运算符,再次观察运行结果。 - 9.2 数组类模板 :将
Array类重新实现为类模板,并在程序中演示新的Array类模板。 - 9.3 区分 “函数模板” 和 “函数模板特化” 。
- 9.4 解释哪个更像模板——类模板还是类模板特化 。
- 9.5 函数模板和重载之间的关系 。
- 9.6 编译器在调用函数时执行匹配过程,在什么情况下匹配尝试会导致编译错误 。
- 9.7 为什么将类模板称为参数化类型是合适的 。
- 9.8 解释为什么 C++ 程序会使用
Array< Employee > workerList( 100 );语句 。 - 9.9 解释为什么 C++ 程序可能会使用
Array< Employee > workerList;语句 。 - 9.10 解释
template< typename T > Array< T >::Array( int s )在 C++ 程序中的用途 。 - 9.11 为什么在容器(如数组或栈)的类模板中使用非类型参数 。
通过这些练习,可以更好地掌握模板的使用和相关概念。例如,在练习 9.1 中,当使用用户定义类类型调用 isEqualTo 但未重载相等运算符时,程序可能会编译错误,因为编译器不知道如何比较该类的对象;重载相等运算符后,程序应该可以正常运行。在练习 9.2 中,将 Array 类实现为类模板可以提高代码的复用性,不同类型的数组可以使用同一个模板来创建。
总之,模板是 C++ 中强大的工具,通过类模板和函数模板,我们可以编写更加通用和可复用的代码,提高开发效率和代码质量。
自定义模板入门
10. 关键知识点总结
为了更清晰地回顾前面的内容,下面通过表格形式总结一些关键知识点:
| 知识点 | 描述 | 示例 |
| ---- | ---- | ---- |
| 类模板 | 用于创建一组相关的类模板特化,是参数化类型,需类型参数定制 | template<typename T> class Stack; |
| 非类型参数 | 可以在类或函数模板声明中使用,视为常量 | template < class T, size_t N > |
| 模板类型参数的默认参数 | 类型参数可指定默认类型 | template < class T, class Container = deque< T > > |
| 重载函数模板 | 可通过提供不同参数的函数模板或非模板函数重载 | 见前面重载相关描述 |
11. 技术点分析
11.1 类模板的优势
类模板最大的优势在于软件复用。以 Stack 类模板为例,我们只需编写一个模板定义,就能创建不同元素类型的栈,如 Stack<double> 和 Stack<int> 。这避免了为每种元素类型都编写一个独立的栈类,大大减少了代码量,提高了开发效率。
11.2 非类型参数的作用
非类型参数可以为模板提供更多的灵活性。例如 array 类模板使用非类型参数 N 来指定数组的大小,这样在创建 array 类模板特化时,可以根据需要指定不同的数组大小,而不需要为每个大小都创建一个新的类。
11.3 默认参数的使用场景
默认参数可以简化模板的使用。如 stack 容器适配器类模板默认使用 deque 作为存储容器,大多数情况下我们不需要指定其他容器,使用默认的 deque 即可。当有特殊需求时,再指定其他容器。
12. 应用场景举例
12.1 数据处理
在数据处理中,经常需要处理不同类型的数据。使用类模板可以方便地创建处理不同数据类型的类。例如,在一个数据分析程序中,可能需要处理 int 、 double 等不同类型的数据,可以使用 Stack 类模板来存储这些数据,方便进行后进先出的操作。
12.2 算法实现
在算法实现中,函数模板可以提高算法的通用性。例如,排序算法可以使用函数模板来实现,这样可以对不同类型的数组进行排序,而不需要为每种类型都编写一个排序函数。
13. 操作步骤总结
以下是使用类模板和函数模板的一般操作步骤:
1. 定义类模板 :使用 template 关键字和类型参数定义类模板,如 template<typename T> class Stack; 。
2. 实现类模板的成员函数 :可以在类模板体内或体外实现成员函数,体外实现时需要使用 template 关键字和类名限定。
3. 实例化类模板特化 :使用类模板创建对象时,指定具体的类型参数,如 Stack<double> doubleStack; 。
4. 定义函数模板 :使用 template 关键字和类型参数定义函数模板,如 template<typename T> void testStack(Stack<T> &theStack, ...); 。
5. 调用函数模板 :传递具体的对象和参数调用函数模板,编译器会根据对象的类型推断类型参数。
14. 流程图展示
下面是一个使用类模板和函数模板的简单流程图:
graph TD;
A[定义类模板] --> B[实现成员函数];
B --> C[实例化类模板特化];
D[定义函数模板] --> E[调用函数模板];
C --> E;
15. 常见错误及解决方法
15.1 模板特化时用户定义类型不满足要求
当创建模板特化时,如果用户定义类型不满足模板的要求,如模板需要比较对象大小,但用户定义类型未重载 < 运算符,会导致编译错误。解决方法是在用户定义类型中重载所需的运算符或提供所需的函数。
15.2 重载函数模板时调用模糊
如果有多个函数模板或非模板函数都匹配函数调用,会导致调用模糊。解决方法是明确指定调用的函数,或者调整函数参数,使匹配更加明确。
16. 总结与展望
模板是 C++ 中非常强大的特性,通过类模板和函数模板,我们可以编写更加通用和可复用的代码。在实际开发中,合理使用模板可以提高开发效率,减少代码冗余。
未来,随着 C++ 标准的不断发展,模板的功能可能会进一步增强。例如,可能会有更多的语法糖来简化模板的使用,或者模板在并行编程等领域有更广泛的应用。我们可以持续关注 C++ 的发展,不断探索模板的更多可能性。
希望通过本文的介绍,大家对自定义模板有了更深入的理解,能够在实际开发中灵活运用模板技术,编写出高质量的代码。
超级会员免费看

被折叠的 条评论
为什么被折叠?



