学习之路:C++模板进阶

61 篇文章 1 订阅
41 篇文章 0 订阅

1.非模板类型参数

模板参数可以分为类型形参和非类型形参
类型形参:跟在class或者typename后的参数类型名称
非类型形参:将一个常量作为类的模板参数,在模板里可以将该参数作为常量使用如下

template <class T,int N = 10>//T为类型形参,N为非类型形参
class Array
{
	public:
		
	Array(int* n,int num)
	{
		for(int i=0;i<num;i++)
		{
			_array[i] = n[i];
			_size++;
		}
	}
			
    T& operator[](int size)
    {
    	return _array[size];
	}
    
    const T& operator[](int size) const
    {
    	return _array[size];
	}
	
	bool empty()
	{
		return _size == 0;
	}
	
	int size()
	{
		return _size;
	}
    
	private:
	T _array[N];
	int _size= 0;
	};

需要注意以下几点

  1. 浮点数,类对象和字符串不能作为非类型形参
  2. 非类型形参的值必须在编译后就是一个确定的值。

2.模板的特化

2.1 有了模板函数可以屏蔽类型的不同,但是有些类型的参数需要特殊处理

template <class T>//一个判是否相等的模板函数
bool isequl(T need1,T need2)
{
		if(need1 == need2)
		return true;
		 return false;
}

可以比较int,char,double等类型的参数,但是如果比较指针类型的参数,它会比较两指针的存的地址,而不是指向地址的值。这不是我们希望的,所以需要模板的特化
模板的特化:在原本的模板函数的基础上,根据特殊的类型进行特殊的实现方式。
模板特化分为函数模板特化与类模板特化。

  1. 必须要先有一个基础的函数模板
  2. 关键词tempalte后面要有空尖括号<>
  3. 函数名后面的尖括号,尖括号的里面写特化的类型
  4. 函数形参表的参数必须和函数模板相同,否则会有大量的错误。
    所以针对判断值是否相同,针对int指针可以这么写
template<>
bool isequl<int*>(int* need1,int* need2)
{
	if(*need1 == *need2)
	return true;
		 return false;
}

注意,如果遇到模板函数无法处理或者处理错误的情况下,一般直接给出相应的函数。

bool isequl<int*>(int* need1,int* need2)
{
	if(*need1 == *need2)
	return true;
		 return false;
}

类特化的方式分为两类:全特化,偏特化

  • 全特化
    将所有的模板参数都确定为具体类型。
template <class T>
class text
{
	public:
	text()
	{
	cout<<"template <class T>"<<endl;		
	}
	private:
	T a;	
};

template<>
class text<int>
{
	public:
	text()
	{
	cout<<"template <class int>"<<endl;		
	}
	private:
	T a;	
};
  • 偏特化:进一步限制类型设置的特化
    下面为基本的模板类
template <class T1,class T2>
class text
{
	public:
	text()
	{
	cout<<"template <class T>"<<endl;		
	}
	private:
	T1 a;
	T2 b;	
};

偏特化有两种形式

  • 部分特化
    将模板类的一部分参数具体化
template<class T1>//注意因为具体化了T2,所以这里只写T1
class text<T1,int>//将T2具体为了int类型
{
	public:
	text()
	{
	cout<<"template <class int>"<<endl;		
	}
	private:
	T1 a;
	int b;	
};
  • 参数进一步限制
    偏特化并不仅仅指一部分参数具体化,还指出将模板参数更进一步的条件限制所设置的特化版本
template<class T1,class T2>
class text<T1*,T2*>//两个参数被特化为指针类型
{
	public:
	text()
	{
	cout<<"template <class int>"<<endl;		
	}
	private:
	T1 a;
	int b;	
};

类似的,可以把参数的类型特化为引用类型

class text<T1&,T2&>

注意只有类模板有偏特化,函数模板并没有
一方面是因为C++规定了函数模板没有
you can’t partially specialize them – pretty much just because the language says you can’t
另一方面也因为有函数重载的存在,作用重复了。
如果试图对函数模板进行特化会产生一系列的错误。

3.类型萃取

