C++ 提高篇 1 之模板

本阶段主要针对 C++ 泛型编程和 STL 技术做详细讲解

模板

模板就是建立通用的模具,大大提高复用性

模板的特点

  1. 模板不能被直接使用,它只是一个框架
  2. 模板的通用并不是万能的

函数模板

C++ 另一种编程思想称为泛型编程,主要利用的技术就是模板

C++ 提供二种模板机制:函数模板类模板

函数模板语法及其作用

》》函数模板语法

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

// 使用函数模板
// 函数名<具体的数据类型>(实参);

解释:template 声明创建模板;typename 表明其后面的符号是一种数据类型,可以用 class 代替;T 通用的数据类型,名称可以代替,通常为大写字母


》》关于函数模板的作用,用一个具体的实例加以演示

#include <iostream>
using namespace std;

// 用于任意数据类型二个数的加法模板
template<typename T>
T Add(T num1, T num2) {
	return (num1 + num2);
}

int main() {

	// 1. 用于二个整型数据的加法运算
	cout << Add<int>(1, 2) << endl; // 3

	// 2. 用于二个浮点数据的加法运算
	cout << Add<float>(1.2, 4.5); // 5.7

	system("pause");
	return 0;
}

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


函数模板注意事项

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

解释:第一条注意事项表明了一个 T 只能推导出一种数据类型;第二条注意事项表明了模板中的 T 必须在函数体的实现中看得出其数据类型,否则不可以使用

#include <iostream>
using namespace std;

// 用于任意数据类型二个数的加法模板
template<typename T>
T Add(T num1, T num2) {
	return (num1 + num2);
}

template<typename T>
void func() {
	cout << "无法使用的模板!" << endl;
}

int main() {
	 
	// 1. 自动推导类型必须推导出一致的数据类型才可以使用
	cout << Add(1, 2) << endl; // 3
	// cout << Add(1, 3.4) << endl; // Error!

	// 2. 模板必须确定出 T 的数据类型才可以使用
	// func(); // Error!

	system("pause");
	return 0;
}

函数模板案例

  1. 利用函数模板封装一个排序的函数,可以对不同数据类型数组进行排序
  2. 排序规则:从小到大;排序算法:选择排序
  3. 分别利用 char 数组和 int 数组进行测试
#include <iostream>
using namespace std;

// 交换二个数的模板
template<typename T>
void Swap(T &a, T &b) {
	T temp = a;
	a = b;
	b = temp;
}

// 打印数组模板
template<typename T>
void PrintArray(T arr[], int length) {
	for (int i = 0; i < length; i++) {
		cout << arr[i] << " ";
	}
	cout << endl;
}

// 排序模板
template<typename T>
void Sort(T arr[], int length) {
	// 选择排序
	for (int i = 0; i < length; i++) {
		int min = i; // 记录最小值的索引
		for (int j = i + 1; j < length; j++) {
			if (arr[min] > arr[j]) {
				min = j;
			}
		}
		if (min != i) { // 交换 arr[min] 和 arr[i] 元素
			Swap(arr[min], arr[i]);
		}
	}
}

int main() {
	 
	// 1. 测试 int 类型的数组
	int arr_int[5] = { 1,3,4,2,5 };
	Sort<int>(arr_int, 5);
	// 排序后输出
	PrintArray(arr_int, 5); // 1 2 3 4 5

	// 2. 测试 char 类型的数组
	char arr_ch[5] = { 'a','c','b','d','e' };
	Sort<char>(arr_ch, 5);
	// 排序后输出
	PrintArray(arr_ch, 5); // a b c d e

	system("pause");
	return 0;
}

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

区别:普通函数调用时可以发生自动类型转换(隐式类型转换);函数模板调用时,如果利用自动类型推导,不会发生隐式类型转换;如果利用指定类型的方式则可以发生自动类型转换

#include <iostream>
using namespace std;

// 普通函数
int Add1(int a, int b) {
	return (a + b);
}

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

