模板知识

二木 王者

模板知识

模板,学习c++,只要深入一点,我们都会解接触到的,然而,就简单的函数模板来说,理解起来并不困难,但是随着深入的学习,我们会发现,c++就开始变成”天书”,各种组合让人难以理解,或者说,得花一些时间去分析才知道它到底是什么?刚开始学习c++的时候,当时因为学的比较浅,感觉和c的区别不是很大,就是多了一个面向对象而已,不过如此。随着深入的学习,恨不得扇当时自己两巴掌,越深入学习,便越感觉c++精妙与强大,当然每个语言都有其优缺点,c++做为一门古老的c语言发展而来的语言,难免相对来说有一些遗留的“不好”的操作,这个每个语言都会存在的,这个学多了我们自然会知道各种特点。

我们为什么需要模板?

对于为什么需要模板?我们在网上应该可以看到许多文章都是用的一个很经典的例子。

这里我们也举例这个例子

我们在编程中会遇到这样一个场景,我们写一个交换函数

1
2
3
4
5
6
7
8
void swap(int &a,int &b)
{
int temp;
temp = a;
a = b;
b = temp;
}

像这样,我们写出了一个对于int类型数据的交换,但是,下一次我需要对两个float类型的数据进行交换,我们就又需要写一个函数来进行接收,下次如果需要double,需要char,需要long……….,如果这样代码就太过于冗杂的,这个时候,模板就产生了。

我们写一个模板函数

1
2
3
4
5
6
7
8
template<typename T>
void swap2(T &a, T &b)
{
T temp;
temp = a;
a = b;
b = temp;
}

T相当于我们不知道的类型的代替,T当我们传入什么类型,我们的编译器就行帮我们推导出它的类型,然后T就变成了那个类型

比如我们传入

1
2
float a = 123.222,b = 3123.22;
swap2(a,b)

这里的T就会推导成float.

模板的使用

我们写模板主要写的是两个方向,一个是写函数模板,一个是写结构体模板

函数模板

对于函数模板我们前面就已经看到了

1
2
3
4
5
6
7
8
template<typename T>
void swap2(T &a, T &b)
{
T temp;
temp = a;
a = b;
b = temp;
}

template是模板关键字,其表示后面即将定义一个模板。后面尖括号中的就是「模板参数」,这里的T 就是一个占位符号,也就是对应推导出的类型,如果传入int类型,那么T 就相当于int. 而且这里的T 是我们给它的名字,其实可以写成T1 , T2 ,T3,t123,都行。

这里我们再声明一个不一样的a,b类型都行,然后函数里面对其进行操作

1
2
3
4
5
template<typename T,typename T2>
void swap2(T a, T2 b)
{
std::cout<<a<<b<<std::endl;
}

对于这里的调用,有两种方式,一种是显式推导,一种是编译器进行隐式推导

1
2
3
4
5
6
7
void text()
{
swap2<int>(a,b);//显式的告诉编译器,我们传入的是int类型
swap2<int>(a,b); //让编译器进行隐式的推导

}

模板类

对于模板类,我们有两种写法

一种是用class来进行声明

1
2
3
4
5
template <typename T1, typename T2>
class Test {
T1 t1;
T2 t2;
};

一种是用struct来进行声明

1
2
3
4
5
template <typename T1, typename T2>
struct Test {
T1 t1;
T2 t2;
};

这两种方法声明的类是等价的

在模板类中,对于两种关键词的声明,如果是用于「生成对象」的模板类,我们一般习惯用class进行声明,而对于「模板元编程」的静态语言描述中,我们更习惯用struct 进行声明。

对于c++中来说,struct与class其实是很相似的,在某些时候甚至可以等价,struct内部也可以声明一个函数啥的。class与struct的最大的区别主要在于默认权限上,class的默认权限是private,而struct的默认权限是public。

类模板的实例化

类模板的实例化也有两种方式

显式的定义类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
template <typename T>
class MyContainer {
public:
MyContainer(const T& value) : data(value) {}
void print() {
std::cout << data << std::endl;
}
private:
T data;
};

int main() {
MyContainer<int> container1(5); // 使用 int 类型实例化 MyContainer
container1.print(); // 输出: 5

MyContainer<double> container2(3.14); // 使用 double 类型实例化 MyContainer
container2.print(); // 输出: 3.14

MyContainer<std::string> container3("Hello"); // 使用 std::string 类型实例化 MyContainer
container3.print(); // 输出: Hello

return 0;
}

