文章参考:黑马程序员匠心之作|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 普通函数与函数模板的调用规则
调用规则如下:
- 如果函数模板和普通函数都可以实现,优先调用普通函数
- 可以通过空模板参数列表来强制调用函数模板
- 函数模板也可以发生重载
- 如果函数模板可以产生更好的匹配,优先调用函数模板
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 类模板和函数模板的区别
-
类模板使用只能用显示指定类型方式
cpptemplate<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; } -
类模板在模板参数列表中可以有默认参数
cpptemplate<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 类模板中成员函数创建时机
-
普通类成员函数:编译时直接生成(不管用不用,都创建)
cppclass Person { public: void show() { noExistFunction(); } }; int main() { Person p; // 不调用show(),依然报错! } -
类模板成员函数:调用时才生成(不用就不创建,调用才创建)
cpptemplate<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 类模板与友元
-
全局函数,类内实现:
cpptemplate<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; } -
全局函数,类外实现:
cpptemplate<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; }