int main() {
	 
	// 1. 普通函数实现二个数的加法
	cout << Add1(1, 3) << endl; // 4
	cout << Add1(1, 'a') << endl; // 98 -- 发生类型转换

	// 2. 函数模板实现二个数的加法
	cout << Add2(2, 3) << endl; // 5
	cout << Add2(2.2, 3.5) << endl; // 5.7
	// cout << Add2(2, 'a') << endl; // Error! -- 函数模板自动类型推导不会发生隐式转换
	cout << Add2<int>(2, 'a') << endl; // 99

	system("pause");
	return 0;
}

总结:建议使用显示指定数据类型的方式,调用函数模板时,可以自己确定通用类型 T

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

调用规则

  1. 如果同时存在普通函数和函数模板,则优先调用普通函数
  2. 可以通过空模板参数列表来强调调用函数模板
  3. 函数模板也可以发生重载
  4. 如果函数模板可以产生更好的匹配,则优先调用函数模板
#include <iostream>
using namespace std;

// 普通函数
void func(int a, int b) {
	cout << "普通函数调用,和为:" << a + b << endl;
}

// 函数模板
template<typename T>
void func(T a, T b) {
	cout << "函数模板调用,和为:" << a + b << endl;
}
// 函数模板重载
template<typename T>
void func(T a, T b, T c) {
	cout << "函数模板调用,和为:" << a + b + c << endl;
}

int main() {
	 
	// 1. 当普通函数和函数模板同时存在时 -- 优先调用普通函数
	func(1, 2); // 普通函数调用,和为:3

	// 2. 利用空模板参数列表强制调用函数模板
	func<>(1, 3); // 函数模板调用,和为:4

	// 3. 函数模板可以发生重载
	func(1, 3, 5); // 函数模板调用,和为:9

	// 4. 函数模板可以产生更好的匹配则优先调用
	// 调用函数模板时只需要将通用参数 T 看成 char 即可;而调用普通函数则会发生隐式类型转换(较麻烦)
	func('a', 'c'); // 函数模板调用,和为:196

	system("pause");
	return 0;
}

模板的局限性

示例 1:求二个数据和的模板

#include <iostream>
using namespace std;

template<typename T>
void Add(T a, T b) {
	cout << a + b << endl;
}

int main() {
	 
	// 1. 求二个整型数据的和
	Add<int>(1, 3); // 4

	// 2. 求二个数组的和
	// Add({ 1,2,3 }, { 4,5,6 }); // Error!

	system("pause");
	return 0;
}

模板并不是万能的,有些特定数据类型需要用具体化的方法做特殊实现


优化上述模板:使其功能为求二个数组所有元素的总和并输出

#include <iostream>
using namespace std;

template<typename T>
void Add_Arr(T *a, T *b, int len1, int len2) {
	// 总和
	int sum = 0;

	for (int i = 0; i < len1; i++) {
		sum += a[i];
	}
	for (int j = 0; j < len2; j++) {
		sum += b[j];
	}
	// 输出总和
	cout << "sum = " << sum << endl;
}

int main() {

	int arr1[] = { 1,2,3 };
	int arr2[] = { 4,5,6,7 };
	// arr1 数组的长度
	int len_arr1 = sizeof(arr1) / sizeof(arr1[0]); // 3
	// arr2 数组的长度
	int len_arr2 = sizeof(arr2) / sizeof(arr2[0]); // 4

	Add_Arr(arr1, arr2, len_arr1, len_arr2); // sum = 28

	system("pause");
	return 0;
}

实例 2:比较二组数据是否相等的模板

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

class Person {
public:
	// 构造函数初始化
	Person(string name, int age) {
		Name_ = name;
		Age_ = age;
	}
public:
	string Name_;
	int Age_;
};

template<typename T>
bool Compare(T a, T b) {
	if (a == b) {
		return true;
	}
	else {
		return false;
	}
}

