C++打怪升级(八)- 泛型编程初见

BlogPicture1.jpg

前言

模板,代码变得简洁!本节将介绍泛型编程中模板的用法。


泛型编程

引子

对于一组功能相同单参数类型不同的函数,在C语言中只能写多个不同名的函数来实现

void Swapc(char& a, char& b) {
	char tmp = a;
	a = b;
	b = tmp;
}
void Swapi(int& a, int& b) {
	int tmp = a;
	a = b;
	b = tmp;
}

void Swapf(float& a, float& b) {
	float tmp = a;
	a = b;
	b = tmp;
}

在C++中我们学习了函数重载,可以写多个同名参数类型不同的函数来实现;
C++函数重载解决了函数同名的问题,但是我们还是要写多个函数,而它们仅仅只有类型不同;

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

void Swap(float& a, float& b) {
	float tmp = a;
	a = b;
	b = tmp;
}

这种方法缺点明显:

需要根据参数类型来手动增加接受该类型的函数,这对于我们来说很麻烦;
这一组函数代码的可维护性差,要改就需要更改一组函数,也很麻烦;

基于类似这样的原因,C++提出了泛型编程的概念,我们只需要写出一个函数模板而不是具体的函数,我们直接使用这个函数模板,具体的函数由编译器自动生成;


泛型编程是啥

编写与类型无关的通用代码,是代码复用的方法之一。
模板是泛型编程中的基本组成部分,分为函数模板和类模板。


函数模板

概念

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

格式

C++模板引入了新关键字template表示模板;
对于函数模板参数类型并不是具体的类型,而是class/typename来表示通用类型;
typename也是一个C++关键字;

template<typename T1,typename T2,...,typename Tn>
返回值类型  模板函数名(函数参数列表){
//模板函数体
}

例子
交换函数模板

//函数模板
template<class T>
void Swap(T& t1, T& t2) {
	T tmp = t1;
	t1 = t2;
	t2 = tmp;
}

//函数模板
template<typename T>
void Swap(T& t1, T& t2) {
	T tmp = t1;
	t1 = t2;
	t2 = tmp;
}
int main() {

	int a = 1, b = 2;
	cout << "前: " << a << " " << b << endl;
	Swap(a, b);
	cout << "后: " << a << " " << b << endl;
	float c = 3.14, d = 9.99;
	cout << "前: " << c << " " << d << endl;
	Swap(c, d);
	cout << "后: " << c << " " << d << endl;
	return 0;
}

image.png


原理分析

我们写了一个函数模板并使用它时,编译器到底做了什么呢?
函数模板只是一个模板,一张图纸,不是一个具体的函数
编译器在编译时根据实参类型顺序推导模板参数的通用类型为某一特定类型,然后根据推倒的类型生成具体的特定类型的函数(函数实例化)

//函数模板
template<typename T>
void Swap(T& t1, T& t2) {
	T tmp = t1;
	t1 = t2;
	t2 = tmp;
}
int main() {
    
	int a = 1, b = 2;
	Swap(a, b);
	float c = 3.14, d = 9.99;
	Swap(c, d);
	return 0;
}

image.png


实例化

函数模板实例化

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

隐式实例化

由编译器在编译阶段根据我们所传实参推导函数模板参数实际类型然后生成某一具体类型的函数;

//函数模板
template<typename T>
T Add(const T& t1, const T& t2) {
	return t1 + t2;
}
int main() {
	int a = 1, b = 2;
	double c = 3.14, d = 9.99;
	cout << Add(a, b) << endl;
	cout << Add(c, d) << endl;

	return 0;
}

image.png
image.png


显式实例化

我们在使用函数模板时在模板函数名后额外加上<具体类型>可以指定模板函数参数的实际类型,这样,编译器不在根据参数类型进行推导,而是直接根据指定类型生成对应的函数;

template<typename T>
T Add(const T& t1, const T& t2) {
	return t1 + t2;
}
int main() {
	int a = 1, b = 2;
	double c = 3.14, d = 9.99;
    Add<int>(a, b);
	Add<double>(c, d);
	cout << Add<int>(a, b) << endl;
	cout << Add<double>(c, d) << endl;

	return 0;
}

image.png
image.png


模板参数的匹配分析

实参与模板参数类型不完全匹配

