C++学习-7 模板之全面深入学习

文章目录

一;C++模板编程入门

1、概念和模板机制介绍

1.1、模板的关键就是编译函数代码原型的时候可以不给定确定具体类型,而是在每次调用的时候根据实参进行推导出参数类型,在后面确定具体类型后再生成确定的函数实体提供链接运行时调用。
1.2、为何可以实现暂时不给具体类型就可以通过编译,编译是按行编译的,编译器是如何实现这种延后到调用的时候再确定具体类型而不报错的。
答案是通过template关键字。
template关键字就表示后面紧接着的是一个模板类型,编译器根据template关键字识别在编译期间就可以允许该函数暂时在该行可以不明确指定参数的具体类型而是用后面<>尖括号里面用的typename或者class修饰的变量名暂时替代使用。再调用的时候根据传参的实参类型再反推回去确定模板参数的具体类型(整个过程其实就是编译器遇到template则表示知道接下来可以不用具体类型,再调用的时候再返回去推导具体类型,再次编译的机制)
注意;但是在编译完成之前template修饰的模板必须要有明确的类型确定,否则编译报错。因此也就有了stl使用模板的库是开源的,使用的时候需要把源码一起编译使用,而不能用只提供库的情况就是因为stl使用了模板的原因。
1.3、为何需要模板
模板的本质其实就是编译器提供的一种语法糖,是编译器提供给我们一种减少编译劳动量代码量的。例如函数重载如果一系列相同函数的函数重载只是因为参数类型不同而需要手动写n个相同函数体的情况,这个时候就需要用模板解决这个问题,只需要写一个函数体,再调用的时候再返回去推导具体类型确定函数体。
注意;模板函数和函数重载在运行代码层次是一模一样的,因为模板也是在编译期间确定好的函数体,不影响运行时的效率的。
实践;对比函数重载和函数模板在使用时的区别

可以看出;模板的本质其实就是编译器提供的一种语法糖,是编译器提供给我们一种减少编译劳动量代码量的。就是利用模板的抽象来代替劳动量的

在这里插入图片描述
实践;对比函数重载和函数模板在反汇编文件的区别并验证之前的结论概念
得到反汇编文件的命令
g++ -c 3.1addfuncReload.cpp -o 3.1Reload.o
objdump -d 3.1Reload.o > 3.1reload.i
在这里插入图片描述
3.1源代码

//函数重载
#include <iostream>
#include <string>

using namespace std;

int add(int a, int b);
string add(string a, string b);
int main()
{
    
    
	int i_left = 1, i_right = 2;
	string str_left = "1.1", str_right = "2.2";
	cout << add(i_left, i_right) << endl;
	cout << add(str_left, str_right) << endl;
	//并且如果实现一个真正的加法函数因为实现所有类型的函数重载才行,
	//那么就需要很多很多重载函数,并且还有可以写不完、有遗留的。
	//这个时候函数重载的弊端就出现了,C++就引入模板类解决这类问题
	
	return 0;
}
int add(int a, int b)
{
    
    
	cout << "int add(int a, int b)" << endl;
	return a+b;
}
string add(string a, string b)
{
    
    
	cout << "string add(string a, string b)" << endl;
	return a+b;
}
//函数模板
#include <iostream>
#include <string>

using namespace std;

//使用模板的语法注意typename也可以使用class替换,作用一致
template <typename T>
T add(T a, T b)
{
    
    
	cout << "T add(T a, T b)" << endl;
	return a+b;
}


int main()
{
    
    
	int i_left = 1, i_right = 2;
	string str_left = "1.1", str_right = "2.2";
	cout << add(i_left, i_right) << endl;
	cout << add(str_left, str_right) << endl;
	
	return 0;
}

2、模板不同场景的使用案例

2.1、函数模板

函数模板;模板类型在函数参数列表中、返回值中进行了使用从而可以泛指很多具体类型的函数体
在这里插入图片描述

#include <iostream>
#include <string>

using namespace std;

template <class Tleft, class Tright>
Tleft add(Tleft a, Tright b);//模板函数的声明式


int main()
{
    
    
	int i_left = 1;
	double f_right = 2.9;
	string str_left = "1.1", str_right = "2.2";
	cout << add<int,double>(i_left, f_right) << endl;//具体类型显示调用 
	cout << add(str_left, str_right) << endl;//自动类型推到调用
	
	return 0;
}
//使用模板的语法注意typename也可以使用class替换,作用一致
//template <typename Tleft, typename Tright>
template <class Tleft, class Tright>
Tleft add(Tleft a, Tright b)
{
    
    
	cout << "Tleft add(Tleft a, Tright b)" << endl;
	return a+b;
}

2.2、类模板

类模板;模板类型在类中(定义类成员变量,或类成员函数的参数列表)使用
使用的时候要记住核心一点就是;如果使用类模板,则在每次使用 的时候都要进行<>尖括号参数指定,如果不是在调用期间,参数类型仍然不确定则需要继续使用template的声明并指定typename,否则编译会过不去。

2.2.1、单模板参数的类模板使用

在这里插入图片描述

#include <iostream>
#include <string>

using namespace std;

template <typename T>
class People
{
    
    
public:
	int age;
	People(){
    
    };
	People(int age);
	~People(){
    
    };
	void print(T x);
};
//在使用类模板的时候如果仍然不确定参数类型
//则要继续使用template <typename T>并也要用<>指定
template <typename T>
People<T>::People(int age):age(age)
{
    
    
	
}
template <typename T>
void People<T>::print(T x)
{
    
    
	cout << x << endl;
}

int main()
{
    
    
	//在实际调用过程中可以明确知道参数类型则可以直接<>显示指定
	People<string> p(12);
	p.print("1234");
	
	
	return 0;
}

2.2.2、多模板参数的类模板

多模板参数的类模板;主要注意一下模板参数传递的顺序即可,其余与单模板一样的
在这里插入图片描述