隐式的自动推导

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
template <typename T>
class MyContainer {
public:
MyContainer(const T& value) : data(value) {}
void print() {
std::cout << data << std::endl;
}
private:
T data;
};
int main() {
MyContainer<int> container1(5); // 使用 int 类型实例化 MyContainer
container1.print(); // 输出: 5
MyContainer<double> container2(3.14); // 使用 double 类型实例化 MyContainer
container2.print(); // 输出: 3.14
MyContainer<std::string> container3("Hello"); // 使用 std::string 类型实例化 MyContainer
container3.print(); // 输出: Hello
return 0;
}

对于类模板的实例化,我们使用时,如果类型知道,防止错误,最好是我们进行指定的显式推导

对于类模板的重命名

在学c编程的时候,我们接触的很多都是使用typedef

1
typedef int enco //将int重命名为enco,之后enco就能声明int类型数据了

C++中的类型重命名主要有两种方法,一种是使用typedef,另一种是使用using,它们都支持模板生成,并且效果是相同的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 普通的模板类
template <typename T, size_t size>
class Array
{
int a;
};

//对模板偏特化作用的模板重命名
template <typename T>
using DefaultArray = Array<T, 16>; //确定了其中一个值或者类型,就称之为偏特化
//重命名了一个DefaultArray来替代声明Array<T, 16>
//typedef语法
template <typename T>
typedef DefaultArray<T *> DefaultPtrArray; //这里使用了模板嵌套,将DefaultPtrArray,重命名为
//DefaultArray<T *>,也就是Array<T *, 16>

void Demo() {
DefaultArray<int> arr1; // 相当于Array<int, 16> arr1;
DefaultPtrArray<char> arr2; // 相当于Array<char *, 16> arr2;
}

关于模板需要注意的地方

我们实例化模板必须要传入,常量,常量表达式,由于模板是编译期语法,因此,这里的我们的数据也必须是编译期能确定的,比如说常数、常量表达式等,而不可以是动态的数据。就算是传入的是指针,这个地址也需要一直只表示确定的数据,不会改变(即为不会被释放,然后重新分配给其他数据)下面是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <iostream>
#include <array>
template <int N>
class MyArray {
public:
MyArray() {
std::cout << "MyArray size: " << N << std::endl;
}

private:
std::array<int, N> data;
};

int main() {
MyArray<5> arr1; // 常数OK

constexpr int a = 5;
MyArray<a> arr2; // 常量表达式OK

const int b = 6;
MyArray<b> arr3; // ERR,b是只读变量,不是常量

MyArray<a * 3> arr4; // 常数运算OK

std::array<int, 3> arr5 {1, 2, 3};
MyArray<arr5.size()> arr6; // ERR,size是运行时数据

int arr7[] {1, 2, 3};
MyArray<arr7[0]> arr8; // ERR,arr7的成员是运行时数据

constexpr int arr9[] {2, 4, 6};
MyArray<arr9[1]> arr10; // 常量表达式修饰的普通数组成员OK

return 0;
}

初学模板让人迷糊的一些写法

在我们看别人的代码的时候,模板这里我们会看到一些我们不知道的声明

比如

1
template<class T>

其实下面的这两个在声明上都是等价的,都是声明一个模板,因为在以前写模板声明参数类型的时候都是用的class,但是class会与我们声明一个类写时容易搞混,所以转而使用typename来代替了class来声明模板,但是class的写法一直兼容,所以出现了这两种写法

1
2
template <class T>
template <typename T>

使用模板来对stl进行扩展

这里实现了一个对stl中vector模板的内容进行复制,这个是一个例子,我们可以写很多关于如此的对stl扩展的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <vector>
#include <cstdlib>
#include <cstddef>

template <typename T>
void CopyVec2Buf(const std::vector<T> &ve, void *buf, size_t buf_size) {
if (buf_size < ve.size() * sizeof(T)) {
return;
}
std::memcpy(buf, ve.data(), ve.size() * sizeof(T));
}

void Demo() {
std::vector<int> ve{1, 2, 3, 4};
std::byte buf[64];
// 把ve的内容连续地复制到buf中
CopyVec2Buf(ve, buf, 64); // 这里会推导出CopyVec2Buf<int>
}
 评论