当遇到实参与模板参数类型不完全匹配时,编译器会报错,因为模板函数不允许自动类型转换;

对于Add()函数模板来说,我们传入两个实参类型不同,而模板函数只有一个通用类型,也只能推导出一个具体的类型,这样就总会有一个实参类型匹配不上;
这里的报错是编译器无法根据实参类型明确推导出一个具体的函数了,不涉及类型转换(发生在具体的函数传参时)

//函数模板
template<typename T>
T Add(const T& t1, const T& t2) {
	return t1 + t2;
}
int main() {

	int a = 1;
	double b = 3.14;
	Add(a, b);
	return 0;
}

image.png

解决方法1:具体函数由我们指定而不是由编译器推导,不过这样会发生隐式类型转换

int main() {

	int a = 1;
	double b = 3.14;
	Add<int>(a, b);
	Add<double>(a, b);
	return 0;
}

image.png

解决方法2:手动强制类型转换,确保编译器类型推导正确

int main() {

	int a = 1;
	double b = 3.14;
	Add(a, (int)b);
	Add((double)a, b);
	return 0;
}

image.png

解决方法3:类模板增加参数

//多参函数模板
template<typename T1, typename T2>
T1 Add(const T1& t1, const T2& t2) {
	return t1 + t2;
}
int main() {
	int a = 1;
	double b = 2;
	Add(a, b);
    Add(b, a);
	return 0;
}

image.png
image.png


模板函数的函数名与实际函数函数名

我们在模板那里写的函数名是模板的函数名,不能称之为实际的函数名;
实际的函数名需要在模板函数名后面<>内顺序加上对应的实参类型;

template<typename T>
T Add(const T& t1, const T& t2) {
	return t1 + t2;
}

模板函数名Add
实际函数名可以是:

Add<char>
Add<int>
Add<float>
//......

template<typename T1, typename T2>
T1 Add(const T1& t1, const T2& t2) {
	return t1 + t2;
}

模板函数名Add
实际函数名可以是:

Add<int, int>
Add<double, double>
Add<int, double>
Add<double, int>

存在实际可用函数时模板参数的匹配原则

编译器将会优先选择我们写好的匹配的可用函数,其次才是编译器通过函数模板自动生成;

int Add(const int& t1, const int& t2) {
	return t1 + t2;
}
template<typename T>
T Add(const T& t1, const T& t2) {
	return t1 + t2;
}

优先调用自己实现的Add函数

int main() {
	int a = 1, b = 2;
	//调用自己实现的Add函数
	cout << Add(a, b) << endl;
	return 0;
}

image.png
指定指定使用函数模板推导生成Add函数

int main() {
	int a = 1, b = 2;
	//指定使用函数模板推导生成的Add函数
	Add<int>(a, b);
	cout << Add<int>(a, b) << endl;
	return 0;
}

image.png

相同功能的实际函数可以与其函数模板同时存在;
这并不冲突,函数模板不是函数,不会与实际函数冲突;
就算模板函数实例化出具体的函数也不会和已经存在的实际函数冲突,因为我们写的函数和函数模板生成的函数虽然完成相同的功能,但是二者是完全不同的函数,函数地址也不相同;

int main() {
	int a = 1, b = 2;
	Add(a, b);
	Add<int>(a, b);
	return 0;
}

image.png


如果模板可以产生一个具有更好匹配的函数, 编译器将会选择模板实例化出的函数;
**也就是说,编译器选择优先考虑是匹配问题; **

int Add(const int& t1, const int& t2) {
	return t1 + t2;
}

template<typename T1, typename T2>
T1 Add(const T1& t1, const T2& t2) {
	return t1 + t2;
}
int main() {
	int a = 1;
	double b = 3.14;
	Add(a, b);
	cout << Add(a, b) << endl;
	return 0;
}

image.png
image.png

具体函数和函数模板都存在时,优先调用具体函数而不是函数模板;
如果我们显式使用函数模板生成的具体函数也可以正常运行得到结果;
这说明我们实现的具体函数和函数模板推导生成的具体函数是不同的函数函数地址不同
我们写的具体函数函数模板推导生成的具体函数函数名修饰规则不同的,否则会报错
所以编译器的原则是在最满足匹配时,优先调用显式实现的


类模板

接下来介绍类模板;
相比函数模板,类模板使用更加广泛

