C++学习第八篇(泛型编程)

泛型编程,主要是利用模板,其主要目的是提高代码复用性,将类型参数化;分为两类:函数模板和类模板

一,函数模板:

1,主要用法:
另外T也可以用于函数的返回值。

#include <iostream>
#incldue <string>
using namespace std;

template<typename T>    //告诉编译器,下面这个函数是一个模板,T为模板类型
void mySwap(T& a, T& b) {
	T temp = a;
	a = b;
	b = temp;
}

int main() {
	int a = 1;
	int b = 2;
	mySwap(a, b);      //使用方式一:直接用,编译器会自动识别参数类型

	cout << "a = " << a << endl;
	cout << "b = " << b << endl;

	double c = 1.1;
	double d = 2.2;
	mySwap<double>(c, d);  //使用方式二:指定好本次使用的类型T

	cout << "c = " << c << endl;
	cout << "d = " << d << endl;
}

2,模板使用注意事项
(1),自动类型推导,必须推导出一致的数据类型才可以
还是上面的例子,改成:

int a = 10;
char ch = 'c';
mySwap(a,c);      //报错,一个是int,一个是char类型,自动类型推导失败

(2),模板必须要确定出T的数据类型

template<class T>
void func(){
	cout << "func 函数调用" << endl;
}
int main(){
	func();    //报错,这里func虽然被声明为一个模板,但函数体内并没有用到,也没有传参,编译器此时已经无法进行自动类型推导
	func<int>();
	func<double>();    //这两句都没问题,因为此时编译器无法自动类型推导,手动给他一个类型就可以继续跑下去
}

3,函数模板与普通函数的区别,主要在于普通函数可以进行隐式类型转换,而函数模板,在自动类型推导时(不显式地指定参数类型),不会进行隐式类型转换,在显示指定参数类型时(显示地给出参数类型),可以隐式类型转换

建议使用模板时显式地给出运算类型,以防编译器自动推导出的类型不是我们期望的类型

int addInt(int a, int b){
	return a + b;
}
template<class T>
T addTemplate(T a, T b){
	return a + b;
}
int main(){
	int a = 10;
	char b = 'b';

	cout << addInt(a,b) << endl;  //108,普通函数调用,自动将字符'b'转换成了98
	cout << addTemplate(a,b) << endl; //报错,自动类型推导时无法隐式转换,参考注意事项2,必须推导出一致的数据类型才可以
	cout << addTemplate<int>(a,b) << endl  //98,显式地给出了参数类型,将会先隐式转换成int,再参与运算
	return 0;
}

4,普通函数与函数模板的调用规则
(1),如果普通函数与函数模板都可以实现,优先调用普通函数;
(2),可以使用空模板参数列表调用函数模板;
(3),函数模板也可以发生重载;
(4),如果函数模板可以发生更好的匹配,那优先调用函数模板。

void myPrint(int a, int b){
	cout << "普通函数调用" << endl;
}
template<class T>
void myPrint(T a, T b){
	cout << "函数模板调用" << endl;
}
template<class T>
void myPrint(T a, T b, T c){
	cout << "函数模板重载调用" << endl;
}
int main(){
	int a = 10;
	int b = 20;
	int c = 30;
	myPrint(a, b); //普通函数调用,规则1
	myPrint<>(a, b); //函数模板调用,规则2
	myPrint(a, b, c);  //函数模板重载调用,规则3

	myPrint('a', 'b');   //函数模板调用,规则4
	return 0;
}

说明:
普通函数与函数模板名字相同,已经形成了重载
1)规则1,如果把普通函数只保留声明,而去掉实现,那么会报错找不到函数实现,而不是转而去调用函数模板,因为编译器已经认定了要去调普通函数;
2)规则3又是发生了一次重载,三个参数,编译器别无选择只能调这个三个参数的函数模板,此时三个函数是重载的(普通函数,两参数模板,三参数模板)
3)规则4中 “更好地匹配”,就是指不用发生隐式类型转换,如果调普通函数要发生转换,而模板只需要自动推导出类型,因此才会去调函数模板
既然提供了函数模板,就最好不要再提供同名的普通函数了,避免出现二义性!

5,模板的局限性
模板不是万能的,比如下面这段代码:对于一些类型当然没问题,比如 int double或者自定义的类型,但是如果传进来的是数组呢?数组中 int arr[] = … 中arr是const修饰的,不可指向其他空间,因此在运行时会报错