假如我们要做一个拷贝函数可以考虑用memcpy函数进行拷贝,但是因为有深浅拷贝的问题,假如是string或者是涉及深拷贝的自定义类型,就需要for循环进行深拷贝,而对于大部分的内置类型,需要浅拷贝。
如果我们可以提前判断拷贝的数据类型就能调用相应的函数
类型萃取就是这样的思路,是实现不同数据类型对于同一函数的不同操作。


#include "pch.h"
#include <iostream>
using namespace std;
//类的静态成员函数可以直接访问类的静态数据和函数成员。而访问非静态
//数据成员必须通过参数传递的方式得到对象名,然后通过对象名来访问。
struct TrueTyp
{
	static bool get()//内置类型 
	{
		return true;
	}
};

struct falseType
{
	static bool get()//涉及深拷贝类型和自定义类型 
	{
		return true;
	}
};

template <class T>
class Typejudge
{
public:
	typedef  falseType isTypedef;
};

template <>
class Typejudge<char>
{
public:
	typedef  TrueType isTypedef;
};

template <>
class Typejudge<int>
{
public:
	typedef  TrueType isTypedef;
};

template <>
class Typejudge<double>
{
public:
	typedef  TrueType isTypedef;
};

template <>
class Typejudge<float>
{
public:
	typedef  TrueType isTypedef;
};

template <>
class Typejudge<long>
{
public:
	typedef  TrueType isTypedef;
};

template<class T>
void copy(T* dst, T* scr, int size)
{
	if (Typejudge<T>::isTypedef::get())
	{
		memcpy(dst, scr, size);
	}
	else
		for (int i = 0; i < size; i++)
		{
			dst[i] = scr[i];
		}

}

需要注意的是这里调用的函数为静态函数,类的静态成员函数可以直接访问类的静态数据和函数成员

4.模板分离编译

  1. 分离编译
    可以理解为将一份代码分解为多个部分,如.h,.c等部分
    一个程序由若干份源文件组成,这些源文件编译成为目标文件,最后所有的目标文件链接起来构成可执行文件,这个过程就叫做分离编译。

  2. 对模板的分离编译
    程序会报错,因为在编译的四个过程中:预编译,编译,汇编,链接,作用如下
    1.预编译:展开头文件,宏替换,去掉注释,条件编译(ifndef等语句的判断)
    2.编译:将代码转换成汇编代码,并且做了两件很重要的事。
    编译器在每个文件中保存一份函数地址符表,保存当前文件里的每个函数的地址
    生成一条条的汇编代码,其中调用函数的代码会生成call指令,call 指令后面跟着一条jmp指令的汇编代码地址,之后跟着的才是“被调用函数的汇编后第一条指令”的地址
    但是这个地址是在链接阶段才填上的。

  3. 汇编:将汇编代码转换成机器码

  4. 链接:编译器将多个.o文件链接在一起,形成可执行文件.
    需要注意的是这个时候编译器会将每个call指令的地址补齐,方式是从当前的函数地址符表,如果没有,继续向其他的文件的函数地址符表找,如果找不到,则链接失败。

//test.h
#pragma once

#include "iostream"
using namespace std;
template<class T>
T Add(const T& num1, const T& num2);


//test.cpp

#include "test.h"
template<class T>
T Add(const T& num1, const T& num2)
{
	return num1 + num2;
}

//main.cpp
#include "test.h"

int main()
{
	Add(1, 2);

	return 0;
}

运行上面的代码会出错误,错误信息。
在这里插入图片描述
原因是因为:预编译生成后,就生成了test.i和main.i文件
test.i有函数的声明实现,main.i有函数的调用和声明
因此编译器做了一件事情,fun.i生成fun.s里面保存了函数的地址,在地址符表里,同理main.i生成了main.s里面有call指令,call指令后面的地址暂时空下,等链接时填入,再汇编生成,main.o和fun.o文件
同时我们知道,模板在没有实例化时是没有代码的。
所以编译失败原因是因为,在展开头文件时,fun.o里只有函数的声明实现,没有它的实例化代码,也就没有函数地址,main.o的call指令里的地址无法填入,导致编译失败
解决方法有两个

  1. 将模板的声明和定义写在一起
  2. 在函数的声明里写上实例化的函数,但是这种不常用。
template<class T>
T Add(const T& num1, const T& num2)
{
	return num1 + num2;
}

template<>//实例化的模板函数
int Add<int>(const int& num1, const int& num2)
{
	return num1 + num2;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值