C++模板是C++中非常强大的特性,他允许我们编写通用的代码,从而避免重复编写相似功能的函数或类。
1 模板的基本概念
1.1 模板是什么?
用简单的程序例子来讲一下:
int add(int& a,int& b){ return a+b; }
void add(double& a,double& b){ return a+b; }
通过上面的代码,我们发现,这两个函数实现的功能都是数据交换,唯一的不同点就是数据类型不同,难道说每一次去交换不同数据类型都要重写函数?显然这样很麻烦,那么是否可以写出一个通用的代码呢?可以的,那就是模板。
官方的概念说模板是C++的一种工具,允许我们编写与数据类型无关的代码。通过模板,我们可以定义函数或类,使其能够处理多种数据类型。
既然已经知道了概念,接下来就是基本用法。
1.2 模板的分类
在讲基本用法之前,我们要知道C++模板的类别,C++模板主要分为两类:
函数模板 :用于定义通用的函数
类模板:用于定义通用的类
1.3 函数模板基本语法
cpp
template <typename T>
T add(T& a,T& b){
return a+b;
}
- template <typename
T
>:声明一个模板,T是一个占位符,表示任意数据类型 - T add(T& a,T& b):定义一个函数,参数和返回值类型都是T
例子:
cpp
#include <iostream>
using namespace std;
template <typename T>
T add(T a, T b) {
return a + b;
}
int main() {
cout << add(3, 5) << endl; // 输出 8
cout << add(3.5, 2.5) << endl; // 输出 6.0
return 0;
}
1.4 函数模板的注意事项
- 自动类型推导,必须推导出一致的数据类型T,才能使用
简单引用例子来说一下:
cpp
#include <iostream>
using namespace std;
template <typename T>
T add(T a, T b) {
return a + b;
}
int main() {
cout << add(3, 5) << endl; //正确:T被推导为int
cout << add(3.5, 2.5) << endl; //正确:T被推导为double
cout << add(3.5,2) <<endl; //错误:T无法同时推导为int 和 double
return 0;
}
我想在上面的代码中,会有疑问。就是在普通函数中,cout << add(3.5,2) <<endl;
是支持隐式类型转换的,而在这里为什么不行。
在模板函数中,类型推导 的规则与普通函数不同。模板参数的类型是根据传入的参数自动推导的,且要求所有参数的类型必须一致。
模板函数的设计初衷是类型安全 和通用性 。模板参数的类型推导是基于传入参数的实际类型,而不是基于可能的隐式转换。这样做的好处是:
1.避免意外行为 :隐式类型转换可能导致意外的精度损失或行为变化。
2.明确类型 : 模板函数要求调用者明确传入参数的类型,避免歧义。
那么如何解决这个问题?
方法1:显示指定类型
在调用模板函数时,显示指定模板参数的类型。像这样:
add<double>(3.5,2);
方法2:使用多个模板参数
定义模板函数时,使用多个模板参数来支持不同类型的参数。例如:
cpp
template <typename T1, typename T2>
T1 add(T1 a, T2 b) {
return a+b;
}
int main() {
cout << add(3.5,2) <<endl; // T1 = double, T2 = int
return 0;
}
其实,上述代码还是有点问题的。如果说T1是int,T2是double。这样调用时候,函数返回的类型是int,就会引起精度损失。如何解决?请看方法三。
方法3:使用std::common_type
使用std::common_type
来推导出两个类型的公共类型。函数模板可以返回统一的类型。
例如:
cpp
#include <iostream>
#include <type_traits> // 包含 std::common_type
using namespace std;
template <typename T1, typename T2>
typename common_type<T1, T2>::type add(T1 a, T2 b) {
return a + b;
}
int main() {
auto result1 = add(3, 5.5); // T1 = int, T2 = double
auto result2 = add(3.5, 2.5); // T1 = double, T2 = double
auto result3 = add(3, 5); // T1 = int, T2 = int
cout << "Result 1: " << result1 << endl; // 输出 8.5
cout << "Result 2: " << result2 << endl; // 输出 6.0
cout << "Result 3: " << result3 << endl; // 输出 8
return 0;
}
- 使用
std::common_type
可以推导出两个类型的公共类型,确保返回值不会引起精度损失。 - 这种方法适用于任意类型的参数组合(如
int
+double
、double
+double
int
+int
等)。 - 通过
auto
关键字,可以方便地接收返回值,而无需手动指定类型。
1.5 函数模板案例:数组排序
案例描述:
●利用函数模板封装一个排序的函数,可以对不同数据类型数组进行排序
●排序规则从大到小,排序算法为选择排序
●分别利用char数组和int数组进行测试
cpp
#include <iostream>
using namespace std;
// 选择排序函数模板
template <typename T>
void selectionSort(T arr[], int size) {
for (int i = 0; i < size - 1; i++) {
int maxIndex = i; // 假设当前最大值的索引是 i
for (int j = i + 1; j < size; j++) {
if (arr[j] > arr[maxIndex]) { // 从大到小排序
maxIndex = j; // 更新最大值的索引
}
}
// 交换 arr[i] 和 arr[maxIndex]
T temp = arr[i];
arr[i] = arr[maxIndex];
arr[maxIndex] = temp;
}
}
// 打印数组的函数模板
template <typename T>
void printArray(T arr[], int size) {
for (int i = 0; i < size; i++) {
cout << arr[i] << " ";
}
cout << endl;
}
int main() {
// 测试 int 数组
int intArr[] = { 5, 3, 1, 4, 2 };
int intSize = sizeof(intArr) / sizeof(intArr[0]);
cout << "原始int数组: ";
printArray(intArr, intSize);
selectionSort(intArr, intSize);
cout << "排序后的int数组: ";
printArray(intArr, intSize);
// 测试 char 数组
char charArr[] = { 'd', 'a', 'c', 'b', 'e' };
int charSize = sizeof(charArr) / sizeof(charArr[0]);
cout << "原始char数组: ";
printArray(charArr, charSize);
selectionSort(charArr, charSize);
cout << "排序后的char数组: ";
printArray(charArr, charSize);
return 0;
}
输出结果是:
1.6 普通函数与函数模板的区别
1.普通函数:
(1)只能处理特定的数据类型。 如int add(int a,int b)
只能处理int 类型。
(2) 可以发生自动类型转换(隐式类型转换)
2.函数模板:
(1)可以处理多种数据类型。例如,template<typename T>T add(T a,T b)
可以处理int、double等类型。
(2)不显示指定类型,不会发生隐式转换。如add< double >(3.5,2);
就可以发生隐式转换。
1.7 普通函数与函数模板的调用规则
1.如果函数模板和普通函数都匹配,优先调用普通函数。
示例:
cpp
#include <iostream>
using namespace std;
void func(int a) { // 普通函数
cout << "普通函数调用: " << a << endl;
}
template <typename T>
void func(T a) { // 函数模板
cout << "函数模板调用: " << a << endl;
}
int main() {
func(10); // 调用普通函数,因为完全匹配
func(3.14); // 调用模板,因为没有匹配的普通函数
return 0;
}
输出
2.如果普通函数和模板都能匹配,但普通函数需要类型转换,模板会被优先选择。
如果普通函数匹配需要隐式类型转换,而函数模板可以直接匹配,那么编译器会优先选择函数模板。
示例
cpp
#include <iostream>
using namespace std;
void func(double a) { // 普通函数
cout << "普通函数调用: " << a << endl;
}
template <typename T>
void func(T a) { // 函数模板
cout << "函数模板调用: " << a << endl;
}
int main() {
func(10); // 函数模板调用(普通函数需要 int -> double 的转换)
return 0;
}
输出
3.使用显式模板参数(或空模板参数)时,强制调用函数模板
示例
cpp
#include <iostream>
using namespace std;
void func(int a) { // 普通函数
cout << "普通函数调用: " << a << endl;
}
template <typename T>
void func(T a) { // 函数模板
cout << "函数模板调用: " << a << endl;
}
int main() {
func<int>(10); // 强制调用函数模板
func<>(10);
return 0;
}
输出
4.函数模板可以重载
如果存在多个模板匹配,编译器会选择最匹配的模板实例。
示例
cpp
#include <iostream>
using namespace std;
template <typename T>
void func(T a) {
cout << "函数模板 1 调用: " << a << endl;
}
template <typename T, typename U>
void func(T a, U b) {
cout << "函数模板 2 调用: " << a << " 和 " << b << endl;
}
int main() {
func(10); // 调用单参数模板
func(10, 3.14); // 调用双参数模板
return 0;
}
输出
5.函数模板可以被特化
可以专门为某个类型定义特化版本,编译器会优先选择特化版本。
示例
cpp
#include <iostream>
using namespace std;
template <typename T>
void func(T a) {
cout << "通用模板: " << a << endl;
}
// 针对 int 类型的特化版本
template <>
void func<int>(int a) {
cout << "int 类型特化: " << a << endl;
}
int main() {
func(10); // 调用 int 类型特化版本
func(3.14); // 调用通用模板
return 0;
}
输出
1.8 类模板基本语法
C++的类模板允许我们编写泛型类,可以适用于多种数据类型,提供代码的复用性和灵活性。类模板的本质是:在编译时生成特定类型的类。
1.类模板的基本定义
语法格式
cpp
template <typename T>
class 类名 {
// 成员变量和成员函数
};
template< typename T>:声明一个模板类型参数T,T可以是任何数据类型。typename可以替换为class。
T在类中用作占位符,等真正使用类模板时,才会替换成具体的数据类型。
2.类模板示例
定义一个通用的数组类
cpp
#include <iostream>
using namespace std;
// 定义一个类模板
template <typename T>
class MyArray {
private:
T data[5]; // 静态数组
public:
void set(int index, T value) {
data[index] = value;
}
T get(int index) {
return data[index];
}
};
int main() {
MyArray<int> intArray; // 实例化为 int 类型
intArray.set(0, 100);
cout << "intArray[0]: " << intArray.get(0) << endl;
MyArray<double> doubleArray; // 实例化为 double 类型
doubleArray.set(0, 3.14);
cout << "doubleArray[0]: " << doubleArray.get(0) << endl;
return 0;
}
输出
1.9 类模板的成员函数的定义
类模板的成员函数可以在类内定义,也可以在类外定义。
在类内定义(内联)
cpp
template <typename T>
class Box {
public:
T value;
void set(T val) { value = val; }
T get() { return value; }
};
在类外定义
cpp
template <typename T>
class Box {
private:
T value;
public:
void set(T val); // 函数声明
T get();
};
// 在类外定义成员函数(注意模板语法)
template <typename T>
void Box<T>::set(T val) {
value = val;
}
template <typename T>
T Box<T>::get() {
return value;
}
int main() {
Box<int> intBox;
intBox.set(42);
cout << "intBox: " << intBox.get() << endl;
return 0;
}
必须加template<typename T>
Box<T>::
作用域修饰符不可省略
1.10 类模板成员函数的创建时机
在C++中,类模板的成员函数并不会立即被创建(实例化),而是在需要时才会生成。这个机制被称为延时实例化。
1.什么时候类模板成员函数会被创建?
类模板的成员函数只有在"被调用"或"被使用"时才会被编译器真正实例化。
示例
cpp
#include <iostream>
using namespace std;
template <typename T>
class MyClass {
public:
void func1() { cout << "调用 func1()" << endl; }
void func2(); // 只是声明,并不会立即创建
};
template <typename T>
void MyClass<T>::func2() { cout << "调用 func2()" << endl; }
int main() {
MyClass<int> obj; // 实例化 MyClass<int>,但不生成任何成员函数
obj.func1(); // 只有调用 func1() 时,func1() 才会被实例化
return 0;
}
解析
MyClass<int> obj;
本身不会导致func1()
和func2()
被实例化。
obj.func1();
时,编译器才会生成func1()
的代码。
func2()
没有被调用,因此func2()
并未实例化。
2.只有被调用的成员函数才会实例化。
举个例子,在函数实现过程中写一个错误来检测是否被实例化。
cpp
#include <iostream>
using namespace std;
template <typename T>
class Test {
public:
void func1() { cout << "func1() 被调用" << endl; }
void func2() { cout << 10 / 0 << endl; } // 存在错误(除以0)
};
int main() {
Test<int> obj;
obj.func1(); // 只调用 func1()
return 0;
}
输出
解析:
func2()
内部存在10/0的错误,但因为func()
没有被调用,所以不会报错。
如果obj.func2();
被调用,编译器会实例化它,并检测到错误。
3.只有用到的成员变量才会实例化
类似于成员函数,类模板的成员变量也是按需实例化。
示例
cpp
#include <iostream>
using namespace std;
template <typename T>
class Example {
public:
int a; // 会实例化
double b; // 不会实例化(因为没有被使用)
};
int main() {
Example<int> obj;
obj.a = 10;
cout << obj.a << endl; // 只用到了 a,b 不会实例化
return 0;
}
解析
由于b未被使用,编译器不会实例化b。
4.纯虚函数不会实例化
在类模板中,如果有纯虚函数,那么除非子类重写并调用它,否则不会实例化。
cpp
#include <iostream>
using namespace std;
template <typename T>
class Base {
public:
virtual void show() = 0; // 纯虚函数
};
class Derived : public Base<int> {
public:
void show() override { cout << "派生类实现 show()" << endl; }
};
int main() {
Derived d; // 只有 Derived 继承并实现 show(),才会实例化 show()
d.show();
return 0;
}
解析
纯虚函数show()
不会在Base<int>
被实例化时创建。
只有Derived
继承并重写show()
时,该函数才会实例化。
5.显式实例化成员函数
我们可以手动强制实例化 某个类模板的特定函数。
示例
cpp
#include <iostream>
using namespace std;
template <typename T>
class MyClass {
public:
void func1() { cout << "func1() 实例化" << endl; }
void func2() { cout << "func2() 实例化" << endl; }
};
// 显式实例化 func2()
template void MyClass<int>::func2();
int main() {
return 0; // 没有调用任何函数
}
解析
由于template void MyClass<int>::func2();
,即使func2()没有被调用,它仍然会被实例化。
1.11 类模板与继承
在C++中,类模板可以作为基类,也可以作为派生类。当类模板与继承结合时,需要注意一些特殊情况,例如基类依赖参数化类型,以及如何正确使用作用域解析符。
1.让普通类继承类模板
普通类可以继承类模板,并在继承时明确指定基类的模板参数 。
示例
cpp
#include <iostream>
using namespace std;
// 类模板
template <typename T>
class Base {
public:
T data;
Base(T val) : data(val) {}
void show() { cout << "Base data: " << data << endl; }
};
// 普通类继承类模板
class Derived : public Base<int> { // 指定 T = int
public:
Derived(int val) : Base<int>(val) {}
void print() { cout << "Derived data: " << data << endl; }
};
int main() {
Derived d(42);
d.show(); // 调用基类函数
d.print(); // 调用派生类函数
return 0;
}
输出
解析
Derived
继承Base<int>
,所以Base
的T变成了int。
Derived
可以直接访问Base
的data
和show()
。
2.让类模板继承类模板
当类模板继承另一个类模板时,派生类可以选择:保留泛型(不指定类型参数) ;指定部分或全部类型参数 。
示例1:保留泛型
cpp
#include <iostream>
using namespace std;
template <typename T>
class Base {
public:
T data;
Base(T val) : data(val) {}
void show() { cout << "Base data: " << data << endl; }
};
// 派生类也是模板类,T 由用户决定
template <typename T>
class Derived : public Base<T> {
public:
Derived(T val) : Base<T>(val) {}
void print() { cout << "Derived data: " << this->data << endl; }
};
int main() {
Derived<double> d(3.14);
d.show();
d.print();
return 0;
}
输出
解析
Derived<T>
继承Base<T>
,派生类仍然是模板类。
this->data
用于访问基类成员,避免编译器报错。
3.让类模板继承具体类型的类模板
如果派生类不想继续保留泛型,它可以直接指定基类的模板参数 。
示例
cpp
#include <iostream>
using namespace std;
template <typename T>
class Base {
public:
T data;
Base(T val) : data(val) {}
void show() { cout << "Base data: " << data << endl; }
};
// 派生类固定 T 为 int
class Derived : public Base<int> {
public:
Derived(int val) : Base<int>(val) {}
void print() { cout << "Derived data: " << data << endl; }
};
int main() {
Derived d(100);
d.show();
d.print();
return 0;
}
输出
4.多重继承与类模板
类模板可以通过多重继承继承多个类模板或普通类。
示例
cpp
#include <iostream>
using namespace std;
template <typename T>
class Base1 {
public:
T data1;
Base1(T val) : data1(val) {}
void show1() { cout << "Base1 data: " << data1 << endl; }
};
template <typename T>
class Base2 {
public:
T data2;
Base2(T val) : data2(val) {}
void show2() { cout << "Base2 data: " << data2 << endl; }
};
// 派生类同时继承两个类模板
template <typename T>
class Derived : public Base1<T>, public Base2<T> {
public:
Derived(T val1, T val2) : Base1<T>(val1), Base2<T>(val2) {}
void print() {
cout << "Derived data1: " << this->data1 << ", data2: " << this->data2 << endl;
}
};
int main() {
Derived<double> d(3.14, 2.71);
d.show1();
d.show2();
d.print();
return 0;
}
输出
5.注意事项
(1)访问基类成员时需要this->
或Base<T>::
由于C++编译器在编译模板时不会立即知道基类的成员变量,所以访问基类成员时可能会报错: 没有找到接收"overloaded-function"类型的右操作运算符(或没有可接收的转换)。
错误示例
cpp
template <typename T>
class Base {
public:
T data;
};
template <typename T>
class Derived : public Base<T> {
public:
void print() {
cout << data << endl; // ❌ 编译错误
}
};
正确示例
cpp
template <typename T>
class Derived : public Base<T> {
public:
void print() {
cout << this->data << endl; // 正确
// cout << Base<T>::data << endl; //另一种正确方式
}
};
(2)构造函数初始化基类
在派生类的构造函数中,基类的构造函数需要显式调用,否则编译器不知道如何初始化基类成员。
错误示例
cpp
template <typename T>
class Base {
public:
T data;
Base(T val) : data(val) {} // 需要初始化
};
template <typename T>
class Derived : public Base<T> {
public:
// ❌ 错误,没有显式调用 Base<T> 构造函数
Derived(T val) {}
};
正确示例
cpp
template <typename T>
class Derived : public Base<T> {
public:
Derived(T val) : Base<T>(val) {} //正确
};
1.12 类模板与函数模板的区别
类模板与函数模板的区别主要体现在定义方式、使用方式、实例化时机以及适用场景等方面。
1.定义方式
定义通用函数 ,可以接受不同类型的参数,不需要重复写多个函数。
特点:编译时自动推导类型;适用于操作简单的逻辑(如加法、排序等)。
定义通用的类 ,可以存储和操作不同类型的数据。
特点:封装数据和行为(不像函数模板只是一个简单函数);实例化时需要指定类型;适用于数据结构和类的设计。
2.作用范围
对比项 | 函数模板 | 类模板 |
---|---|---|
作用 | 定义通用函数 | 定义通用类 |
适用场景 | 适用于逻辑简单的算法(如加法、排序等) | 适用于封装数据和操作(如容器类) |
实例化时机 | 编译时自动推导(可以手动指定) | 必须手动指定类型 |
代码复用性 | 代码复用程度较低 | 代码复用程度高,适合面向对象设计 |
3.成员函数创建时机
函数模板:只有在被调用时才会实例化。
类模板:只有当类被实例化时才会生成代码。
4.继承方式
函数模板不能继承,但类模板可以继承。
5.什么时候用函数模板?什么时候用类模板?
用函数模板:当需要单个函数的泛型处理。
用类模板:当需要封装数据和行为。
1.13 类模板的分文件编写
类模板的分文件编写(即**.h+**.cpp)。由于模板的特殊性,普通的分文件方式不适用于类模板,而是需要特殊的处理方法。
1.为什么类模板不能像普通类一样分为.h和.cpp文件?
C++编译器在编译.cpp文件时,并不会提前实例化模板,而是只有在使用类模板时才会生成代码。
如果我们把类模板的定义放在.h头文件,而实现放在.cpp文件,编译器在编译.cpp时并不会知道具体的模板参数,因此不会生成代码,导致链接错误(未定义引用)。
错误示例
cpp
// MyClass.h
#pragma once
template <typename T>
class MyClass {
public:
MyClass(T val);
void show();
private:
T data;
};
// MyClass.cpp
#include "MyClass.h"
#include <iostream>
using namespace std;
template <typename T>
MyClass<T>::MyClass(T val) : data(val) {}
template <typename T>
void MyClass<T>::show() {
cout << "Data: " << data << endl;
}
// main.cpp
#include "MyClass.h"
int main() {
MyClass<int> obj(10);
obj.show(); // ❌ 这里会报链接错误 (undefined reference)
return 0;
}
报错原因:
在MyClass.h
里,编译器不知道具体的T是什么,不会提前实例化MyClass<T>
的代码。
MyClass.cpp
里没有显式实例化MyClass<int>
,导致main.cpp
里无法找到show()
的实现。
2.解决方案
方法1:把实现全部放到.h头文件
最常用的方法是把所有模板代码都写在头文件中。
cpp
// MyClass.h
#pragma once
#include <iostream>
using namespace std;
template <typename T>
class MyClass {
public:
MyClass(T val);
void show();
private:
T data;
};
// 直接在头文件实现函数
template <typename T>
MyClass<T>::MyClass(T val) : data(val) {}
template <typename T>
void MyClass<T>::show() {
cout << "Data: " << data << endl;
}
cpp
// main.cpp
#include "MyClass.h"
int main() {
MyClass<int> obj(10);
obj.show(); //正确输出 "Data: 10"
return 0;
}
方法2:使用.h+.hpp方式
如果想要分离模板的声明和实现,可以创建一个.hpp文件,专门存放模板的实现代码。
cpp
// MyClass.h (头文件: 声明)
#pragma once
template <typename T>
class MyClass {
public:
MyClass(T val);
void show();
private:
T data;
};
// 让编译器包含实现文件
#include "MyClass.hpp"
cpp
// MyClass.hpp (实现文件)
#include <iostream>
using namespace std;
template <typename T>
MyClass<T>::MyClass(T val) : data(val) {}
template <typename T>
void MyClass<T>::show() {
cout << "Data: " << data << endl;
}
cpp
// main.cpp
#include "MyClass.h"
int main() {
MyClass<int> obj(10);
obj.show(); //正确输出 "Data: 10"
return 0;
}
优点:
分离声明和实现,提高可读性;避免.cpp文件编译错误。
方法3:显式实例化
如果一定要分为.h+.cpp文件,可以用显式实例化来强制编译器实例化特定类型的模板。
cpp
// MyClass.h (头文件)
#pragma once
template <typename T>
class MyClass {
public:
MyClass(T val);
void show();
private:
T data;
};
cpp
// MyClass.cpp (源文件)
#include "MyClass.h"
#include <iostream>
using namespace std;
// 模板实现
template <typename T>
MyClass<T>::MyClass(T val) : data(val) {}
template <typename T>
void MyClass<T>::show() {
cout << "Data: " << data << endl;
}
// 显式实例化
template class MyClass<int>; // 仅实例化 MyClass<int>
template class MyClass<double>; // 仅实例化 MyClass<double>
cpp
// main.cpp
#include "MyClass.h"
int main() {
MyClass<int> obj(10);
obj.show(); //正确输出 "Data: 10"
return 0;
}
缺点:只能实例化特定类型,比如MyClass<float>
在这里不能使用,扩展性差。
1.14类模板与友元
在C++中,友元(friend)关键字可以用于让某个函数或类访问类的私有成员。当类涉及模板时,友元的声明和使用会有所不同。下面通过不同情况来讲解类模板与友元的使用方法。
1.友元函数(全局函数)与类模板
如何让全局函数访问类模板的私有成员?
在普通类中,我们可以直接使用friend关键字声明友元函数,但在类模板中,友元函数也需要是模板函数,或者显式声明具体类型。
解决方案
cpp
#include <iostream>
using namespace std;
// 类模板
template <typename T>
class MyClass {
private:
T data;
public:
MyClass(T val) : data(val) {}
// 声明友元函数(全局函数)
friend void show(const MyClass<T>& obj) {
cout << "数据: " << obj.data << endl;
}
};
// 测试代码
int main() {
MyClass<int> obj(42);
show(obj); //输出:数据: 42
return 0;
}
输出
解释
这里friend void show(const MyClass<T>& obj);
让show()
成为所有MyClass<T>
实例友元。这样show()
可以直接访问MyClass的私有成员data
。show()
仍然是一个普通的非模板函数,只是可以访问MyClass<T>
的私有数据。
2.友元模板函数
如果希望所有类型的MyClass<T>
都使用相同的友元函数,可以将友元函数声明为模板函数。
cpp
#include <iostream>
using namespace std;
// 类模板
template <typename T>
class MyClass {
private:
T data;
public:
MyClass(T val) : data(val) {}
// 友元模板函数
template <typename U>
friend void showData(const MyClass<U>& obj);
};
// 友元模板函数的实现
template <typename U>
void showData(const MyClass<U>& obj) {
cout << "数据: " << obj.data << endl;
}
// 测试代码
int main() {
MyClass<int> obj1(100);
MyClass<double> obj2(3.14);
showData(obj1); // 输出:数据: 100
showData(obj2); // 输出:数据: 3.14
return 0;
}
输出
解释
showData<U>
是一个友元模板函数,可以访问所有类型MyClass<T>
的私有成员。 这样MyClass<int>
和MyClass<double>
都可以共享showData()
。
3.友元类(让另一个类访问类模板的私有成员)
如何让一个类访问MyClass<T>
的私有成员?
如果我们想要另一个类访问MyClass<T>
的私有成员,我们可以把这个类声明为友元类。
解决方案
cpp
#include <iostream>
using namespace std;
// 预先声明 FriendClass
template <typename T>
class FriendClass;
// 类模板
template <typename T>
class MyClass {
private:
T data;
public:
MyClass(T val) : data(val) {}
// 友元类声明
friend class FriendClass<T>;
};
// 友元类
template <typename T>
class FriendClass {
public:
void show(const MyClass<T>& obj) {
cout << "FriendClass 访问私有数据: " << obj.data << endl;
}
};
// 测试代码
int main() {
MyClass<int> obj(77);
FriendClass<int> friendObj;
friendObj.show(obj); //输出: FriendClass 访问私有数据: 77
return 0;
}
输出
解释
friend class FriendClass<T>;
使得FriendClass<T>
可以访问MyClass<T>
的私有成员;FriendClass<T>
只能访问相同类型的MyClass<T>
(比如FriendClass<int>
只能访问MyClass<int>
);模板类FriendClass
需要提前声明,否则编译器无法识别。
4.友元类模板
如果我们希望所有MyClass<T>
都可以被FriendClass<X>
访问,不管T和X是否相同,那么FriendClass
也应该是模板类。
cpp
#include <iostream>
using namespace std;
// 预先声明 FriendClass
template <typename T, typename U>
class FriendClass;
// 类模板
template <typename T>
class MyClass {
private:
T data;
public:
MyClass(T val) : data(val) {}
// 让所有 FriendClass<U, V> 都是友元
template <typename U, typename V>
friend class FriendClass;
};
// 友元模板类
template <typename U, typename V>
class FriendClass {
public:
void show(const MyClass<U>& obj) {
cout << "FriendClass<U> 访问私有数据: " << obj.data << endl;
}
void show(const MyClass<V>& obj) {
cout << "FriendClass<V> 访问私有数据: " << obj.data << endl;
}
};
// 测试代码
int main() {
MyClass<int> obj1(88);
FriendClass<int, double> friendObj1; // X = int, Y = double
friendObj1.show(obj1); // 输出: FriendClass 访问私有数据: 88
MyClass<double> obj2(99);
FriendClass<double, int> friendObj2; // X = double, Y = int
friendObj2.show(obj2); // 输出: FriendClass 访问私有数据: 99
FriendClass<char, int> friendObj3;
friendObj3.show(obj1); // FriendClass<int, double> 是 MyClass<double> 的友元
return 0;
}
输出
解释
friend class FriendClass<U,V>;
允许所有FriendClass<U,V>
访问MyClass<T>
的私有成员;这样FriendClass<int,double>
,FriendClass<double,int>
也可以访问MyClass<int>
。
1.15模板的局限性
C++模板虽然强大,但也存在一些局限性。这些局限性可能会影响代码的可读性、编译时间、调试难度以及跨平台兼容性。
1.编译时间增加
模板代码在编译时会实例化为具体的类型,如果模板被广泛使用或嵌套层次较深,编译时间会显著增加。
cpp
template <typename T>
void foo(T value) {
// 复杂的模板逻辑
}
foo(10); // 实例化为 foo<int>
foo(3.14); // 实例化为 foo<double>
解决方法:
- 尽量减少模板的嵌套层次。
- 使用显式实例化来减少重复实例化。
2.代码膨胀
模板会为每种类型生成一份代码,导致二进制文件体积增大。
cpp
template <typename T>
class MyClass {
public:
void doSomething(T value) {
// 复杂的逻辑
}
};
MyClass<int> obj1;
MyClass<double> obj2;
编译器会生成MyClass<int>
和MyClass<double>
两份代码。
解决方法:
- 使用类型查出技术(如
std::function
、std::any
)。 - 将通用逻辑提取到非模板代码中。
3.调试难度增加
模板错误信息通常非常冗长且难以理解,尤其是在模板嵌套较深的情况下。
cpp
template <typename T>
void foo(T value) {
value.non_existent_method(); // 编译错误
}
foo(10); // 错误信息可能非常复杂
解决方法:
- 使用静态断言(static_assert)提前检测类型约束。
- 使用概念(concepts)来约束模板参数。
4.跨平台兼容性问题
不同编译器对模板的支持可能不一致,尤其是在模板的高级特性(如 SFINAE、可变参数模板)上。
cpp
template <typename T>
void foo(T value) {
// 使用 SFINAE 或可变参数模板
}
某些编译器可能不支持这些特性,或者支持不完整。
解决方法:
- 使用跨平台兼容的模板特性。
- 在代码中增加编译器特定的条件编译
5.类型推导的局限性
模板类型推导在某些情况下可能无法正常工作,尤其是涉及隐式类型转换或重载时。
cpp
template <typename T>
void foo(T value) {}
foo(10); // T 推导为 int
foo(3.14); // T 推导为 double
foo("hello"); // T 推导为 const char*
如果希望 T
推导为 std::string
,需要显式指定类型:
foo<std::string>("hello");
解决方法:
- 使用显式类型指定。
- 使用
std::decay
或std::remove_reference
等类型转换工具。
6.模板特化的复杂性
模板特化(全特化或偏特化)可能导致代码复杂性增加,尤其是在处理多种类型组合时。
cpp
template <typename T>
class MyClass {
public:
void doSomething() {
cout << "通用实现" << endl;
}
};
template <>
class MyClass<int> {
public:
void doSomething() {
cout << "int 特化实现" << endl;
}
};
如果需要对多种类型进行特化,代码会变得冗长。
解决方法:
- 尽量减少模板特化的使用。
- 使用
if constexpr
(C++17)或概念(C++20)来替代部分特化。
7.模板的隐式实例化
模板会在使用时隐式实例化,可能导致未使用的代码也被实例化,增加编译时间和二进制体积。
cpp
template <typename T>
class MyClass {
public:
void doSomething() {
// 复杂的逻辑
}
};
MyClass<int> obj; // 即使未调用 doSomething,代码也会被实例化
解决方法:
- 使用显式实例化(
template class MyClass<int>;
)。 - 将模板代码分离到独立的源文件中。
8.模板的调试信息不友好
模板实例化后的调试信息通常非常冗长,难以阅读。
cpp
template <typename T>
void foo(T value) {
// 复杂的逻辑
}
foo(10); // 调试信息可能包含大量模板实例化细节
解决方法:
- 使用类型别名(
using
)简化模板类型。 - 使用
static_assert
或概念(C++20)提前检查类型约束。
9.模板元编程的复杂性
模板元编程(TMP)可以实现强大的编译时计算,但代码通常难以理解和维护。
cpp
template <int N>
struct Factorial {
static const int value = N * Factorial<N - 1>::value;
};
template <>
struct Factorial<0> {
static const int value = 1;
};
cout << Factorial<5>::value << endl; // 输出 120
这种代码虽然强大,但可读性差。
解决方法:
- 使用
constexpr
函数替代模板元编程。 - 使用 C++20 的概念和约束来简化模板逻辑。
10.动态多态与静态多态的冲突
模板是基于静态多态的,而虚函数是基于动态多态的。两者结合时可能导致设计复杂。
cpp
template <typename T>
class MyClass {
public:
void doSomething(T value) {
value.doSomething();
}
};
class Base {
public:
virtual void doSomething() = 0;
};
class Derived : public Base {
public:
void doSomething() override {
cout << "Derived::doSomething" << endl;
}
};
MyClass<Base> obj; // 错误:Base 不是具体类型
解决方法:
-
使用类型擦除技术(如
std::function
、std::any
)。 -
避免将模板与虚函数混合使用。
1.16 类模板案例
案例描述:实现一个通用的数组类,要求如下:
●可以对内置数据类型以及自定义数据类型的数据进行存储
●将数组中的数据存储到堆区
●构造函数中可以传入数组的容量
●提供对应的拷贝构造函数以及operator=防止浅拷贝问题
●提供尾插法和尾删法对数组中的数据进行增加和删除
●可以通过下标的方式访问数组中的元素
●可以获取数组中当前元素个数和数组的容量
示例:
cpp
#include <iostream>
#include<string>
using namespace std;
template<typename T>
class MyArray {
private:
T* data; //指向堆区数据
int capacity; //数组容量
int size; //当前元素个数
public:
//构造函数(初始化容量)
MyArray(int cap) {
this->capacity = cap;
this->size = 0;
this->data = new T[cap]; //在堆区分配内存
}
//拷贝构造函数(深拷贝)
MyArray(const MyArray& other) {
this->capacity = other.capacity;
this->size = other.size;
this->data = new T[this->capacity];
for (int i = 0; i < this->size; i++){
this->data[i] = other.data[i]; //拷贝数据
}
}
//赋值运算符重载(防止浅拷贝)
MyArray& operator=(const MyArray& other) {
if (this == &other)
return *this; //防止自赋值
//释放旧内存
delete[] this->data;
//深拷贝
this->capacity = other.capacity;
this->size = other.size;
this->data = new T[this->capacity];
for (int i = 0; i < this->size; i++) {
this->data[i] = other.data[i];
}
return *this;
}
//尾插法(增加元素)
void push_back(const T& value) {
if (size >= capacity) {
cout << "数组已满,无法插入新的元素!" << endl;
return;
}
data[size++] = value;
}
//尾删法(删除最后一个元素)
void pop_back() {
if (size > 0) {
size--;
}
else {
cout << "数组为空,无法删除元素!" << endl;
}
}
//下标运算符重载
T& operator[](int index) {
if (index < 0 || index >= size) {
cout << "索引超出范围" << endl;
return data[0]; //直接返回第一个元素,放在系统崩溃
}
return data[index];
}
//获取当前数组中的元素个数
int getSize() const {
return size;
}
//获取数组容量
int getCapacity() const {
return capacity;
}
//析构函数(释放堆区内存)
~MyArray(){
delete[] data;
}
};
//测试
class Person {
public:
string m_Name;
int m_Age;
//默认构造
Person() :m_Name(""), m_Age(0) {}
//带参构造
Person(string name, int age) :m_Name(name), m_Age(age) {}
void show() const {
cout << "姓名:" << m_Name << ",年龄:" << m_Age << endl;
}
};
int main()
{
//存储int类型
MyArray<int> arr(5);
//存放三个元素
arr.push_back(10);
arr.push_back(20);
arr.push_back(30);
cout << "数组大小:" << arr.getSize() << ",容量:" << arr.getCapacity() << endl;
cout << "第2个元素:" << arr[1] << endl;
//删除最后一个元素
arr.pop_back();
cout << "删除之后,元素的个数:" << arr.getSize() << endl;
cout << "=====================================" << endl; //分隔符
//存储自定义类型Person类
MyArray<Person> persons(3);
//存储2个人
persons.push_back(Person("张伟", 25));
persons.push_back(Person("李明", 26));
cout << "==存储Person情况==" << endl;
for (int i = 0; i < persons.getSize(); i++){
persons[i].show();
}
cout << "=====================================" << endl;
//测试重载=符号
MyArray<int> arr2(4);
//存放三个元素
arr2.push_back(40);
arr2.push_back(50);
arr2.push_back(60);
arr2 = arr;
cout << "arr2 复制后大小: " << arr2.getSize() << ", 容量: " << arr2.getCapacity() << endl;
cout << "arr2 的元素: ";
for (int i = 0; i < arr2.getSize(); i++){
cout << arr2[i] << " ";
}
cout << endl;
return 0;
}