目录
在 C++ 编程中,模板是实现泛型编程的核心机制。通过模板,我们可以创建通用的函数、类和算法,而不必预先指定具体的数据类型。通过模板,我们可以编写更加灵活、复用性更高的代码,极大地提升开发效率。
一、函数模板的定义与使用
1.1 基本概念
函数模板允许我们定义一个通用的函数,其参数类型和返回值类型可以是泛型的。语法格式如下:
template <typename T>
T max(T a, T b) {
return a > b ? a : b;
}
其中,typename
关键字也可以用 class
替代,二者在模板定义中没有区别。 template <typename T>
声明了一个模板参数 T
,它可以是任何类型。函数 max
可以接受两个类型为 T
的参数,并返回类型为 T
的结果。
1.2 模板参数推导
当调用函数模板时,编译器会根据传入的实参自动推导模板参数的类型:
int a = 5, b = 10;
int result = max(a, b); // 自动推导 T 为 int
double x = 3.14, y = 2.71;
double d_result = max(x, y); // 自动推导 T 为 double
1.3 显式指定模板参数
在某些情况下,编译器无法自动推导模板参数,此时需要显式指定:
// 显式指定 T 为 double
auto mixed_result = max<double>(a, x); // int 和 double 的混合比较
1.4 函数模板示例:通用交换函数
下面是一个通用的交换函数模板,它可以交换任意类型的两个变量:
template <typename T>
void swap(T& a, T& b) {
T temp = a;
a = b;
b = temp;
}
// 使用示例
int main() {
int x = 5, y = 10;
swap(x, y); // 交换两个 int
double a = 3.14, b = 2.71;
swap(a, b); // 交换两个 double
std::string s1 = "hello", s2 = "world";
swap(s1, s2); // 交换两个 string
return 0;
}
二、类模板的定义与使用
2.1 基本概念
类模板允许我们定义一个通用的类,其中的成员变量和成员函数可以使用泛型类型。语法格式如下:
template <typename T>
class Container {
private:
T value;
public:
Container(T val) : value(val) {}
T getValue() const { return value; }
void setValue(T val) { value = val; }
};
2.2 实例化类模板
使用类模板时,必须显式指定模板参数的类型:
Container<int> intContainer(42); // T 被实例化为 int
Container<double> doubleContainer(3.14); // T 被实例化为 double
2.3 类模板的成员函数
类模板的成员函数可以在类内部定义,也可以在类外部定义。在类外部定义时,需要使用完整的模板声明:
template <typename T>
class Container {
private:
T value;
public:
Container(T val);
T getValue() const;
void setValue(T val);
};
// 类外定义构造函数
template <typename T>
Container<T>::Container(T val) : value(val) {}
// 类外定义成员函数
template <typename T>
T Container<T>::getValue() const {
return value;
}
template <typename T>
void Container<T>::setValue(T val) {
value = val;
}
2.4 类模板示例:动态数组
下面是一个简单的动态数组类模板实现:
template <typename T>
class DynamicArray {
private:
T* data;
size_t size;
size_t capacity;
public:
DynamicArray() : data(nullptr), size(0), capacity(0) {}
~DynamicArray() { delete[] data; }
void push_back(const T& value) {
if (size >= capacity) {
resize(capacity == 0 ? 1 : capacity * 2);
}
data[size++] = value;
}
T& operator[](size_t index) { return data[index]; }
const T& operator[](size_t index) const { return data[index]; }
size_t getSize() const { return size; }
private:
void resize(size_t newCapacity) {
T* newData = new T[newCapacity];
for (size_t i = 0; i < size; ++i) {
newData[i] = data[i];
}
delete[] data;
data = newData;
capacity = newCapacity;
}
};
// 使用示例
int main() {
DynamicArray<int> intArray;
intArray.push_back(10);
intArray.push_back(20);
std::cout << "Array size: " << intArray.getSize() << std::endl;
std::cout << "Element at index 0: " << intArray[0] << std::endl;
DynamicArray<std::string> stringArray;
stringArray.push_back("Hello");
stringArray.push_back("World");
std::cout << "Element at index 1: " << stringArray[1] << std::endl;
return 0;
}
三、模板形参详解
3.1 模板类型形参
模板类型形参是最常见的模板参数,使用 typename
或 class
关键字声明:
template <typename T> // typename 和 class 在这里等价
class MyClass {};
3.2 非类型模板形参
非类型模板形参允许我们传递一个常量值作为模板参数。常见的非类型参数包括整数、指针和引用:
template <int N>
class FixedArray {
private:
int data[N];
public:
int& operator[](int index) { return data[index]; }
const int& operator[](int index) const { return data[index]; }
int size() const { return N; }
};
// 使用示例
FixedArray<10> arr; // 创建一个大小为 10 的数组
3.3 模板模板形参
模板模板形参允许将一个模板作为另一个模板的参数:
template <template <typename> class Container, typename T>
class Wrapper {
private:
Container<T> container;
public:
void add(const T& value) { container.push_back(value); }
};
// 使用示例
#include <vector>
#include <list>
Wrapper<std::vector, int> vecWrapper; // 使用 vector 作为容器
Wrapper<std::list, double> listWrapper; // 使用 list 作为容器
3.4 模板参数默认值
模板参数可以有默认值,类似于函数参数的默认值:
template <typename T = int, int N = 10>
class DefaultArray {
private:
T data[N];
public:
// ...
};
// 使用示例
DefaultArray<> defaultArray; // 使用默认参数:int, 10
DefaultArray<double, 20> customArray; // 自定义参数
四、重载操作符与模板结合
4.1 操作符重载基础
操作符重载允许我们为自定义类型重新定义操作符的行为。例如,为自定义类重载 +
操作符:
class Complex {
private:
double real, imag;
public:
Complex(double r = 0, double i = 0) : real(r), imag(i) {}
// 重载 + 操作符
Complex operator+(const Complex& other) const {
return Complex(real + other.real, imag + other.imag);
}
};
4.2 模板与操作符重载结合
当操作符重载与模板结合时,可以为泛型类提供通用的操作符行为:
template <typename T>
class Pair {
private:
T first, second;
public:
Pair(const T& a, const T& b) : first(a), second(b) {}
// 重载 + 操作符
Pair operator+(const Pair& other) const {
return Pair(first + other.first, second + other.second);
}
};
4.3 友元函数与模板
友元函数可以与模板结合,实现更灵活的操作符重载:
template <typename T>
class Point {
private:
T x, y;
public:
Point(T x = 0, T y = 0) : x(x), y(y) {}
// 声明友元函数
friend Point operator+(const Point& a, const Point& b) {
return Point(a.x + b.x, a.y + b.y);
}
};
五、类型转换与模板
5.1 隐式类型转换
模板类可以定义转换构造函数,实现从其他类型到模板类型的隐式转换:
template <typename T>
class SmartPtr {
private:
T* ptr;
public:
// 转换构造函数
template <typename U>
SmartPtr(U* p) : ptr(p) {}
// ...
};
// 使用示例
class Base {};
class Derived : public Base {};
SmartPtr<Base> ptr(new Derived); // 隐式转换
5.2 类型转换操作符
模板类也可以定义类型转换操作符,实现从模板类型到其他类型的转换:
template <typename T>
class Number {
private:
T value;
public:
Number(T val) : value(val) {}
// 类型转换操作符
template <typename U>
operator U() const {
return static_cast<U>(value);
}
};
// 使用示例
Number<int> num(42);
double d = num; // 隐式转换为 double
六、编写泛型程序的最佳实践
6.1 遵循最小特权原则
模板应该只依赖于完成任务所需的最小接口,提高代码的通用性:
// 不好的设计:依赖具体类型的接口
template <typename T>
void printAll(const std::vector<T>& vec) {
for (const auto& elem : vec) {
elem.print(); // 依赖 T 有 print() 方法
}
}
// 好的设计:只依赖必要的操作符
template <typename T>
void printAll(const std::vector<T>& vec) {
for (const auto& elem : vec) {
std::cout << elem << std::endl; // 只依赖 << 操作符
}
}
6.2 使用概念(Concepts)约束模板
C++20 引入的概念(Concepts)可以约束模板参数必须满足的条件:
// C++20 概念示例
template <typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::same_as<T>;
};
template <Addable T>
T sum(T a, T b) {
return a + b;
}
6.3 避免模板代码膨胀
过度使用模板可能导致代码膨胀,可以通过显式实例化和模板复用减少代码体积:
// 显式实例化
template class std::vector<int>; // 只在一处实例化 vector<int>
七、实战案例:实现一个通用矩阵类
下面是一个使用模板实现的通用矩阵类,展示了模板、操作符重载和类型转换的综合应用:
#include <iostream>
template <typename T, size_t Rows, size_t Cols>
class Matrix {
private:
T data[Rows][Cols];
public:
// 默认构造函数
Matrix() {
for (size_t i = 0; i < Rows; ++i) {
for (size_t j = 0; j < Cols; ++j) {
data[i][j] = T();
}
}
}
// 元素访问
T& operator()(size_t i, size_t j) {
return data[i][j];
}
const T& operator()(size_t i, size_t j) const {
return data[i][j];
}
// 矩阵加法
template <typename U>
Matrix<T, Rows, Cols> operator+(const Matrix<U, Rows, Cols>& other) const {
Matrix<T, Rows, Cols> result;
for (size_t i = 0; i < Rows; ++i) {
for (size_t j = 0; j < Cols; ++j) {
result(i, j) = data[i][j] + other(i, j);
}
}
return result;
}
// 矩阵乘法(需要满足列数等于另一个矩阵的行数)
template <size_t OtherCols>
Matrix<T, Rows, OtherCols> operator*(const Matrix<T, Cols, OtherCols>& other) const {
Matrix<T, Rows, OtherCols> result;
for (size_t i = 0; i < Rows; ++i) {
for (size_t j = 0; j < OtherCols; ++j) {
for (size_t k = 0; k < Cols; ++k) {
result(i, j) += data[i][k] * other(k, j);
}
}
}
return result;
}
// 添加打印函数
void print() const {
for (size_t i = 0; i < Rows; ++i) {
for (size_t j = 0; j < Cols; ++j) {
std::cout << data[i][j] << "\t";
}
std::cout << std::endl;
}
}
};
使用示例:
int main() {
Matrix<int, 2, 2> m1;
m1(0, 0) = 1; m1(0, 1) = 2;
m1(1, 0) = 3; m1(1, 1) = 4;
Matrix<int, 2, 2> m2;
m2(0, 0) = 5; m2(0, 1) = 6;
m2(1, 0) = 7; m2(1, 1) = 8;
std::cout << "Matrix m1:" << std::endl;
m1.print();
std::cout << "\nMatrix m2:" << std::endl;
m2.print();
Matrix<int, 2, 2> sum = m1 + m2;
std::cout << "\nm1 + m2:" << std::endl;
sum.print();
Matrix<int, 2, 2> product = m1 * m2;
std::cout << "\nm1 * m2:" << std::endl;
product.print();
return 0;
}
八、总结
模板是 C++ 中最强大的特性之一,它使得我们能够编写高度通用、灵活的代码。通过函数模板和类模板,我们可以创建不依赖于具体类型的算法和数据结构。模板参数可以是类型参数,也可以是非类型参数,甚至可以是模板本身。
重载操作符与模板结合,可以为泛型类提供直观的操作符语义,增强代码的可读性。类型转换机制则使得模板类能够与其他类型无缝协作。
在编写泛型程序时,我们应该遵循最小特权原则,使用 C++20 概念约束模板参数,并注意避免代码膨胀问题。通过合理使用模板、重载操作符和类型转换,我们可以创建出既通用又高效的 C++ 代码。
模板的实例化机制是连接模板定义与具体类型的桥梁,理解隐式实例化、显式实例化和特化的区别,对于编写高效、可维护的模板代码至关重要。随着 C++ 标准的不断发展,模板功能也在不断完善,如 C++20 引入的概念机制,使得模板更加易用和安全。