引子

类模板的出现是为了解决一些问题,与函数模板相似,解放我们,辛苦辛苦编译器;
对于一个写好的具体的类来说,其成员变量的类型是确定的某一类型,这是理所当然的;

class A {
public:
	A(int a = 1)
		:_a(a) {}
	void Print() {
		cout << _a << endl;
	}
private:
	int _a;
};

如果有这样的需求,保持类的结构大体不变,只改变成员变量的类型;
没有类模板的话我们只能在写一个相似的类出来,这样效率不高,还造成代码冗余,但是没有类模板也只能这样做;

class A1 {
public:
	A1(int a = 1)
		:_a(a) {}
	void Print() {
		cout << _a << endl;
	}
private:
	int _a;
};

class A2 {
public:
	A2(float a = 1.1)
		:_a(a) {}
	void Print() {
		cout << _a << endl;
	}
private:
	float _a;
};

#define定义宏typedef重命名类型可以用一个通用类型代替具体的类型,更换时只需要在一处修改类型;

但是有什么问题呢?

#define TypeDate int
class A {
public:
	A(TypeDate a = 1)
		:_a(a) {}
	void Print() {
		cout << _a << endl;
	}
private:
	TypeDate _a;
};
typedef int TypeDate;
class A {
public:
	A(TypeDate a = 1)
		:_a(a) {}
	void Print() {
		cout << _a << endl;
	}
private:
	TypeDate _a;
};

通用类型是比较方便的,但是没有解决不同类型成员变量同时存在的问题,比如既需要int型有需要float型时
typedef只能满足其中一种类型,而不是多种;
类模板随之而来,利落的解决了这个问题,达到了我们想创建哪个类型的类都可以的目的。


格式

template<typename T1, typename T2,...,typename Tn>
class className{
//...
}

例子

template<typename T>
class A {
public:
	A(T a = 1)
		:_a(a){}
	void Print() {
		cout << _a << endl;
	}
private:
	T _a;
};
int main() {
	A<char> a1('a');
	A<int> a2(10);
	A<double> a3(3.14);
	a1.Print();
	a2.Print();
	a3.Print();
}

image.png


实例化

类模板实例化与函数模板实例化有些差别,类模板实例化必须在类模板名字后跟<><>中写实例化的类型
,注意类模板名字不是真正的类,而实例化的结果才是真正的类(也就是类模板名加上具体的类型是真正的类名);
这里有个问题,类模板实例化为什么必须在其后加上<具体类型>呢?
或者说为什么我们需要指定类模板实例化的类型而不是像函数模板实例化那样由编译器推导类型再实例化呢?

编译器对于类模板类型一般没有推导时机,而是需要我们对类模板显式实例化

类模板函数定义在类模板外时相比普通函数需要更多的处理:
完整地类名是类模板名+<类型>
指定类外函数作用域时也要使用完整的类名,在函数定义开头还需要模板参数出现;

template<typename T>
class A {
public:
	A(T a = 1)
		:_a(a){}
	void Print();
private:
	T _a;
};
template<typename T>//类模板参数
void A<T>::Print() {//完整类名
	cout << _a << endl;
}

类模板不支持分离编译

原因分析

类模板分离编译会报链接错误
一般建议类模板在同一个文件中声明和定义分离,这是最好的方式了,达到了类中简洁只有函数声明,同时没有各种错误;
来看看类声明和定义分离且不在一个文件会遇到的问题:
image.png
image.png
image.png
image.png
image.png
程序运行报错 - 链接错误
image.png
test.o文件找不到要调用的由类模板实例化的成员函数,那么为什么找不到呢?
这牵扯到了多个源文件的编译链接过程
链接错误,说明不是语法问题,而是链接时,test.oclass.o中找不到要调用的类模板实例化出来的函数,即类模板没有实例化处具体的函数,class.o符号表中也就没有相应函数的地址;
为什么在类模板没有实例化出具体的函数呢?

因为类模板成员函数定义与类模板分离,test.cppclass.cpp各自的预处理/编译/汇编都是独立进行的;
test.c中有类模板的实例化(我们显式实例化的A),class.cpp中没有类模板的实例化A,类模板函数无法实例化成具体类型的函数,class.o符号表中也就没有类具体函数的地址;
test.o中虽有实例化A,但是头文件class.h展开后,test.cpp只有类模板函数的声明,只实例化了类的函数的声明,而函数的声明没有实际有效地址,故test.o会在链接期间到class.o中寻找函数有效地址(类函数实例化后才有);
class.o符号表中是没有具体函数的地址的,结果是test.o哪里都找不到待调用函数有效地址,而这又发生在链接阶段,导致链接错误;

