点击下载PDF版(极力推荐)

C++程序设计基础 第十章.pdf

前言

本文档由 @ItsJiale 创作,作者博客:https://jiale.domcer.com/,作者依据数学与大数据学院 2024 级大数据教学班的授课重点倾向编写而成。所有内容均为手动逐字录入,其中加上了不少自己的理解与思考,耗费近一周时间精心完成。_

此文档旨在助力复习 C++ 程序设计基础,为后续学习数据结构筑牢根基。信计专业的同学,也可参考本文档规划复习内容。需注意,若个人学习过程中存在不同侧重点或对重难点的理解有差异,应以教材内容为准。倘若文档内容存在任何不妥之处,恳请各位读者批评指正。

By:ItsJiale

2025.5.4

第十章 模版

为什么要使用模版?因为对于相同结构但数据类型不同的函数而言,只需要关注逻辑代码的编写,而不用关注实际具体的数据类型,对于代码的维护而言大大增强。

10.1 模版的概念

模板(Template)是 C++ 中一项强大的特性(在前端开发中也有模版的相关概念),它允许你编写通用的代码,这些代码可以处理多种数据类型,而不需要为每种数据类型都单独编写一份代码。模板主要分为函数模板和类模板。函数模版生成的实例叫做模版函数,类模版生成的实例叫做模板类

10.2 函数模版

函数模板可以创建一个通用的函数,它能够处理不同的数据类型。定义函数模板时使用 template 关键字,后跟一个模板参数列表,通常用 表示,这里的 T 是一个类型参数,可以代表任意数据类型。

10.2.1 函数模版的定义

格式如下:

template<class 类型占位符>

返回值类型 函数名(参数列表)

{

//函数体;

}

上述语法格式中,template是声明模板的关键字,< >中的参数称为模板形参class关键字用于标识模板形参,可以用typename关键字代替class关键字, typenameclass并没有区别。模板形参不能为空,但是一个函数模板中可以有多个模板形参,模板形参和函数形参相似。template下面就是定义的一个函数模板,函数模板与普通的函数定义方式相同,只是参数列表中的数据类型要使用< >中的形参名表示。

#include <iostream>
using namespace std;
template<class T>
T add(T t1, T t2) {
    return t1 + t2;
}
int main()
{
    cout << add(3, 2) << endl;
    cout << add(4.3, 3.2) << endl;
    return 0;
}

10.2.2 函数模版实例化

所谓实例化,就是用类型参数替换模版中的模版参数,生成具体类型的函数。

隐式实例化

隐式实例化是指在使用模板时,编译器根据提供的参数类型自动推导模板参数的类型,并生成对应的模板实例。

#include <iostream>
using namespace std;
// 函数模板
template <typename T>
T maximum(T a, T b) {
    return (a > b)? a : b;
}

int main() {
    int x = 5, y = 10;
    // 隐式实例化,编译器根据传入的参数类型 int 自动实例化 maximum 函数
    cout << "The maximum of " << x << " and " << y << " is " << maximum(x, y) << endl;

    double m = 3.14, n = 2.71;
    // 隐式实例化,编译器根据传入的参数类型 double 自动实例化 maximum 函数
    cout << "The maximum of " << m << " and " << n << " is " << maximum(m, n) << endl;

    return 0;
}

显示实例化

显式实例化是程序编写者明确指定模板参数的具体类型,要求编译器生成该特定类型的模板实例。

#include <iostream>
using namespace std;
// 函数模板
template <typename T>
T minimum(T a, T b) {
    return (a < b)? a : b;
}

// 显式实例化 minimum 函数模板为 int 类型
template int minimum<int>(int a, int b);

int main() {
    int p = 15, q = 20;
    // 可以直接使用显式实例化后的 minimum 函数
    cout << "The minimum of " << p << " and " << q << " is " << minimum(p, q) << endl;
    return 0;
}

在上述例子中,不是int类型的数据会转换int类型再进行计算。

10.2.3 函数模版重载

函数模版可以进行实例化,以支持不同类型的参数,不同类型的参数调用会产生一系列重载函数。

