前言:

如何实现一个通用交换函数?

void Swap(int& left, int& right)
{
 int temp = left;
 left = right;
 right = temp;
}
void Swap(double& left, double& right)
{
 double temp = left;
 left = right;
 right = temp;
}
void Swap(char& left, char& right)
{
 char temp = left;
 left = right;
 right = temp;
}
使用函数重载虽然可以实现,但是有一下几个不好的地方: 1. 重载的函数仅仅只是类型不同,代码的复用率比较低,只要有新类型出现时,就需要增加对应的函数 2. 代码的可维护性比较低,一个出错可能所有的重载均出错 那能否 告诉编译器一个模子,让编译器根据不同的类型利用该模子来生成代码 呢?
 

上图的冰块模子我们可以加入不同的原料来制造不同口味的冰块 如果在 C++ 中,也能够存在这样一个 模具 ,通过给这个模具中 填充不同材料 ( 类型 ) ,来 获得不同冰块 ( 生成具体类型的代码) ,那将会节省许多头发。巧的是前人早已将树栽好,我们只需在此乘凉。 泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。

目录

一、函数模板   

1、函数模板概念

2、函数模板格式

3、函数模板的原理

4 函数模板的实例化

5、模板参数的匹配原则

二、类模板

1、类模板的定义格式

2、类模板的实例化

1.默认参数

2.非类型参数

3.零初始化

注意事项

总结:

模板适用情景

模板的优势和劣势


一、函数模板   

1、函数模板概念

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

2、函数模板格式

template<typename T1, typename T2,......,typename Tn> 返回值类型 函数名 ( 参数列表 ){}
template<typename T>
void Swap( T& left, T& right)
{
 T temp = left;
 left = right;
 right = temp;
}
注意: typename 用来定义模板参数 关键字 也可以使用 class( 切记:不能使用 struct 代替 class)

3、函数模板的原理

那么如何解决上面的问题呢?大家都知道,瓦特改良蒸汽机,人类开始了工业革命,解放了生产力。机器生产淘汰掉了很多手工产品。本质是什么,重复的工作交给了机器去完成。有人给出了论调:懒人创造世界。
函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模 板就是将本来应该我们做的重复的事情交给了编译器
  在编译器编译阶段 ,对于模板函数的使用, 编译器需要根据传入的实参类型来推演生成对应类型的函数 以供 调用。比如:当用 double 类型使用函数模板时,编译器通过对实参类型的推演,将 T 确定为 double 类型,然 后产生一份专门处理 double 类型的代码 ,对于字符类型也是如此。

4 函数模板的实例化

用不同类型的参数使用函数模板时 ,称为函数模板的 实例化 。模板参数实例化分为: 隐式实例化和显式实例
1. 隐式实例化:让编译器根据实参推演模板参数的实际类型
template<class T>
T Add(const T& left, const T& right)
{
 return left + right;
}
int main()
{
 int a1 = 10, a2 = 20;
 double d1 = 10.0, d2 = 20.0;
 Add(a1, a2);
 Add(d1, d2);
 
 /*
 该语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型
 通过实参a1将T推演为int,通过实参d1将T推演为double类型,但模板参数列表中只有一个T,
 编译器无法确定此处到底该将T确定为int 或者 double类型而报错
 注意:在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要背黑锅
 Add(a1, d1);
 */
 
 // 此时有两种处理方式:1. 用户自己来强制转化 2. 使用显式实例化
 Add(a, (int)d);
 return 0;
}
2. 显式实例化:在函数名后的 <> 中指定模板参数的实际类型
int main(void)
{
 int a = 10;
 double b = 20.0;
 
 // 显式实例化
 Add<int>(a, b);
 return 0;
}
如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错。

5、模板参数的匹配原则

1. 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函
// 专门处理int的加法函数
int Add(int left, int right)
{
 return left + right;
}
// 通用加法函数
template<class T>
T Add(T left, T right)
{
 return left + right;
}
void Test()
{
 Add(1, 2); // 与非模板函数匹配,编译器不需要特化
 Add<int>(1, 2); // 调用编译器特化的Add版本
}
2. 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模 板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板
// 专门处理int的加法函数
int Add(int left, int right)
{
 return left + right;
}
// 通用加法函数
template<class T1, class T2>
T1 Add(T1 left, T2 right)
{
 return left + right;
}
void Test()
{
 Add(1, 2); // 与非函数模板类型完全匹配,不需要函数模板实例化
 Add(1, 2.0); // 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的Add函
数
}
3. 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换

二、类模板

1、类模板的定义格式

template<class T1, class T2, ..., class Tn>
class 类模板名
{
 // 类内成员定义
};
template<typename Type> //具备默认参数
class SeqList
{
public:
	SeqList(int sz = DEFAULT_SEQLSIT_SIZE)
	{
		capacity = sz > DEFAULT_SEQLSIT_SIZE ? sz : DEFAULT_SEQLSIT_SIZE;
		base = new Type[capacity];
		size = 0;
	}
	~SeqList()
	{
		delete []base;
		base = nullptr;
		capacity = size = 0;
	}
public:
	void push_back(const Type &v);
public:
	enum{DEFAULT_SEQLSIT_SIZE = 8};
private:
	Type *base;
	size_t capacity;
	size_t size;
};
// 注意:类模板中函数放在类外进行定义时,需要加模板参数列表
template<typename Type>
void SeqList<Type>::push_back(const Type &v)
{

}

