c++ 模板和泛型、特化讲解

C++ 模板和泛型


提前声明:注意!!!!模板和泛型理解难度较大,需要多花时间才能理解,请注意看文章中代码注释,有些知识的解释已经详细的写入注释。如果不能理解可以和我一起交流学习!!!!


概述

泛型编程是指独立于任何类型的方式编写代码。
泛型编程和面向对象编程,都依赖与某种形式的多态。面向对象编程的多态性在运行时应用于存在继承关系的类,一段代码可以可以忽略基类和派生类之间的差异。**在泛型编程中,编写的代码可以用作多种类型的对象**。面向对象编程所依赖的多态性称为运行时多态性,泛型编程所依赖的多态性称为编译时多态性或参数式多态性。
模板时泛型编程的基础,是创建类或者函数的公式
在这里插入图片描述

为什么要使用模板?

以下面例子举例:

int sum(int a,int b){return a+b;}
char sum(char a,char b){return a+b;}
float sum(float a,float b){return a+b;}

这些函数几乎相同,唯一的区别就是形参类型不同,在这种情况下,不必定义多个函数,只需要在模板中定义一次即可。在调用哦函数时系统会根据实参的类型来取代模板中的虚拟类型,从而实现不同的函数功能。
函数模板定义方式:

template<typename T>
T sum(T a,T b)
{
    return a+b;
}

实例化以及对象注意事项:

#include <iostream>
#include <exception>
using namespace std;

class MyClass
{
public:
	MyClass();
	~MyClass();

	/// <summary>
	/// int sum(int a,int b){return a+b;}
	/// char sum(char a, char b) { return a + b; }
	/// float sum(float a, float b) { return a + b; }
	/// </summary>
	/// <typeparam name="T"></typeparam>
	/// <param name="a"></param>
	/// <param name="b"></param>
	/// <returns></returns>

	//模板形参表使用typename或者class定义都可以, 
	//没有任何区别,为了区分类的定义,一般使用typename
	template<typename T>
	T sum(T a, T b) {
		return a + b;
	}

	// 当函数模板和普通函数参数都符合时,优先选择普通函数
	int sum(int a, int b) {
		return a + b;
	}
	
	// 需要传两个不一样的参数,只需要定义两个即可
	template <typename T, typename U>
	auto sum(T a, U b)
	{
		return a + b;
	}


private:

};

MyClass::MyClass()
{
}

MyClass::~MyClass()
{
}

int main(){
	MyClass my;
	// 自动类型推导  
	cout << my.sum(1, 3)<<endl;
	cout << my.sum(1.3, 2.5) << endl;
	cout << my.sum('a', 'c') << endl;

	// 显示调用方式: 函数名<参数类型>
	// 若显示使用模板函数,则使用<>类型列表
	cout << my.sum<int>(2, 3) << endl;


	// 模板不提供隐式类型转换,必须是严格匹配.此处要指定模板类型。
	// 此处要求以整形输出结果,必须显示转换。
	// 调用普通函数,可以隐式类型转换
	cout << my.sum<int>('a','b') << endl;


	// 模板不提供隐式类型转换,必须是严格匹配.
	cout << my.sum(1, 'c') << endl;
	cout << my.sum('a', 34) << endl;	
	cout << my.sum<char>('a', 'c') << endl;

	return 0;
}

编译模型

使用C/C++进行编程时,一般会使用头文件以使定义和声明分离,并使得程序以模块方式组织。将函数声明、类的定义放在头文件中,而将函数实现以及类成员函数的定义放在独立的文件中。但是对于模板来说,这种方式是行不通的,具体的例子如下:

传统的编译模型—报错原因分析

包含模板声明的头文件temlpateClass.h

#pragma once
#include<iostream>
using namespace std;

template<typename T>
void show(T t);

class temlpateClass
{
public:
	temlpateClass();
	~temlpateClass();
public:
	template<typename T>
	void display(T t);

private:

};