#include<iostream>
using namespace std;
int max(const int& a,const int& b){    //非模板函数,求两个int类型数据的最大者
    return a>b ? a:b;
}
template<class T>               //定义求两个任意类型数据的最大值
T max(const T& t1,const T& t2){
    return t1>t2 ? t1:t2;
}
template<class T>              //定义求三个任意类型数据的最大值
T max(const T& t1,const T& t2,const T&t3){
    return max(max(t1,t2),t3);
}
int main()
{
    cout<< max(1,2)<<endl;      //调用非模板函数
    cout<< max(1,2,3)<<endl;     //调用三个参数的函数模板
    cout<< max('a','e')<<endl;     //调用两个参数的函数模板
    cout<< max(6,3.2)<<endl;      //调用非模板函数
    return 0;
}

形参个数不同——>函数模版重载

补充:为什么这里要用常量引用

  1. 复制开销:值传递会复制实参,当传递的对象较大时,会增加内存和时间开销。
  2. 函数内可能意外修改参数:因为没有const的限制,函数内部可能会不小心修改传入的参数。

使用函数模版需要注意的问题:

  1. 函数模板中的每一个类型参数在函数参数表中必须至少使用一次。(_定义了就一定要用_)

template<class T1, class T2>

void func(T1 t){}

函数模板声明了两个参数T1T2,但在使用时只使用了T1,没有使用T2

  1. 全局作用域中声明的与模板参数同名的对象、函数或类型,在函数模板中将被隐藏,例如:

int num;

template<class T>

void func(T t){ T num; }

在函数体内访问num是访问的T类型的num,而不是全局变量num。因为_局部变量优先_。

  1. 函数模板中声明的对象或类型不能与模板参数同名,例如:

template<class T>

void func(T t){

typedef float T; //错误,定义的类型与模板参数名相同

}

  1. 模板参数名在同一模板参数列表中只能使用一次,但可在多个函数模板声明或定义之间重复使用。例如:

template <class T, class T> //错误,在同一个模板中重复定义模板参数

void func1(T t1, T t2){}

template <class T>

void func2(T t1){}

template <class T> //在不同函数模板中可重复使用相同模板参数名

void func3(T t1){}

  1. 模板的定义和多处声明所使用的模板参数名不一定要必须相同,例如:

//模板的前向声明

template <class T>

void func1(T t1, T t2);

//模板的定义

template<class U>

void func1(U t1, U t2){}

  1. 函数模板如果有多个模板参数,则每个模板类型前都必须使用关键字class或typename修饰,例如:

template <class T,class U> //两个关键字可以混用

void func(T t, U u){}

template<class T,U> //错误,每一个模板参数前都要有关键字修饰

void func(T t, U u){}

函数模版的注意事项要比考贵系都要繁琐呢(笑)

10.3 类模板

类也可以像函数一样被不同的类型参数化。

10.3.1 类模版定义与实例化

函数可以定义模板,对于类来说,也可以定义一个类模板,类模板是针对成员数据类型不同的类的抽象,它不是一个具体的实际的类,而是一个_类型_的类,一个类模板可以生成多种具体的类,定义类模板的格式如下所示:

template<class 类型占位符>

class 类名

{

}

类模版中的关键字含义与函数模版相同。类模板定义时必须有模板形参,就像给通用模具留个 “空位”,有了它之后,在类里面要指定数据类型的地方,都能用这个 “空位” 代表的模板形参名来声明成员变量和函数。定义如下:

template
class A
{
public:

T a;
T b;
T func(T a, T b);

};

定义了类模板后就要使用类模板创建对象以及实现类中的成员函数,这个过程其实也是类模板实例化的过程,实例化出的具体类称为模板类

如果用类模板创建类的对象,例如,用上述定义的类模板A创建对象,则在类模板A后面加上一个< >,并在里面表明相应的类型。
A a;
当类模板有两个模板形参时,创建对象时,类型之间要用逗号分隔开。
template
class B
{
public:

T1 a;
T2 b;
T1 func(T1 a, T2& b);

};
B<int,string> b;
使用类模板时,必须要为模板形参显式指定实参,不存在实参推演过程(即谁叫谁得提前写出来),也就是说不存在将整型值10推演为int类型传递给模板形参,必须要在< >中指定int类型,这一点与函数模板不同。

