引言
在编程的世界里,泛型编程思想(模板化思想)是一种极具魅力的编程范式。它允许我们编写出具有高度通用性和可重用性的代码,极大地提高了开发效率和代码质量。无论你是初学者还是有一定经验的开发者,掌握泛型编程思想都至关重要。在本博客中,小杨将带你深入浅出地了解泛型编程思想,让你一次性学懂这一重要概念。
1.模板的概念
什么是模板呢?模板就是建立的一种通用的模具,模式,做法,来提高做事做产品的效率,提高复用性。编程中的模板可以提升代码可以提升代码的复用性。它允许在编程时使用抽象类型而非具体的类型。这种范式使得算法和数据处理方法可以独立于它们操作的数据类型,从而提高代码的复用性和灵活性。泛型编程的核心思想是编写尽可能通用的代码,这些代码可以在多种数据类型上工作,而不需要对每种数据类型都写一套单独的代码。
我们学习模板,主要是学习STL(标准模板库)的使用,因为STL大量使用了模板技术来实现。
2.函数模板
2.1函数模板的语法
函数模板的作用:建立一个通用的函数,它的返回值类型和参数类型不具体指定,而是用一个虚拟的类型(泛型)来表示。
语法:在函数头的上一行添加声明:template<typename T>或者template<class T>
使用:自动类型推导、显式指定泛型类型
代码示例:
#include <iostream>using namespace std;// 写函数,完成两个值的交换// 1.交换两个int
void mySwap(int& a, int& b)
{int temp = a;a = b;b = temp;
}
// 2.交换两个double,需要继续定义函数,使用同名函数来重载
void mySwap(double& a, double& b)
{double temp = a;a = b;b = temp;
}
// 接下来使用模板技术,实现任意类型的值交换
template<typename T>
void mySwap(T& a, T& b)
{T temp = a;a = b;b = temp;
}
// 使用模板技术可用下边一个函数模板代替上述重载函数两个函数。
// 下面测试函数mySwap()
void test01()
{// 交换两个intint n1 = 10;int n2 = 20;mySwap(n1, n2);cout << "n1=" << n1 << ",n2=" << n2 << endl;// 交换两个doubledouble d1 = 2.45;double d2 = 24.2;mySwap(d1, d2);cout << "d1=" << d1 << ",d2=" << d2 << endl;// 交换两个charchar c1 = 'a';char c2 = 'b';mySwap(c1,c2);cout << "c1=" << c1 << ",c2=" << c2 << endl;// 以上使用的是自动类型推导的方式,来确定T的类型// 接下来使用显式手动的方式,指定泛型T的类型int n3 = 5;int n4 = 8;mySwap<int>(n3, n4);cout << "n3=" << n3 << ",n4=" << n4 << endl;// mySwap<int>(c1,c2);// 报错了,因为显式指定了泛型为int,那就要传入int参数,不能传入char
}
2.2函数模板的注意事项
- 自动类型推导时,必须推导出一致的类型T
- 模板必须确定出T的类型才可以使用。如果不能自动推导,就需要手动指定。
- 代码示例:
template<class T>
void func(){ cout << "func被调用" << endl; }
void test02()
{// 1)自动类型推导时,必须推导出一致的类型Tint n = 10;char c = 'a';// mySwap(n, c); // 报错,因为n推导T为int,而c推导T为char,不一致// 2)模板必须确定出T的类型才可以使用。如果不能自动推导,就需要手动指定。// func();// 报错,因为T的类型无法确定func<int>(); func<double>();// 可以通过手动指定T的类型来使用
}
// 课堂练习
// 1.写一个函数模板,实现两个值的大小比较,返回较大的那个值
template<class T>
T getLarger(T a, T b) { return a > b ? a : b; } // 三元运算符
void test03()
{int n1 = 10;int n2 = 20;double d1 = 2.54;double d2 = 5.43;cout << "n1和n2的比较结果:" << getLarger(n1, n2) << endl;cout << "d1和d2的比较结果:" << getLarger(d1, d2) << endl;
}
// 2.写一个函数模板,可以返回一个数组中的最大值
template<class T>
T getMax(T arr[], int len)
// 注意:数组传参不是传递整个数组,而是数组的首元素地址,这里T arr[]相当于T* arr,既然传的是地址,那就会丢失长度信息,所以必须增加第二个参数传长度信息
{// 遍历数组,找出最大值,返回T temp = arr[0];for (int i = 1; i < len; i++){if (arr[i]>temp){temp = arr[i];}}return temp;
}
void test04()
{// 整型数组int int_arr[] = { 4,33,64,2,35,41,34,17 };cout << "int_arr数组中的最大值:" << getMax(int_arr, 8) << endl;// char型数组char char_arr[] = { 'a','b','c','d' };cout << "char_arr数组中的最大值:" << getMax(char_arr, 4) << endl;
}
2.3普通函数和函数模板的区别
- 普通函数调用时可以发生自动类型转换(隐式类型转换)
- 函数模板调用时,如果利用自动类型推导,不会发生隐式类型转换。如果显式指定泛型类型,也可以发生隐式类型转换。
- 建议:使用显式指定泛型的方式来调用函数模板。
- 代码示例:
// 普通函数
int add(int a, int b) { return a + b; }
// 函数模板
template<class T>
T add_tpl(T a, T b) { return a + b; }
void test05()
{// 1)普通函数调用时可以发生自动类型转换(隐式类型转换)int a = 10;char c = 'a';cout << add(a, c) << endl;// 这里发生了隐式类型转换,将char型转成了int,然后进行运算,而'a'字符的ASCII码是97,所以转成了97,结果是10+97=107// 2)函数模板调用时,如果利用自动类型推导,不会发生隐式类型转换。如果显式指定泛型类型,也可以发生隐式类型转换// cout << add_tpl(a, c) << endl;// 报错,不会发生隐式类型转换,所以推导出不一致的类型,报错cout << add_tpl<int>(a, c) << endl; // 显式指定泛型的类型后,发生了隐式类型转换
}
2.4普通函数和函数模板的调用区别
- 如果普通函数和函数模板都可以实现,优先调用普通函数(具体的高于通用的)
- 可以通过空模板参数列表的语法来强制调用模板。<>
- 函数模板也可以发生重载(多个同名的函数模板,根据参数来重载)
- 建议:既然写了函数模板,就没必要再写普通函数了。
- 代码示例:
void myPrint(int a, int b)
{ cout << "a=" << a << ",b=" << b << ",调用普通函数" << endl;
}
template<class T>
void myPrint(T a,T b)
{ cout << "a=" << a << ",b=" << b << ",调用函数模板,两个参数" << endl;
}
template<class T>
void myPrint(T a, T b,T c)
{cout << "a=" << a << ",b=" << b<<",c="<<c << ",调用函数模板,三个参数" << endl;
}
void test06()
{// 1)如果普通函数和函数模板都可以实现,优先调用普通函数(具体的高于通用的)int a = 10;int b = 20;myPrint(a, b);// 优先调用的是普通函数// 2)可以通过空模板参数列表的语法来强制调用模板。<>myPrint<>(a, b);// 强制调用模板// 3)函数模板也可以发生重载(多个同名的函数模板,根据参数来重载)int c = 30;myPrint(a, b, c);// 这里发生重载,自动调用匹配的函数char c1 = 'a';char c2 = 'b';myPrint(c1, c2);// 这里调用两个参数的模板,因为它才能匹配,其他的不匹配
}
2.5函数模板的局限性
模板的通用性不是万能的,在遇到自定义类型的数据时,就无法处理了。
- 代码示例:
class Student
{
public:string m_Name;int m_Age;Student(string name, int age){m_Name = name;m_Age = age;}
};
//比较两个值是否相等
template<class T>
bool myCompare(T a, T b)
{if (a==b){return true;}else{return false;}
}
//写个重载函数,专门用于对象的比较
bool myCompare(Student& s1, Student& s2)
{if (s1.m_Name == s2.m_Name and s1.m_Age==s2.m_Age){return true;}else{return false;}
}
void test07()
{int n1 = 10;int n2 = 20;cout << "n1和n2比较结果:" << myCompare(n1, n2) << endl;Student s1("Tom", 10);Student s2("Lucy", 12);Student s3("Lucy", 12);cout << "s1和s2比较结果" << myCompare(s1, s2) << endl;cout << "s2和s3比较结果" << myCompare(s2, s3) << endl;
}
3. 类模板
类模板的作用:建立一个通用的类,类中的数据成员不具体指定类型,也用泛型来代替。此时我们可能需要多个泛型。
3.1类模板的语法
在类前面加一行声明即可template<typename T,...>
或者template<class T,...>
注意:类模板和函数模板的最大区别是:类模板不能自动推导泛型,必须显示指定泛型的类型才可以使用。
代码示例:
#include <iostream>
#include <string>
using namespace std;// 3.1类模板的语法
template<class NameType,class AgeType=int>
class Person
{NameType m_Name;AgeType m_Age;
public:Person(NameType name, AgeType age){m_Name = name;m_Age = age;}void show() { cout << "name:" << m_Name << ",age:" << m_Age << endl; }
};
void test01()
{Person<string, int> p1("tom", 10);// 类模板使用时必须指定泛型类型,不能自动推导Person<string, string> p2("john","八岁");Person<string> p3("lucy", 12);// AgeType使用默认值p1.show();p2.show();p3.show();
}
3.2类模板结合函数模板来使用
当类模板实例化出来的对象作为参数传递的时候,常见的有以下几种方式:
1)指定传入的泛型类型(使用较多的,因为便于理解,灵活度不高)
2)对象参数模板化(进一步使用函数模板将对象的泛型模板化,提高灵活度)
3)整个对象模板化(对象用泛型代替,灵活度最高)
代码示例:
// 1)指定传入的泛型类型(使用较多的,因为便于理解,灵活度不高)
void show_type(Person<string, int>& p) { p.show(); }
// 2)对象参数模板化(进一步使用函数模板将对象的泛型模板化,提高灵活度)
template<class T1,class T2>
void show_tpl_para(Person<T1, T2>& p)
{p.show();cout << "T1的类型是:" << typeid(T1).name() << endl;cout << "T2的类型是:" << typeid(T2).name() << endl;
}
// 3)整个对象模板化(对象用泛型代替,灵活度最高)
template<class T>
void show_tpl_class(T& p)
{p.show();cout << "T的类型是:" << typeid(T).name() << endl;
}
// 再写一个学生类,来测试整个对象模板化
class Student
{
public:string m_Name;int m_Age;Student(string name, int age){m_Name = name;m_Age = age;}void show() { cout << "name:" << m_Name << ",age:" << m_Age << endl; }
};
void test02()
{Person<string, int> p1("tom", 10);// 类模板使用时必须指定泛型类型,不能自动推导Person<string, string> p2("john", "八岁");Person<string> p3("lucy", 12);//AgeType使用默认值// 接下来将上面几个对象作为函数参数使用// 1)指定传入的泛型类型(使用较多的,因为便于理解,灵活度不高)show_type(p1);// 必须传入Person<string,int>类型的对象才可以// show_type(p2);//类型不匹配,则报错// 2)对象参数模板化(进一步使用函数模板将对象的泛型模板化,提高灵活度)show_tpl_para(p1);// 自动类型推导show_tpl_para<string, string>(p2);// 手动指定泛型类型// 3)整个对象模板化(对象用泛型代替,灵活度最高)show_tpl_class(p1);show_tpl_class(p2);Student s1("jerry", 9);show_tpl_class(s1);// 即使使用其他类型的对象,也没问题
}
3.3类模板遇到继承
- 如果父类是类模板,子类既可以是类模板,也可以是普通类。
- 父类子类都是类模板,子类和父类的泛型可以不同,各自指定。
- 父类子类都是类模板,让它们使用同一个泛型。
- 代码示例:
//定义父类水果类Fruit,是个类模板
template<class T>
class Fruit
{
public:Fruit();// 只在这声明,去类外实现
};
// 在函数外实现的时候,也需要加模板声明
template<class T>
Fruit<T>::Fruit() { cout << "Fruit类在构造,泛型类型是:" << typeid(T).name() << endl; }// 1)如果父类是类模板,子类既可以是类模板,也可以是普通类。
// 子类,是个普通类,继承父类的时候指定父类的泛型类型
class Apple :public Fruit<int>
{
public:Apple(){ cout << "Apple类在构造,它是普通类,同时父类被指定成int" << endl; }
};
// 2)父类子类都是类模板,子类和父类的泛型可以不同,各自指定。
template<class T>
class Banana :public Fruit<int>
{
public:Banana() { cout << "Banana类在构造,它是类模板,他的泛型是:" << typeid(T).name() << ",他的父类也是类模板,父类的泛型是int" << endl; }
};
// 3)父类子类都是类模板,让它们使用同一个泛型。
template<class T>
class Orange :public Fruit<T>
{
public:Orange() { cout << "Orange类在构造,它是类模板,它的父类也是类模板,并且它们共用一个泛型:" << typeid(T).name() << endl; }
};
void test03()
{Apple a;// Apple是普通类,它的父类是类模板,父类的泛型是intBanana<double> b;// Banana和父类Fruit都是类模板,各有各的泛型类型Orange<long> o;// Orange和父类Fruit都是类模板,并且共用同一泛型类型
}
结语
通过小杨的介绍,相信你已经对泛型编程思想(模板化思想)有了更深入的了解。泛型编程不仅能够提高代码的通用性和可重用性,还能够降低代码的复杂性,使得我们的程序更加健壮和易于维护。虽然泛型编程的概念相对抽象,但只要我们通过实践和思考,逐渐掌握其精髓,就能在编程的道路上更进一步。
希望本章能为你打开泛型编程思想的大门,激发小伙伴们深入学习的兴趣。编程之路漫长而充满挑战,但只要我们勇于探索和不断学习,定能在这条道路上越走越远。接下来,就让我们一起努力,将泛型编程思想融入到实际开发中,提升我们的编程技能吧。小伙伴们要加油呀!!!!!