#include <iostream>
#include <string>

using namespace std;

template <typename T1, typename T2>
class People
{
    
    
public:
	int age;
	People(){
    
    };
	People(int age);
	~People(){
    
    };
	void print(T1 x, T2 y)//如果在类声明的时候就实现函数体
	{
    
    
		cout << " T1 x = " << x << " T2 y = " << y  << endl;
	}
};
//在使用类模板的时候如果仍然不确定参数类型
//则要继续使用template <typename T>并也要用<>指定
template <typename T1, typename T2>
People<T1, T2>::People(int age):age(age)
{
    
    
	
}
/*
template <typename T1, typename T2>
void People<T1, T2>::print(T1 x, T2 y)
{
	cout << " T1 x = " << x << " T2 y = " << y  << endl;
}
*/
int main()
{
    
    
	//在实际调用过程中可以明确知道参数类型则可以直接<>显示指定
	//这里是真正决定T1是什么类型,T2是什么类型的
	People<string, int> p(12);
	p.print("1234", 2);
	
	
	return 0;
}

2.3、模板友元函数

模板友元函数使用的三种情况
友元函数参数中不带模板的情况;则是在类模板中声明该类的友元函数,但是却指定明确的类模板参数类型,这种情况其实相当于削弱了类模板参数的模板化。
友元函数参数中带模板参数但是其友元函数的声明定义都在类模板内部;这种情况该友元函数可以适配任何模板参数了。注意;该函数仍然是友元函数而不是成员函数,仍属于外部函数。
友元函数参数中带模板参数并且该友元函数在类模板内部声明,外部定义;但是使用的时候需要注意几点,声明时函数名加后缀,而定义时不用加、需要class和friend function的2个前置声明、调用friend function时可加<实参类型>后缀,也可以不加,但是加就必须加对了。

实践;
友元函数参数中带模板参数并且该友元函数在类模板内部声明,外部定义的情况的两种报错信息截图
1、声明时函数名未加后缀
在这里插入图片描述

2、没有进行class和friend function的2个前置声明、
在这里插入图片描述
三种情况使用的代码案例

#include <iostream>
#include <string>
using namespace std;
//1、没有使用模板的时候友元函数的调用
class People
{
    
    
private:
	int age;
public:
	People(){
    
    };
	People(int a){
    
    age = a;};
	void print();
	friend void friendPrint(const People &cn_p);
	
};
void friendPrint(const People &cn_p)
{
    
    
	cout << cn_p.age << endl;
}
int main()
{
    
    
	People p(12);
	friendPrint(p);
	
	return 0;
}
/*
2、友元函数参数中不带模板的情况**;
则是在类模板中声明该类的友元函数,
但是却指定明确的类模板参数类型,
这种情况其实相当于削弱了类模板参数的模板化

*/
template <typename T>
class People
{
    
    
private:
	T age;
public:
	People(){
    
    };
	People(T a){
    
    age = a;};
	void print();
	
	//这种使用友元函数的情况实际是削弱了模板参数在使用
	//因为把传参T可以表示任何类型的在这种友元函数这里写死成固定类型了,其他类型就不兼容了
	//在任何使用使用类模板的时候都要用<>指定参数类型
	//并且这里可以根据传参的类型从而参数类型不同可以进行函数重载
	friend void friendPrint(const People<string> &cn_p);
	friend void friendPrint(const People<double> &cn_p);
	
};
//在任何使用使用类模板的时候都要用<>指定参数类型
void friendPrint(const People<string> &cn_p)
{
    
    
	cout << cn_p.age << endl;
}
void friendPrint(const People<double> &cn_p)
{
    
    
	cout << cn_p.age << endl;
}


int main()
{
    
    
	//改用传string 则p的age是string形式表示
	People<string> p("1s3");
	friendPrint(p);
	People<double> p1(12.3);
	friendPrint(p1);
	
	return 0;
}

/*
3、友元函数参数中带模板参数但是其友元函数的声明定义都在类模板内部
这种情况该友元函数可以适配任何模板参数了。
注意;该函数仍然是友元函数而不是成员函数,仍属于外部函数,因此使用的时候也要<>指定参数类型

*/
template <typename T>
class People
{
    
    
private:
	T age;
public:
	People(){
    
    };
	People(T a){
    
    age = a;};
	void print();
	
	//友元函数在内部实现也要用<>指定,但是可以继续沿用T模板参数类型表示
	//在类模板外部的任何情况都要加,友元函数不是类的成员函数,是外部的,则也要加<>指定的
	friend void friendPrint(const People<T> &cn_p)
	{
    
    
		cout << cn_p.age << endl;
	}
	
};
int main()
{
    
    
	//改用传string 则p的age是string形式表示
	People<string> p("1s3");
	friendPrint(p);
	People<double> p1(12.3);
	friendPrint(p1);
	
	return 0;
}

/*
4、友元函数参数中带模板参数并且该友元函数在类模板内部声明,外部定义
需注意几点
**声明时函数名加<T>后缀,而定义时不用加、**
需要class和friend function的2个前置声明、
调用friend function时可加<实参类型>后缀,也可以不加,但是加就必须加对了。

//因为下面友元函数的声明用到了这个类,则也需要类模板声明
template <typename T> class People;
//因为都为模板类型,则编译器编译的时候需要提前声明 
template <typename T> void friendPrint(const People<T> &cn_p);

*/
template <typename T>
class People
{
    
    
private:
	T age;
public:
	People(){
    
    };
	People(T a){
    
    age = a;};
	void print();
	
	//友元函数在内部实现也要用<>指定,但是可以继续沿用T模板参数类型表示
	//在类模板外部的任何情况都要加,友元函数不是类的成员函数,是外部的,则也要加<>指定的
	friend void friendPrint<T>(const People<T> &cn_p);
	
};
template <typename T>
void friendPrint(const People<T> &cn_p)
{
    
    
	cout << cn_p.age << endl;
}
int main()
{
    
    
	//改用传string 则p的age是string形式表示
	People<string> p("1s3");
	friendPrint(p);
	People<double> p1(12.3);
	friendPrint(p1);
	
	return 0;
}

