20240214
文章目录
- C++泛型编程技术
- 模版的概念
- 函数模版
- 函数模版语法
- 不使用模版的模版完成两个数交换
- 使用模版的方式完成两个数的交换
- 模版注意事项
- 函数模版案列
- 使用模版实现升序选择排序
- 模版函数和普通函数
- 区别点
- 调用规则
- 模版的局限性
- 模版的通用性问题
- 模版重载
- 类模板
- 类模板语法
- 类模板和函数模版区别
- 类模板中成员函数创建时机
- 类模板对象做函数参数
- 指定传入类型
- 参数模板化
- 类模版化
- 类模版和继承
- 类模板成员函数类外实现
- 类模板分文件编写
- 问题
- 解决方法
- 类模板与友元
- 类模板案例
- 实现一个通用数组类
- 功能需求
- 功能实现
- 功能测试
C++泛型编程技术
模版的概念
- 模版就是建立通用的模具,大大提高通用性
- C++除了面向对象的编程思想之外,还有另一种编程思想
泛型编程
,主要利用的技术就是模版 - c++提供两种模版模版机制:
函数模版和类模板
函数模版
函数模版语法
- 函数模版作用:建立一个通用函数,其函数返回值和参数类型不具体指定,用一个虚拟的类型来表示
- 语法:
template <typename T>
函数声明或定义
-
说明:
- template:声明创建模版
- typename:表名其后面的符号是一种数据类型,可以用class代替
- T:通用数据类型,名称可以替换,通常为大写英文字母
-
函数模版使用方式
- 自动类型推导:
swap(a, b)
- 显示指定类型:
swap<int>(a, b)
- 自动类型推导:
不使用模版的模版完成两个数交换
- 假设现在有交换整数和交换浮点数的需求
- 后面可能还会有交换其他类型数的需求
- 这样每多增一种类型比较就需要重载一份类型不同的交换函数
- 代码示例
#include <iostream>using namespace std;// 交换两个整形数
void swapNum(int &a, int &b) {int tmp = a;a = b;b = tmp;
}// 交换两个浮点型数
void swapNum(float &a, float &b) {float tmp = a;a = b;b = tmp;
}void test() {int a = 10, b = 20;cout << "整形交换前 a: " << a << " b: " << b << endl;swapNum(a, b);cout << "整形交换后 a: " << a << " b: " << b << endl;float c = 3.14, d = 5.21;cout << "整形交换前 c: " << c << " d: " << d << endl;swapNum(c, d);cout << "整形交换后 c: " << c << " d: " << d << endl;
}int main() {test();return 0;
}
使用模版的方式完成两个数的交换
- 根据上面不使用模版的方式,交换两个数时只是进行函数重载,变更交换的数据类型
- 这种情况恰好使用泛型编程的思想,参数类型不定其它都确定
- 代码示例
#include <iostream>using namespace std;template<typename T>
void swapNum(T &a, T &b) {T tmp = a;a = b;b = tmp;
}void test() {int a = 10, b = 20;cout << "整形交换前 a: " << a << " b: " << b << endl;// 自动类型推导swapNum(a, b);cout << "整形交换后 a: " << a << " b: " << b << endl;float c = 3.14, d = 5.21;cout << "整形交换前 c: " << c << " d: " << d << endl;// 显示指定类型 swapNum<float>(c, d);cout << "整形交换后 c: " << c << " d: " << d << endl;
}int main() {test();return 0;
}
- 结果示例
模版注意事项
- 使用自动推导的方式时,推导出来的数据类型必须一致才可以使用
template<typename T>
void swapNum(T &a, T &b) {T tmp = a;a = b;b = tmp;
}void test1() {int a = 10;char b = 20;// error: no matching function for call to 'swapNum'// candidate template ignored: deduced conflicting types for parameter 'T' ('int' vs. 'char') // swapNum(a, b);
}
- 模版必须要确定出数据类型T才可以使用
#include <iostream>using namespace std;template<class T>
void func() {cout << "func()" << endl;
}// 模版必须要确定出类型T才可以使用
void test1() {// error: no matching function for call to 'func'// func();func<int>();
}int main() {test1();return 0;
}
函数模版案列
使用模版实现升序选择排序
- 需求分析
- 首先要实现主要的升序排序函数
- 在排序中需要实现交换函数
- 在排序前后要实现不同类型的数组打印
- 代码示例
#include <iostream>using namespace std;// 定义模版交换
template<class T>
void swapNum(T &a, T &b) {T tmp = a;a = b;b = tmp;
}// 定义打印模版
template<class T>
void printArr(const T &arr) {for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++) {cout << arr[i] << " ";}cout << endl;
}// 定义模版选择排序
template<class T>
void selectAscSort(T *arr, int count) {for (int i = 0; i < count; i++) {for (int j = 0; j < count - i - 1; j++) {if (arr[j] > arr[j + 1]) {swapNum(arr[j], arr[j + 1]);}}}
}// 测试整形数组排序
void testInt() {int arr[5] = {3, 6, 1, 5, 9};cout << "整型数组排序前:";printArr(arr);selectAscSort<int>(arr, sizeof(arr) / sizeof(arr[0]));cout << "整型数组排序后:";printArr(arr);
}// 测试浮点型数组排序
void testDouble() {double arr[10] = {3.12, 4.32, 1.65, 5.6, 7.91, 5.05, 0.19, 4.32, 8.22, 3.97};cout << "浮点型数组排序前:";printArr(arr);selectAscSort<double>(arr, sizeof(arr) / sizeof(arr[0]));cout << "浮点型数组排序后:";printArr(arr);
}int main() {testInt();testDouble();return 0;
}
- 结果示例
模版函数和普通函数
区别点
- 普通函数调用时,可以发生自动类型转换(隐式类型转换)
#include <iostream>using namespace std;// 普通函数
int myAdd(int a, int b) {return a + b;
}// 普通函数类型自动类型转换测试
void test1() {int a = 10;int b = 10;char c = 'a';cout << "调用普通函数不发生隐式类型转换:" << myAdd(a, b) << endl;cout << "调用普通函数发生隐式类型转换:" << myAdd(a, c) << endl;
}int main() {test1();return 0;
}
- 模版函数调用时,如果利用自动类型推导,不会发生自动类型转换
- 如果利用显示指定类型方式时,可以发生自动类型转换
#include <iostream>using namespace std;// 定义模版加法函数
template<typename T>
T myAdd(T a, T b) {return a + b;
}void test() {int a = 1;char b = 'a';// 调用时使用自动类型推导不发生隐式类型转换// candidate template ignored: deduced conflicting types for parameter 'T' ('int' vs. 'char')// cout << "自动类型推导时不发生隐式类型转换:" << myAdd(a, b) << endl;// 显示指定类型调用时,发生自动类型转换cout << "显示指定类型调用时,发生自动类型转换:" << myAdd<int>(a, b) << endl;
}int main() {test();return 0;
}
调用规则
- 如果普通函数和模版函数都可以实现,优先调用普通函数
- 可以通过空模版参数列表来强制调用函数模版
- 函数模版也可以发生重载
- 如果函数模版可以产生更好的匹配,优先使用函数模版
- 代码示例
#include <iostream>using namespace std;// 普通函数
int add(int a, int b) {cout << "int add(int a, int b)" << endl;return a + b;
}// 模版函数
template<typename T>
T add(T a, T b) {cout << "T add(T a, T b)" << endl;return a + b;
}// 模版函数
template<typename T>
T add(T a, T b, T c) {cout << "T add(T a, T b, T c)" << endl;return a + b + c;
}void test() {int a = 10;int b = 20;int c = 20;// 如果普通函数和模版函数都可以实现,优先调用普通函数add(a, b);// 可以通过空模版参数列表来强制调用函数模版add<>(a, b);// 函数模版也可以发生重载add(a, b, c);// 如果函数模版可以产生更好的匹配,优先使用函数模版char c1 = 'a', c2 = 'b';// char可以隐式类型转换成int,也可以直接调用add(char, char),显然模版的匹配更好add(c1, c2);
}int main() {test();return 0;
}
模版的局限性
模版的通用性问题
- 例如给类型直接传数组或者自定类型,在类似赋值之类的操作中就不能直接使用
#include <iostream>using namespace std;template<class T>
void func(T a, T b) {if (a == b) {cout << "a == b" << endl;} else {{cout << "a != b" << endl;}}return;
}// 普通类型直接比没问题
void test1() {int a = 1;int b = 2;func(a, b);
}void test2() {int a[3] = {1, 2, 3};int b[3] = {1, 2, 3};// a != b func(a, b);
}struct A {int a;
};void test3() {A a1;A a2;// error: invalid operands to binary expression ('A' and 'A')// func(a1, a2);
}int main() {test1();test2();test3();return 0;
}
模版重载
- C++为了解决模版通用性的局限问题,可以为这些特殊的类型提供具体化的模版
- 使用具体化模版比较自定义数据类型
#include <iostream>using namespace std;template<class T>
void func(T a, T b) {if (a == b) {cout << "a == b" << endl;} else {{cout << "a != b" << endl;}}return;
}struct A {int a;
};// 实现A结构具体化的比较操作
template<>
void func(struct A a, struct A b) {if (a.a == b.a) {cout << "a.a == b.a" << endl;} else {{cout << "a.a != b.a" << endl;}}return;
}void test() {struct A a1;struct A a2;a1.a = 1;a2.a = 2;// error: invalid operands to binary expression ('A' and 'A')func(a1, a2);
}int main() {test();return 0;
}
类模板
类模板语法
- 类模板作用:建立一个通用类,类中的成员数据类型可以不具体指定,用一个虚拟的类型来表示
- 语法:
template <class T>
类
- 类模板代码示例
#include <iostream>using namespace std;template<class NameType, class AgeType>
class Person {
public:Person(NameType name, AgeType age) {this->name = name;this->age = age;}NameType name;AgeType age;
};void test() {Person<string, int> p("zsx", 18);cout << p.name << " " << p.age << endl;
}int main() {test();return 0;
}
类模板和函数模版区别
- 类模板没有自动类型推导的使用方式
- 类模板在模版参数中可以有默认参数
- 代码示例
#include <iostream>using namespace std;//AgeType=int 给定 AgeType 的默认参数为int
template<class NameType, class AgeType=int>
class Person {
public:Person(NameType name, AgeType age) {this->name = name;this->age = age;}NameType name;AgeType age;
};void test() {// 不能进行自动类型推导// too few template arguments for class template 'Person'// Person<> p("zsx", 18);// 缺省使用AgeType的默认参数列表类型intPerson<string> p("zsx", 18);cout << p.name << " " << p.age << endl;
}int main() {test();return 0;
}
类模板中成员函数创建时机
- 类模板中的成员函数并不是在一开始创建的,而是在模版调用时在生成
- 代码示例
#include <iostream>using namespace std;class P1 {
public:void showPInfo1() {cout << "P1 showPInfo()" << endl;}
};class P2 {
public:void showPInfo2() {cout << "P2 showPInfo()" << endl;}
};template<class T>
class myClass {
public:// 类模板中的成员函数并不是在一开始创建的,而是在模版调用时在生成void func1() {obj.showPInfo1();}void func2() {obj.showPInfo2();}T obj;
};void test() {myClass<P1> p1;p1.func1();// showPInfo2 不是P1中的对象// error: no member named 'showPInfo2' in 'P1'// p1.func2();
}int main() {test();return 0;
}
类模板对象做函数参数
- 类模板实例化出对象,向函数传参的方式
- 创建类模版代码示例
- 通过类模板创建对象,向函数中进行传参的方式
- 指定出入类型(最常用的方式)
- 参数模版化
- 类模版化
#include <iostream>using namespace std;template<class T1, class T2>
class Person {
public:Person(T1 name, T2 age) {this->name = name;this->age = age;}void showPerson() {cout << "姓名:" << name << " 年龄:" << age << endl;}T1 name;T2 age;
};
指定传入类型
- 直接显示对象的数据类型
- 代码示例
// 指定传入类型
void printPerson1(Person<string, int> &p) {p.showPerson();
}void test1() {Person<string, int> p("zs", 12);printPerson1(p);
}
参数模板化
- 将对象中的参数变为模版进行传递
- 代码示例
// 参数模板化
template<class T1, class T2>
void printPerson2(Person<T1, T2> &p) {p.showPerson();cout << "查看T1传入的数据类型:" << typeid(T1).name() << endl;cout << "查看T2传入的数据类型:" << typeid(T2).name() << endl;
}void test2() {Person<string, int> p("li", 13);printPerson2(p);
}
类模版化
- 将这个对象类型模板化进行传递
- 代码示例
// 类模版化
template<class T>
void printPerson3(T &p) {p.showPerson();cout << "查看T传入的数据类型:" << typeid(T).name() << endl;
}void test3() {Person<string, int> p("ww", 18);printPerson3(p);
}
类模版和继承
-
当子类继承的父类是一个类模板的时候,子类在声明的时候,要指出父类中T的类型
-
如果不指定,编译器无法给子类分配内存
-
如果想灵活指定父类中T的类型,子类也需要变为类模板
-
代码示例
#include <iostream>using namespace std;template<class T>
class Base {
public:T m;
};// 字了继承父类时指定父类的类型
class B1 : public Base<string> {
};void test1() {B1 b;cout << "父类的类型是:" << typeid(b.m).name() << endl;
} 子类继承父类不指定父类的类型
//class B2 : public Base {
//};void test2() {// 子类继承父类不指定父类的类型不能实例化对象// error: expected class name// B2 b;
}// 子类继承父类,想灵活指定父类中T的类型,子类也需要变为类模板
template<class T1, class T2>
class B3 : public Base<T2> {
public:T1 t;
};void test3() {B3<int, int> b;cout << "父类的类型是:" << typeid(b.m).name() << endl;cout << "子类的类型是:" << typeid(b.t).name() << endl;
}int main() {test1();test2();test3();return 0;
}
类模板成员函数类外实现
#include <iostream>using namespace std;template<class T>
class AAA {
public:AAA(T a);void show();T a;
};// 类模板构造函数类外实现
template<class T>
AAA<T>::AAA(T a) {this->a = a;
}// 类模板普通函数类外实现
template<class T>
void AAA<T>::show() {cout << a << endl;
}void test() {AAA<int> Aaa(1);Aaa.show();
}int main() {test();return 0;
}
类模板分文件编写
问题
- 按照将类声明和类实现分文件的方式,将类模版文件的声明和类实现分文件的方式,这个时候在调用时就会发生链接错误
linker command failed with exit code 1
解决方法
-
方式一:包含.cpp文件
-
可以解决但不常用
-
底层原理:类模板中的成员函数是在创建的时候生成的,如果只引入头文件,在调用时只会看到类及其成员函数的声明找不到实现方法;而引入cpp文件,则可以通过cpp文件中引入的头文件,以及cpp中的实现,从而解决这个问题
-
-
方式二:将类模板的声明和定义都写在.hpp文件
类模板与友元
-
实现方式
-
全局函数类内实现
-
全局函数内外实现
-
-
代码示例
#include <iostream>using namespace std;// 需要让编译器提前知道Pp类
template<class T1, class T2>
class Pp;// 需要将全局函数的类外实现放到最前面提前让编译器知道
template<class T1, class T2>
void printPerson2(Pp<T1, T2> p) {cout << "name: " << p.name << " age: " << p.age << endl;
}template<class T1, class T2>
class Pp {
public:// 1.全局函数类内实现friend void printPerson1(Pp<T1, T2> p) {cout << "name: " << p.name << " age: " << p.age << endl;}// 2.全局函数类外实现// 加空模版参数列表,如果全局函数需要在类外实现,需要让编译器提前知道这个函数存在friend void printPerson2<>(Pp<T1, T2> p);Pp(T1 name, T2 age) {this->name = name;this->age = age;}private:T1 name;T2 age;
};void test() {Pp<string, int> p1("zsx", 18);printPerson1(p1);printPerson2(p1);
}int main() {test();return 0;
}
类模板案例
实现一个通用数组类
功能需求
-
可以对内置数据类型以及自定义数据类型的数据进行存储
-
将数组中的数据存储到堆区
-
构造函数中可以传入数组的容量
-
提供对应的拷贝构造函数以及operator=防止浅拷贝问题
-
提供尾插法和尾删法对数组中的数据进行增加和删除
-
可以通过下标的方式访问数组中的元素
-
可以获取数组中当前元素个数和数组的容量
功能实现
#pragma once#include <iostream>
#include <string>using namespace std;template<class T>
class MyArray {
public:// 可以对内置数据类型以及自定义数据类型的数据进行存储// 构造函数中可以传入数组的容量MyArray(int cap) {// cout << "MyArray 有参构造函数" << endl;this->cap = cap;this->len = 0;this->pArr = new T[this->cap];}MyArray(const MyArray &ma) {// cout << "MyArray 拷贝构造函数" << endl;// 深拷贝this->pArr = new T[ma.cap];this->len = ma.len;this->cap = ma.cap;// 拷贝原来数组中的元素for (int i = 0; i < ma.len; i++) {this->pArr[i] = ma.pArr[i];}return;}// 提供对应的拷贝构造函数以及operator=防止浅拷贝问题// MyArray & 需要返回自身的引用,这样可以在使用的地方支持连等操作MyArray &operator=(const MyArray<T> &ma) {// cout << "operator=赋值运算符重载函数" << endl;// 需要先判断原来的数组中是否有元素,如果已经有了需要先将原来堆区数据释放干净if (this->pArr != NULL) {delete[] this->pArr;this->pArr = NULL;this->len = 0;this->cap = 0;}// 深拷贝this->pArr = new T[ma.cap];this->len = ma.len;this->cap = ma.cap;// 拷贝原来数组中的元素for (int i = 0; i < ma.len; i++) {this->pArr[i] = ma.pArr[i];}return *this;}// 提供尾 插法和尾删法对数组中的数据进行增加和删除void PushTail(T *elem) {if (this->len == this->cap) {cout << "数组已满,请扩容" << endl;return;}this->pArr[this->len] = *elem;this->len++;}void PopTail() {if (this->len == 0) {cout << "数组已空" << endl;return;}this->len--;}// 可以通过下标的方式访问数组中的元素// 返回&是为了可以支持链式左值操作T &operator[](int index) {return this->pArr[index];}// 可以获取数组中当前元素个数和数组的容量int GetArrLen() {return this->len;}int GetArrCap() {return this->cap;}friend void printArray(MyArray ma) {cout << "打印arr内容:";for (int i = 0; i < ma.len; i++) {cout << ma.pArr[i] << " ";}cout << endl;}// 析构函数~MyArray() {// cout << "MyArray 析构函数" << endl;if (this->pArr != NULL) {delete[] this->pArr;this->pArr = NULL;}}private:// 指针指向堆区开辟的真实数组T *pArr;// 数组中元素个数int len;// 数组容量int cap;
};
功能测试
#include "myArray.hpp"void test() {MyArray<int> ma1(10);MyArray<int> ma2(ma1);MyArray<int> ma3(100);ma3 = ma1;for (int i = 0; i < 4; i++) {ma3.PushTail(&i);}cout << ma3.GetArrLen() << endl;cout << ma3.GetArrCap() << endl;printArray(ma3);cout << ma3[2] << endl;ma3.PopTail();cout << ma3.GetArrLen() << endl;
}// 测试自定义数据类型
class PersonArr {
public:PersonArr() {}PersonArr(string name, int age) {this->name = name;this->age = age;}string name;int age;
};void printPersonArr(MyArray<PersonArr> &arr) {for (int i = 0; i < arr.GetArrLen(); i++) {cout << arr[i].name << " " << arr[i].age << endl;}cout << endl;
}void test1() {MyArray<PersonArr> arr(10);PersonArr p1("zsx1", 12);PersonArr p2("zsx2", 13);PersonArr p3("zsx3", 14);PersonArr p4("zsx4", 15);arr.PushTail(&p1);arr.PushTail(&p2);arr.PushTail(&p3);arr.PushTail(&p4);arr.PopTail();cout << arr.GetArrLen() << endl;cout << arr.GetArrCap() << endl;printPersonArr(arr);
}int main() {test();test1();return 0;
}