10.3.2 类模版的派生

类模板和普通类一样也可以继承和派生,以实现代码的复用。一般有三种情况:类模版派生普通类、类模板派生类模板、普通类派生类模板。

类模板派生普通类

在派生过程中,类模版先实例化出一个模板类,这个模板类作为基类派生出普通类。

#include <iostream>
using namespace std;

// 定义一个类模板
template <typename T>
class BaseTemplate {
public:
    T value;
    BaseTemplate(T v) : value(v) {}
    void printValue() {
        cout << "Value: " << value << endl;
    }
};

// 从类模板派生普通类
class DerivedClass : public BaseTemplate<int> {
public:
    DerivedClass(int v) : BaseTemplate<int>(v) {}
};

int main() {
    DerivedClass obj(10);
    obj.printValue();
    return 0;
}

class DerivedClass : public BaseTemplate<int> 注意这里,类模板实例化了模板类<int>

DerivedClass(int v) : BaseTemplate<int>(v)这里是DerivedClass 类的构造函数,该构造函数接收一个整数类型的参数 v,并使用这个参数来初始化其基类 BaseTemplate<int> 的成员。

在派生过程中需要指定模版参数类型。

类模板派生类模板

类模板也可以派生出一个新的类模板,它和普通类之间的派生几乎完全相同。但是,派生类模版的模版参数受基类模版的模版参数影响。

#include <iostream>
using namespace std;

// 定义一个类模板
template <typename T>
class BaseTemplate {
public:
    T value;
    BaseTemplate(T v) : value(v) {}
    void printValue() {
        cout << "Value: " << value << endl;
    }
};

// 从类模板派生类模板
template <typename U>
class DerivedTemplate : public BaseTemplate<U> {
public:
    DerivedTemplate(U v) : BaseTemplate<U>(v) {}
    void printDerivedValue() {
        cout << "Derived Value: " << this->value << endl; 
        //this->value 表示访问当前对象的 value 成员变量
    }
};

int main() {
    DerivedTemplate<double> obj(3.14);
    obj.printValue();
    obj.printDerivedValue();
    return 0;
}

普通类派生类模版

普通类派生类模板可以把现存类库中的类转换为通用的类模板,但是在实际编程中,这种派生方式并不常见,各位只需要了解即可。

#include <iostream>
using namespace std;

// 定义一个普通类
class BaseClass {
public:
    int baseValue;
    BaseClass(int v) : baseValue(v) {}
    void printBaseValue() {
        cout << "Base Value: " << baseValue << endl;
    }
};

// 从普通类派生类模板
template <typename T>
class DerivedFromBaseClass : public BaseClass {
public:
    T derivedValue;
    DerivedFromBaseClass(int bv, T dv) : BaseClass(bv), derivedValue(dv) {}
    void printDerivedFromBaseValue() {
        cout << "Base Value: " << this->baseValue << ", Derived Value: " << derivedValue << endl;
    }
};

int main() {
    DerivedFromBaseClass<char> obj(10, 'A');
    obj.printBaseValue();
    obj.printDerivedFromBaseValue();
    return 0;
}

10.3.3 类模板与友元函数

在类模板中声明友元函数有三种情况:非模版友元函数、约束模版友元函数和非约束模版友元函数。

非模版友元函数

非模板友元就是在类模板中声明普通的友元函数,例如,在一个类模板中声明一个友元函数:

template <class T>

class A{

T _t;

public:

friend void func();

};

在类模板A中声明了一个普通的友元函数func(),则func()函数是类模板A所有实例(模板类)的友元函数,它可以访问全局对象,也可以使用全局指针访问非全局对象;可以创建自己的对象,也可以访问独立于对象的模板的静态数据成员。

也可以为类模板的友元函数提供模板类参数,示例代码如下:

template <class T>

class A

{

`T _t;`

public:

friend void show(const A<T>& a); // A<T> 类名——>数据类型

};