包含模板声明的源文件temlpateClass.cpp

#include "temlpateClass.h"

temlpateClass::temlpateClass()
{
}

temlpateClass::~temlpateClass()
{
}

template<typename T>
inline void temlpateClass::display(T t)
{
	cout << "t: " << t << endl;
}

template<typename T>
inline void show(T t)
{
	cout << "show:" << t << endl;
}

主函数文件main.cpp

#include"temlpateClass.h"

int main() {
	temlpateClass t;
	t.display("this is temlpateClass's display");

	show("this is show's show");
	return 0;
}

在这里插入图片描述

链接报错:严重性 代码 说明 项目 文件 行 禁止显示状态
错误 LNK2019 无法解析的外部符号 “public: void __cdecl temlpateClass::display<char const *>(char const *)” (??$display@PEBD@temlpateClass@@QEAAXPEBD@Z),函数 main 中引用了该符号 Project1 C:\Users\cw\source\repos\Project1\main.obj 1

原因分析:
当源码文件main.cpp中涉及到模板函数的调用时,因为模板函数的定义在另一个源码文件temlpateClass.cpp中,编译器目前仅仅知道它们的声明。所以,在main.cpp中调用到的的show函数,以及void Test::display()函数,编译器认为这些函数的实现是在其他源码文件中的,编译器不会报错,因为连接器会最终将所有的二进制文件进行连接,从而完成符号查找,形成一个可执行文件。
​ 尽管编译器也编译了包含模板定义的源码文件temlpateClass.cpp,但是该文件仅仅是模板的定义,而并没有真正的实例化出具体的函数来。因此在链接阶段,编译器进行符号查找时,发现源码文件中的符号,在所有二进制文件中都找不到相关的定义,因此就报错了。

模板的编译模型

编译器并不是把模板编译成一个可以处理任何类型的单一实体;而是如果调用了模板的时候,编译器才产生特定类型的模板实例
一般而言,当调用函数的时候,编译器只需要看到函数的声明。类似地,定义类类型的对象时,类定义必须可用,但成员函数的定义不是必须存在的。因此,应该将类定义和函数声明放在头文件中,而普通函数和类成员函数的定义放在源文件中。
模板则不同:要进行实例化,编译器必须能够访问定义模板的源代码。当调用函数模板或类模板的成员函数的时候,编译器需要函数定义,需要那些通常放在源文件中的代码。
标准 C++ 为编译模板代码定义了两种模型。分别是包含编译模型和分别编译模型。
所谓包含编译模型,说白了,就是将函数模板的定义放在头文件中。因此,对于上面的例子,就是将test.cpp的内容都放到test.h中。
为了区分,申明和定义放在一起的文件可以取名叫做.hpp
​ **所以,结论就是,把模板的定义和实现都放到头文件中。
也就是将下面这部分放在头文件定义当中:

template<typename T>
inline void temlpateClass::display(T t)
{
	cout << "t: " << t << endl;
}

template<typename T>
inline void show(T t)
{
	cout << "show:" << t << endl;
}

类模板

类模板与函数模板的定义和使用类似。 有时,有两个或多个类,其功能是相同的,仅仅是数据类型不同,模板的参数类型定义写在类的定义之前,在类里的任意位置都可以使用,如下面语句声明了一个类:

//模板的参数类型定义写在类的定义之前,在类里的任意位置都可以使用
template<typename T>
class Display
{
public:
	Display(T val) :_value(val){}
	void display()
	{
		cout << _value << endl;
	}
private:
	T _value;
};

类模板用于实现类所需数据的类型参数化 。
类模板在表示如数组、表、图等数据结构显得特别重要,这些数据结构的表示和算法不受所包含的元素类型的影响(STL)

单个类模板语法

定义一个类模板非常简单,重要的是如何去用类模板定义对象
同时 必须指定模板的参数列表,否则会报错。在这里插入图片描述

Display d(20);			//error C2955: “Display”: 使用 类 模板 需要 模板 参数列表