2.4、模板运算符重载

模板运算符重载:实现+发的运算符重载

#include <iostream>
#include <string>

using namespace std;

template <typename T>
class People
{
    
    
public:
	T age;
	People(){
    
    };
	People(T a){
    
    age = a;};
	
	//在内部成员函数在使用类模板的时候也是需要<>指定类型的
	People<T> operator+(const People<T> & rightPeo);
	
};
//在外部使用类模板类的时候如果还没有确定类型,
//则需要template再次使用typename来暂时代替参数类型
template <typename T>
People<T> People<T>::operator+ (const People<T>& rightPeo)
{
    
    
	this->age += rightPeo.age;
	return *this;
}

int main()
{
    
    
	People<int> p1(12);
	People<int> p2(12);
	People<int> p3;
	p3 = p1 + p2;
	cout << p3.age << endl;
	
	return 0;
}

2.5、模板友元运算符重载实现

使用模板友元运算符重载函数的两种情况
1、在类内部声明和外部实现
注意其在类内部声明的时候需要重新定义template,并且运算符的operator函数名不像其他友元函数名一样在声明的时候需要在函数名后面也添加<>的,
template friend People operator+ (const People& Left, const People& Right);

2、在类内部实现友元运算符重载函数、
friend People operator+ (const People& Left, const People& Right){}

#include <iostream>
#include <string>

using namespace std;

//template <typename T> class People;
//template <typename T> People<T> operator+ (const People<T>& Left, const People<T>& Right);
//template <typename T> People<T> operator+= (const People<T>& Left, const People<T>& Right);
template <typename T>
class People
{
    
    
private:
	T age;
public:
	
	People(){
    
    };
	People(T a){
    
    age = a;};
	/*
	/实现模板友元+法运算符
	//注意这里又需要用到模板参数,但是不能再沿用T了,因为他是外部函数
	//现在在函数名后加<>编译报错提示非法使用函数名
	//invalid use of template-id ‘operator+<T1>’ in declaration of primary template
	//template <typename T1> friend People<T1> operator+<T1> (const People<T1>& Left, const People<T1>& Right);
	
	//在内部定义外部实现,模板参数类型的对标问题,class中的T是与age对标的,而这里的是外部函数,要有自己的对标模板,而实际参数都是调用传参的
	template <typename T1> friend People<T1> operator+ (const People<T1>& Left, const People<T1>& Right);//可以通过编译
	
	//根据模板友元函数的在内声明外定义的方式定义相同类型的运算符友元函数
	//但是在我当前编译器是编译不通过的,但是在网上百度却有这样的案例,原因未知
	//编译出错 friend declaration ‘People<T> operator+(const People<T>&, const People<T>&)’ declares a non-template function [-Wnon-template-friend]
	//friend People<T> operator+ (const People<T>& Left, const People<T>& Right);
	//friend People<T> operator+<T> (const People<T>& Left, const People<T>& Right);//报错,底层错误
	
	//在内部定义外部实现
	friend People<T> operator+ (const People<T>& Left, const People<T>& Right)//可以通过编译
	{
		People<T> p;
		p.age = Left.age + Right.age;
		return p;
	}
	*/
	实现模板友元+=法运算符
	//同样在外部定义也是不行的,但是在有的电脑编译器是可以通过的,暂不知道原因
	//friend People<T> operator+= (const People<T>& Left, const People<T>& Right);
	//template <typename T1> friend People<T1> operator+= (const People<T1>& Left, const People<T1>& Right);//这样就可以通过编译
	friend People<T> operator+= (const People<T>& Left, const People<T>& Right)
	{
    
    
		People<T> p;
		p.age = Left.age + Right.age;
		return p;
	}
	
	
	void print();
};
/*
template <typename T>
People<T> operator+ (const People<T>& Left, const People<T>& Right)
{
	People<T> p;
	p.age = Left.age + Right.age;
	return p;
}
*/
/*
template <typename T>
People<T> operator+= (const People<T>& Left, const People<T>& Right)
{
	People<T> p;
	p.age = Left.age + Right.age;
	return p;
}
*/
template <typename T>
void People<T>::print()
{
    
    
	cout << age << endl;
}
int main()
{
    
    
	People<int> p1(12);//类模板使用的时候必须<>确定参数类型,不能自动推导
	People<int> p2(12);
	People<int> p3;
	//p3 = p1 + p2;测试+法运算符
	//p3.print();
	
	p1 += p2;
	p1.print();
	
	return 0;
}

2.6、模板类继承

模板类继承分为很多情况,但是其基本核心其实是不变的,还是模板参数类型的传递问题。
在继承体系中,模板类的基础就相当于用子类的模板参数Tx去实例化填充父类的模板参数Ty,仔细理解,其实是父类使用类模板参数延迟具体类型的时期,而子类又继承类模板的父类(从而在实例化子类的时候默认调用父类构造方法时就需要传入子类模板参数来实例化父类的模板参数),从而让子类也可以达到延迟具体参数类型的时期。所以模板类继承的作用就是用于构建模板化的类体系,便于写模板化的大框架。

2.6.1、类模板继承类模板-单参数时

父类是单模板参数类型,单模板参数的子类继承父类、两种情况
1、单模板参数的子类与父类模板参数类型一致,
2、单模板参数的子类与父类模板参数类型不一致,则子类需要引入两个typename,一个用于自己,一个传递给父类实例化

#include <iostream>
#include <string>

using namespace std;

template <typename T>
class People
{
    
    
public:
	T age;
	People(){
    
    };
	People(T a){
    
    age = a;};
	
};

