Java 转 C++ 系列:函数模板

文章参考:黑马程序员匠心之作|C++教程从0到1入门编程,学习编程不再难

后文部分案例,省去头文件的书写


文章目录

  • 一、函数模板
    • [1.1 函数模板语法](#1.1 函数模板语法)
    • [1.2 函数模板注意事项](#1.2 函数模板注意事项)
    • [1.3 函数模板数组排序案例](#1.3 函数模板数组排序案例)
    • [1.4 普通函数与函数模板的区别](#1.4 普通函数与函数模板的区别)
    • [1.5 普通函数与函数模板的调用规则](#1.5 普通函数与函数模板的调用规则)
    • [1.6 模板的局限性](#1.6 模板的局限性)
  • 二、类模板
    • [2.1 类模板语法](#2.1 类模板语法)
    • [2.2 类模板和函数模板的区别](#2.2 类模板和函数模板的区别)
    • [2.3 类模板中成员函数创建时机](#2.3 类模板中成员函数创建时机)
    • [2.4 类模板对象做函数参数](#2.4 类模板对象做函数参数)
    • [2.5 类模板与继承](#2.5 类模板与继承)
    • [2.6 类模板成员函数类外实现](#2.6 类模板成员函数类外实现)
    • [2.7 类模板的分文件编写](#2.7 类模板的分文件编写)
    • [2.8 类模板与友元](#2.8 类模板与友元)

一、函数模板

  • C++另一种编程思想称为 泛型编程 ,主要利用的技术就是模板
  • C++提供两种模板机制:函数模板类模板

1.1 函数模板语法

函数模板作用:建立一个通用函数,其函数返回值类型和形参类型可以不具体制定,用一个虚拟的类型来代表。

语法:

cpp 复制代码
template<typename T>
函数声明或定义

template --- 声明创建模板

typename --- 表面其后面的符号是一种数据类型,可以用class代替

T --- 通用的数据类型,名称可以替换,通常为大写字母

cpp 复制代码
//利用模板提供通用的交换函数
template<typename T>
void mySwap(T& a, T& b) {
	T temp = a;
	a = b;
	b = temp;
}

int main() {
	system("chcp 65001> nul");

	//利用模板实现交换
	//1、自动类型推导
	int a = 10;
	int b = 20;
	mySwap(a, b);

	//2、显示指定类型
	mySwap<int>(a, b);
	
	double c = 1.5;
	double d = 2.5;
	mySwap(c, d);

	system("pause");
	return 0;
}

1.2 函数模板注意事项

  • 自动类型推导,必须推导出一致的数据类型T,才可以使用
  • 模板必须要确定出T的数据类型,才可以使用
cpp 复制代码
//利用模板提供通用的交换函数
template<class T> // typename可以替换为class
void func() {
	cout << "func调用" << endl;
}

int main() {
	system("chcp 65001> nul");
	
	//func(); // 错误,编译器无法推导出T的类型
	func<int>(); // 显式指定T为int类型,调用成功
	func<double>(); // 显式指定T为double类型,调用成功

	system("pause");
	return 0;
}

1.3 函数模板数组排序案例

cpp 复制代码
//利用模板提供通用的交换函数
template<typename T> // typename可以替换为class
void mySwap(T& a, T& b)
{
	T temp = a;
	a = b;
	b = temp;
}

// 利用模板提供通用的排序函数
template<typename T>
void sortArr(T arr[], int size) {
	for (int i = 0; i < size - 1; i++) {
		for (int j = 0; j < size - 1 - i; j++) {
			if (arr[j] > arr[j + 1]) {
				mySwap(arr[j], arr[j + 1]);
			}
		}
	}
}

// 利用模板提供通用的打印函数
template<typename T>
void printArr(T arr[], int size) {
	cout << "排序后的数组: ";
	for (int i = 0; i < size; i++) {
		cout << arr[i] << " ";
	}
	cout << endl;
}

int main() {
	system("chcp 65001> nul");

	char chArr[] = "nodfsbfi";
	int arr[] = { 9, 5, 2, 7, 1 };
	double arr2[] = { 3.14, 2.71, 1.41, 0.577 };

	sortArr(chArr, sizeof(chArr) / sizeof(char) - 1); // 注意字符串数组的大小需要减1,因为最后一个元素是'\0'结束符
	sortArr(arr, sizeof(arr) / sizeof(int));
	sortArr(arr2, sizeof(arr2) / sizeof(double));

	printArr(chArr, sizeof(chArr) / sizeof(char) - 1);
	printArr(arr, sizeof(arr) / sizeof(int));
	printArr(arr2, sizeof(arr2) / sizeof(double));	

	system("pause");
	return 0;
}

1.4 普通函数与函数模板的区别

  • 普通函数调用时可以发生自动类型转换(隐式类型转换)
  • 函数模板调用时,如果利用自动类型推导,不会发生隐式类型转换
  • 如果利用显示指定类型的方式,可以发生隐式类型转换
cpp 复制代码
// 普通函数
// 普通函数调用时可以发生自动类型转换(隐式类型转换)
int myAdd01(int a,int b) {
	return a + b;
}

// 函数模板
template<typename T>
T myAdd02(T a, T b) {
	return a + b;
}

int main() {
	system("chcp 65001> nul");
	// 普通函数调用
	cout << myAdd01(10, 20) << endl;
	cout << myAdd01(3.14, 2.71) << endl; // 隐式类型转换,结果为5
	cout << myAdd01('a', 2) << endl; // 隐式类型转换,结果为99('a'的ASCII码值是97)

	// 函数模板调用
	cout << myAdd02(10, 20) << endl; // 自动类型推导,T为int
	cout << myAdd02(3.14, 2.71) << endl; // 自动类型推导,T为double
	//cout << myAdd02('a', 2) << endl; // 自动类型推导失败,编译错误,因为T无法同时满足char和int
	cout << myAdd02<int>('a', 2) << endl; // 显式指定T为int,'a'会被隐式转换为97,结果为99
	system("pause");
	return 0;
}

1.5 普通函数与函数模板的调用规则

调用规则如下:

  1. 如果函数模板和普通函数都可以实现,优先调用普通函数
  2. 可以通过空模板参数列表来强制调用函数模板
  3. 函数模板也可以发生重载
  4. 如果函数模板可以产生更好的匹配,优先调用函数模板
cpp 复制代码
void myPrint(int a, int b) {
	cout << "myPrint(int, int)调用: " << a << ", " << b << endl;
}

template<typename T>
void myPrint(T a, T b) {
	cout << "myPrint(T, T)调用: " << a << ", " << b << endl;
}

template<typename T>
void myPrint(T a, T b, T c) {
	cout << "重载myPrint(T, T, T)调用: " << a << ", " << b << ", " << c << endl;
}

int main() {
	system("chcp 65001> nul");

	int a = 10, b = 20;
	myPrint(a, b); // 优先调用普通函数
	// 如果普通函数只是声明,没有定义,编译器会报错

	myPrint<>(a, b);// 空模板参数列表强制调用函数模板

	myPrint(a, b, 30); // 函数模板重载

	char c1 = 'x', c2 = 'y';
	myPrint(c1, c2); // 函数模板匹配更好,优先调用函数模板

	system("pause");
	return 0;
}

总结:既然提供了函数模板,最好就不要提供普通函数,否则容易出现二义性


1.6 模板的局限性

cpp 复制代码
template<class T>
void f(T a, T b) { 
	a = b;
}

在上述代码中提供的赋值操作,如果传入的a和b是一个数组,就无法实现了

cpp 复制代码
template<class T>
void f(T a, T b) { 
	if(a > b) { ... }
}

在上述代码中,如果T的数据类型传入的是像Person这样的自定义数据类型,也无法正常运行

因此C++为了解决这种问题,提供模板的重载,可以为这些 特定的类型 提供 具体化的模板


方法一:重载==运算符

cpp 复制代码
template<typename T>
bool Compare(T& a, T& b) {
	return a == b;
}

class Person {
public:
	Person(string name, int age) : m_Name(name), age(age) {}
	// 方法一:重载==运算符,只比较年龄是否相等
	bool operator ==(const Person& other) const {
		return this->age == other.age; // 只比较年龄是否相等
	}
	string m_Name;
	int age;
};

int main() {
	system("chcp 65001> nul");

	Person p1("Tom", 10);
	Person p2("Alice", 10);
	bool ret2 = Compare(p1, p2); // 如果没有重载==,则会报错,因为编译器无法比较两个Person对象
	cout << "ret2: " << ret2 << endl; // 输出ret2: 1(true),因为我们重载了==运算符只比较年龄是否相等

	system("pause");
	return 0;
}

方法二:提供专门的比较函数

cpp 复制代码
template<typename T>
bool Compare(T& a, T& b) {
	return a == b;
}

class Animal {
public:
	Animal(string name, int age) : m_Name(name), age(age) {}
	string m_Name;
	int age;
};

// 方法二:提供专门的比较函数,比较名字是否相等
template<> bool Compare<Animal>(Animal& a, Animal& b) {
	return a.m_Name == b.m_Name; // 专门为Animal类型提供比较函数
}

int main() {
	system("chcp 65001> nul");

	Animal a1("Tom", 10);
	Animal a2("Tom", 10);
	bool ret3 = Compare(a1, a2); // 调用专门为Animal类型提供的比较函数,比较名字是否相等,结果为true
	cout << "ret3: " << ret3 << endl; // 输出ret3: 1(true)

	system("pause");
	return 0;
}

注:下面通用模板函数也不可删除,因为专门比较函数必须依赖通用模板函数存在。

cpp 复制代码
template<typename T>
bool Compare(T& a, T& b) {
	return a == b;
}

二、类模板

2.1 类模板语法

类模板作用:建立一个通用类,类中的成员 数据类型可以不具体制定,用一个 虚拟的类型 来代表。

语法:

cpp 复制代码
template<typename T>
类的定义

如:

cpp 复制代码
// 类模板
template<class NameType, class AgeType>
class Person {
public:
	Person(NameType name, AgeType age) : m_Name(name), m_Age(age) {}
	void showPerson() {
		cout << "name: " << this->m_Name << " age: " << this->m_Age << endl;
	}
public:
	NameType m_Name;
	AgeType m_Age;
};


int main() {
	system("chcp 65001> nul");

	Person<string, int> p1("Tom", 10);
	Person<string, int> p2("Alice", 20);

	p1.showPerson();
	p2.showPerson();
	
	system("pause");
	return 0;
}

注:类模板和函数模板语法相似,在声明模板 template 后面加类,此类称为类模板。


2.2 类模板和函数模板的区别

  • 类模板使用只能用显示指定类型方式

    cpp 复制代码
    template<class NameType, class AgeType>
    class Person {
    public:
    	Person(NameType name, AgeType age) : m_Name(name), m_Age(age) {}
    	void showPerson() {
    		cout << "name: " << this->m_Name << " age: " << this->m_Age << endl;
    	}
    	NameType m_Name;
    	AgeType m_Age;
    };
    
    int main() {
    	system("chcp 65001> nul");
    
    	// 1. 类模板没有自动类型推导功能,必须在创建对象时指定具体的类型参数
    	//Person p1("Tom", 10); // 理论上错误,编译器无法推导出NameType和AgeType的具体类型
    	Person<string, int> p1("Tom", 10); // 创建一个NameType为string,AgeType为int的Person对象
    	p1.showPerson();
    	
    	system("pause");
    	return 0;
    }
  • 类模板在模板参数列表中可以有默认参数

    cpp 复制代码
    template<class NameType, class AgeType = int> // 默认参数
    class Animal {
    public:
    	Animal(NameType name, AgeType age) : m_Name(name), m_Age(age) {}
    	void showAnimal() {
    		cout << "name: " << this->m_Name << " age: " << this->m_Age << endl;
    	}
    	NameType m_Name;
    	AgeType m_Age;
    };
    
    int main() {
    	system("chcp 65001> nul");
    
    	// 2. 类模板在模板参数列表中可以有默认参数,那么在创建时可省略,会使用默认参数
    	Animal<string> a1("Dog", 5);
    	a1.showAnimal();
    
    	system("pause");
    	return 0;
    }

2.3 类模板中成员函数创建时机

  • 普通类成员函数:编译时直接生成(不管用不用,都创建)

    cpp 复制代码
    class Person {
    public:
    	void show() {
    		noExistFunction();
    	}
    };
    int main() {
    	Person p;
    	// 不调用show(),依然报错!
    }
  • 类模板成员函数:调用时才生成(不用就不创建,调用才创建)

    cpp 复制代码
    template<class T>
    class Person {
    public:
    	void show() {
    		// 调用一个不存在的函数
    		noExistFunction();
    	}
    };
    
    int main() {
    	Person<int> p;
    	// p.show(); // 不调用,就不报错
    	p.show(); // 调用才报错
    }

    理论上,这段代码不会报错,但是VisualStudio中有检查,所以会提示报错。


2.4 类模板对象做函数参数

类模板对象做函数参数一共有三种传入方式:

  • 指定传入类型:直接显示对象的数据类型
  • 参数模板化:将对象中的参数变为模板进行传递
  • 整个类模板化:将这个对象类型模板化进行传递
cpp 复制代码
template<class T1, class T2>
class Person {
public:
	Person(T1 name, T2 age) : name(name), age(age) {}
	void showPerson() {
		cout << "name: " << this->name << " age: " << this->age << endl;
	}
	T1 name;
	T2 age;
};

//1.指定传入类型
void printPerson(Person<string, int>& p) {
	cout << "name: " << p.name << " age: " << p.age << endl;
}

//2.参数模板化
template<class T1, class T2> // 函数模板
void printPerson2(Person<T1, T2>& p) {
	cout << "name: " << p.name << " age: " << p.age << endl;
	// 查看 T1、T2 的类型
	cout << "T1 type: " << typeid(T1).name() << endl;
	cout << "T2 type: " << typeid(T2).name() << endl;
}

//3.整个类模板化
template<class T> // 函数模板
void printPerson3(T& p) {
	cout << "name: " << p.name << " age: " << p.age << endl;
	// 查看 T 的类型
	cout << "T type: " << typeid(T).name() << endl;
}

int main() {
	Person<string, int> p1("Tom", 10);
	printPerson(p1);
	Person<string, int> p2("Jerry", 20);
	printPerson2(p2);
	Person<string, int> p3("Alice", 30);
	printPerson3(p3);
	system("pause");
	return 0;
}

注:方式一更常用


2.5 类模板与继承

当类模板碰到继承时,需要注意一下几点:

  • 当子类继承的父类是一个类模板时,子类在声明的时候,要指定出父类中T的类型
  • 如果想灵活指定出父类中T的类型,子类也需变为类模板
cpp 复制代码
template<class T>
class Base { //父类是一个类模板
	T m;
};

class Son : public Base<int> { // 子类在声明的时候,要指定出父类中T的类型
};

template<class T>
class Son2 : public Base<T> { // 这里父类中T的类型由子类中的T来指定
	T obj;
};

template<class T1, class T2> 
class Son3 : public Base<T2> { // 这里父类中T的类型由子类中的T来指定
	T1 obj;
};

int main() {
	Son s;
	Son2<int> s2; // 这里指定了父类中T的类型为int,子类中T的类型也为int
	Son3<int, char> s3; // 这里指定了父类中T的类型为char,子类中T1的类型为int
	system("pause");
	return 0;
}

总结:如果父类是类模板,子类需要指定出父类中T的数据类型


2.6 类模板成员函数类外实现

类模板中构造函数和成员函数类外实现时,需要加上模板参数列表

cpp 复制代码
template<class T1, class T2>
class Person {
public:
	Person(T1 name, T2 age);
	void showPerson();
	T1 m_Name;
	T2 m_Age;
};

// 构造函数的类外实现
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>::showPerson() {
	cout << "姓名:" << m_Name << " 年龄:" << m_Age << endl;
}

2.7 类模板的分文件编写

问题: 类模板中成员函数创建时机是在调用阶段,导致分文件编写时链接不到。

person.h:

cpp 复制代码
#pragma once // 防止头文件重复包含
#include <iostream>

template<class T1, class T2>
class Person {
public:
	Person(T1 name, T2 age);
	void showPerson();
	T1 m_Name;
	T2 m_Age;
};

person.cpp:

cpp 复制代码
#include "person.h"
using namespace std;

// 构造函数的类外实现
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>::showPerson() {
	cout << "姓名:" << m_Name << " 年龄:" << m_Age << endl;
}

main.cpp:

cpp 复制代码
#include<iostream>
#include<string>
#include"person.h" // 链接不到!
using namespace std;

int main() {
	system("chcp 65001> nul");

	Person<string, int> p("Tom", 10);
	p.showPerson();

	system("pause");
	return 0;
}

原因: 整个程序只有一次编译的机会,编译 main.cpp 时,只看到了 person.h 里的声明,看不到函数实现。(编译器没法生成模板函数的具体代码,就直接跳过了)编译一结束,就再也没有机会生成代码了。等后面运行调用函数时,再想找实现,彻底找不到了,链接直接失败。 另外,模板的 person.cpp 会被编译,但白编译,里面也不会生成具体的机器码,因为里面全是泛型T,可理为是图纸,而生成具体的机器码要求具体到int、string等类型。

而普通类第一次编译的时候,普通类的 .cpp 就把函数实现编译成了实体代码。链接器直接拿来用,根本不用等调用的时候再找。


解决:

  • 方式一:直接包含.cpp源文件(不常用)

    cpp 复制代码
    #include<iostream>
    #include<string>
    #include"person.cpp" // 直接引用.cpp源文件
    using namespace std;
    
    int main() {
    	...
    }
  • 方法二:将 .h声明和 .cpp实现写到同一个文件中,并更改后缀名为.hpp。(hpp是约定的名称,并不是强制)

    person.hpp:

    cpp 复制代码
    #pragma once // 防止头文件重复包含
    #include <iostream>
    using namespace std;
    
    template<class T1, class T2>
    class Person {
    public:
    	Person(T1 name, T2 age);
    	void showPerson();
    	T1 m_Name;
    	T2 m_Age;
    };
    
    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>::showPerson() {
    	cout << "姓名:" << m_Name << " 年龄:" << m_Age << endl;
    }

    main.cpp:

    cpp 复制代码
    #include<iostream>
    #include<string>
    #include"person.hpp" // 里面包含声明和实现
    using namespace std;
    
    int main() {
    	...
    }

另外:也可以类内做实现

cpp 复制代码
template<class T1, class T2>
class Person {
public:
	Person(T1 name, T2 age) {
		m_Name = name;
		m_Age = age;
	}
	void showPerson() {
		cout << "姓名:" << m_Name << " 年龄:" << m_Age << endl;
	}
	T1 m_Name;
	T2 m_Age;
};

2.8 类模板与友元

  • 全局函数,类内实现:

    cpp 复制代码
    template<class T1, class T2>
    class Person {
    	// 全局函数 类内实现
    	friend void showPerson(Person<T1, T2> p) {
    		cout << "姓名:" << p.m_Name << ",年龄:" << p.m_Age << endl;
    	}
    public:
    	Person(T1 name, T2 age) : m_Name(name), m_Age(age) {}
    private:
    	T1 m_Name;
    	T2 m_Age;
    };
    
    int main() {
    	system("chcp 65001> nul");
    
    	Person<string, int> p("Tom", 10);
    	showPerson(p);
    
    	system("pause");
    	return 0;
    }
  • 全局函数,类外实现:

    cpp 复制代码
    template<class T1, class T2>
    class Person;
    
    // 全局函数 类外调用
    template<class T1, class T2>
    void showPerson2(Person<T1, T2> p) {
    	cout << "姓名:" << p.m_Name << ",年龄:" << p.m_Age << endl;
    }
    
    template<class T1, class T2>
    class Person {
    	// 全局函数 类外实现
    	// 需要加空模板的参数列表,且需要让编译器提前知道这个函数的存在
    	friend void showPerson2<>(Person<T1, T2> p);
    public:
    	Person(T1 name, T2 age) : m_Name(name), m_Age(age) {}
    private:
    	T1 m_Name;
    	T2 m_Age;
    };
    
    int main() {
    	system("chcp 65001> nul");
    
    	Person<string, int> p("Tom", 10);
    	showPerson2(p);
    
    	system("pause");
    	return 0;
    }
相关推荐
froginwe112 小时前
如何使用 AppML
开发语言
程序员清风2 小时前
独立开发者必看:推荐几个可直接用的开源项目!
java·后端·面试
YJlio2 小时前
4月14日热点新闻解读:从金融数据到平台治理,一文看懂今天最值得关注的6个信号
java·前端·人工智能·金融·eclipse·电脑·eixv3
格林威2 小时前
工业相机“心跳”监测脚本(C# 版) 支持海康 / Basler / 堡盟工业相机
开发语言·人工智能·数码相机·opencv·计算机视觉·c#·视觉检测
小苗卷不动2 小时前
OJ练习之加减(中等偏难)
c++
我能坚持多久2 小时前
String类常用接口的实现
c语言·开发语言·c++
落魄江湖行2 小时前
基础篇三 一行 new String(“hello“) 到底创建了几个对象?90% 的人答错了
java·面试·八股文
花间相见2 小时前
【大模型微调与部署03】—— ms-swift-3.12 命令行参数(训练、推理、对齐、量化、部署全参数)
开发语言·ios·swift
青衫码上行2 小时前
【从零开始学习JVM】栈中存的是指针还是对象 + 堆分为哪几部分
java·jvm·学习·面试