// 利用具体化的 Person 版本实现代码,具体化优先调用
template<> bool Compare(Person a, Person b) {
	if (a.Name_ == b.Name_ && a.Age_ == b.Age_) {
		return true;
	}
	else {
		return false;
	}
}

int main() {

	// 1. 比较二个整型数据是否相等
	cout << Compare(1, 2) << endl; // 0 -- false
	cout << Compare(1, 1) << endl; // 1 -- true

	// 2. 比较二组对象是否相等
	Person p1("苏苏", 19);
	Person p2("涵涵", 18);
	cout << Compare(p1, p2) << endl; // 0 -- false
	cout << Compare(p1, p1) << endl; // 1 -- true

	system("pause");
	return 0;
}

类模板

作用

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


程序示例

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

template<typename N, typename A>
class Person {
public:
	// 构造函数初始化
	Person(N name, A age) {
		this->Name_ = name;
		this->Age_ = age;
	}
public:
	N Name_;
	A Age_;
};

int main() {

	Person<string, int> p1("Su", 19);
	cout << p1.Name_ << endl; // Su

	system("pause");
	return 0;
}

总结:类模板需要指定参数列表


类模板和函数模板的区别

  1. 类模板没有自动类型推导的使用方式
  2. 类模板在模板参数列表中可以有默认参数

解释:第一条在上述代码中已经实现;第二条表明可以将通用类型指定为 typename T = 数据类型;

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

template<typename N, typename A = int>
class Person {
public:
	// 构造函数初始化
	Person(N name, A age) {
		this->Name_ = name;
		this->Age_ = age;
	}
public:
	N Name_;
	A Age_;
};

int main() {

	Person<string> p1("Su", 19);
	cout << p1.Name_ << endl; // Su

	system("pause");
	return 0;
}

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

  1. 普通类中的成员函数一开始就可以创建
  2. 类模板中的成员函数在调用时才创建
#include <iostream>
using namespace std;
#include <string>

class Person1 {
public:
	void ShowPerson1() {
		cout << "我是 Person1" << endl;
	}
};
class Person2 {
public:
	void ShowPerson2() {
		cout << "我是 Person2" << endl;
	}
};

template<typename T>
class MyPerson {
public:
	T obj;
public:
	void func1() {
		obj.ShowPerson1();
	}
	void func2() {
		obj.ShowPerson2();
	}
};

int main() {

	MyPerson<Person1> p1;
	p1.func1(); // 我是 Person1
	// p1.func2(); // Error!

	MyPerson<Person2> p2;
	// p2.func1(); // Error!
	p2.func2(); // 我是 Person2

	system("pause");
	return 0;
}

类模板对象做函数参数

三种传参方式

  1. 指定传入的类型 – 直接显示对象的数据类型
  2. 参数模板化 – 将对象中的参数变成模板进行传递
  3. 整个类模板化 – 将这个对象类型模板化进行传递
#include <iostream>
using namespace std;
#include <string>

template<typename T1, typename T2>
class Person {
public:
	// 构造函数初始化
	Person(T1 name, T2 age) {
		this->Name_ = name;
		this->Age_ = age;
	}
	// 显示学生信息的函数
	void ShowPerson() {
		cout << "姓名:" << Name_ << " 年龄:" << Age_ << endl;
	}
public:
	T1 Name_;
	T2 Age_;
};

void printPerson1(Person<string, int> &p) {
	p.ShowPerson();
}

template<typename T1, typename T2>
void printPerson2(Person<T1, T2> &p) {
	p.ShowPerson();
}

template<typename T>
void printPerson3(T &p) {
	p.ShowPerson();
}

int main() {

	// 1. 指定传入类型
	Person<string, int> p1("Su", 19);
	printPerson1(p1); // 姓名:Su 年龄:19

	// 2. 参数模板化
	Person<string, int> p2("Du", 18);
	printPerson2(p2); // 姓名:Du 年龄:18

	// 3. 整个类模板化
	Person<string, int> p3("Qu", 8);
	printPerson3(p3); // 姓名:Qu 年龄:8

	system("pause");
	return 0;
}