//单参数;子类的模板参数类型和父类的一致
template <typename T>
class Man:public People<T>
{
    
    
public :
	T sold;
	Man(){
    
    };
	Man(T s){
    
    sold = s;};
	Man(T s, T d):People<T>(d){
    
    sold = s;};
	//注意细节;类模板继承中在子类内部调用public继承父类public成员,访问时要加this否则无法调用。提示age没有声明
	//但是在非类模板继承中则是没有问题的
	//void print(){cout << "sold = " << this->sold << "   age = " << age << endl;};
	void print(){
    
    cout << "sold = " << this->sold << "   age = " << this->age << endl;};
};

//单参数;子类的模板参数类型和父类的不一致,则子类需要创建两个typename,一个实例化自己一个传入给父类实例化
template <typename Tx, typename T2>
class Woman:public People<T2>
{
    
    
public :
	Tx sold;
	Woman(){
    
    };
	Woman(Tx s){
    
    sold = s;};
	Woman(Tx s, T2 d):People<T2>(d){
    
    sold = s;};
	//注意细节;类模板继承中在子类内部调用public继承父类public成员,访问时要加this否则无法调用。提示age没有声明
	//但是在非类模板继承中则是没有问题的
	//void print(){cout << "sold = " << this->sold << "   age = " << age << endl;};
	void print(){
    
    cout << "sold = " << this->sold << "   age = " << this->age << endl;};
};

int main()
{
    
    
	Man<int> man(12,13);
	//man.print();
	
	Woman<int, string> woman(12,"adsklufgj");
	woman.print();
	
	return 1;
}

2.6.2、类模板继承类模板-多参数时

多参数与单参数其实差不多,唯一要注意的是参数的匹配顺序或名称匹配
在这里插入图片描述

#include <iostream>
#include <string>

using namespace std;

template <typename T1, typename T2>
class People
{
    
    
public:
	T1 age;
	T2 sex;
	
	People(){
    
    };
	People(T1 a, T2 s){
    
    age = a; sex = s;};
	
};

//父类两个模板参数,子类两个模板参数类型
//则子类需要四个模板参数(但是注意其实模板参数类型是可以重用的,如果父类的两个参数类型其中有和子类一致的则可以使用同一个)
//但是传递的时候要注意传递顺序,People<T3, T4>这里的传递到父类是按照顺序的,在子类或父类内部的模板参数是跟名称匹配的
template <typename T1, typename T2, typename T3, typename T4>
class Man:public People<T3, T4>
{
    
    
public :
	T1 sold;
	T2 wight;
	Man(){
    
    };
	Man(T1 s, T2 w){
    
    sold = s; wight = w;};
	Man(T1 s, T2 w, T3 a, T4 sex):People<T3, T4>(a, sex){
    
    sold = s; wight = w;};
	void print()
	{
    
    
		cout << "sold = " << this->sold << "   age = " << this->wight << endl;
		cout << "age = " << this->age << "   sex = " << this->sex << endl;
	};
};

int main()
{
    
    
	Man<string, int, double, string > man("asd", 13, 12.3, "opopop");
	//Man<string, int > man1("asd", 13);这样编译报错,因为还需要给父类的模板参数也要传递,
	Man<string, int, double, string > man1("asd", 13);//通过编译,而父类的两个参数没有初始化则暂时乱码
	man1.print();
	
	
	return 1;
}

2.6.3、类模板继承模板类

模板类其实就是确定了参数类型的类模板,这里的意思就是
template class Man:public People这种,继承的时候将参数直接传递过去了,则意味着Man子类中的父类的模板参数不能在变化了,只能是继承时指定的模板类类型了。

#include <iostream>
#include <string>

using namespace std;

template <typename T>
class People
{
    
    
public:
	T age;
	People(){
    
    };
	People(T a){
    
    age = a;};
	
};

//类模板继承模板类(直接传入了具体类型,
//从而父类模板参数没有再延长到调用时在传递参数类型,而是在继承是就确定好了)
template <typename T>
class Man:public People<double>
{
    
    
public :
	T sold;
	Man(){
    
    };
	Man(T s){
    
    sold = s;};
	Man(T s, double d):People<double>(d){
    
    sold = s;};//从而在Man子类中的父类都只能是double类型的类,限制死了
	void print(){
    
    cout << "sold = " << this->sold << "   age = " << this->age << endl;};
};

int main()
{
    
    
	Man<int> man(12,13.9);
	man.print();
	
	
	return 1;
}

2.6.4、类模板继承普通类

这个其实与类模板继承模板类差不多,只是在继承体系上,将之前的类模板的父类变成了普通类,彻底无法模板化了,而之前2.6.3的People在man类中无法模板化了,但是在其他地方可以,而继承普通类,则继承体系变了。

#include <iostream>
#include <string>

using namespace std;

class People
{
    
    
public:
	double	age;
	People(){
    
    };
	People(double a){
    
    age = a;};
	
};

//模板类继承普通类,注意下传参即可
template <typename T>
class Man:public People
{
    
    
public :
	T sold;
	Man(){
    
    };
	Man(T s){
    
    sold = s;};
	Man(T s, double d):People(d){
    
    sold = s;};
	void print(){
    
    cout << "sold = " << this->sold << "   age = " << this->age << endl;};
};

int main()
{
    
    
	Man<int> man(12,13.9);
	man.print();
	
	return 1;
}

2.6.5、普通类继承类模板

这个其实与类模板继承类模板一样,只是子类不使用模板而已,其他都一样,只是声明template的时候声明的typename都是给父类使用的而子类不使用

#include <iostream>
#include <string>

using namespace std;

template <typename T>
class People
{
    
    
public:
	T age;
	People(){
    
    };
	People(T a){
    
    age = a;};
	
};

