• C++模板初阶 —— 函数模板、类模板、模板的声明和定义分离(多文件使用的注意事项)


    1.泛型编程

    当我们没有对泛型编程产生了解时,我们写一个交换函数可以是这么写:

    void Swap(int& a, int& b)
    {
    	int tmp = a;
    	a = b;
    	b = tmp;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这段代码确实实现了两个整型数据的交换,但如果我们想要交换的数据不是整型,而是字符型、浮点型、长整型等,那我们必须使用函数重载去手动定义多个函数:

    void Swap(char& a, char& b)
    {
    	char tmp = a;
    	a = b;
    	b = tmp;
    }
    
    void Swap(double& a, double& b)
    {
    	double tmp = a;
    	a = b;
    	b = tmp;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    这样直接的方式确实能够帮助我们完成任务,但是却存在许多问题:
    1.这些重载的函数仅仅是类型不同,完成的任务都一模一样。只要涉及到一个新的类型,我们就必须手动写出一个新的函数。
    2.如果一个函数出错,那么可能导致一连串的错误

    有没有什么办法能够解决并优化这方面的问题?我们把这个稳定映射到生活当中去:在冶炼的过程中,我们希望能够有绿色和蓝色的成品,但这并不意味着需要单独的为绿色或蓝色的成品从新设计一套制作流程。我们仅仅只需要一个模具,将铁水倒进模具中,在最后一步加上颜料即可。
    在这里插入图片描述
    在C++中,针对同种功能但参数类型不同的函数我们也可以使用模具的办法,我们把它叫做函数模板。而模板又是泛型编程的基础

    2.函数模板

    2.1函数模板的概念与基本格式

    函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本

    函数模板的基本格式为:

    template <typename T1,typename T2 ......>
    返回值 函数名(参数列表)
    
    • 1
    • 2

    模板参数可以有多个,因为是模板,所以函数类型的类型不确定,故使用typename关键字(可以使用class替换)。函数模板定义好后,其使用范围在模板定义之后碰到的第一个函数

    template <typename T>
    void Swap(T& a, T& b)//合法
    {
    	T tmp = a;
    	a = b;
    	b = tmp;
    }
    
    //函数模板只对其定义之后的第一个函数生效
    T Add(T& a, T& b)//不合法
    {
    	return a + b;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    2.2函数模板的使用

    我们定义好了交换函数的模板:

    template <typename T>
    void Swap(T& a, T& b)
    {
    	T tmp = a;
    	a = b;
    	b = tmp;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    我们可以正常的传参去调用函数:

    int main()
    {
    	int a = 1, b = 3;
    	Swap(a, b);
    
    	char c = '5', d = '7';
    	Swap(c, d);
    
    	double e = 2.2, f = 3.4;
    	Swap(e, f);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    我们要有一个常识,函数模板并不是一个函数,那我们调用的函数是怎么产生的呢?

    2.3函数模板的原理

    编译器的功能是十分强大的,它能够帮助我们完成我们看似不可能完成的任务。

    当我们定义好了函数模板之后,在以后需要调用此函数的时候,会先经过函数模板,函数模板会智能的将实参的类型转化为具体的模板参数类型,从而生成一份具体的函数

    在这里插入图片描述
    当然编译器不是“傻子”,它并不是每次调用都会经过函数模板。例如第一次传入的两个类型都为整型,第二次还传入整型,那么第二次调用函数则不会经过函数模板了,而是直接调用第一次生成的函数。

    2.4函数模板的实例化

    用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化

    隐式实例化,实际上就是让编译器根据实参的类型推演出模板参数的类型

    template <typename T>
    void Swap(T& a, T& b)
    {
    	T tmp = a;
    	a = b;
    	b = tmp;
    }
    int main()
    {
    	int a = 1, b = 3;
    	Swap(a, b);
    
    	char c = '5', d = '7';
    	Swap(c, d);
    
    	Swap(a, c);//当模板参数只有一个时,不能够这样去使用
    	//当前一个模板参数被推演成int,后一个模板参数被推演为char
    	//就会产生矛盾,引发报错
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    当模板参数只有一个时,我们的实参类型又有两个。此时解决的方法只有两个:一是将不同的实参类型强转为相同的实参类型;二是使用显式实例化先确定模板参数的类型,即使实参的两个类型不同,在实参传给形参的过程中也会发生强制类型转换

    同时需要注意,强制类型转换之后的结果是一份临时变量,此临时变量具有常属性,所以模板参数需要使用const修饰。

    template <typename T>
    void Swap(const T& a, const T& b)
    {
    	T tmp = a;
    	a = b;
    	b = tmp;
    }
    int main()
    {
    	int a = 1, b = 3;
    	Swap(a, b);
    
    	char c = '5', d = '7';
    	Swap(c, d);
    
    	Swap(a, (int)c);
    	//或者
    	Swap((char)a, c);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    显式实例化:在函数名之后使用<>指定模板参数的类型

    template <typename T>
    void Swap(const T& a, const T& b)
    {
    	T tmp = a;
    	a = b;
    	b = tmp;
    }
    int main()
    {
    	int a = 1, b = 3;
    	char c = '5', d = '10';
    	//显示实例化先确定模板参数的类型
    	//若实参与模板参数的类型不一致则会发生类型转换
    	Swap<int>(a, c);
    	Swap<char>(b, d);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    2.5模板参数的匹配原则

    1. 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数。
    //非模板函数
    int Add(int a, int b)
    {
    	return a + b;
    }
    
    //同名的模板函数
    template <typename T1,typename T2>
    T1 Add(T1 a, T2 b)
    {
    	return a + b;
    }
    int main()
    {
    	Add(1, 3);//直接调用非模板函数
    	Add<int,int>(5, 8);//此时需要经过模板特化
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    1. 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板。
    //非模板函数
    int Add(int a, int b)
    {
    	return a + b;
    }
    
    //同名的模板函数
    template <typename T1,typename T2>
    T1 Add(T1 a, T2 b)
    {
    	return a + b;
    }
    int main()
    {
    	Add(1, 3);//直接选择非模板函数
    	Add(5, 3.1);//调用模板函数更加方便
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    1. 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换。

    3.类模板

    如果我们不使用类模板,我们需要定义多个用来存储不同元素类型的栈,那将是一件麻烦事:

    class Stack_Int
    {
    	//...
    public:
    	void Push(int x);
    };
    
    class Stack_Char
    {
    	//...
    public:
    	void Push(char x);
    };
    
    //...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    所以C++中不仅有函数模板,也有类模板。

    3.1类模板的基本格式

    template<class T1, class T2, ..., class Tn>
    class 类模板名
    {
    	// 类内成员定义
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5

    与函数模板一样,类模板的有效范围在模板定义之后碰到的第一个类中

    我们以栈类举例:

    template <typename StackDate>
    class Stack
    {
    public:
    	Stack(int capacity=4,int top=0)
    		:_a(new StackDate[capacity])
    		,_top(top)
    		,_capacity(capacity)
    	{}
    	~Stack()
    	{
    		delete[] _a;
    		_top = _capacity = 0;
    	}
    	void Push(StackDate x);
    private:
    	StackDate* _a;
    	int _top;
    	int _capacity;
    };
    
    //在类模板外定义成员函数,需要指明此函数属于哪个类模板
    template <typename StackDate>
    void Stack<StackDate>::Push(StackDate x)
    {
    	_a[_top++] = x;
    }
    
    • 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

    3.2类模板的实例化

    类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。

    那么我么想创建对象时,应该这么写:

    int main()
    {
    	//实例化之后才是真正的类
    	Stack<int> s1;
    	Stack<char> s2;
    	Stack<double> s3;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    4.模板在多文件中的使用注意事项

    我们上述的代码都是在一个文件下写的,所以在生成可执行文件的时候不涉及链接的问题。

    而且我们知道,函数模板和类模板不是一个具体的函数和具体的类,想要使用真正的函数或者真正的类必须对模板实例化。所以,在多文件中,模板的声明与定义分离会造成链接错误

    例如我们在某个头文件中写下这段代码:

    template <class T>
    T Add(T& a, T& b);
    
    • 1
    • 2

    然后在另一个源文件中对其定义:

    template <class T>
    T Add(T& a, T& b)
    {
    	return a + b;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    然后我们在主函数中去调用所谓的函数:

    int main()
    {
    	int a = 3, b = 4;
    	Add(a, b);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    此时就会发生报错:
    在这里插入图片描述
    为什么会发生链接错误?我们必须通过编译链接的角度去分析。我们知道,头文件在预处理的过程中会在源文件中展开,所以编译的时候仅仅处理两个文件。
    在这里插入图片描述
    此时编译器看到的只有两个文件:
    在这里插入图片描述
    与函数模板一样,类模板在多文件中声明和定义分离的时候,也会发生链接错误。那么解决方法有两种,一是声明和定义依然分离,在定义的文件当中显式实例化

    template <class T>
    T Add(T& a, T& b)
    {
    	return a + b;
    }
    
    //显示实例化:告诉编译器有类型
    template int Add<int>(int&,int&);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    那么这种方法可以确定是一种搓的方法。因为这样已经丧失模板的意义了,我们为何不干脆直接写普通的函数,而写一个模板呢?

    所以方法二就是:不要把声明和定义分离。没有别的办法了,即使是语言的短板我们也必须去接收它,没有一种语言是完美的。

    对于类模板也是一样,这里随便给一个类,主要是展示类模板的声明和定义分离如何显式实例化:

    头文件中的类模板:

    template <class T>
    class A
    {
    public:
    	A(int size = 4);
    private:
    	T* _a;
    	int _size;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    源文件中的显示实例化:

    template <class T>
    A<T>::A(int size)
    {
    	//……
    }
    
    //显示实例化
    template class A<int>;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
  • 相关阅读:
    App前端开发跨平台框架比较:React Native、Flutter、Xamarin等
    Hive实践——将查询的数据按照分区进行存储
    C#自定义窗体更换皮肤的方法:创建特殊窗体
    算法通关村第一关——链表青铜挑战笔记
    Linux命令(80)之sort
    ARM-V9 RME(Realm Management Extension)系统架构之系统能力的内存隔离和保护
    页面懒加载
    【项目】 基于BOOST的站内搜索引擎
    linux文件锁
    STL应用——vector
  • 原文地址:https://blog.csdn.net/weixin_59913110/article/details/127387813