类模板与继承

注意事项

  1. 当子类继承的父类是一个类模板时,子类在声明的时候要指定出父类中 T 的类型(如果不指定,编译器无法给子类分配内存)
  2. 如果想要灵活的指定出父类中 T 的类型,子类也需要变为类模板
#include <iostream>
using namespace std;
#include <string>

template<typename T>
class Base {
public:
	T Id_;
};

// class Son :public Base { // 错误!必须知道父类中 T 的数据类型,才能继承给子类
// class Son :public Base<int> { // 指定父类中 T 的数据类型
template<typename T1, typename T2>
class Son :public Base<T2> { // 子类也变成模板
public:
	Son(T1 score) {
		this->Score_ = score;
	}
	void printMessage() {
		cout << "成绩:" << Score_ << endl;
	}
public:
	T1 Score_;
};

int main() {

	Son<int, int> s(100);
	s.printMessage(); // 成绩:100

	system("pause");
	return 0;
}

类模板成员函数类外实现

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

template<typename T1, typename T2>
class Person {
public:
	// 构造函数初始化
	Person(T1 name, T2 age);
	// 显示学生信息的函数
	void ShowPerson();
public:
	T1 Name_;
	T2 Age_;
};

template<typename T1, typename T2>
Person<T1,T2>::Person(T1 name, T2 age) { // Person<T1,T2>:: 代表类模板作用域,Person:: 代表普通类作用域
	this->Name_ = name;
	this->Age_ = age;
}

template<typename T1, typename T2>
void Person<T1, T2>::ShowPerson() {
	cout << "姓名:" << Name_ << " 年龄:" << Age_ << endl;
}

int main() {

	Person<string, int> p("Su", 19);
	p.ShowPerson(); // 姓名:Su 年龄:19

	system("pause");
	return 0;
}

总结:类模板中成员函数类外实现时需要加上模板参数列表 template<typename T1, typename T2>

类模板分文件编写

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


person.h

#pragma once
#include <iostream>
using namespace std;

template<typename T1, typename T2>
class Person {
public:
	// 构造函数初始化
	Person(T1 name, T2 age);
	// 显示学生信息的函数
	void ShowPerson();
public:
	T1 Name_;
	T2 Age_;
};

person.cpp

#include "person.h"

template<typename T1, typename T2>
Person<T1, T2>::Person(T1 name, T2 age) { // Person<T1,T2>:: 代表类模板作用域,Person:: 代表普通类作用域
	this->Name_ = name;
	this->Age_ = age;
}

template<typename T1, typename T2>
void Person<T1, T2>::ShowPerson() {
	cout << "姓名:" << Name_ << " 年龄:" << Age_ << endl;
}

main.cpp

#include <iostream>
using namespace std;
#include <string>
#include "person.h"

int main() {

	Person<string, int> p("Su", 19);
	p.ShowPerson(); // 姓名:Su 年龄:19

	system("pause");
	return 0;
}

如上代码将运行出错!解决办法如下

第一种:修改 main.cpp 中的包含文件

#include <iostream>
using namespace std;
#include <string>
#include "person.cpp"

int main() {

	Person<string, int> p("Su", 19);
	p.ShowPerson(); // 姓名:Su 年龄:19

	system("pause");
	return 0;
}

第一种:将 person.h 和 person.cpp 中的内容写到一个文件中(后缀为 .hpp)

person.hpp

#pragma once
#include <iostream>
using namespace std;

template<typename T1, typename T2>
class Person {
public:
	// 构造函数初始化
	Person(T1 name, T2 age);
	// 显示学生信息的函数
	void ShowPerson();
public:
	T1 Name_;
	T2 Age_;
};

template<typename T1, typename T2>
Person<T1, T2>::Person(T1 name, T2 age) { // Person<T1,T2>:: 代表类模板作用域,Person:: 代表普通类作用域
	this->Name_ = name;
	this->Age_ = age;
}