//这个与单参数的类模板继承类模板是是一样的,只是子类内部不使用模板技术
template <typename T>
class Man:public People<T>//创建和调用都是没有变化的
{
    
    
public :
	int sold;//只是子类内部不使用模板化
	Man(){
    
    };
	Man(int s){
    
    sold = s;};
	Man(int s, T d):People<T>(d){
    
    sold = s;};
	void print(){
    
    cout << "sold = " << this->sold << "   age = " << this->age << endl;};
};


int main()
{
    
    
	Man<double> man(12,13.9);
	man.print();
	
	return 1;
}

2.7、非类型模板参数

非类型模板其实就是定义使用模板的时候不是定义不确定类型T来泛指而是使用值(非类型)
使用如下;

2.7.1、在类模板中使用

就是这样使用,作用就是相当于传入了一个值

#include <iostream>

using namespace std;

//使用非类型模板参数,就是定义的时候传值
template <typename T, int max>
class People
{
private:
	T age;
	T temp[max];//使用非类型模板参数,就是定义的时候传值
public:
	People(){age = 0;};
	People(T age1){age = age1;};
	void print()
	{
		cout << age << endl;
	}
};

int main()
{
	//People p1(1);//类模板使用的时候必须<>确定参数类型,不能自动推导
	People<int,10> p1(1);
	People<int,20> p2(2);
	People<int,10> p3;
	
	return 0;
}

2.7.2、在模板函数中使用

其实语法基本一样,都是在<typename类型参数后面追加一个非类型的值参数>然后在里面直接使用这个值

template<typename T, int VAL>
T addValue(const T& x)
{
    return x + VAL;
}

看着这个没有什么作用,但是结合模板库等一系列数据,再添加上这个小功能可以传入值,如果需要对很多数据都加上这个值,那么此时非类型模板参数就可以实现作用了。

2.7.3、非类型模板参数的限制

非类型模板参数是有类型限制的。一般而言,它可以是常整数(包括enum枚举类型)或者指向外部链接对象的指针。

浮点数和类对象(class-type)不允许作为非类型模板参数:
但是我们可以转换一下,变成指针指向即可使用了。

template<double* PVAL>
double process(const double& x)
{
    return x * (*PVAL);
}

2.8、模板类型推导

模板的本质其实就是延迟绑定,延迟到真正使用实例化对象的时候再使用。

2.8.1、类型推导的隐式类型转换

在决定模板参数类型前,有的特殊情况编译器会做一些隐式类型转换,因此有时候实际类型和看起来会不一样。
编译器用值类型实例化模板而不是用相应的引用类型
用指针类型实例化函数模板而不是相应的数组类型(数组只是一个语法糖)
去除const修饰,绝不会用const类型实例化函数模板,而是用相应的非const。

2.8.2、模板与库

模板无法单独编译的,因为模板中使用到的虚拟参数T,而T必须在编译时确定的只是可以延迟编译。因此也就不能通过lib链接静态库的形式隐藏实现,从而导致C++的模板库都是开源的,通常会把声明和定义写在头文件而在cpp文件使用。

二、STL学习

1、STL的容器类和迭代器

直接参考标准库网站学习
https://zh.cppreference.com/w/cpp

2、STL的泛型算法

2.1、泛型算法

独立于容器类的一些操作方法,并且可以用于多种容器,所以叫“泛型”算法,是一种更高级别的抽象。
泛型算法参考:标准库泛型算法

注意泛型算法和容器之间有一个适配问题,不是一个算法能够适配所有容器,只是适配一类型的迭代器,而所有容器都会支持迭代器,因此只有他们的迭代器相兼容则该算法就能适配该容器。

注意STL迭代器也是存在层级关系的。

在这里插入图片描述
在这里插入图片描述

2.2、谓词predicate

谓词就是动作,在C/C++中函数function就是predicate,
C++ STL中的谓词类似这样:bool func(T& a); 一元谓词或者 bool func(T&a, T& b);二元谓词
返回值是bool类型,则常常用于泛型算法的传参,

常见的谓词:函数,函数指针,lambda表达式,函数对象,库定义的函数对象

2.3、函数对象的引入

2.3.1、函数对象的概念

函数对象(function object)是一个仿函数,本质是一个类,只是调用形式看起来像函数()类对象调用()操作符重载函数。

//定义
template <typename T>
class IsGreater
{
public:
	// 只需要实现一个()符号的运算符重载函数即可
	bool operator()(T a)
	{
		return (a > 0);
	}
};

//调用,与调用普通函数一样
IsGreater<int> isGreater;//只是这里不同,因为函数对象本身是一个类
bool b = isGreater(-5);

2.3.2、自定义函数对象给泛型算法使用

对比stl中的函数对象,及自己仿照实现一个给泛型算法使用的函数对象

//对比stl中的函数对象,及自己仿照实现一个给泛型算法使用的函数对象
#include <iostream>
#include <array>
#include <algorithm>
#include <list>

using namespace std;

// 对比2个字符串,我希望按照字符串的长度来从大到小排序
template <typename T>
class MyGreater
{
    
    
public:
	bool operator()(const T& s1, const T& s2)
	{
    
    
		return (s1 > s2);
	}
};
int main(void)
{
    
    
	array<string, 3> a1 = {
    
    "linux", "android", "harmonyos"};

	//这是stl中配搭的函数对象greater,因为他也是模板类型的因此使用的时候需要<>指明类型
	//可以查看std::greater他的()操作符其实是要传两个参数的,但是这是再给sort使用,传参会在sort里面传入的
	//sort(a1.begin(), a1.end(), std::greater<string>());
	sort(a1.begin(), a1.end(), MyGreater<string>());
	
	for (auto ax : a1)
	{
    
    
		cout << ax << "  ";
	}
	cout << endl;
	
	return 0;
}

2.3.3、函数对象的优势;

函数对象可以使用template技术实现多类型支持,这比函数的重载技术更有优势
函数对象可以有自己的状态。我们可以在类中定义状态变量(类私有成员变量),这样一个函数对象在多次的调用中可以共享这个状态。

