引子:如何交换两个变量的值

C++中有几种交换变量的方法 ?

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
#define SWAP(t, a, b)    \
do \
{ \
t c = a; \
a = b; \
b = c; \
}while(0)


void Swap(int& a, int& b)
{
int c = a;
a = b;
b = c;
}

void Swap(double& a, double& b)
{
double c = a;
a = b;
b = c;
}

void Swap(string& a, string& b)
{
string c = a;
a = b;
b = c;
}

定义宏代码块
- 优点 : 代码复用 , 适合所有的类型
- 缺点 : 编译器不知道宏的存在,缺少类型检查

定义函数
- 优点 : 真正的函数调用 , 编译器对类型进行检查
- 缺点 : 根据类型重复定义函数,**无法代码复用

C+ + 中有没有解决方案集合两种方去的优点 ?

泛型编程

泛型编程的概念
不考虑具体数据类型的编程方式

对于 Swap 函数可以考虑下面的泛型写法

1
2
3
4
5
void Swap(T& a,T& b){
T t = a;
a = b;
b = t;
}

Swap 泛型写法中T的不是一个具体的数据类型,而是泛指任意的数据类型

C+ +中泛型编程

函数模板

  • 一种特殊的函数可用不同类型进行调用
  • 看起来和普通函数很相似 , 区別是类型可被参数化
1
2
3
4
5
6
template <typename T>
void Swap(T& a,T& b){
T t = a;
a = b;
b = t;
}

函数模板的语法规则

  • template 关键字用于声明开始进行泛型编程
  • typename 关键字用于声明泛指类型

image-20220301115844129

函数模板的使用

  • 自动类型推导调用
  • 具体类型显示调用
1
2
3
4
5
6
7
int a = 0;
int b = 1;
Swap(a, b); // 自动推导

float c = 2;
float d = 3;
Swap<float>(c, d); // 显示调用

小结

  • 函数模板是泛型编程在 C+ + 中的应用方式之一
  • 函数模板能够根据实参对参数类型进行推导
  • 函数模板支持显示的指定参数类型
  • 函数模板是 C+ + 中重要的代码复用方式

函数模板

函数模板深入理解

  • 编译器从函数模板通过具体类型产生不同的函数
  • 编译器会对函数模板进行两次编译
    • 对模板代码本身进行编译
    • 对参数替换后的代码进行编译

注意事项 :
函数模板本身不允许隐式类型转换
自动推导类型时 , 必须严格匹配(严格匹配就不存在隐式类型转换了)
显示类型指定时 , 能够进行隐式类型转换Swap<float>(a,b)

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
36
37
38
39
40
41
42
#include <iostream>
#include <string>

using namespace std;

class Test
{
Test(const Test&);//拷贝构造函数private
public:
Test()
{
}
};

template < typename T >
void Swap(T& a, T& b)
{
T c = a; //Test类拷贝构造失败
a = b;
b = c;
}

typedef void(FuncI)(int&, int&);
typedef void(FuncD)(double&, double&);
typedef void(FuncT)(Test&, Test&);

int main()
{
FuncI* pi = Swap; // 编译器自动推导 T 为 int,自动推导的时候类型必须严格匹配,这里会生成一个实际的函数
FuncD* pd = Swap; // 编译器自动推导 T 为 double,自动推导的时候类型必须严格匹配,这里会生成一个实际函数

int a = 1;double b = 2;
//Swap(a,b);//error,类型不是严格匹配的,模板是不可以进行隐式类型转换的error

//FuncT* pt = Swap; // 编译器自动推导 T 为 Test,但是Test拷贝构造是private,二次编译的时候才会报错,

cout << "pi = " << reinterpret_cast<void*>(pi) << endl;
cout << "pd = " << reinterpret_cast<void*>(pd) << endl;
//cout << "pt = " << reinterpret_cast<void*>(pt) << endl;

return 0;
}

多参数函数模板

1
2
3
4
5
6
7
8
template
<typename T1, typename T2, typename T3>
T1 Add (T2 a, T3 b){
return static_cast<T1> (a + b) ;
}


int r = ADD<int,float,double>(0.5,0.8);

对于多参数函数模板

  • 无法自动推导返回值类型
  • 可以从左向右部分指定类型参数