Display<string> d("maye"); //指定参数列表只需要在类名的后面加上<类型>即可
Display<string> d1(string("hello"));

类模板不代表一个具体的、实际的类,而代表一类类。实际上,类模板的使用就是将类模板实例化成一个具体的类
只有那些被调用的成员函数,才会产生这些函数的实例化代码。**对于类模板,成员函数只有在被使用的时候才会被实例化。**显然,这样可以节省空间和时间;
如果类模板中含有静态成员,那么用来实例化的每种类型,都会实例化这些静态成员。

#include<iostream>
#include<string>
using namespace std;

template<typename T>
class Array
{
public:
	Array(int size) 
		:_capacity(size),_size(0),_base(nullptr)
	{
		if (_capacity == 0)
			_capacity = 1;
		_base = new T[_capacity]{T()};
	}
	T& operator[](int index)
	{
		if (index < 0 || index >= _capacity)
		{
			//return T(); //不能返回临时对象的引用,对于int() 是一个0
			throw std::out_of_range("Array 越界啦");	//抛异常是最合适的
		}
		return _base[index];
	}
private:
	T* _base;
	int _size;
	int _capacity;
};
int main()
{
	Array<int> arr(10);
	for (size_t i = 0; i < 10; i++)
	{
		cout << arr[i] << " ";
	}
	cout << endl;
	Array<string> arr1(10);
	arr1[0] = string("maye");
	for (size_t i = 0; i < 10; i++)
	{
		cout << arr1[i] << " ";
	}
	return 0;
}

继承中的类模板——类模板派生普通类

子类从模板类继承的时候,需要让编译器知道,父类的数据类型具体是什么(数据类型的本质:如何分配内存空间)

template<typename T>
class Display
{
public:
	Display(T val) :_value(val){}
	void display()
	{
		cout << _value << endl;
	}
protected:
	T _value;
};
class A:public Display<int>	//指定具体类型
{
public:
	using Display<int>::Display;
	void show()
	{
		cout << "A" <<" "<<_value<< endl;
	}
};

继承中的类模板——类模板派生模板类

template<typename T>
class Display
{
public:
	Display(T val) :_value(val){}
	void display()
	{
		cout << _value << endl;
	}
protected:
	T _value;
};
template<typename U>
class A:public Display<U>
{
public:
	using Display<U>::Display;
	void show()
	{
		cout << "A" <<" "<<_value<< endl; //子类在使用父类成员时候,会提示找不到_value标识符号。
	}
};

void show()
{
cout << “A” <<" "<<_value<< endl; //error C2065: “_value”: 未声明的标识符
}
解决办法
1,通过this指针访问:this->_value
2,通过父类访问: Display::_value
因为this有类型Display,依赖的类型T。所以this有依赖类型。所以需要this->_value做_value一个从属名称。

模板特化

提到特化这个概念,就想到泛化的概念。模板函数的T参数只能传入类类型的参数;特化函数的参数只能传入对应的参数类型。

函数模板特化

假设有一个比较两个对象的模板函数,对于支持operator== 和 operator>操作的类型, 包括基本的int,float,double等类型,是完全没有问题的,但是它不能用来比较字符串(char*),因为这个函数比较的是串指针,而不是字符串本身

template<typename T>
int compare(T a, T b)
{
	cout << "T" << endl;
	return a == b ? 0 : (a > b ? 1 : -1);
}

cout << compare("A", "a") << endl;	//类型是const char*,比较的是地址,需要做特化版本才能比较

特化版本:
这样,就能正确比较字符串了,前面提到过,特化版本和普通版本的函数是可以实现重载的,并且优先匹配普通函数。

template<>	 //必须写,不然就是重载函数,而不是函数模板,特化版本了
int compare(const char* str1, const char* str2)
{
	cout << "const char *" << endl;
	return strcmp(str1, str2);
}

