亲宝软件园·资讯

展开

C++印刷模板使用方法详解

The s.k.y. 人气:0

在了解string之前,我们需要了解模板等等的一些铺垫知识,让我们开始吧!

一、泛型编程

泛型编程是什么意思呢?我们通过下面的例子来具体了解:

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;
}
int main()
{
	int a = 1, b = 2;
	Swap(a, b);	
    double c=1.33,d=2.33;
    Swap(a,b)
}

就拿交换函数来说,当我们交换不同类型的变量的值,那就需要不停的写交换函数的重载,这样代码复用率就较低,那我们能不能创造一个模板呢??

一个Swap的模板,但是我可以用不同的类型去实现这个模板,继而试用它。

如果在 C++ 中,也能够存在这样一个 模具 ,通过给这个模具中 填充不同材料 ( 类型 ) ,来 获得不同材料的铸件 ( 即生成具体类型的代码)。 泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。

二、模板(初阶)

模板分为:函数模板和类模板

1.函数模板

1.单参数类型

函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定 类型版本。 就拿Swap来说: typename 是 用来定义模板参数 关键字,T是类型(也可以用class,(class T))

template<typename T>
void Swap(T& left, T& right)
{
	T temp = left;
	left = right;
	right = temp;
}
int main()
{
	    int a = 1, b = 2;
	    Swap(a, b);
        int x = 1, y = 2;
		Swap(x, y);
		double m = 1.1, n = 2.2;
		Swap(m, n);
		char p = 'a', q = 'b';
		Swap(p, q);
        Swap(m,a);//不同类型
}

那么,具体是怎样实现的呢?

函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模 板就是将本来应该我们做的重复的事情交给了编译器。

编译器通过类型推演,将函数模板进行实例化,对应的T就会替换成具体的类型,模板实例化是用几个实例化几个,不是所有不同类型都提前模板实例化。

1.当变量类型相同,但是变量不同,调用Swap();模板实例化只会实例化一个,因为虽然变量不同,但类型相同,模板实例化就是将T换成具体的类型。

2.当Swap(m,a),变量是不同类型时,会发生什么??

因为在推演void Swap(T& left, T& right);时,T的类型不明确,就会发生错误(推演报错),直接报错

但如果不用模板,我们自己这样:

void Swap(int& left, int& right)
{
	int temp = left;
	left = right;
	right = temp;
}
int main()
{
  int a=2;
  double b=2.22;
  Swap(a,b);
}

可能有人就会想:在Swap(a,b)中,会不会a和b发生饮食类型转化呢?较小的类型转化成较大的类型。

当然不会:隐式类型转化只有在 赋值:b=3;(产生临时变量);函数传参的时候(产生临时变量),才会发生隐式类型转化。

函数形参是引用,当类型是引用时,我们就要小心:是否会发生权限放大?当b传值时,中间的临时变量具有常性(只读),而形参是可读可写,权限就会放大,也是不可以通过的,除非加了const,但是加了const就无法交换了,所以这样还是行不通的!

自动推演实例化和显式实例化:

template<class T>
T Add(const T& left, const T& right)
{
	return left + right;
}
int main()
{
	int a1 = 10, a2 = 20;
	double d1 = 10.1, d2 = 20.2;
	// 自动推演实例化
	cout << Add(a1, a2) << endl;
	cout << Add(d1, d2) << endl;
	cout << Add((double)a1, d2) << endl; //强制类型转化也是产生临时变量,不是改变a1
	cout << Add(a1, (int)d2) << endl;
	// 显示实例化
	cout << Add<double>(a1, d2) << endl;//隐式类型转化
	cout << Add<int>(a1, d2) << endl;
	return 0;
}

在自动推演实例化中,必须强转,不然还是和之前问题一样,该语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型通过实参a1将T推演为int,通过实参d1将T推演为double类型,但模板参数列表中只有一个T,编译器无法确定此处到底该将T确定为int 或者 double类型而报错。 (推演报错)

不强转情况:显示实例化,:在函数名后的<>中指定模板参数的实际类型(我让你怎么来你就怎么来!)

在函数名后加入了指定模板参数后,就会在实例化时,T直接是指定的类型,这样就会发生隐式类型转换。

注意:在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要背黑锅 Add(a1, d1); 此时有两种处理方式:1. 用户自己来强制转化 2. 使用显式实例化

2.多参数类型

template<typename T1,typename T2),T1,T2为不同类型,当然参数个数大于等于2

template<class t1,class t2>
t1 Add(const t1& left, const t2& right)
{
	return left + right;
}
int main()
{
	int a = 1, b = 2;
	double m = 2.22, n = 3.33;
	cout << Add(a, b) << endl;
	cout << Add(m, n) << endl;
	cout << Add(a, m) << endl;
	cout << Add(n, b) << endl;
}

此时,当Add(不同类型时),就不会发生推演错误,你是什么类型就会推演成什么模板函数。

3.模板函数和自定义函数

当模板函数和自己实现的函数是否可以同时存在时?

//专门处理int的加法函数
int Add(int left, int right) 
{
	return left + right;
}
// 通用加法函数
template<class T>
T Add(T left, T right) 
{
	return left + right;
}
int main()
{
	int a = 1, b = 2;
	Add(a, b);
	Add<int>(a, b);
	return 0;
}

