C++------模板初阶
引言
C++ 模板是一种强大的特性,它允许你编写与类型无关的代码,从而实现代码复用。模板分为函数模板和类模板。
泛型编程
基本概念
泛型编程允许你创建通用的函数、类或数据结构,这些通用组件可以处理多种数据类型,而不需要为每种数据类型单独编写代码。例如,一个通用的排序函数可以对整数数组、浮点数数组或字符串数组进行排序。
C++ 使用模板(templates)实现泛型编程。模板分为函数模板和类模板:
// 函数模板示例:交换两个变量的值
template <typename T>
void swap(T& a, T& b) {T temp = a;a = b;b = temp;
}// 类模板示例:动态数组
template <typename T>
class Vector {
private:T* data;size_t size;
public:// 类成员函数
};
泛型编程的优势
- 代码复用:避免为不同数据类型编写重复代码
- 类型安全:在编译时进行类型检查,减少运行时错误
- 性能优化:避免装箱和拆箱操作(如 Java 和 C# 中的值类型)
- 可读性:代码更加清晰,表达意图明确
接下来我们我们依次学习函数模板和类模板。
函数模板
基本语法
函数模板的声明以 template 关键字开始,后跟模板参数列表,然后是函数定义:
template <typename T> // 模板参数列表
返回类型 函数名(参数列表) {
// 函数体
}
- template:声明这是一个模板
- typename T:定义一个类型参数 T,typename 也可以用 class 替代
- T:在函数定义中作为通用类型使用
简单示例:交换函数
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); // 自动推导 T 为 intdouble a = 3.14, b = 2.71;swap(a, b); // 自动推导 T 为 double
}
多模板参数
函数模板可以有多个类型参数和非类型参数:
template <typename T, typename U, int Size>
T add(T a, U b) {return a + b;
}int main() {int result = add<int, double, 10>(5, 3.14); // 显式指定类型auto result2 = add(5, 3.14); // 自动推导 T=int, U=double
}
模板参数推导
编译器可以根据函数调用的实参自动推导模板参数
template <typename T>
T max(T a, T b) {return (a > b) ? a : b;
}int main() {int x = max(5, 10); // T 推导为 intdouble y = max(3.14, 2.71); // T 推导为 double// max(5, 3.14); // 错误:T 无法唯一推导(int vs double)max<double>(5, 3.14); // 显式指定 T,允许不同类型参数
}
函数模板重载
函数模板可以与普通函数重载,也可以相互重载:
// 模板函数
template <typename T>
T add(T a, T b) {return a + b;
}// 普通函数(重载)
int add(int a, int b) {return a + b;
}// 模板函数重载(不同参数)
template <typename T, typename U>
auto add(T a, U b) -> decltype(a + b) {return a + b;
}
模板特化
当模板函数需要针对特定类型提供不同的实现时,可以使用模板特化:
// 主模板
template <typename T>
bool isEqual(const T& a, const T& b) {return a == b;
}// 特化版本:针对 C 风格字符串
template <>
bool isEqual<const char*>(const char* const& a, const char* const& b) {return std::strcmp(a, b) == 0;
}
非类型模板参数
模板参数可以是类型参数,也可以是非类型参数(如整数、指针等):
template <typename T, int Size>
class Array {
private:T data[Size];
public:T& operator[](int index) { return data[index]; }
};// 使用示例
Array<int, 5> arr; // 创建一个包含5个int的数组
常见应用场景
- 通用算法:如排序、查找、交换等
- 容器操作:如容器元素的遍历、转换等
- 类型安全的函数包装:如 std::function
- 元编程:在编译时执行计算
注意事项
- 模板定义通常放在头文件中:因为模板实例化发生在编译阶段,需要看到完整定义
- 模板错误信息可能很复杂:编译器在实例化时才会检查类型兼容性
- 隐式实例化 vs 显式实例化:
// 显式实例化声明
extern template void swap<int>(int&, int&);
// 显式实例化定义
template void swap<double>(double&, double&);
函数模板的原理
一、底层机制:参数化类型与代码生成
函数模板的本质是参数化类型(Parameterized Types),即把数据类型作为参数传递给模板,让编译器在编译时生成对应类型的具体函数。这个过程分为两个阶段:
1模板定义阶段:
- 程序员编写模板代码,使用通用类型参数(如
T
)代替具体类型。 - 模板代码本身不会被编译成可执行代码,而是作为编译器生成代码的 “蓝图”。
2.模板实例化阶段:
- 当代码中调用模板函数时,编译器根据实参类型推导出模板参数的具体类型(如 int、double)。
- 编译器根据推导出的类型,用具体类型替换模板中的类型参数,生成对应版本的函数代码。
示例:
template <typename T>
T max(T a, T b) {return (a > b) ? a : b;
}int main() {int x = max(5, 10); // 实例化 max<int>(int, int)double y = max(3.14, 2.71); // 实例化 max<double>(double, double)
}
编译器会生成两个独立的函数:
// 编译器生成的代码
int max(int a, int b) {return (a > b) ? a : b;
}double max(double a, double b) {return (a > b) ? a : b;
}
二、编译过程:从模板到可执行代码 函数模板的编译分为三个关键步骤:
函数模板的编译分为三个关键步骤:
1.模板解析:
- 编译器读取模板定义,检查语法正确性,但不编译具体实现。
2.模板实例化:
- 当模板函数被调用时,编译器根据实参类型推导出模板参数的具体类型。
- 如果该类型组合的实例尚未存在,编译器生成对应的函数代码。
3.代码优化:
- 编译器对生成的代码进行优化,如同普通函数一样。
实例化触发条件:
- 函数调用(如 max(5, 10))
- 取函数地址(如 auto func = &max<int>;)
- 显式实例化声明 / 定义(如 template void swap<int>(int&, int&);)
三、类型推导与重载解析
1. 自动类型推导
编译器根据函数调用时的实参类型自动推导模板参数:
template <typename T>
void print(T value) {std::cout << value << std::endl;
}print(42); // T 推导为 int
print("hello"); // T 推导为 const char*
2. 显式指定类型
当自动推导失败时(如函数参数与模板参数无关),需显式指定类型:
template <typename T>
T fromString(const std::string& str) {// ...
}int num = fromString<int>("123"); // 显式指定 T 为 int
3. 重载解析规则
当存在多个重载版本(普通函数、模板函数、特化模板)时,编译器按以下顺序选择:
- 完全匹配的普通函数
- 模板函数的特化版本
- 主模板的实例化版本
// 普通函数
void print(int value) {std::cout << "int: " << value << std::endl;
}// 模板函数
template <typename T>
void print(T value) {std::cout << "T: " << value << std::endl;
}int main() {print(42); // 调用普通函数(完全匹配)print("hello"); // 调用模板函数(实例化为 const char*)
}
总结
函数模板的核心原理是编译时的类型参数替换和代码生成,通过自动类型推导和重载解析,实现了通用代码的高效复用。理解模板的实例化机制有助于避免常见错误(如分离编译问题、代码膨胀),并充分发挥 C++ 泛型编程的威力。
类模板
类模版与函数模版类似,只不过作用对象是类。
一、基本语法
类模板的声明以template关键字开始,后跟模板参数列表,然后是类定义:
template <typename T> // 模板参数列表 class ClassName { private:T data; // 使用泛型类型T public:ClassName(T value) : data(value) {}T getData() const { return data; } };
- template <typename T>:声明一个类型参数T
- T:在类定义中作为通用类型使用
二、简单示例:动态数组类
template <typename T>
class Array {
private:T* data;size_t size;
public:Array(size_t size) : size(size) {data = new T[size];}~Array() {delete[] data;}T& operator[](size_t index) {return data[index];}const T& operator[](size_t index) const {return data[index];}size_t getSize() const {return size;}
};// 使用示例
Array<int> intArray(5); // 整数数组
Array<double> doubleArray(3); // 双精度数组
三、类模板的特性
1. 多模板参数
类模板可以有多个类型参数和非类型参数:
template <typename T, int Size>
class FixedArray {
private:T data[Size];
public:// ...
};// 使用示例
FixedArray<int, 10> fixedIntArray; // 包含10个整数的数组
2. 成员函数模板
类的成员函数可以是模板函数:
template <typename T>
class Container {
private:T value;
public:Container(T val) : value(val) {}template <typename U>U convert() const {return static_cast<U>(value);}
};// 使用示例
Container<int> c(42);
double d = c.convert<double>(); // 将int转换为double
3. 静态成员
类模板的静态成员变量会为每个实例化的类型单独生成:
template <typename T>
class Counter {
public:static int count;Counter() { count++; }
};template <typename T>
int Counter<T>::count = 0; // 静态成员初始化// 使用示例
Counter<int> c1, c2; // Counter<int>::count = 2
Counter<double> c3; // Counter<double>::count = 1
四、类模板特化
当类模板需要针对特定类型提供不同的实现时,可以使用类模板特化:
1. 全特化
为特定类型提供完全不同的实现:
// 主模板
template <typename T>
class Storage {
public:void store(T value) { /* 通用实现 */ }
};// 全特化:针对bool类型
template <>
class Storage<bool> {
public:void store(bool value) { /* 针对bool的优化实现 */ }
};
2. 偏特化
为部分类型参数提供特化实现:
// 主模板
template <typename T, typename U>
class Pair {
public:// ...
};// 偏特化:第二个参数为int
template <typename T>
class Pair<T, int> {
public:// 针对U=int的特殊实现
};
六、类模板的应用场景
- 容器类:如std::vector、std::list、std::map
- 智能指针:如std::unique_ptr、std::shared_ptr
- 算法包装器:如std::function
- 元编程:编译时计算和类型操作
- 泛型工具类:如数学向量、矩阵、任意类型的包装器
七、注意事项
- 模板定义通常放在头文件中:因为编译器在实例化时需要看到完整定义
- 显式实例化:如果需要控制实例化位置,可以使用显式实例化:
// 显式实例化定义
template class Array<int>;
模板参数依赖名称:在使用依赖于模板参数的类型时,需使用typename关键字:
template <typename T>
void func() {typename T::iterator it; // 使用typename表明iterator是类型
}
八、与函数模板的对比
特性 | 函数模板 | 类模板 |
实例化触发 | 函数调用或取地址 | 创建对象或显式实例化 |
特化方式 | 全特化 | 全特化和偏特化 |
静态成员 | 不支持(每个实例共享) | 每个实例化类型单独生成 |
继承 | 不支持继承 | 支持复杂的继承关系 |
类模板为何优于 typedef
类模板相比于typedef在C++编程中具有显著的优越性,这主要体现在它们各自的功能和应用场景上。typedef主要用于为现有的类型定义一个新的名称(别名),而类模板则提供了一种更灵活、更强大的方式来定义可以在多种数据类型上工作的类。
类模板:通过类模板,可以定义一个与具体数据类型无关的类,这个类可以在实例化时指定数据类型。这种特性使得类模板能够在多种数据类型上重用相同的代码,减少了代码冗余,提高了代码的复用性。同时,它也支持泛型编程,使得函数或类可以更加通用,不依赖于具体的数据类型。
typedef:typedef只是为现有的类型定义了一个新的名称,它本身并不支持泛型编程,也不具备在不同数据类型上重用代码的能力。