template<typename T1, typename T2>
void Person<T1, T2>::ShowPerson() {
	cout << "姓名:" << Name_ << " 年龄:" << Age_ << endl;
}

main.cpp

#include <iostream>
using namespace std;
#include <string>
#include "person.hpp"

int main() {

	Person<string, int> p("Su", 19);
	p.ShowPerson(); // 姓名:Su 年龄:19

	system("pause");
	return 0;
}

类模板与友元

学习目标:掌握类模板配合友元函数的类内和类外实现

》》全局函数类内实现:直接在类内声明友元即可

》》全局函数类外实现:需要提前让编译器知道全局函数的存在

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

// 提前声明 Person 类
template<typename T1, typename T2>
class Person;

// 提前让编译器知道全局函数的存在
template<typename T1, typename T2>
void printMessage_2(Person<T1, T2> &p) {
	cout << "姓名:" << p.Name_ << " 年龄:" << p.Age_ << endl;
}

template<typename T1, typename T2>
class Person {
	// 1. 全局函数类内实现
	friend void printMessage_1(Person<string, int> &p) {
		cout << "姓名:" << p.Name_ << " 年龄:" << p.Age_ << endl;
	}
	// 2. 全局函数类外实现
	// 2.1 空参数列表使其为 “类模板函数”
	// 2.2 提前声明全局函数
	friend void printMessage_2<>(Person<T1, T2> &p);
public:
	// 构造函数初始化
	Person(T1 name, T2 age) {
		Name_ = name;
		Age_ = age;
	}
private: // 私有成员属性
	T1 Name_;
	T2 Age_;
};

int main() {

	// 1. 利用全局函数类内实现打印信息
	Person<string, int> p1("Su", 19);
	printMessage_1(p1); // 姓名:Su 年龄:19

	// 2. 利用全局函数类外实现打印信息
	Person<string, int> p2("Ku", 9);
	printMessage_2(p2); // 姓名:Ku 年龄:9

	system("pause");
	return 0;
}

类模板案例

案例描述:实现一个通用的数组类,要求如下

  1. 可以对内置数据类型以及自定义数据类型的数据进行存储
  2. 将数组中的数据存储到堆区
  3. 构造函数中可以传入数组的容量
  4. 提供对应的拷贝构造函数以及 operator= 防止浅拷贝问题
  5. 提供尾插法和尾删法对数组中的数据进行增加和删除
  6. 可以通过下标的方式访问数组中的元素
  7. 可以获取数组中当前元素个数和数组的容量

MyArray.hpp(通用数组模板类)

// 自己通用的数组类
#pragma once
#include <iostream>
using namespace std;