#include <iostream>
#include <array>
#include <algorithm>
#include <list>

using namespace std;

// 用函数对象来实现
template <typename T>
class IsGreater
{
    
    
public:
	// 只需要实现一个()符号的运算符重载函数即可
	bool operator()(T a)
	{
    
    
		n++;
		return (a > 0);
	}
	void print()
	{
    
    
		cout << n << endl;
	}
private:
	int n = 0;
};


int main(void)
{
    
    
	
	IsGreater<int> isGreater;
	bool b = isGreater(-5);
	bool b1 = isGreater(5);
	//boolalpha 设置一个属性给cout对象,让后面的bool类型输出以true和false形式
	cout << boolalpha << b << endl;
	isGreater.print();//则可以输出该对象调用 operator()的次数
	
	return 0;
}

2.4、典型算法的学习

举例学习一种算法
all_of函数

定义于头文件 <algorithm>
template< class InputIt, class UnaryPredicate >
bool all_of( InputIt first, InputIt last, UnaryPredicate p );
泛型算法接受模板类型
返回值为bool类型,从first到last范围内,UnaryPredicate 一元谓词就是接收一个参数返回bool类型的函数对象

函数功能为
若一元谓词对范围中所有元素返回 true 则为 true ,否则为 false 。
若范围为空则返回 true 

在这里插入图片描述
其余的泛型算法也是根据标准库使用即可

2.5、lamda表达式

2.5.1、概念

就是一个匿名函数,相当于一次使用的、直接原地展开调用的函数。
也可以称为闭包,因为没有名字无法在其他地方调用。
注意;lambda表达式其实也是一个函数对象,在内部创建了一个重载()操作符的类