解决方法

在函数定义文件中主动显式实例化

这是一个不太好(实用)的方法

既然链接错误是因为,类模板成员函数只有声明显式实例化了,那么我们也在类模板成员函数定义文件内显式实例化即可;

本例中即是在class.cpp源文件中额外加上我们所写的类模板显式实例化

template
class A<int>;

template class A<int>;

程序便可以正常运行;

class.h

#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <cassert>
using namespace std;

template<typename T>
class A {
public:
	//构造
	A(T a = 1);
	//拷贝构造
	A(A& a);
	//赋值运算符重载
	A& operator=(const A& a);
	//析构
	~A();
	void Print();
private:
	T _a;
};

class.cpp

#include "class.h"
template class A<int>;

template<typename T>
A<T>::A(T a)
	:_a(a) {}

template<typename T>
A<T>::A(A& a) {
	_a = a._a;
}

//说模板实参列表要与形参列表匹配
template<typename T>
A<T>& A<T>::operator=(const A<T>& a) {
	_a = a._a;
	return *this;
}

template<typename T>
A<T>::~A() {
	_a = 0;
}

template<typename T>
void A<T>::Print() {
	cout << _a << endl;
}

test.cpp

#include "class.h"

int main() {

	A<int> a1(10);
	A<int> a2(20);
	a1.Print();
	a1 = a2;
	a1.Print();
	return 0;
}

类模板成员函数声明和定义分离但在同一个文件内

本例即是

class.hclass.hpp
类模板成员函数声明和定义分离但在同一个文件,这样就不会报错了;

#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <cassert>
using namespace std;

template<typename T>
class A {
public:
	//构造
	A(T a = 1);
	//拷贝构造
	A(A& a);
	//赋值运算符重载
	A& operator=(const A& a);
	//析构
	~A();
	void Print();
private:
	T _a;
};

template<typename T>
A<T>::A(T a)
	:_a(a) {}

template<typename T>
A<T>::A(A& a) {
	_a = a._a;
}
template<typename T>
A<T>& A<T>::operator=(const A<T>& a) {
	_a = a._a;
	return *this;
}

template<typename T>
A<T>::~A() {
	_a = 0;
}

template<typename T>
void A<T>::Print() {
	cout << _a << endl;
}

一个练习

对类的使用像普通数组一样(本质不一样)

//定义在命名空间中,防止和库里面的类名冲突
namespace weihe {
	template<typename T>
	class Array {
	public:
        //[]运算符重载
		T& operator[](size_t i) {
			assert(i < 10);
			return _a[i];
		}
	private:
		T _a[N];
	};
}
int main() {
	weihe::Array<int> a;
	for (int i = 0; i < 10; ++i) {
		a[i] = i;
	}
	for (int i = 0; i < 10; ++i) {
		cout << a[i] << " ";
	}
	cout << endl;
	for (int i = 0; i < 10; ++i) {
		a[i] *= 10;
	}
	for (int i = 0; i < 10; ++i) {
		cout << a[i] << " ";
	}
	cout << endl;
	return 0;
}

image.png

[]运算符重载,返回类成员数组的下标为i的元素;
防止类名Array和标准库std中的名字(本例中命名空间std被完全展开了)冲突,建立一个命名空间域weihe


优化数组检查 - 抽查–>断言检查

assert断言用于检查任何数组越界的情况,比编译器检查的抽查形式更加严格;

编译器对数组下标越界的检查是抽查,即在数组边界写容易检查出来,远离数组边界的越界写不容易检查出来;在数组边界读和远离数组边界读基本不被检查出来
而我们的assert断言形式的检查绝对不放过任何可能的越界读和写,统统报错;


后记

本节主要介绍了泛型编程基础概念 – 模板。模板的存在帮助我们减轻了负担,其中类模板需要重点关注。
下次再见!


E N D END END

  • 5
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

re怠惰的未禾

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

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

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

打赏作者

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

抵扣说明:

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

余额充值