作者主页
📚lovewold少个r博客主页
⚠️本文重点:c++模板初阶知识点讲解
👉【C-C++入门系列专栏】:博客文章专栏传送门
😄每日一言:花有重开日,人无再少年
目录
前言
泛型编程
函数模板
函数模板概念
函数模板格式
函数模板的原理
函数模板的实例化
模板参数的匹配原则
类模板
类模板的定义格式
类模板的实例化
总结
前言
C++是一门面向对象的语言,很多情况下我们不需要在编写程序时候去过多的考虑底层。而在前面我们学习C++的输入输出好像就是这样子,编译器会自动帮我们做很多的事情,而不需要自己去传递输入输出的变量类型等因素。这种方式肯定对于编写程序的人来讲是轻松的。世界上的各种科技的进步其实都离不开人对于懒惰的追求,对于方便的执着,而今天我要讲解的模板好像就算是一种特殊的产物。
首先我们先写一个比较简单的程序来细致的探究一下我们为何需要模板。这里我们写了一个用于整形变量交换的函数,交换函数无论是在什么排序亦或者一些计算的时候都是常用函数。
void swap(int& x, int& y) { int temp = x; x = y; y = temp; } int main() { int a = 10; int b = 5; cout << "a=" << a << " " << "b=" << b << endl; swap(a, b); cout << "swap~" << endl; cout << "a=" << a << " " << "b=" << b << endl; return 0; }
问题来了,我们这里需要特别强调这里是用于整形类型的交换函数,因此这对于其他类型变量并不合适。那么我们如何去实现长整形,短整形,浮点型,字符型······。函数重载?
C++提供了函数重载的方式,对于一些传参会直接以其传递参数而决定对应的函数。
但是缺陷也很明显,我们的函数仅仅是类型不同,那么多类型我们都需要去重载么。亦或者我们先创建我们需要的类型,等到新类型出现的时候,用户再自己去增加对应的函数重载么。
另一个关键点是,我们这里仅仅只是交换函数,比较简单,而对于一份各种函数相互嵌套的代码,一份代码出错,其他重载函数全得改,代码的可维护性比较低。
泛型编程
我们再看先前的代码,会发现仅仅只是类型的不同罢了,我们可不可以提供一种方式和cout以及cin一样,把类型识别的任务交给编译器去完成,自己只需要给他传递参数变量即可。也就是说我们只需要提供一份代码作为模具,编译器可以根据不同的类型利用这个模具生成相应的代码。
void swap(int& x, int& y)
{
int temp = x;
x = y;
y = temp;
}
void swap(double& x, double& y)
{
double temp = x;
x = y;
y = temp;
}
void swap(char& x, char& y)
{
char temp = x;
x = y;
y = temp;
}
int main()
{
int a = 10;
int b = 5;
cout << "a=" << a << " " << "b=" << b << endl;
swap(a, b);
cout << "swap~" << endl;
cout << "a=" << a << " " << "b=" << b << endl;
return 0;
}
无论是活字印刷术还是现在的模具浇筑技术,其根本的目的就是维持其功能一致即可,你可以注入不同的材料以改变其最后成品的效果,但本质上实现的功能是一样的,外形是一致的。
之所以cv工程师能有独特的cv大法,也是因为前人拥有特定的已经可以使用的板子,而只需要去改吧改吧然后切合自己的工程内容即可实现一个全新的项目成果。这也就是一种代码复用的常规手段而已。
话不多说,接下来直入正题。我们先谈一谈何为泛型编程。
泛型编程是一种编程范式,其目标是编写与特定数据类型无关的通用代码,以便更广泛地重用代码。泛型编程使得程序员可以编写与数据类型无关的算法和数据结构,从而提高代码的灵活性、可重用性和可维护性。
具体而言,泛型编程通过使用参数化类型(parameterized types)来实现。参数化类型是一种允许在代码中使用未指定具体类型的抽象类型。这样,可以编写算法和数据结构,而不必在编写时指定具体的数据类型。在需要使用这些算法和数据结构的地方,可以通过提供具体的类型来实现参数的具体化。
在C++中,泛型编程主要通过模板来实现。模板允许程序员编写与数据类型无关的代码,可以用于不同的数据类型。这使得在不同的上下文中重用代码成为可能。例如,可以编写通用的排序算法、容器类、以及其他算法和数据结构,而不必为每种数据类型都编写一套特定的代码。而模板就是泛型编程的基础。
函数模板
函数模板概念
函数模板是C++中用于创建通用函数的一种机制,允许程序员编写与特定数据类型无关的函数代码。函数模板通过使用参数化类型来实现,使得可以在编写代码时使用未指定具体类型的抽象类型。这样,函数模板可以适用于多种数据类型,提高了代码的灵活性和重用性。
函数模板格式
template<typename T> void Swap(T& x,T& y) { T temp = x; x = y; y = temp; }
template T 表示模板参数,T是一个占位符,代表任意数据类型。在函数模板中,可以使用T作为函数的参数类型、返回类型,以及在函数体中进行通用的操作。
注意:typename是用来定义模板关键字,也可以使用class(不能使用struct代替class)
函数模板的原理
人类从农业时期到工业时期,很多重复机械的工作直接交给了机器去完成,极大的解放的生产力。机器生产淘汰了很多手工创作的东西,其本质上就是把这些工作交给了机器去完成。
函数模板本身并不是一个函数,而是像类一样作为一个蓝图,是编译器用使用方式产生具体类型函数的一个模具。
在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如int类型使用函数模板时,编译器根据传递的实参类型的推演将T确定为int类型,然后再专门产生一份处理int类型的代码,对于double类型还是字符类型皆是这样
函数模板的实例化
用不同类型的参数使用函数模板的时候称之为函数模板的实例化。模板参数实例化分为隐式实例化和显式实例化。
隐式实例化:让编译器根据传递的实参类型推演模板参数的实际类型。
template <class T>
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
int a1 = 10;
int a2 = 5;
Add(a1, a2);
double b1 = 10.0;
double b2 = 5.0;
Add(b1, b2);
return 0;
}
而当调用的时候将不同类型混合传参是不被允许的,因为在编译期间编译器需要推演,对于a1可以推演为int,b1可以推演为double,但是模板参数列表中只有一个T,编译器无法确定T被换为int还是double而报错。在模板中,编译器一般不会去做类型转换,否则出错了编译器就会承担不小的后果。
因此对于这种问题我们通常有两种方式进行解决:用户自己进行强制类型转换或者使用显示实例化
显示实例化:在函数名的后面指定模板参数的实际类型
int main()
{
int a1 = 10;
int a2 = 5;
Add(a1, a2);
double b1 = 10.0;
double b2 = 5.0;
Add(b1, b2);
Add<int>(a1, b1);//显示实例化
return 0;
}
模板参数的匹配原则
一个非模板函数和模板函数可以同时存在,而且该模板函数还可以实例化为这个非模板函数。(这种实例化为非模板函数指在参数传递上可能维持一样,但是依据优先匹配的原则,在调用函数的时候可以做一些特殊处理)。这里我们通过不同函数之间打印信息而进行调用优先级的查看。
//专门处理整形类型加法函数 int Add(int left, int right) { cout << "非模板函数" << endl; return left + right; } //通用加法模板 template<class T> T Add(T left, T right) { cout << "模板函数" << endl; return left + right; } void test() { Add(1,2);//与非模板函数优先匹配 Add(1.0, 2.0);//模板函数 Add<int>(1, 2);//调用特定版本的Add版本,走模板函数 } int main() { test(); return 0; }
对于非模板函数与同名函数模板,如果有其他条件相同,在调用的时候会优先去调用非函数模板而不会从该函数模板中生成实例化函数,如果一个函数可以产生一个具有更好匹配的函数,那么选择模板。
int Add(int left, int right)
{
cout << "非模板函数" << endl;
return left + right;
}
//通用加法模板
template<class T1 , class T2>
T1 Add(T1 left, T2 right)
{
cout << "模板函数" << endl;
return left + right;
}
void test()
{
Add(1, 2);//与非模板函数优先匹配
Add(1, 2.0);//具备更加匹配的版本而不需要类型转换,编译器优先生成更加匹配的Add函数版本
}
int main()
{
test();
return 0;
}
模板函数不能进行自动类型转换(帮你推演就够忙了,类型转换发生错误你还得骂他自然就不会帮你做这件没意义的事情)。但是普通函数可以进行自动类型转换
类模板
类模板的定义格式
template<class T1, class T2,class T3>//参数列表可以定义多个模板变量
class 类模板名
{
//类成员定义
};
我们前面学习顺序表提到过一点,使用typedef对类型名进行重命名。C语言中的类型重命名是指通过使用typedef关键字来为已有的类型创建一个新的别名。这样可以简化代码,提高可读性,并且方便批量修改具体类型,便于维护代码。但是当我们学习了类模板之后我们发现我们并不需要这样做,而是使用类模板。这里我们简要的构造一个动态顺序表的类模板来体会。
template<class T>
class Vector
{
public:
Vector(size_t capacity = 10)
: _pData(new T[capacity])
, _size(0)
, _capacity(capacity)
{}
~Vector();
// 其他成员函数在这里...
size_t Size() { return _size; };
T& operator[](size_t pos)
{
assert(pos < _size);
return _pData[pos];
}
private:
T* _pData;
size_t _size;
size_t _capacity;
};
// 类模板函数定义可以放在类定义外面。
template<class T>
Vector<T>::~Vector()
{
if (_pData)
{
delete[] _pData;
}
_size = _capacity = 0;
}
类模板的实例化
类模板的实例化与函数模板实例化不同,类模板实例化需要在类模板的名字前面跟<>,然后需要讲实例化的类型放在<>中即可(就像必须显式的实例化)。类模板名字不是真正的类,而实例化的结果才是真正的类(蓝图和用蓝图进行建筑的关系)。实例化时,编译器会生成针对具体数据类型的类定义,从而使得类模板变得具体化,可以像普通类一样使用。
Vector类名,Vector<int>才是类型
Vector<int> s1;
Vector<double> s2;
总结
泛型编程是一种编程范式,其目标是编写可重用、通用的代码,以便能够适应多种数据类型而无需针对每种类型重复编写相似的代码。在C++中,泛型编程主要通过函数模板和类模板来实现。其优势如下:
-
提高代码的重用性和可维护性。
-
允许在不同数据类型上进行抽象,减少代码冗余。
函数模板(Function Templates):
概念:函数模板是一种定义通用函数的方式,其中函数的参数或返回类型可以是通用的类型参数。
语法:
template <class T> //typename也可以 T Add(T a, T b) { return a + b; }
实例化:通过指定具体的数据类型,编译器会生成对应类型的函数定义。
实例化用法:
int r_int = Add(3, 4); // 实例化为 Add<int>(3, 4) double r_double = Add(3.14, 2.5);// 实例化为 Add<double>(3.14, 2.5)
类模板(Class Templates):
概念:类模板是一种定义通用类的方式,其中类的成员或行为可以依赖于通用的类型参数。
语法:
template <class T> class MyClass { public: MyClass(T value) : data(value) {} void play() { /* ... */ } private: T data; };
实例化:通过指定具体的数据类型,编译器会生成对应类型的类定义。
使用:
MyClass<int> intObject(42); MyClass<double> doubleObject(3.14);
作者水平有限,如有错误欢迎指正!