lambda表达式格式
(1)完整格式5部分:
[参数捕获] (操作符重载函数参数) mutable或exception声明 ->返回值类型 {函数体}
(2)最简单的lambda表达式:{},调用执行时为{}();
(3)带传参的lambda表达式:[](int i){//i在这里可以用}
(4)使用auto将lambda表达式定义为一个变量,再以变量方式调用
(5)使用->int这种方式让lambda表达式函数返回相应类型的值

2.5.2、 什么是lambda的参数捕获

在lambda表达式外面定义int a,在表达式内部试图访问。在[]中增加捕获说明即可。所谓参数捕获,就是让lambda表达式内部可以捕获并使用外部的变量

lambda表达式的捕获列表
(1)[] 空,完全不捕获
(2)[=] 等号,以值传参方式捕获,捕获范围是表达式所在作用范围(包括所在类的this)
(3)[&] &号,以引用传参方式捕获,捕获范围是表达式所在作用范围(包括所在类的this)
(4)[this] 只捕获lambda表达式所在类的this可访问的那些
(5)[a] 仅以值方式捕获a,其他全部不捕获
(6)[&a] 仅以引用方式捕获a,其他全部不捕获
(7)[a, &b] 仅以值方式捕获a,以引用方式捕获b,其余完全不捕获
(8)[=, &a, &b] 仅以引用方式捕获a和b,其余以值方式捕获
(9)[&, a, b] 仅以值方式捕获a和b,其余以引用方式捕获

2.5.3、lambda各种情况的实践

#include <iostream>
#include <array>
#include <algorithm>
#include <list>

using namespace std;

int main()
{
    
    
	[](){
    
    }();//空的lamda表达式,[](){}这是定义,加()调用
	[](){
    
    cout << "hello" << endl;}();
	[](int a){
    
    cout << a << endl;}(5);//接收一个int类型的lamda,调用时(5)传入
	
	auto func = [](int a) ->bool {
    
    cout << "hello " << a << endl; return a;};
	cout << boolalpha << func(4) << endl;//利用auto来调用
	
	std::vector<int> v(10, 2);
	v.insert(v.end(), 6);
	//如果这个判断是否能被2整除的函数只使用一次,那么则很可以使用lamda
	if (std::all_of(v.cbegin(), v.cend(), [](int a) ->bool{
    
    return a % 2 == 0;})) 
	{
    
    
        std::cout << "All numbers are even\n";
    }
	else
	{
    
    
		std::cout << " Not all numbers are  even\n";
	}
	
	int number = 10;
	
	[number](){
    
    cout << number << endl;}();
	[&](){
    
    cout << number++ << endl;}();
	cout << number << endl;
	
	return 0;
}

2.6、函数适配器

适配器,adapter,用来在不适配的2端间对接的连接器。就是用来在不同传参个数的函数间进行适配的技术。
首先明白几个概念,一元函数,二元函数,一元谓词,二元谓词,谓词就是返回bool类型的函数。
再就是在很多stl泛型算法函数中常常有规定传参为一元谓词,但是此时我有一个二元谓词则刚好满足算法要求,则需要函数适配器让二元谓词中的一个参数固定起来,而另外一个参数就相当于变成一元谓词可以从泛型算法中传入了。

C++的函数适配器经历了两代
首先是C++98 常用bind1st,bind2st;
后面C++11起引入加强版 std::bind

//就以这段代码为例;
1、首先可以查找到std::none_of的原型是需要一个一元谓词
template< class InputIt, class UnaryPredicate >
bool none_of( InputIt first, InputIt last, UnaryPredicate p );
2、该代码需要完成的功能是判断有没有偶数
可以知道std::modulus<int>()函数作用为
二元谓词,计算除法余数的函数对象,返回lhs % rhs 的结果。
3、从而可以利用函数适配器std::bind函数,将std::modulus这个函数的的第二个传参固定为2,第一个传参再从none_of中传入,即可达到判断有无偶数的情况。

实现如下
if (std::none_of(v.cbegin(), v.cend(), std::bind(std::modulus<int>(), 
                                                     std::placeholders::_1, 2))) {
    
    
        std::cout << "None of them are odd\n";
    }

4、
定义于头文件 <functional>
template< class F, class... Args >
/*unspecified*/ bind( F&& f, Args&&... args );
函数模板 bind 生成 f 的转发调用包装器。调用此包装器等价于以一些绑定到 args 的参数调用 f 。
std::placeholders::_1 表示第一个参数占位由后续传入,而其他参数要写死
std::placeholders::_2  就是表示第二参数占位,

3、模板特化与类型萃取
3.1、引入模板特化
模板特化是指的让模板参数T在某个具体类型时可以特殊化指定处理。
以一个案例说明GreaterThan函数,可以对比各种数据类型的大小。
注意特殊要求;int等比较数值大小,但string类型对比时,不比较字典序,而是以字符串长短来比较。
我们之前讲解了因为涉及很多类型,则要是有模板函数来实现,而不采用普通函数的函数重载实现。

#include <iostream>

using namespace std;

//模板函数
template <typename T>
bool GreaterThan(T left, T right)
{
    
    
	cout << "GreaterThan(T a, T b)" << endl;
	if (left > right)
		return true;
	else
		return false;
}
//这个就是模板特化,就是template <>不再用T,而直接写具体类型
//调用时会写判断是否有模板特化具体的更合适的匹配,再匹配模板
template <>
bool GreaterThan(string left, string right)
{
    
    
	cout << "GreaterThan(string left, string right)" << endl;
	if (left.size() > right.size())
		return true;
	else
		return false;
}

//如果存在普通函数,类型一致,那么则优先普通函数
//优先级为普通函数>模板特化>模板函数
bool GreaterThan(string left, string right)
{
    
    
	cout << "common function" << endl;
	if (left.size() > right.size())
		return true;
	else
		return false;
}

int main()
{
    
    
	cout << GreaterThan<int>(1,2) << endl;
	//实践得知调用的是T a类型的函数,应该是做了处理没有识别到为string
	//cout << GreaterThan("1","123") << endl;  
	string str1 = "1";
	string str2 = "123";
	cout << GreaterThan(str1,str2) << endl;
	return 0;
}

注意他们的优先级;普通函数>模板特化>模板函数
模板特化与函数重载,模板函数都是一元的,都是再编译链接时确定好函数体,而非运行时确定,从而效率高,因为运行时已经确定好了。
模板特化的时候定义的时候要加空的template <>,表示这是模板函数,只是写了具体的类型,为模板特化。但是在使用的时候可以不加,因为反正编译器可以反向推导出来的。注意上面代码那里直接传入的时候没有推导准确,而需要定义成类型的变量传入

3.2、全特化与偏特化

全特化;特化原模板的所有模板类型为具体类型,就是例如上面的案例,那个模板特化与普通函数其实都是一样的了,没有模板特性了。
偏特化;又叫局部特化,特化原模板的部分类型,或部分特化原模板的类型,对于模板参数还保留一部分需要模板推导的而非全都是具体类型的。

3.2.1、全特化案例

就是把模板参数具体化,前面加template<>,之前案例就是全特化的
//多参数类的全特化

//多参数类的全特化
template<typename T1, typename T2>
class People
{
    
    
public:
	void func(T1 a, T2 b)
	{
    
    
		cout << "in people func(T1 a, T2 b), a = " << a << ", b = " << b << endl;
	}
};

template<>
class People<double, string>
{
    
    
public:
	void func(double a, string b)
	{
    
    
		cout << "in people func(double a, string b), a = " << a << ", b = " << b << endl;
	}
	
};

//多参数模板函数的全特化

template<typename T1, typename T2>
void func(T1 a, T2 b)
{
    
    
	cout << "in func(T1 a, T2 b),   a = " << a << ", b = " << b << endl;
}
template<>
void func(int a, double b)
{
    
    
	cout << "in func(int a, double b),   a = " << a << ", b = " << b << endl;
}

3.2.2、偏特化案例

偏特化有点特殊可以保存原来的模板类型,但是我们可以在原来模板类型基础上具体化也是偏特化,例如把T进行偏特化为T*,T&,verctor都是可以的


template<typename T>
class People
{
    
    
public:
	void func(T a)
	{
    
    
		cout << "in people func(T a) " << endl;
	}
};

// 将T偏特化为T *
template<typename T>
class People<T *>
{
    
    
public:
	void func(T *a)
	{
    
    
		cout << "in People<T *>,  *a = " << (*a)[0] << endl;
	}
};

// 将T偏特化为T &
template<typename T>
class People<T &>
{
    
    
public:
	void func(void)
	{
    
    
		cout << "People<T &>" << endl;
	}
};

// 将T偏特化为 const T &
template<typename T>
class People<const T &>
{
    
    
public:
	void func(void)
	{
    
    
		cout << "People<const T &>" << endl;
	}
};

// 将T偏特化为vector<T>
template<typename T>
class People<vector<T>>
{
    
    
public:
	void func(void)
	{
    
    
		cout << "in People<vector<T>>" << endl;
	}
};

调用方法;
People<const int&> p1;//T为int
People<vector<int>> p1;		// T是int

多参数类模板偏特化,只具体化其中个别参数,还有模板参数没有具体化的情况

// 2参数类模板
template<typename T1, typename T2>
class People
{
    
    
public:
	void func(T1 a, T2 b)
	{
    
    
		cout << "in people func(T1 a, T2 b), a = " << a << ", b = " << b << endl;
	}
	
};

template<typename T>
class People<T, int>
{
    
    
public:
	void func(T a, int b)
	{
    
    
		cout << "in partial specialize people, a = " << a << ", b = " << b << endl;
	}
	
};

3.2.3、函数模板为什么不能偏特化

函数模板确实不支持偏特化,只能全特化,这是编译器决定的,为什么要这样决定呢?因为利用函数模板重载就能够完成,认为函数模板就不必支持偏特化,而类模板没有重载,则需要偏特化机制。

因为模板的反正是参数或返回值,而函数重载也是只有参数或返回值不同的结构,因此函数重载可以替换函数的偏特化,函数偏特化则没必要支持


/*
// 偏特化版本
template<typename T>
void func<T *>(T a)
{
	cout << "func<T *>(T a), a = " << a << endl;
}
*/

// 用函数重载来实现T *的偏特化完全相同的效果,因为模板的反正是参数或返回值,而函数重载也是只有参数或返回值不同的结构,因此函数重载可以替换函数的偏特化,函数偏特化则没必要支持
template<typename T>
void func(T *a)
{
    
    
	cout << "func(T *a), a = " << a << endl;
}

3.2.4、编译器的匹配规则

(1)第1步先匹配非模版函数,也就是普通函数,如果匹配到就执行,匹配不到进入下一步
(2)第2步再匹配基础泛化版函数,如果匹配不到就报错了,匹配到进入下一步
(3)第3步再匹配完全特化版本,如果匹配到就执行,匹配不到就执行上一步匹配到的泛化版本
(4)一个小细节:函数模板的特化(当然是全特化)不参与函数重载,层级不一样,函数模板的特化是属于函数模板的下一级,不能参与函数重载的。

3.2.5、特化与递归结合

模板特化用于编译期条件判断,实例如下:
了解有这种用法,实践看到再具体分析再项目中的作用

#include <iostream>
 
template<int i>
void print()
{
    
    
  print<i-1>();
  std::cout << i << std::endl;
}
 
//特例,终止递归。
template<>
void print<1>()
{
    
    
  std::cout << 1 << std::endl;
}
 
int main()
{
    
    
  print<100>();
}

3.2.6、特化总结

特化本质上是我们顶替了编译器的工作,我们帮编译器做了类型推导。

3.3、类型萃取

3.3.1、类型萃取是用途

模板函数中区分T是源生类型POD还是自定义类型。
POD,Plain Old Data,简单理解就是C++从C继承而来的基本类型,如int、double等,就是简单类型,没有添加C++那些高级特征(构造析构,拷贝构造,移动语义,虚函数等)。

例如复制函数中,如果是POD类型则直接赋值=即可,但是如果是非POD类型则需要涉及深浅拷贝问题,不能简单的直接=复制。
实践;int数组和string数组的复制对比

template<typename T>
void mycopy(T *dest, const T *src, int cnt)
{
    
    
	// 在这里使用is_pod可以去区分,T到底是pod还是非pod
	if (std::is_pod<T>::value)
	{
    
    
		memcpy(dest, src, cnt*sizeof(T));
	}
	else
	{
    
    
		for (int i=0; i<cnt; i++)
		{
    
    
			dest[i] = src[i];			// 非pod类型使用operator=是可以复制的
		}
	}
}

注意;std中还提供了其他类型的萃取,如函数is_funciton。。。等等

3.3.2、类型萃取如何实现

首先最简单的一种可能就是将有限的POD类型都放置到一个列表中,进行判断来决定是不是POD类型。但是这个是在运行时候去判断,效率太底下了。
还有就是利用函数对象,类的特化来处理。
根据std中标准的类型萃取方法使用;std::is_pod::value
可以得到是一个模板类,需要传入类型,并且其中有一个静态成员变量value来记录是否是POD类型。从而我们可以特化如果是POD类型的则将该
value设置为true,反之都是false。
这就是利用特化实现在编译时进行判断处理。


// 泛化版本的my_is_pod
template<typename T>
struct my_is_pod
{
    
    
	static bool value;
};

template<typename T>
bool my_is_pod<T>::value = false;


// int类型的特化版本
template<>
struct my_is_pod<int>
{
    
    
	static bool value;
};
bool my_is_pod<int>::value = true;

调用
if (my_is_pod<T>::value)

还有一种优秀的方法只是在调用的时候不一样,就是采用中间层的方式。


struct TrueType
{
    
    
	bool GetType()
	{
    
    
		return true;
	}	
};

struct FalseType
{
    
    
	bool GetType()
	{
    
    
		return false;
	}	
};

// 泛化版本的my_is_pod
template<typename T>
struct my_is_pod
{
    
    
	typedef FalseType value_type;
};


// int类型的特化版本
template<>
struct my_is_pod<int>
{
    
    
	typedef TrueType value_type;	
};

调用的时候;有一个中间层
my_is_pod<int>::value_type().GetType()

3.3.3、迭代器萃取与泛型算法

首先STL是C++提供的一套标准实现的template模板化的library库,其中涉及的两个核心就是泛型容器和泛型算法。

为了实现泛型容器,引入了迭代器,迭代器是指针的泛化抽象。
容器不管是怎么存取数据的,但是迭代器是直接指向数据的,数据怎么存取都得靠迭代器。

泛型算法可以接受多种容器,每种容器内可以存储多种数据载体,这就是泛型算法的2级泛化支持,

而泛型算法的实现其实有两个大难题;
1、泛型算法无法预知自己处理的是什么容器
因此泛型算法对接的其实不是容器,而是容器里面的迭代器。

2、泛型算法无法预知容器内存储的元素类型,是否POD
则需要提供类型萃取,又因为泛型算法对接的是迭代器,那么就是需要提供迭代器萃取器,在泛型算法内预先萃取并使用容器元素类型。

细节实现可以查看这两篇博客
https://blog.csdn.net/virtual_func/article/details/48398451
https://blog.csdn.net/terence1212/article/details/52287762

4、特化和萃取总结

关键是特化和萃取

特化的核心价值是,让模板类/函数按一定优先级规则去匹配,从而可以对泛型模板类型进行一些具体类型的特殊化处理

萃取的核心价值是,让我们在写泛型算法时可以预先得知未来传参容器及容器内元素的型别特征

三、杂项遗落

1、智能指针

猜你喜欢

转载自blog.csdn.net/zw1996/article/details/112168862
今日推荐