在上述代码中,show()函数并不是模板函数,而只是使用一个模板作参数,这就要求在使用友元函数时必须要显式具体化,指明友元函数要引用的参数的类型,例如:

void show(const A<int>& a);

void show(const A<double>& a);

也就是说模板形参为int类型的show()函数是A<int>类的友元函数,模板形参为double类型的show()函数是A<double>类的友元函数。

复习:怎么使用友元函数?

class MyClass {
private:
    int privateData;
public:
    // 声明友元函数
    friend void friendFunction(MyClass obj);
};

// 友元函数定义在类外部
void friendFunction(MyClass obj) {
    // 能访问类的私有成员,但不是类的成员函数
}

下面是简单的例子

#include <iostream>
using namespace std;

class MyClass {
private:
    int value;
public:
    MyClass(int v) : value(v) {}
    // 声明非模板友元函数
    friend void printValue(MyClass obj);
};

// 非模板友元函数的定义
void printValue(MyClass obj) {
    cout << "Value: " << obj.value << endl;
}

int main() {
    MyClass obj(10);
    printValue(obj);
    return 0;
}

约束模版友元函数

约束模板友元函数本身就是一个模板,但它的实例化类型取决于类被实例化时的类型,类实例化时会产匹配的具体化友元函数。__(又是不讲人话)

改写一下:

有一个友元函数本身是个模板函数,它的具体实例化类型由所在类的实例化类型来决定。当类被实例化时,会自动生成与之匹配的特定友元函数版本。

在使用约束模版友元函数时,首先需要再类模板定义的前面声明函数模版

template
void func();
template
void show(T &t);
下面是简单例子

#include <iostream>
using namespace std;

// 前置声明类模板
template <typename T> 
class MyTemplateClass;

// 前置声明模板友元函数
template <typename T> 
void printTemplateValue(MyTemplateClass<T> obj);

// 定义类模板
template <typename T>
class MyTemplateClass {
private:
    T value;
public:
    MyTemplateClass(T v) : value(v) {}
    // 声明约束模板友元函数
    friend void printTemplateValue<T>(MyTemplateClass<T> obj);
};

// 定义模板友元函数
template <typename T>
void printTemplateValue(MyTemplateClass<T> obj) {
    cout << "Template Value: " << obj.value << endl;
}

int main() {
    MyTemplateClass<int> obj(20);
    printTemplateValue(obj);
    return 0;
}

friend void printTemplateValue<T>(MyTemplateClass<T> obj); 中参数列表MyTemplateClass<T> obj的意思是printTemplateValue 函数接收一个 MyTemplateClass 类的对象作为参数,并且这个对象的类型参数和函数的模板参数 T 是一致的。

非约束模版友元函数

非约束模版友元函数是将函数模版声明为类模版的友元函数,但函数模版的模版参数不受类模板影响,即友元函数模版的模版参数与类模版的模版参数是不同的。(这就是防自学教材吗(笑))

改写一下:

非约束模板友元函数指的是,把函数模板声明成类模板的友元函数,此时函数模板的模板参数不会受类模板的影响,二者的模板参数相互独立。

下面是简单的例子:

#include <iostream>
using namespace std;

// 定义类模板
template <typename T>
class MyNonConstrainedTemplateClass {
private:
    T value;
public:
    MyNonConstrainedTemplateClass(T v) : value(v) {}
    // 声明非约束模板友元函数
    template <typename U>
    friend void printNonConstrainedValue(MyNonConstrainedTemplateClass<U> obj);
};

// 定义非约束模板友元函数
template <typename U>
void printNonConstrainedValue(MyNonConstrainedTemplateClass<U> obj) {
    cout << "Non - Constrained Template Value: " << obj.value << endl;
}

int main() {
    MyNonConstrainedTemplateClass<double> obj(3.14);
    printNonConstrainedValue(obj);
    return 0;
}

MyNonConstrainedTemplateClass类模板中,使用template <typename U> friend void printNonConstrainedValue(MyNonConstrainedTemplateClass<U> obj);声明了非约束模板友元函数。这意味着任何类型的printNonConstrainedValue函数都可以访问MyNonConstrainedTemplateClass的私有成员。在main函数中创建了MyNonConstrainedTemplateClass<double>的对象obj,并调用printNonConstrainedValue函数输出其私有成员的值。