1
2
3
4
5
6
7
8
//T1 = int, T2 = double, T3 = double;   T2类型和T3类型由编译器自动推导
int r1 = Add<int>(0.5, 0.8);

//T1 = int, T2 = float, T3 = double; T3类型由编译器自动推导
int r2 = Add<int,float>(0.5, 0.8);

////T1 = int, T2 = float, T3 = float;
int r3 = Add<int,float,float>(0.5, 0.8);

工程中将返回值参数作为第一个类型参数 (因为函数参数类型可以由编译器自动推导,返回值参数要显示指定)

重载函数模板

函数模板可以像普通函数一样被重载

  • C++编译器优先考虑普通函数
  • 如果函数模板可以产生一个更好的匹配,那么选择模板
  • 可以通过空模板实参列表限定编译器只匹配模板
1
2
int r1 = Max(1,2);
double r2 = Max<>(0.5, 0.8);

注意<>限定了编译器只匹配函数模板,不要去考虑其他普通函数了。

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
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <iostream>
#include <string>

using namespace std;


template < typename T >
T Max(T a, T b)
{
cout << "T Max(T a, T b)" << endl;

return a > b ? a : b;
}

int Max(int a, int b)
{
cout << "int Max(int a, int b)" << endl;

return a > b ? a : b;
}

template < typename T >
T Max(T a, T b, T c)
{
cout << "T Max(T a, T b, T c)" << endl;

return Max(Max(a, b), c);
}