template<class T>
void func(T a, T b){
	a = b;
}

另外下面代码,如果传参是自定义类型呢?普通类型可以比较,自定义类型要想比较,就要重写==运算符了,如果我们的模板很多,还想判定是否大于、小于…那么就得重新很多很多运算符,很麻烦

class Person{
public:
	int mAge;
	string mName;
}
template<class T>
bool myCompare(T& a, T& b){
	if(a == b){
		return true;
	}
	return false;
}

对于上面的不方便,可以使用特定类型的函数模板,template<>就表明这是一个针对特定类型的函数模板,参数列表里也不是写T了,而是直接写明了Person& 类型,当传参是Person类型时会优先走这个函数

template<> bool myCompare(Person& p1, Person& p2){
	//自定义操作
}

二,类模板

与函数模板类似,其主要用于定义类模板
1,主要用法:

#include <iostream>
#include <string>

using namespace std;

template<class NameType, class AgeType>
class Person {
public:
	Person(NameType name, AgeType age) {
		this->mName = name;
		this->mAge = age;
	}
	void showInfo() {
		cout << "name:" << mName << "	age:" << mAge << endl;
	}
	NameType mName;
	AgeType mAge;
};
void test0110() {
	Person<string, int> p1("zhangsan", 18);
	p1.showInfo();             //zhangsan  18

}
int main() {
	test0110();

	return 0;
}

2,类模板与函数模板的区别
1)在实例化对象时,只能显式地给出类型,不能进行自动类型推导,以下语句将报错:

Person p2(“lisi”, 19);

2)类模板支持默认参数类型,一旦使用了默认参数,在实例化时此参数的类型可以省略,将使用默认的类型
template<class NameType, class AgeType = int> //第二个参数类型默认使用int

Person< string> p2(“lisi”, 19); //不会报错,因为默认使用了int
Person< string> p3(“wangwu”, ‘a’); //不会报错,将字符’a’转成了int类型(97)
Person<string, string> p4(“zhaoliu”, “abc”); //不会报错,第二个参数类型不使用默认类型int,而是手动指定string

3,类模板中成员函数的创建时机:在运行时才会创建,编译时不会

class Person1 {
public:
	void showPerson1() {
		cout << "show Person1" << endl;
	}
};
class Person2 {
public:
	void showPerson2() {
		cout << "show Person2" << endl;
	}
};
template <class T>
class Person0 {
public:
	T obj;
	void showPerson1() {
		obj.showPerson1();
	}
	void showPerson2() {
		obj.showPerson2();
	}
};

int main(){
	Person0<Person1> p1;
	p1.showPerson1();   // flag1行
	p1.showPerson2();   // flag2行
	return 0;
}

以上代码中,如果注掉flag1、flag2这两行,将不会报错,为什么?我怎么知道obj是什么类型?既然不知道是什么类型,怎么会知道其有没有showPerson1()与showPerson2()函数呢?万一最后实例化的时候传进来的是个int类型,int类型哪有这两个函数啊?
这也从侧面验证了类模板的成员函数,是在运行时才会创建,编译时不会做这些检查,取消注释,那么flag2行将报错,因为这时候已经实例化了,编译器知道了p1对象使用的参数类型是Person1,有showPerson1()函数,没有showPerson2()函数,因此flag1行不会报错,flag2行报错。
那么为什么在运行时才会创建呢?我想应该是为了保证模板类的通用性吧。

4,类模板对象作为函数参数
既然我们的类模板对象,是在实例化时才确定类型,那么这个对象作为函数的参数时,又该如何写参数类型呢?有三种方式:

class Person {
public:
	Person(NameType name, AgeType age) {
		this->mName = name;
		this->mAge = age;
	}
	void showInfo() {
		cout << "name:" << mName << "	age:" << mAge << endl;
	}
	NameType mName;
	AgeType mAge;
};
//方式一:直接写清除什么类型
void printPerson1(Person<string, int>& p) {
	cout << "方式一结果:" << endl;
	p.showInfo();
}
//方式二:参数模板化,将对象中的参数作为模板传递
template<class T1, class T2>
void printPerson2(Person<T1, T2>& p) {
	cout << "方式二结果:" << endl;
	p.showInfo();
	cout << "第二种方式,函数模板自动类型推导,推出的类型" << typeid(T1).name() << endl;
	cout << "第二种方式,函数模板自动类型推导,推出的类型" << typeid(T2).name() << endl;
}
//方式三:整个类模板化,把函数也写成函数模板,然后实际传参时,会进行自动类型推导
template<class T>
void printPerson3(T& t) {
	cout << "方式三结果:" << endl;
	t.showInfo();
	cout << "第三种方式,函数模板自动类型推导,推出的类型" << typeid(T).name() << endl;
}
int main(){
	Person<string, int> p1("zhangsan", 18);
	p1.showInfo();

	printPerson1(p1);
	printPerson2(p1);
	printPerson3(p1);
	return 0;
}

typeid关键字可以看类型信息,其返回值是个type_info类型,再调用name()函数可以看到其类型的名字
结果:这个长串就是string的原本真实名字,可以看出推导的没问题
在这里插入图片描述
建议使用第一种,简单直接,便于阅读维护,也不会产生什么歧义和非预期结果。

5,类模板与继承
如果父类是一个类模板

class Base {
public:
	Base() {
		cout << "父类中T的类型为:" << typeid(T).name() << endl;
	}
	T member;
};

template<class T1, class T2>        //flag1
class Son :public Base<T2> {        //flag2
public:
	Son() {
		cout << "子类中T1的类型为:" << typeid(T1).name() << endl;
		cout << "子类中T2的类型为:" << typeid(T2).name() << endl;
	}
	T1 memberOfSon;
};
int main(){
	Son<int,char> s;
	return 0;
}

当父类是一个类模板时,其子类在继承时必须给出父类的类型,或者将子类也定义为类模板
1)上面代码中,在Son类型创建对象时两个类型分别是int、char,其中第二个类型char继承自父类,相当于又指定了父类的参数类型,因此跑起来的结果是:
在这里插入图片描述

2)如果将flag1行注掉,flag2行改为:

class Son :public Base { //报错

将会报错,因为父类是个类模板,在继承时必须要给出其父类的参数类型,比如这样:

class Son :public Base< int>

6,类模板成员函数的类外实现

class Dog {
public:
	Dog(T1 name, T2 age);
	void showInfo();
	T1 mName;
	T2 mAge;
};
template<class T1, class T2 >
Dog<T1, T2>::Dog(T1 name, T2 age) {
	this->mName = name;
	this->mAge = age;
}

template<class T1, class T2>
void Dog<T1, T2>::showInfo() {
	cout << "name:" << this->mName << "age:" << this->mAge << endl;
}

注意:(以这块代码为例)
1),在类外实现时必须要加入template<class T1, class T2>的标注;
2),在写作用域时要写成Dog<T1, T2>
以告诉编译器 ,这是一个类模板的成员函数的类外实现。

7,类模板的分文件编写
由于类模板成员函数创建时机是在调用阶段,因此在.h中写声明,在.cpp中写实现这种方式,会导致编译器不知道.cpp里面的内容,因为调用阶段才回去访问cpp里面的内容,因此会报链接找不到的错误,解决办法:(主流使用第二种)
(1),直接包含.cpp,不包含.h了;
(2),不分文件编写,把声明和实现写在一个文件里,然后把后缀改为.hpp(这是一种约定俗成,一看到hpp大家就会明白这是个类模板)

8,类模板与友元

template<class T1, class T2>       //类外实现,必须要先让编译器知道有这个函数printPerson2存在,而这个函数又用到了Person类
class Person;                      //所以要先声明class Person      
template<class T1, class T2>
void printPerson2(Person<T1, T2>& p){
	cout << "姓名:"<< p.mName << "年龄:" << p.mAge << endl;
}

template<class T1, class T2>
class Person{
	friend void printPerson1(Person<T1, T2>& p){
		cout << "姓名:"<< p.mName << "年龄:" << p.mAge << endl;
	}
	
	friend void printPerson2<>(Person<T1, T2>& p){
	}
public:
	Person(T1 name, T2 age){
		this->mName = name;
		this->mAge = age;
	}
private:
	T1 mName;
	T2 mAge;
}

int main(){
	Person<string, int> p("张三", 20);
	
	printPerson1(p);       //友元全局函数 类内实现
	printPerson2(p);       //友元全局函数 类外实现,
	return 0;
}
  • 21
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值