0. 模板

C++中除了有面向对象的思想,还有泛型编程的思想,泛型编程利用的主要技术就是模板。
C++中两种模板:函数模板、类模板

语法:

template<typename T>
函数声明或定义

含义:

  • template 固定的,声明创建模板
  • typename 表明后边的T是一种数据类型,也可以用class代替
  • T 类似形参,任意的合法字符都可以,习惯上用 T

模板有两种类型推导的方式,一种是编译器自动推导,另外一种是程序猿手动指定:

#include <iostream>

using namespace std;

template<typename T>
void func(T &a, T &b){	
	T s = a;
	a = b;
	b = s;
} 

int main(){
	int a = 10, s = 9;
	// func(a, s);  // 自动类型推导
	// func<int>(a, s);   // 指定类型推导 
	cout << a << endl;
	cout << s << endl; 
	
	return 0;
} 

1. 函数模板注意事项

  • 自动类型推导,必须推导出一致的数据类型才可以使用。
  • 模板必须要确定 T 的数据类型,才可以使用。

eg:二分排序的模板实现

template<typename T>
void mySort(T a[], int len, int l, int r){	
	// quickSort
	if(l >= r) return ;
	int x = a[(l + r) / 2];
	int i = l - 1, j = r + 1;
	
	while(i < j){
		while(a[++i] < x);
		while(a[--j] > x);
		if(i < j) swap(a[i], a[j]);
	}
	mySort(a, len, l, j);
	mySort(a, len, j + 1, r);	
} 

2. 普通函数与函数模板对比

2.1 区别

  • 普通函数调用时,参数可以发生自动类型转换(隐式转换)
  • 函数模板,在自动类型推导形式下,不会自动类型转换。
  • 函数模板,在指定类型形式下,会发生自动类型转换。

2.2 调用规则

  • 如果普通函数和函数模板同时存在,则会调用普通函数,如果普通函数只有声明,没有实现,那也不会去调用函数模板,会直接报错
  • 可以用过空模板参数列表,强制调用函数模板
  • 函数模板也可以重载
  • 如果函数模板可以更好的匹配调用,会优先调用函数模板
#include <iostream>

using namespace std;


void fuc(int a, int b){
	cout << "普通" << endl;
}

template<class T>
void fuc(T a, T b){
	cout << "模板" << endl;
}

template<class T>   // 重载函数模板 
void fuc(T a, T b, T c){
	cout << "模板 重载" << endl;
}

void test(){
	int a = 1;
	int b = 2;
	fuc(a, b); // 调用普通函数 
	fuc<>(a, b);  // 空模板参数列表,调用函数模板 
	fuc<>(a, b, 1); // 模板 重载 
	
	
	char c1 = 'a';
	char c2 = 's';
	fuc(c1, c2);  // 传入char型,普通函数需要做自动类型转换,而函数模板不需要转换,所以优先调用函数模板。 
	
}

int main(){
	test();
	return 0;
} 

3. 模板的局限性

比如模板是一个赋值操作,那么传入的是数组的话,就不正确了。
再比如,比较两个对象的大小关系,类型不确定,比较方式也不确定。

为了解决上边问题,C++提供了模板的重载,可以为特定的类型提供具体化的模板(当然,也可以在重载特定类型的运算符)

语法:

template<class T>
void fuc(T a, T b){
	cout << "原模板" << endl;
}

// test是自定义的数据类型
// 这里将函数模板为test数据类型具体化,如果传入是test类型的话,优先调用具体化的模板
template<> void fuc(test a, test b){
	// ...  code 
} 

4. 类模板

作用:建立一个通用类,类的成员的数据类型可以不指定,用一个虚拟的类型来代表。
语法:和函数模板一样

template<typename T> 

例程:

#include <iostream>
#include <string>

using namespace std;

template<class nameType, class ageType>
class Person{
	
public:
	
	Person(nameType name, ageType age) : m_name(name), m_age(age){
		
	};
	
	void print(){
		cout << m_name << endl;
		cout << m_age << endl;
	}
	
	string m_name;
	int m_age;
}; 

int main(){
	Person<string, int> a("张三", 18);
	a.print();
	return 0;
}

5. 类模板与函数模板区别

  • 类模板没有自动类型推导,必须指定类型。
  • 类模板在参数列表中,可以有默认参数,在C++11标准以后,函数模板也可以有默认参数了。

上边的代码可以改成这:

template<class nameType = string, class ageType = int>

6. 类模板中成员函数创建的时机