那么将这个特化放在何处?显然是要放在模板的头文件中。但这样会导致符号多重定义的错误。原因很明显,模板特化是一个函数,而非模板。
在这里插入图片描述

//test.h
#pragma once
#include<iostream>
using namespace std;

template<typename T>
int compare(T a, T b)
{
	cout << "T" << endl;
	return a == b ? 0 : (a > b ? 1 : -1);
}
template<>
int compare(const char* str1, const char* str2)
{
	cout << "特化 const char *" << endl;
	return strcmp(str1, str2);
}


//main.cpp
int main()
{
   cout << compare("A", "a") << endl;
   //compare<char const *>(char const *,char const *)" 已经在 main2.obj 中定义
   return 0;
}

解决方案
没有理由不在头文件中定义函数——但是一旦这样做了,那么便无法在多个文件中 #include 该头文件。肯定会有链接错误。怎么办呢?
函数模板特化即函数,而非模板的概念,完全与普通函数一样;加上inline关键字或者分文件实现都是可以的。

template<>
inline int compare(const char* str1, const char* str2)
{
	cout << "特化 const char *" << endl;
	return strcmp(str1, str2);
}

因为编译器直接扩展内联函数不产生外部符号,在多个模块中 #include 它们没有什么问题。链接器不会出错,因为不存在多重定义的符号。对于像 compare 这样的小函数来说,inline 怎么说都是你想要的(它更快)。
但是,如果你的特化函数很长,或出于某种原因,你不想让它成为 inline,那要如何做呢?声明和实现分开即可:

//test.h
template<>
int compare(const char* str1, const char* str2);
//test.cpp
template<>
int compare(const char* str1, const char* str2)
{
	cout << "特化 const char *" << endl;
	return strcmp(str1, str2);
}

类模板特化

全特化:所有类型模板参数都用具体类型代表,特化版本模板参数列表为空 template<>

template<typename T,typename U>
struct Test
{
    void show()
    {
        cout<<"非特化版本"<<endl;
    }
};
//全特化版本
template<>		//模板参数列表为空 template<>
struct Test<int,int>
{
    void show()
    {
        cout<<"int,int特化版本"<<endl;
    }
};
//特化版本可以有任意多个
template<>		//模板参数列表为空 template<>*
struct Test<double,string>
{
    void show()
    {
        cout<<"double,string特化版本"<<endl;
    }
};
//测试
int main()
{
    Test<int,int> t;
    t.show();					//int,int特化版本
    Test<double, string> t1;	
    t1.show();					//double,string特化版本
    Test<char, char> t2;
    t2.show();					//非特化版本
    
    return 0;
}

局部特化(偏特化):指定一部分模板参数用具体类型代替

  • 从模板参数数量上
  • 从模板参数范围上(int -> int&)
//从模板参数数量上
template<typename T,typename U>			//指定一部分模板参数用具体类型代替
struct Test
{
    void show()
    {
        cout<<"非特化版本"<<endl;
    }
};
//局部特化
template<typename U>
struct Test<int,U>
{
    void show()
    {
        cout<<"非特化版本"<<endl;
    }
};
//测试
int main()
{
    Test<int,string> tt;
    tt.show();				//局部特化版本
    return 0;
}


//从模板参数范围上
template<typename T>
struct Test
{
    void show()
    {
        cout<<"非特化版本"<<endl;
    }
};
//const T
template<typename T>
struct Test<T&>
{
    void show()
    {
        cout<<"T&特化版本"<<endl;
    }
};
//T*
template<typename T>
struct Test<T*>
{
    void show()
    {
        cout<<"T*特化版本"<<endl;
    }
};
//测试
int main()
{
    Test<int> t;
    t.show();			//非特化版本
    Test<int*> t1;
    t1.show();			//T*特化版本
    Test<int&> t2;
    t2.show();			//T&特化版本
    return 0;
}



—2022…4.17对于特化的理解可能有些难度,后期会结合更多实例讲解

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Warm wolf

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值