当自己写的函数和模板函数同时存在时,二者不会冲突,在之前我们讲过他们的函数名修饰规则是不同的。

同时存在,且调用时,首先会调用自己写的函数。因为模板函数相当于一个半成品,他需要推演实例化才会生成具体的函数,所以当然先使用自己实现的。

如果一定要使用模板函数的话,就需要显示实例化:Add<int>(a,b);

这就叫泛型编程,与具体的类型无关!

2.类模板

类模板与函数模板不同的是:类模板统一显式实例化,不需要推演,或者说没有推演的时机,而函数模板实参传递形参时,就会发生推演实例化。

格式:

template<typename T>
class Stack
{
public:
	Stack(int capacity = 4)
	{
		_a = (T*)malloc(sizeof(T)*capacity);
		if (_a == nullptr)
		{
			perror("malloc fail");
			exit(-1);
		}
		_top = 0;
		_capacity = capacity;
	}
	 ......
private:
	T* _a;
	int _top;
	int _capacity;
};
int main()
{
	// 显示实例化
	Stack<double> st1; // double
	st1.Push(1.1);
	Stack<int> st2; // int
	st2.Push(1);
	// s1,s2是同一个类模板实例化出来的,但是模板参数不同,他们就是不同类型
	return 0;
}

可能有人会问:s1=s2; 会不会发生隐式类型转换呢?当然不会,隐式类型转换只有在类型相近才会发生。

接下来创建一个数组类模板:

namespace mj
{
	template<class T>
	class array
	{
	public:
		inline T& operator[](size_t i)  //这里引用做返回值的目的是除了减少拷贝构造,还有可以修改返回值,直接可以修改数组里的值
		{
			assert(i < N);  //严格控制越界访问的情况
			return _a[i];
		}
	private:
		T _a[N];
	};
}
int main()
{
	mj::array<int> a1;
	for (size_t i = 0; i < N; ++i)
	{
		//相当于: a1.operator[](i)= i;
		a1[i] = i;
	}
	for (size_t i = 0; i < N; ++i)
	{
		// a1.operator[](i)
		cout << a1[i] << " ";
	}
	cout << endl;
	for (size_t i = 0; i < N; ++i)
	{
		a1[i]++;
	}
	for (size_t i = 0; i < N; ++i)
	{
		cout << a1[i] << " ";
	}
	cout << endl;
	return 0;
}

我们可以发现,类对象居然也可以使用数组那一套了??当然是取决于运算符重载。

他与普通数组最大的区别是:

1. 普通数组对于数组越界的这种情况,只能随机的抽查!而我们自己实现的类模板可以严格的控制越界访问这种情况!别说越界修改,越界访问都不行!

2.效率上因为[]是运算符重载,使用就会调用函数开辟栈帧,但是若定义到类中,并且加inline,就对于效率来说,那真是完美!

3.模板不支持分离编译

我们在实现数据结构的时候,是不是会经常去分几个文件去实现不同模块的功能?

(个人习惯).h文件中,我们写声明;.cpp文件中,我们写定义;test.cpp中,我们测试使用

但今天学习的模板就不能这么做了!!具体不能怎么做,我们上代码:

如果这样写的话,他就会报链接错误(就是在使用时找不到定义)

我们知道,在预处理阶段,就会将.h头文件展开,test.cpp中只有声明,在调用函数时,就会去找他的地址(call stack()),那么在编译的时候,编译器允许只有声明没有函数,相当于你可以先给他一个承诺,兑不兑现后面再说。

但在链接的时候,test.cpp中,却不能找到它的地址,这是为什么??这就是模板和其他的区别!

链接错误原因:

.cpp中的定义,不是实例化模板,他只是一个模板,没有任何实例化成任何类型。所以你在使用类模板的时候,压根就找不到它的定义,当然也找不到地址了,这不就链接错误了吗?

看上图:stack<int> st; 显示实例化,但是.h中只有声明,test.cpp用的地方实例化了,但是定义的地方stack.cpp却没有实例化,只是一个模板。

用的地方在实例化,但是有声明,没有定义;

定义的地方没有实例化。

解决方法:

那转来转去就是一个问题:stack.cpp中定义没有实例化!!

办法一:

你没有实例化,我给你补上:在定义后面加一个实例化

template<class T>
Stack<T>::Stack(int capacity = 4)
{
	cout << "Stack(int capacity = )" << capacity << endl;
	_a = (T*)malloc(sizeof(T)*capacity);
	if (_a == nullptr)
	{
		perror("malloc fail");
		exit(-1);
	}
	_top = 0;
	_capacity = capacity;
	template
	class Stack<int>;

但是就会有另一个问题,我如果使用的时候,创建不同类型,那模板实例化就要有不同类型,那就要一直补实例化,总不肯用一个补一个吧。

方法二:

那就是模板的编译不分离:(不要将定义和声明一个到.cpp,一个到.h)

当放在一个文件中时,在编译时,.h 文件展开后,定义和声明都在test.cpp中,那直接就会完成模板实例化,就有了函数地址,不需要再去链接了。

链接:只有声明没有定义才会到处去找定义。

那有人就会问,加inline可以吗?

inline当然不可以,加了inline后,直接不产生符号表,还存在什么地址吗?

直接放类中也不行,当数据量大的时候,都挤到一推,代码阅读性很差,会傻傻搞不清!

加载全部内容

相关教程
猜你喜欢
用户评论