类模板中的成员函数与普通类的成员函数创建时机有所区别:

  • 普通类的成员函数一开始就可以创建。
  • 类模板的成员函数在调用时才创建。

类模板的成员函数在调用时才创建的原因如下:

因为类中的某些类型并不确定,传入的类型到底能不能执行成员函数中的动作也不知道,所以不能提前创建。

#include <iostream>
#include <string>

using namespace std;

class Person1{
public:
	void f1(){
		cout << "f1" << endl;
	}
};

class Person2{
public:
	void f2(){
		cout << "f2" << endl;
	}
};

template<class T>
class tt{
public:
	T obj;
	
	void f1(){
		obj.f1();
	}
	
	void f2(){
		obj.f2();
	}
};

int main(){
	tt<Person1> s;
	s.f1(); // 合法 
	s.f2();	// 不合法 因为类模板指定的类型是Person1,而该类型没有f2的方法。 
	
	return 0;
}

7. 类模板实例化的对象 做 函数参数

函数参数有三种类型进行接收:

  • 指定类型 : 对象是什么类型,直接将该类型作为函数参数
  • 把类模板的参数 模板化: 将对象中的参数变为模板
  • 把 类模板整体 模板化

第一种在实际开发中最常用,因为后两种需要函数模板配合类模板,麻烦。

#include <iostream>
#include <string>

using namespace std;

template<class T1, class T2>
class Person{
public:
	T1 m_name;
	T2 m_age;
	
	Person(T1 name, T2 age){
		m_name = name;
		m_age = age;
	} 
	
	void print(){
		cout << "name : " << m_name << "  age : " << m_age << endl;
	} 
}; 

//1. 指定类型 
void printP1(Person<string, int> &a){
	a.print();	
}

// 2. 参数模板化
template<class T1, class T2> 
void printP2(Person<T1, T2> &a){
	a.print();
}

// 3. 类模板 模板化
template<class T>
void printP3( T &a){
	a.print();
}

int main(){
	Person<string, int> a("张三", 33);
	printP1(a); 
	printP2(a);
	printP3(a);
	
	return 0;
}

8. 类模板遇到继承

  • 在继承时,如果父类是一个类模板,那么必须指定父类模板的数据类型。
  • 如果想灵活继承父类模板,那么子类也需要做成类模板。

9. 类模板成员函数类外实现

template<class T1, class T2>
class Person{
public:
	T1 m_name;
	T2 m_age;
	
	Person(T1 name, T2 age);
	
	void print();
}; 

template<class T1, class T2>
Person<T1, T2>::Person(T1 name, T2 age){
	m_name = name;
	m_age = age;
}

template<class T1, class T2>
void Person<T1, T2>::print(){
	cout << "name : " << m_name << "  age : " << m_age << endl;
} 

10. 类模板分文件编写

类模板的声明在.h文件中,类模板的成员函数在相关联的.cpp文件中实现。 这种情况下,如果在别的文件中(比如main.cpp)引入该 .h文件,使用该类,会无法编译(无法解析外部符号),为什么?

原因是,类模板的成员函数在调用时才会创建。main.cpp引入了.h文件,该文件中只有类模板的声明,没有具体实现。

为了解决这个问题,有两种解决方法:

  • 法1:不include类的.h文件,而是include类的.cpp文件。
  • 法2:可以将类模板的声明和实现写在同一个文件中,并且习惯上把该文件的后缀名写成 .hpp。

实际中,第二种方法更加普遍。

11. 类模板的友元

  • 类内定义:与普通类定义友元一样。
  • 类内声明,类外定义:类外定义的话,肯定要声明称模板,而类内的声明语句,会认为是一个普通的函数,所以,类内的声明要加上空参数列表friend test<>(test<T1, T2>);说明这是一个模板,并不是一个普通的函数。

例程:

#include <iostream>

using namespace std;


// 声明有Person类  要不然test2找不到Person类
template<class T>
class Person;


// 声明test2是个模板函数,要不然下边的Person类中找不到test2这个模板函数
template<class T>
void test2(Person<T> a){
    cout << a.a << endl;
}

template<class T>
class Person{

    // 1. 类内实现友元
    friend void test1(){   // 友元函数声明 + 定义,如果不加friend的话,相当于一个普通的成员
        cout << 123 << endl;
    }

    // 2. 类外实现友元  这里声明要加空参数列表 说明需要的是一个模板函数
    friend void test2<>(Person<T>);

public:
    Person(T age){
        a = age;
    }
    T a;
};

//  test1函数的声明 供main调用。
void test1();

int main(){
    Person<int> a(10);
    test2(a); 
    
    return 0;
}