int main()
{
int a = 1;
int b = 2;

//首先考虑普通函数,普通函数不匹配再考虑函数模板

cout << Max(a, b) << endl; // 普通函数 Max(int, int)

cout << Max<>(a, b) << endl; // 函数模板 Max<int>(int, int)

cout << Max(3.0, 4.0) << endl; // 函数模板 Max<double>(double, double)

cout << Max(5.0, 6.0, 7.0) << endl; // 函数模板 Max<double>(double, double, double)

cout << Max('a', 100) << endl; // 普通函数 Max(int, int),进行隐式类型转换

return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
fengyun@ubuntu:~/share$ ./test
int Max(int a, int b)
2
T Max(T a, T b)
2
T Max(T a, T b)
4
T Max(T a, T b, T c)
T Max(T a, T b)
T Max(T a, T b)
7
int Max(int a, int b)
100

类模板

C++STL库就包括许多的函数模板和类模板

类模板的概念和意义

  • 一些类主要用于存储和组织数据元素
  • 类中数据组织的方式和数据元素的具体类型无关
  • 如 : 数组类,链表类,Stack 类,Queue 类,等

C++ 中将模板的思想应用于类 , 使得类的实现不关注数据元素的具体类型 , 而只关注类所需要实现的功能。

C++的类模板

C+ +中的类模板

  • 以相同的方式处理不同的类型
  • 在类声明前使用 template 进行标识
  • < typename T >用于说明类中使用的泛指类型 T
1
2
3
4
5
template <typename T>
class Operator{
public:
T op(T a,T b);
};

类模板的应用

  • 只能显示指定具体类型 , 无法自动推导
  • 使用具体类型 定义对象
1
2
3
4
5
6
Operator<int> op1;
operator<string> op2;

int i = op1.op(1,2);

string s = op2.op("feng","yun");
  • 声明的泛指类型 T 可以出现在类模板的任意地方
  • 编译器对类模板的处理方式和函数模板相同
    - 从类模板通过具体类型产生不同的类
    - 在声明的地方对类模扳代码本身进行编译
    - 在使用的地方对参数替换后的代码进行编译 (和函数模板一样两次编译)
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
36
37
38
39
#include <iostream>
#include <string>

using namespace std;

template <typename T>
class Operator{
public:
T add(T a,T b){
return a + b;
}
T minus(T a,T b){
return a - b;
}
T multiply(T a,T b){
return a * b;
}
T divide(T a,T b){
return a / b;
}
};

//取消一下注释程序才可以正常通过
//string operator - (string& s1,string& s2){
// return "minus";
//}

int main()
{
Operator<int> op1;

cout << op1.add(1,2) << endl;

Operator<string> op2;
cout << op2.add("feng","yun") << endl;
cout << op2.minus("feng","yun") << endl;

return 0;
}

编译运行报错

1
2
3
test.cpp:13:18: error: no match for ‘operator-’ (operand types are ‘std::__cxx11::basic_string<char>’ and ‘std::__cxx11::basic_string<char>’)
13 | return a - b;
| ~~^~~

这是因为string类内部没有实现operator - 两个string类的相减在C++中没有实现,必然报错。
类模板也是经过两次编译的,第一次是对类模板的语法进行检查,
第二次编译是分布编译的,针对每个独立成员函数来编译,包括构造函数和op2.add,op2.minus,针对minus的二次编译就过不了了。

因此我们应该重载string 类对象的operator -操作符。
当编译器执行到op2.minus()发现可以使用重载函数。

类模板的工程应用

  • - 类模板必须在头文件中定义
  • - 类模板不能分开实现在不同的文件中
  • - 类模板外部定义的成员函数需要加上模板< > 声明
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
36
37
38
39
40
41
#ifndef _OPERATOR_H_
#define _OPERATOR_H_

template < typename T >
class Operator
{
public:
T add(T a, T b);
T minus(T a, T b);
T multiply(T a, T b);
T divide(T a, T b);
};

template < typename T >
T Operator<T>::add(T a, T b)
{
return a + b;
}


template < typename T >
T Operator<T>::minus(T a, T b)
{
return a - b;
}


template < typename T >
T Operator<T>::multiply(T a, T b)
{
return a * b;
}


template < typename T >
T Operator<T>::divide(T a, T b)
{
return a / b;
}

#endif
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <string>
#include "Operator.h"

using namespace std;


int main()
{
Operator<int> op1;

cout << op1.add(1, 2) << endl;
cout << op1.multiply(4, 5) << endl;
cout << op1.minus(5, 6) << endl;
cout << op1.divide(10, 5) << endl;

return 0;
}

小结

  • 泛型编程的思想可以应用于类
  • 类模板以相同的方式处理不同类型的数据
  • 类模板非常适用于编写数据结构相关的代码
  • 类模板在使用时只能显示指定类型

类模板特化

类模板可以被特化

  • - 指定类模板的特定实现
  • - 部分类型参数必须显示指定
  • - 根据类型参数分开实现类模板

编译器认为这两个是同一个类模板

image-20220301115853387

类模板的特化类型

  • 部分特化 - 用特定规则约束类型参数
  • 完全特化 - 完全显不指定类型参数

image-20220301120109162

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#include <iostream>
#include <string>

using namespace std;

template
< typename T1, typename T2 >
class Test
{
public:
void add(T1 a, T2 b)
{
cout << "void add(T1 a, T2 b)" << endl;
cout << a + b << endl;
}
};

template
< typename T1, typename T2 >
class Test < T1*, T2* > // 关于指针的特化实现
{
public:
void add(T1* a, T2* b)
{
cout << "void add(T1* a, T2* b)" << endl;
cout << *a + *b << endl;
}
};

template
< typename T >
class Test < T, T > // 当 Test 类模板的两个类型参数完全相同时,使用这个实现
{
public:
void add(T a, T b)
{
cout << "void add(T a, T b)" << endl;
cout << a + b << endl;
}
void print()
{
cout << "class Test < T, T >" << endl;
}
};

template
< >
class Test < void*, void* > // 当 T1 == void* 并且 T2 == void* 时
{
public:
void add(void* a, void* b)
{
cout << "void add(void* a, void* b)" << endl;
cout << "Error to add void* param..." << endl;
}
};

int main()
{
Test<int, float> t1;
Test<long, long> t2;
Test<void*, void*> t3;

t1.add(1, 2.5);

t2.add(5, 5);
t2.print();

t3.add(NULL, NULL);

Test<int*, double*> t4;
int a = 1;
double b = 0.1;

t4.add(&a, &b);

return 0;
}

注意事项

  • 特化只是模板的分开实现。本质上是同一个类模板
  • 特化类模板的使用方式是统一的。必须显示指定每一个类型参数

重定义和特化的不同

  • 重定义
    类模板和一个新类( 或者两个类模板)
    使用的时候需要考虑如何选择的问题
  • 特化
    以统一的方式使用类模板和特化类
    编译器自动优先选择特化类

函数模板只支持类型参数完全特化

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
#include <iostream>
#include <string>

using namespace std;

template
< typename T1, typename T2 >
class Test
{
public:
void add(T1 a, T2 b)
{
cout << "void add(T1 a, T2 b)" << endl;
cout << a + b << endl;
}
};

/*
template
< >
class Test < void*, void* > // 当 T1 == void* 并且 T2 == void* 时
{
public:
void add(void* a, void* b)
{
cout << "void add(void* a, void* b)" << endl;
cout << "Error to add void* param..." << endl;
}
};
*/

class Test_Void
{
public:
void add(void* a, void* b)
{
cout << "void add(void* a, void* b)" << endl;
cout << "Error to add void* param..." << endl;
}
};

template
< typename T >
bool Equal(T a, T b)
{
cout << "bool Equal(T a, T b)" << endl;

return a == b;
}

template
< >
bool Equal<double>(double a, double b)
{
const double delta = 0.00000000000001;
double r = a - b;

cout << "bool Equal<double>(double a, double b)" << endl;

return (-delta < r) && (r < delta);
}

bool Equal(double a, double b)
{
const double delta = 0.00000000000001;
double r = a - b;

cout << "bool Equal(double a, double b)" << endl;

return (-delta < r) && (r < delta);
}

int main()
{
cout << Equal( 1, 1 ) << endl;
cout << Equal<>( 0.001, 0.001 ) << endl;//‘<>’显示调用模板而不是考虑普通函数

return 0;
}

当需要重载函数模板时,优先考虑使用模板特化 ;
当模板特化无法满足需求,再使用函数重载 !

小结

  • 类模板可以定义任意多个不同的类型参数
  • 类模板可以被部分特化和完全特化
  • 特化的本质是模板的分开实现
  • 函数模板只支持完全特化
  • 工程中使用模板特化代替类( 函数 ) 重定义

模板参数可以是数值型参数(非类型参数)

image-20220301133742120

数值型模板参数的限制

  • - 变量不能作为模扳参数
  • - 浮点数不能作为模板参数(浮点数在计算机内部表示不精确)
  • - 类对象不能作为模板参数

本质 :
模板参数是在编译阶段被处理的单元 , 因此在编译阶段必须准确无误的唯一确定

最高效的方式求1+2+3+..+N的和

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
36
37
38
39
40
41
42
43
44
45
46
#include <iostream>
#include <string>

using namespace std;

template
< typename T, int N >
void func()
{
T a[N] = {0};

for(int i=0; i<N; i++)
{
a[i] = i;
}

for(int i=0; i<N; i++)
{
cout << a[i] << endl;
}
}

template
< int N >
class Sum
{
public:
static const int VALUE = Sum<N-1>::VALUE + N;
};

template
< >
class Sum < 1 >
{
public:
static const int VALUE = 1;
};


int main()
{
cout << "1 + 2 + 3 + ... + 10 = " << Sum<10>::VALUE << endl;
cout << "1 + 2 + 3 + ... + 100 = " << Sum<100>::VALUE << endl;

return 0;
}

编译时就能确定1+2+3+…+N的值。秀的头皮发麻。

typename和class

历史上的原因。 。。

  • 早期的 C+ + 直接复用 class 关键字来定义模板
  • 但是泛型编程针对的不只是类类型
  • class 关键字的复用使得代码出现二义性
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
#include <iostream>
#include <string>

using namespace std;

int a = 0;

class Test_1 {
public:
static const int TS = 1;
};

class Test_2 {
public:
struct TS {
int value;
};
};

template
< class T >
void test_class() {
typename T::TS *a; // 1. 通过泛指类型 T 内部的数据类型 TS 定义指针变量 a (推荐的解读方式)
// 2. 使用泛指类型 T 内部的静态成员变量 TS 与全局变量 a 进行乘法操作
}
int main(int argc, char *argv[]) {
//test_class<Test_1>();
test_class<Test_2>();

return 0;
}

typename 诞生的直接诱因

  • 自定义类类型内部的嵌套类型
  • 不同类中的同一个标识符可能导致二义性
  • 编译器无法辨识标识符究竟是什么

typename 的作用 :

  1. 在模板定义中声明泛指类型
  2. 明确告诉编译器其后的标识符为类型