10.4 模版的参数

模版的参数有三种类型:类型参数、非类型参数和模板类型参数。

类型参数

模板参数是由class或者typename标记,称为类型参数,类型参数是使用模板的主要目的。例如下列模板声明:

template<class T>

T add(T t1,T t2);

其中,T就是一个类型形参,类型形参的名字由用户自行确定,表示的是一个未知类型,模板类型形参可以作为类型说明符用在模板中的任何地方,与内置类型说明符或类类型说明符的使用方式完全相同。

可以为模板定义多个类型参数,也可以为类型参数指定默认值,示例代码如下所示:

template<class T, class U = int> //只能在最右边初始化定义

class A{

public:

void func(T, U);

};

在上述代码中,把U默认设置成为int类型,类模板类型形参和函数默认参数规则一致。

非类型参数

非类型参数也就是内置类型形参,例如,定义如下模板:

template<class T, int a>

class A{};

在上述代码中,int a就是非类型的模板形参,非类型模板形参为函数模板或类模板预定义一些常量,在生成模板实例时,也要求必须是常量,即整型、枚举、指针和引用。

非类型模板参数在模板所有实例中都具有相同的值,而类型模板参数在不同的模板实例中拥有不同的值。

使用非类型模板参数时,有以下几点需要注意:

(1)调用非类型模板形参的实参必须是常量表达式,即必须能在编译时计算出结果。

(2)任何局部对象、局部变量的地址都不是常量表达式,不能用作非类型模板的实参,全局指针类型、全局变量也不是常量表达式,也不能用作非类型模板的实参。

(3)sizeof()表达式结果是一个常量表达式,可以用作非类型模板的实参。

(4)非类型模板形参一般不用于函数模板。

(上面的看看就行了,谁是超人吗能记这么多东西。正常开发时候是可以查资料的,写不对就查呗)

模版类型参数

模版类型参数就是模版的参数为另一个模版。声明如下:

template<class T, template<class U,class Z> class A>

class Paramete{

A<T,T> a;

};

class T:这是一个普通的模板类型参数。在使用 Parameter 类模板时,你需要为 T 提供一个具体的类型,比如 intdouble、自定义类等。这个类型参数将用于后续 A<T, T> 中对 A模板类的实例化。
template<class U, class Z> class A:这是一个模板模板参数。A 代表一个模板类,这个模板类本身需要有两个类型参数 U Z。在实例化 Parameter 类模板时,你需要传入一个满足该要求的模板类。
A<T, T> a;:在 Parameter 类内部,定义了一个名为 a 的成员变量,其类型为 A<T, T>。这意味着在使用 Parameter 类模板时,a 会根据传入的 TA 进行具体的实例化。具体来说,T 会被同时作为 A 模板类的两个类型参数 U Z 的实例化类型。

10.5 模版特化

特化就是将泛型的东西具体化,模版特化就是为已经有的模版参数进行具体化的制定,使得不受任何约束的模版参数受到特定约束或完成被指定。

偏特化

偏特化就是模板中的模板参数没有被全部确定,需要编译器在编译时进行确定。例如,定义一个类模板,如下所示:

template<class T, class U>

class A{};

将其中一个模板形参特化为int类型,另一个参数由用户指定,示例代码如下:

template<class T>

class A<T, int>{};

全特化

全特化就是模板中的模板参数全部被指定为确定的类型,其标志就是产生出完全确定的东西。例如,有类模板定义,如下所示:

template<class T>

class Compare{

public:

bool isEqual(const T& var1,const T& var2){

return var1==var2;

}

};

将其特化为对float类型数据的比较,定义如下所示:

template<>

class Compare<float>{

public:

bool IsEqual(const float& var1,const float& var2){

return abs(var1–var2)<10e-3;

}

};

上述定义中template<>就是告诉编译器这是一个特化的模板,模板特化之后就可以处理特殊情况,需要注意的是函数模板支持全特化。

最后修改:2025 年 05 月 05 日
如果觉得我的文章对你有用,请随意赞赏