2、类模板的实例化

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

1.默认参数

对于类模板,还可以为模板参数定义缺省值;这些值就被称为缺省模板实参;

template<typename Type, typename Type1=int> //具备默认参数
class SeqList
{
public:
	SeqList(int sz = DEFAULT_SEQLSIT_SIZE)
	{
		capacity = sz > DEFAULT_SEQLSIT_SIZE ? sz : DEFAULT_SEQLSIT_SIZE;
		base = new Type[capacity];
		size = 0;
	}
	~SeqList()
	{
		delete []base;
		base = nullptr;
		capacity = size = 0;
	}
public:
	void push_back(const Type &v);
public:
	enum{DEFAULT_SEQLSIT_SIZE = 8};
private:
	Type *base;
	size_t capacity;
	size_t size;
};
void main()
{
	SeqList<int> mylist;      //A
    SeqList<int,char> mylist;//B
	SeqList<char> youlist(1);
	SeqList<string> hilist;
}
上述代码A和B都可以执行,这是因为模板中对于第二个参数已经给予了默认值,这使得我们在生成一个模板类时第二个参数可有可无,写代码时非常方便。

2.非类型参数

模板参数分为类型形参与非类型形参。类型模板参数,即出现在模板参数列表中,跟在class或typename后的参数类型名称。

非类型模板参数,就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。

template<typename Type, size_t _N=8> //非类型模板参数
class SeqList
{
public:
	SeqList(int sz = _N)
	{
		capacity = sz > _N ? sz : _N;
		base = new Type[capacity];
		size = 0;
	}
	~SeqList()
	{
		delete []base;
		base = nullptr;
		capacity = size = 0;
	}
public:
	void push_back(const Type &v);


private:
	Type *base;
	size_t capacity;
	size_t size;
};


void main()
{
	SeqList<int, 20>  mylist;
	SeqList<int, 200> mylist1;

	SeqList<char, 10> youlist(1);
	SeqList<string, 8> hilist;
}

_N不是用来传递类型的,只能是无符号整型,

3.零初始化

对于int、double 或者指针等基本类型,并不存在“用一个有用的缺省值来对它们进行初始化”的缺省构造函数;相反,任何未被初始化的局部变量都具有-一个不确定(undefined) 值:

void main() {
	int a;
	int* ptr;
}

a具有一个不确定值,ptr指向某块内存(并非无所指)

 但是在自己编写的模板中,例如以下模板



template<typename Type>
void fun()
{
	Type x ; 
}

void main()
{
	fun<int>();
	
}

x为内置类型并不会初始化,如果x时自定义类型会调用模板内的构造函数自己初始化。

由于这个原因,我们就应该显式地调用内建类型的缺省构造函数,并把缺省值设为0 (或者false,对于bool类型而言),其形式。

class Test
{
public:
	Test(int data=0) : m_data(data)
	{}
private:
	int m_data;
};

template<typename Type>
void fun()
{
	Type x = Type(); //零初始化
}

void main()
{
	int a    = int();
	double d = double();
	float f  = float();
	bool flag = bool();

	fun<int>();
	fun<Test>();
}

譬如调用int()我们将获得缺省值0。于是,借助如下代码,我们可以确保对象已经执行了适当的缺省初始化,即便对内建类型对象也是如此:

template<typename Type>
class Test
{
public:
	//零初始化
	Test(Type data=Type()) : m_data(data)
	{}
private:
	Type m_data;
};

void main()
{
	Test<int> t;
	Test<bool> t1;
}

注意事项

模版类的定义和实现不能分开写在不同文件中,否则会导致编译错误

原因:在C++中,在编译阶段才确定对象所占用的空间。模板类只有被真正使用的时候,编译器才知道,模板套用的是什么类型,应该分配多少空间。然后根据套用的类型进行编译。套用不同类型的模板类实际上就是两个不同的类型,因此这两个类型的共同成员函数实质上也不是同一个函数,仅仅是具有相似的功能。因此,模板类在套用不同类型以后,会被编译出不同的代码。

总结:

模板适用情景

模板(有时称为参模板(有时称为参数化类型)是用于生成基于类型参数的函数和类的机制。通过使用模板,可以设计操作多种类型的数据的单个类或函数,而不必为每种类型创建单独的函数或类。

模板的优势和劣势

但是模板也有一些不太为人知的缺点。首先,由于C++没有二进制实时扩展性,所以模板不能像库那样被广泛使用。模板的数据类型只能在编译时才能被确定。因此,所有用基于模板算法的实现必须包含在整个设计的头文件中。通过分析标准模板库(STL)的头文件,你可以很清楚的认识到这一点。

另外,由于模板只是最近加入C++标准中,所以有些C++编译器还不支持模板,当使用这些编译器时编译含有模板的代码时就会发生不兼容问题。例如,Mozilla浏览器开发组之所以没有使用模板就是因为交叉平台会导致模板的不兼容。同样的,如果当开发者需要跨越好几个平台而有的平台可能只有老的C++编译器的时候,使用模板也是不明智的。

即使到现在,模板的一些高级特性,例如局部特殊化和特殊化顺序在不同的C++标准实现中也还是不统一的。

尽管如此,结合STL使用模板还是可以大大减少开发时间。模板可以把用同一个算法去适用于不同类型数据,在编译时确定具体的数据类型。

更多推荐

C++模板初阶详解