// 类模板
template<typename T>
class MyArray {
public:
	// 有参构造函数
	MyArray(int capacity) {
		this->Capacity_ = capacity;
		this->Size_ = 0;
		// 从堆区开辟数组:指针 = new 数据类型[长度];
		this->pAddress_ = new T[this->Capacity_];
	}
	// 拷贝构造函数
	MyArray(const MyArray &arr) {
		this->Capacity_ = arr.Capacity_;
		this->Size_ = arr.Size_;
		// this->pAddress_ = arr.pAddress_; // 浅拷贝

		// 深拷贝
		this->pAddress_ = new T[arr.Capacity_];
		// 将 arr 中的数据都拷贝过来
		for (int i = 0; i < arr.Size_; i++) {
			this->pAddress_[i] = arr.pAddress_[i];
		}
	}
	// operator= 防止浅拷贝
	MyArray& operator=(const MyArray& arr) {
		// 先判断原有堆区是否有数据,如果有先释放
		if (this->pAddress_ != NULL) {
			delete[]this->pAddress_;
			this->pAddress_ = NULL;
			this->Capacity_ = 0;
			this->Size_ = 0;
		}
		// 深拷贝
		this->Capacity_ = arr.Capacity_;
		this->Size_ = arr.Size_;
		this->pAddress_ = new T[arr.Capacity_];
		// 将 arr 中的数据都拷贝过来
		for (int i = 0; i < arr.Size_; i++) {
			this->pAddress_[i] = arr.pAddress_[i];
		}
		return *this;
	}
	// 尾插法
	void Push_Back(const T &value) {
		// 判断容量是否等于大小
		if (this->Capacity_ == this->Size_) {
			cout << "数组容量已满!" << endl;
			return; // 插入失败
		}
		this->pAddress_[this->Size_] = value; // 在数组尾部插入数据
		this->Size_++; // 更新数组大小
	}
	// 尾删法
	void Pop_Back() {
		// 让用户访问不到最后一个元素,为逻辑上的尾删
		if (this->Size_ == 0) {
			cout << "数组中无元素!无法进行删除操作!" << endl;
			return;
		}
		this->Size_--;
	}
	// 通过下标访问数组元素
	// 想让返回的值 arr[0] = 1 作为左值存在,则函数类型为引用
	T& operator[](int index) {
		return this->pAddress_[index];
	}
	// 返回数组容量
	int getCapacity() {
		return this->Capacity_;
	}
	// 返回数组大小
	int getSize() {
		return this->Size_;
	}
	// 析构函数
	~MyArray() {
		if (this->pAddress_ != NULL) {
			delete []this->pAddress_;
			this->pAddress_ = NULL;
		}
	}
private:
	T *pAddress_; // 指针指向堆区开辟的真实数组
	int Capacity_; // 数组容量
	int Size_; // 数组大小
};

main.cpp(测试 int 数据类型)

#include <iostream>
using namespace std;
#include "MyArray.hpp"

int main() {

	MyArray<int> arr(5); // 容量为 5 的整型数组
	// 利用尾插法插入元素
	for (int i = 0; i < arr.getCapacity(); i++) {
		arr.Push_Back(i + 1);
	}
	// 通过下标的方式访问数组中的元素
	for (int i = 0; i < arr.getSize(); i++) {
		cout << arr[i] << " "; // 1 2 3 4 5
	}
	cout << endl;
	// 打印容量及大小
	cout << "数组容量为:" << arr.getCapacity() << endl; // 数组容量为:5
	cout << "数组大小为:" << arr.getSize() << endl; // 数组大小为:5

	// 尾删法删除元素
	arr.Pop_Back();
	arr.Pop_Back();
	// 打印容量及大小
	cout << "数组容量为:" << arr.getCapacity() << endl; // 数组容量为:5
	cout << "数组大小为:" << arr.getSize() << endl; // 数组大小为:3

	system("pause");
	return 0;
}

main.cpp(测试自定义数据类型)

#include <iostream>
using namespace std;
#include "MyArray.hpp"

class Person {
public:
	// 构造函数
	Person() {}
	Person(int id, int score) {
		this->Id_ = id;
		this->Score_ = score;
	}
public:
	int Id_;
	int Score_;
};

void printPersonArray(MyArray<Person> & arr) {
	for (int i = 0; i < arr.getSize(); i++) {
		cout << "学号:" << arr[i].Id_ << " 分数:" << arr[i].Score_ << endl;
	}
}

int main() {

	MyArray<Person> arr(5);
	// 创建数据组
	Person p1(1, 100);
	Person p2(2, 90);
	Person p3(3, 80);
	Person p4(4, 70);
	Person p5(5, 60);
	// 尾插法插入数据
	arr.Push_Back(p1);
	arr.Push_Back(p2);
	arr.Push_Back(p3);
	arr.Push_Back(p4);
	arr.Push_Back(p5);
	// 输出数组
	printPersonArray(arr);
	// outputs:学号:1 分数:100
	//		   学号:2 分数:90
	//		   学号:3 分数:80
	//	       学号:4 分数:70
	//		   学号:5 分数:60

	system("pause");
	return 0;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

是我来晚了!

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值