Day29 C++ 模板
2024.12.21 C++ 模板
问自己一以下几个问题:
1.什么是模板?
2.为什么用模板?
3.什么情况下用模板呢?
模板是泛型编程的基础,泛型编程即以一种独立于任何特定类型的方式编写代码。
模板是创建泛型类或函数的蓝图或公式。库容器,比如迭代器和算法,都是泛型编程的例子,它们都使用了模板的概念。
每个容器都有一个单一的定义,比如 向量,我们可以定义许多不同类型的向量,比如 vector 或 vector 。
您可以使用模板来定义函数和类,接下来让我们一起来看看如何使用。
函数模板
模板函数定义的一般形式如下所示:
template <typename type> ret-type func-name(parameter list)
{
// 函数的主体
}
在这里,type 是函数所使用的数据类型的占位符名称。这个名称可以在函数定义中使用。
下面是函数模板的实例,返回两个数中的最大值:
实例
#include <iostream>
#include <string>
using namespace std;
template <typename T>
inline T const& Max (T const& a, T const& b) //类型为T的常量引用
{
return a < b ? b:a;
}
int main ()
{
int i = 39;
int j = 20;
cout << "Max(i, j): " << Max(i, j) << endl;
double f1 = 13.5;
double f2 = 20.7;
cout << "Max(f1, f2): " << Max(f1, f2) << endl;
string s1 = "Hello";
string s2 = "World";
cout << "Max(s1, s2): " << Max(s1, s2) << endl;
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
Max(i, j): 39
Max(f1, f2): 20.7
Max(s1, s2): World
template <typename T> //
inline T const& Max (T const& a, T const& b)
{
return a < b ? b:a;
}
我们具体分析一下这个代码:
-
template <typename T>
:- 这是一个模板声明,表示这个函数是一个 模板函数。模板函数使得这个函数能够接受任何类型
T
,并且返回类型也是T
。 T
是一个 占位符类型,可以在调用时由具体的类型替代。- 举个例子,当你调用
Max(5, 10)
时,T
会被推断为int
,而如果你调用Max(5.5, 10.5)
,T
会被推断为double
。
- 这是一个模板声明,表示这个函数是一个 模板函数。模板函数使得这个函数能够接受任何类型
-
inline
:inline
是一个建议,告诉编译器尽量将该函数的代码 内联展开,即在调用该函数的地方直接插入函数体。这样可以减少函数调用的开销,通常用于小函数。- 但是,是否真的内联展开由编译器决定,这只是一个提示。
inline
的作用是告诉编译器,将函数的代码 内联展开,即直接将函数体的代码插入到调用的地方,而不是通过常规的函数调用机制来执行。它通常用于小且频繁调用的函数,能够提高程序的执行效率,减少函数调用的开销。
举个例子
假设有两个函数:
传统的函数调用:
int add(int a, int b) {
return a + b;
}
int main() {
int x = 3, y = 5;
int z = add(x, y); // 这里会发生函数调用
return 0;
}
在上面的代码中,调用 add(x, y)
需要:
- 压入函数调用的参数到栈中。
- 跳转到函数的代码执行
a + b
。 - 执行完后再返回到
main
函数的位置。
这虽然开销很小,但如果 add()
这个函数被非常频繁地调用,开销会累积起来。
使用 inline
的情况:
inline int add(int a, int b) {
return a + b;
}
int main() {
int x = 3, y = 5;
int z = add(x, y); // 编译器可能会将这个函数的代码直接插入到这里
return 0;
}
通过在函数声明前加上 inline
,编译器会尝试将 add(x, y)
函数的代码直接插入到 main
函数中。这样就避免了函数调用的所有开销:
- 不再需要压栈,也不需要跳转到函数代码执行。
main
函数中直接执行a + b
。
这会使得程序在性能上有所提升,尤其是在调用频繁且函数体非常简单时。
3.T const&`:
T const&
是返回类型,表示该函数返回的是类型为T
的 常量引用。- 之所以使用
const&
是为了避免不必要的拷贝操作,同时确保返回的值不能被修改。 const
保证了你不能通过这个引用修改返回值,而&
是引用,避免了拷贝,提升效率。
类模板
正如我们定义函数模板一样,我们也可以定义类模板。泛型类声明的一般形式如下所示:
template <class type> class class-name {
.
.
.
}
在这里,type 是占位符类型名称,可以在类被实例化的时候进行指定。您可以使用一个逗号分隔的列表来定义多个泛型数据类型。
下面的实例定义了类 Stack<>,并实现了泛型方法来对元素进行入栈出栈操作:
实例
#include <iostream>
#include <vector>
#include <cstdlib>
#include <string>
#include <stdexcept>
using namespace std;
// template <class T> 中的 class 其实是可以用 typename 来代替的。
// class 和 typename 在模板声明中是等价的。
template <class T>
class Stack {
private:
vector<T> elems; // 元素
public:
void push(T const&); // 入栈
void pop(); // 出栈
T top() const; // 返回栈顶元素
bool empty() const{ // 如果为空则返回真。
return elems.empty();
}
};
template <class T>
void Stack<T>::push (T const& elem)
{
// 追加传入元素的副本
elems.push_back(elem);
}
template <class T>
void Stack<T>::pop ()
{
if (elems.empty()) {
throw out_of_range("Stack<>::pop(): empty stack");
}
// 删除最后一个元素
elems.pop_back();
}
template <class T>
T Stack<T>::top () const
{
if (elems.empty()) {
throw out_of_range("Stack<>::top(): empty stack");
}
// 返回最后一个元素的副本
return elems.back();
}
int main()
{
try {
Stack<int> intStack; // int 类型的栈
Stack<string> stringStack; // string 类型的栈
// 操作 int 类型的栈
intStack.push(7);
cout << intStack.top() <<endl;
// 操作 string 类型的栈
stringStack.push("hello");
cout << stringStack.top() << std::endl;
stringStack.pop();
stringStack.pop();
}
catch (exception const& ex) {
cerr << "Exception: " << ex.what() <<endl;
return -1;
}
}
当上面的代码被编译和执行时,它会产生下列结果:
7
hello
Exception: Stack<>::pop(): empty stack
我们分析一下这个代码:
// T 是一个占位符,表示栈的元素类型可以是任意的(如 int、double、std::string 等)
template <class T>
// 这里定义了一个 Stack 类,表示栈数据结构。类中的成员函数和数据成员定义了栈的操作和存储方式。
class Stack {
// 数据成员
private:
//std::vector 是 C++ 标准库中的容器,可以根据需要扩展大小,方便我们动态添加或移除元素。
vector<T> elems; // 元素 elems 是一个 std::vector<T> 类型的成员变量,存储栈中的元素。
public:
// 常量引用 参数 T const&,意味着你传入的参数不会被修改,而且避免了不必要的拷贝。
void push(T const&); // 入栈
//在实现中,pop() 函数通常会调用 elems.pop_back() 来删除 vector 中的最后一个元素。
void pop(); // 出栈
// top() 是一个 const 函数,表示这个函数不会修改栈的状态,它只会读取栈顶元素
// 返回类型是 T,即栈中元素的类型,可能会返回 elems.back(),即 vector 中的最后一个元素。
T top() const; // 返回栈顶元素
// empty 用于检查栈是否为空。如果栈中没有元素,则返回 true,否则返回 false。
bool empty() const{ // 如果为空则返回真。
return elems.empty();
}
};
const放在前后的区别:
举个例子:
假设你有一个类 Stack
,并且它有一个 pop()
函数和一个 empty()
函数:
class Stack {
private:
std::vector<int> elems;
public:
// 不加 const 的成员函数,可能会修改类的成员
void pop() {
elems.pop_back(); // 这会改变对象的状态
}
// 加上 const 的成员函数,不会修改类的成员
bool empty() const {
return elems.empty(); // 这只是读取状态,不修改任何成员
}
};
关键点:
pop()
函数不加const
,因为它会改变对象的状态(移除栈顶元素)。empty()
函数加上const
,因为它 不会 修改对象的状态,只是检查栈是否为空。
如果没有 const
会发生什么?
如果 empty()
没有 const
,就表示它可能修改类的成员变量,那么你就不能在一个 const
对象上调用 empty()
函数了。例如:
const Stack s; // 创建一个常量 Stack 对象
s.empty(); // 错误!不能调用非-const 成员函数
上面的代码会出错,因为你不能对 const
对象调用可能修改它的成员变量的函数。如果 empty()
是 const
,则允许调用。
为什么是 class
而不是 type
或其他?
class
作为模板类型参数的关键字其实是历史遗留下来的设计。最初,C++模板机制只允许类作为模板参数,因此使用了 class
关键字来表示类型参数。后来,C++ 增强了模板机制,允许不仅仅是类,还有其他类型(如基本数据类型、指针、函数等)作为模板的参数,但为了保持兼容性,class
依然保留了下来。
总结
empty() const
中的const
放在函数声明的末尾,是用于修饰成员函数,表示这个成员函数不会修改对象的状态(即不会修改类的成员变量)。- 如果没有
const
,那么在const
对象上不能调用这个函数,因为它承诺可能会修改对象的状态。
这是 C++ 中的一个设计约定,使得程序员能够明确知道哪些成员函数不会修改对象的状态,从而允许在常量对象上调用这些函数。
好了我们现在再来回答最开始的三个问题:
1.什么是模板?
在 C++ 中,模板(Template)是一种 泛型编程 的技术,它允许我们编写可以处理多种数据类型的代码,而不必重复编写每种数据类型的实现。模板使得程序可以更加灵活和通用。通过模板,程序员可以定义 类模板 或 函数模板,使得相同的代码可以处理不同的数据类型。
模板的基本形式:
- 函数模板:使得一个函数可以操作不同的数据类型。
- 类模板:使得一个类可以操作不同的数据类型。
示例:函数模板
template <typename T>
T add(T a, T b) {
return a + b;
}
示例:类模板
template <typename T>
class Box {
private:
T value;
public:
Box(T v) : value(v) {}
T getValue() const { return value; }
};
这里,Box
是一个模板类,T
是类型参数,Box
可以存储任意类型的数据。
2.为什么用模板?
模板的使用可以带来以下好处:
- 代码重用:
模板使得相同的代码可以在不同的数据类型上复用。你不需要为每个类型写多个版本的函数或类。通过模板,程序可以一次性处理多种类型,避免了代码的重复。
- 提高灵活性和通用性:
使用模板可以编写通用的代码,使得同一段代码可以适应不同类型的数据。这使得代码更加灵活,适应性强。
- 类型安全:
模板是类型安全的,因为在编译时,编译器会根据模板实例化时传入的类型进行检查,确保类型的正确性。这样,你可以在编译时发现潜在的类型错误。
- 性能优化:
使用模板可以让编译器在编译时生成类型特定的代码,这种方式比运行时进行动态类型检查更加高效。许多模板函数和类会通过 内联 和 类型特定优化 提高执行效率。
- 减少代码重复:
模板使得开发者不需要为不同的类型编写相似的代码。一次性编写模板函数或类,可以避免重复代码的维护和管理。
3.什么情况下用模板呢?
模板通常用于以下几种情况:
- 需要泛型函数或类的情况:
当你希望编写一个函数或类,可以处理多种数据类型时,模板非常有用。例如,你可以写一个排序函数,它可以处理整数、浮点数、字符串等不同的数据类型。
示例:
// typename 是必须的,它告诉编译器 T 是一个类型,不能直接省略。
template <typename T>
void printArray(T arr[], int size) {
for (int i = 0; i < size; ++i) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;
}
在这个例子中,printArray
函数是一个模板函数,它可以接受任何类型的数组并打印出来。
- 实现容器类(例如:栈、队列、链表等):
如果你需要定义一个通用的容器类(如栈、队列、链表等),并且希望它能存储不同类型的数据,模板是一个非常好的选择。C++ 标准库中的 std::vector
、std::list
、std::map
等容器类都是模板类。
示例:
template <typename T>
class Stack {
private:
std::vector<T> elems; // 存储元素的容器
public:
void push(const T& elem) { elems.push_back(elem); }
void pop() { elems.pop_back(); }
T top() const { return elems.back(); }
bool empty() const { return elems.empty(); }
};
在上面的代码中,Stack
是一个模板类,可以存储任何类型的元素。
- 类型不确定时:
如果你在编写的函数或类中无法事先确定数据类型,或者需要处理多种类型的对象,可以使用模板。模板允许在编译时指定类型,使得函数或类可以在不同场景下使用。
- 编写库或工具类:
当你开发一个通用库或工具类(例如数学库、数据结构库等)时,模板可以使你编写一个只需实现一次的代码,而不是为每个特定类型重写代码。
示例:
template <typename T>
T max(T a, T b) {
return (a > b) ? a : b;
}
在这个例子中,max
函数模板可以用来比较任意类型的数据,如整数、浮点数等。
- 性能敏感的应用:
模板可以在编译时生成特定类型的代码,避免了运行时的性能开销。在需要高度优化性能的场景中,模板可以让你在编译期进行更多的优化,例如 内联、常量折叠 等。
- 类型特征(Type Traits):
在一些高级编程场景中,你可能需要根据类型的不同特性(比如是否为某种特定类型、是否有某种成员函数等)来调整代码的行为。模板和类型特征(如 std::is_integral
)可以帮助你根据类型做出决策。
总结
1. 什么是模板?
模板是 C++ 中的一种机制,允许你定义类或函数的通用形式,使用类型参数来处理不同的数据类型。
2. 为什么用模板?
- 提高代码复用性。
- 提供更高的灵活性和通用性。
- 在编译时进行类型检查,保证类型安全。
- 通过编译期生成特定类型的代码,优化性能。
- 减少代码重复。
3. 什么时候用模板?
- 当需要编写泛型函数或类时。
- 实现容器类或数据结构时。
- 在类型不确定的情况下处理不同类型的数据。
- 编写通用库或工具类时。
- 在性能敏感的场景中进行编译期优化。
- 使用类型特征来做类型特定的选择。