admin管理员组

文章数量:1794759

泛型编程与模板

泛型编程与模板

C++模板

文章目录
  • C++模板
    • 1.泛型编程
      • 1.1 泛型编程与模板
    • 2.函数模板
      • 2.1 函数模板概念
      • 2.2 函数模板格式
      • 2.3 函数模板原理
      • 2.4 函数模板实例化
      • 2.5 函数模板参数匹配原则
    • 3.类模板
      • 3.1 类模板概念
      • 3.2 类模板的定义格式
      • 3.3 类模板的实例化
    • 4.非类型模板参数
    • 5.模板特化
      • 5.1 模板特化的概念
      • 5.2 函数模板特化
      • 5.3 类模板特化
    • 6.模板的分离编译
      • 6.1 模板分离编译概念
      • 6.2 模板的分离编译
      • 6.3 模板不支持分离编译的解决方法
    • 7.模板的优缺点


1.泛型编程

泛型编程:编写与类型无关的通用代码,是代码复用的一种手段

泛型编程使用模板来实现,模板分为:函数模板、类模板


1.1 泛型编程与模板

如果在C++中,能够存在这样一个模具,通过给这个模具中填充不同类型,生成具体类型的代码,那将会节省许多开发时间,巧的是前人早已将树栽好,我们只需在此乘凉

模板使用:

  • 使用template表示泛型编程
  • 使用typename或class表示泛型类型—通常用typename

补充:早期使用class来表示泛型类型,但我们知道class是类的关键字,后来为了避免class在这两个地方的使用可能给人带来混淆,所以引入了typename这个关键字

注意:typename是用来定义模板参数关键字,也可以使用class(切记:不能使用struct代替class)

举例:


2.函数模板 2.1 函数模板概念

函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本


2.2 函数模板格式

举例:

template<typename T> void Swap( T& x, T& y) { ​ T temp = x; ​ x = y; ​ y = temp; } //函数模板的声明 template<typename T> void Swap( T& x, T& y); //函数模板的定义 template<typename T> void Swap( T& x, T& y) { T temp = x; x = y; y = temp; } int main() { int a=1;int b=2; Swap(a,b); double c=1.1;double d=2.2; Swap(c,d); }

这里可以看到,调用了模板去自动匹配类型,但是调用的是两个不同的函数!一个是Swap(int&,int&) 一个是Swap(double&,double&)


2.3 函数模板原理

函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器

在编译器编译阶段,对于函数模板的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用

比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码,对于其他类型也是如此


2.4 函数模板实例化

不同类型的参数使用函数模板时,称为函数模板的实例化

模板参数实例化分为:隐式实例化和显式实例化

  • 隐式实例化:让编译器根据实参推演模板参数的实际类型
  • 显式实例化:在函数名后的 <> 中指定模板参数的实际类型

举例:

1.显示实例化:在函数名后的<>中指定模板参数的实际类型 int main(void) { int a = 10; double b = 20.0; // 显式实例化 Add<int>(a, b); return 0; } //如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错 2.隐式实例化:让编译器根据实参推演模板参数的实际类型 template<class T> T Add(const T& left, const T& right) { return left + right; } int main() { int a1 = 10, a2 = 20; double d1 = 10.0, d2 = 20.0; Add(a1, a2); Add(d1, d2); Add(a, (int)d);//强制转换 Add<int>(a,d);//显示实例化 return 0; } /* 该语句Add(a1, d1);不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型 通过实参a1将T推演为int,通过实参d1将T推演为double类型,但模板参数列表中只有一个T,编译器无 法确定此处到底该将T确定为int 或者 double类型而报错 注意:在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要背黑锅 此时有两种处理方式:1.用户自己来强制转化 2.使用显式实例化 */
2.5 函数模板参数匹配原则
  • 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数

    // 专门处理int的加法函数 int Add(int left, int right) { return left + right; } // 通用加法函数 template<class T> T Add(T left, T right) { return left + right; } void Test() { Add(1, 2); // 与非模板函数完全匹配,编译器不需要特化 Add<int>(1, 2); // 调用编译器特化的Add版本 }
  • 如果模板可以产生一个具有更好匹配的函数, 那么将选择模板

    // 专门处理int的加法函数 int Add(int left, int right) { return left + right; } // 通用加法函数 template<class T1, class T2> T1 Add(T1 left, T2 right) { return left + right; } void Test() { Add(1, 2); // 与非函数模板类型完全匹配,不需要函数模板实例化 Add(1, 2.0); // 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的Add函数 }
  • 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换

  • 模板匹配原则总结:有现成完全匹配的,直接调用;没有现成完全匹配的,调用模板实例化生成的


    3.类模板 3.1 类模板概念

    类模板是对一批仅仅成员数据类型不同的类的抽象,程序员只要为这一批类所组成的整个类家族创建一个类模板,给出一套程序代码,就可以用来生成多种具体的类(这些类可以看作是类模板的实例),从而大大提高编程的效率

    注意:「类模板」中的「成员函数」都是「函数模板」

    类模板成员函数的模板形参由调用该函数的对象的类型确定,对象的模板实参能够确定成员函数的模板形参


    3.2 类模板的定义格式

    template<class T1,class T2…class Tn>

    class 类模板名

    {

    ​ 类内成员定义

    };

    // 类模板例子 // 注意:Stack不是具体的类,是编译器根据被实例化的类型生成的具体类的模具 template<class T> class Stack { private: T* _a; size_t _top; size_t _capacity; public: // ... Stack(size_t capacity = 10) :_a(new T[capacity]) ,_top(0) ,_capacity(capacity) {} ~Stack(); // 析构函数,在类中声明,类外定义 }; // 注意:类模板中函数放在类外进行定义时,需要加模板参数列表 template<class T> Stack<T>::~Stack()//类模板函数在类外定义 { if (_a) { delete[] _a; _a = nullptr; } _top = _capacity = 0; } int main() { // 类模板的使用都是显式实例化 // Stack是类名,Stack<int>才是类型 Stack<int*> st1; Stack<int> st2; return 0; }

    注意:模板不支持把声明写到 .h头文件中,定义写到 .cpp源文件的这种声明与定义分离在不同文件中的方式,会出现链接错误


    3.3 类模板的实例化

    类模板实例化与函数模板实例化不同,类模板都是显式实例化,类模板实例化需要在类模板名字后跟 <>,然后将实例化的类型放在 <> 中即可,类模板名字不是真正的类,而实例化的结果才是真正的类

    举例:下面的SeqList只是模板名字,SeqList和SeqList才是类名


    4.非类型模板参数

    模板参数分为「类型形参」与「非类型形参」,注意:不管哪种模板参数,都可以给「缺省值」

    • 类型形参:出现在模板参数列表中,跟在 class 或者 typename 之后的参数类型名称T
    // T: 类型模板参数,它是一个类型 template <class T> class A;
    • 非类型形参:就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用
    // T: 类型模板参数,它是一个类型 // N: 非类型模板参数,它是一个常量 template <class T, size_t N = 10> class A;

    比如:C++11 增加了一个容器 array,就是一个静态数组,通过非类型模板参数来控制数组的大小:

    template < class T, size_t N > class array; // 不推荐使用,容易导致栈溢出(栈是非常小的)

    补充:浮点数、类对象以及字符串是不允许作为非类型模板参数的,一般都是用的 int或char 整型

    使用情况举例:

    /* 现在实现了一个静态栈,可以存 10 个数据,实例化的每个对象都可以存 10 个数据,如果 我想要第一个对象st1 存 100 个数据,第二个对象存 200 个数据,这种结构就非常的不好 */ #define N 10 template<class T> // 静态栈 class Stack { private: T _a[N]; size_t _top; }; void test() { Stack<int> st1; // 10 Stack<int> st2; // 10 } //那该如何解决呢?-- 定义非类型模板参数(常量): template<class T, size_t N> // 静态栈 class Stack { private: T _a[N]; size_t _top; }; void test() { Stack<int, 100> st1; // 100 Stack<int, 200> st2; // 200 }
    5.模板特化 5.1 模板特化的概念

    模板参数在某种特定类型下的具体实现称为模板的特化。模板特化有时也称之为模板的具体化,分别有函数模板特化和类模板特化

    模板特化分为:函数模板特化与类模板特化

    通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理

    比如:交换两个元素

    template<class T> void Swap(T& x, T& y) { T tmp(x); x = y; y = tmp; } void test() { int x = 1, y = 2; Swap(x, y); vector<int> v1 = { 1, 2, 3, 4 }; vector<int> v2 = { 10, 20, 30, 40 }; Swap(v1, v2); // 深拷贝式交换,代价太大,效率低 }

    解决方案一:函数的匹配原则,写一个专门针对 vector 类型对象交换的函数

    // 函数匹配原则,专门针对 vector<int> 类型对象交换的函数 -- 推荐这种 void Swap(vector<int>& v1, vector<int>& v2) { v1.swap(v2); //这种交换只需要交换对象内部的几个指针即可 //swap(v1,v2);是调用的拷贝构造 //v1.swap(v2);是交换的指针 }

    解决方案二:函数模板的特化,针对 vector 类型对象的交换进行特殊化处理:

    // 函数模板的特化(针对某些具体类型进行特殊化处理) -- 最后还是要经过模板推演 template<> void Swap<vector<int>>(vector<int>& v1, vector<int>& v2) { v1.swap(v2); }
    5.2 函数模板特化

    函数模板的特化步骤:

  • 必须要先有一个基础的函数模板
  • 关键字template后面接一对空的尖括号<>
  • 函数名后跟一对尖括号,尖括号中指定需要特化的类型
  • 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误
  • template<class T1, class T2> // 基础函数模板 void add(T1& x, T2& y) { cout << "void add(T1& x, T2& y)" << endl; } template<> void add<int, char>(int& x, char& y) // 函数模板的特化 { cout << "void add<int, char>" << endl; } void test() { int a = 1, b = 2; add(a, b); // 走基础函数模板 int c = 1; char d = 'a'; add(c, d); // 走特化的void add<int, char>版本 }

    注意:一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出

    void add(int& x, char& y) { cout << "void add(int& x, char& y)" << endl; }
    5.3 类模板特化

    类模板的特化步骤:

  • 必须要先有一个基础的类模板
  • 关键字 template 后面接一对空的尖括号 <>
  • 类名后跟一对尖括号 <>,尖括号中指定需要特化的类型
  • 类模板的特化分为:全特化 和 偏特化

    • 全特化:将模板参数列表中所有参数都确定化
    template<class T1, class T2> // 基础类模板 class Data { public: Data() { cout << "Data<T1, T2>" << endl; } private: T1 _d1; T2 _d2; }; // 类模板的全特化 -- 最后还是要经过模板推演 // 将所有参数都确定化 template<> class Data<double, double>//参数都确定化 { public: Data() { cout << "Data<double, double>" << endl; } private: double _d1; double _d2; }; void test() { Data<int, int> d1; // 走基础类模板 Data<double, double> d2; // 走特化的double版本 }
    • 偏特化:任何针对模版参数进一步进行条件限制设计的特化版本

    偏特化有两种表现方式:部分特化和参数进一步限制

    • 部分特化:将模板参数类表中的一部分参数特化

    • template<class T1, class T2> // 基础类模板 class Data { public: Data() { cout << "Data<T1, T2>" << endl; } private: T1 _d1; T2 _d2; }; // 类模板的偏特化,将部分参数确定化 template<class T1> class Data<T1, char>//确定部分参数char { public: Data() { cout << "Data<T, char>" << endl; } private: T1 _d1; char _d2; }; void test() { Data<int, int> d1; // 走基础类模板 Data<int, char> d3; // 走特化版本 }
    • 参数更进一步的限制:偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本

    • template<class T1, class T2> // 基础类模板 class Data { public: Data() { cout << "Data<T1, T2>" << endl; } private: T1 _d1; T2 _d2; }; // 不一定特化部分参数,而是对参数更进一步的限制,两个参数偏特化为指针类型 template<class T1, class T2>//基础类模板 class Data<T1*, T2*> { public: Data() { cout << "Data<T1*, T2*>" << endl; } private: T1* _d1; T2* _d2; }; // 两个参数偏特化为引用类型 template<class T1, class T2> class Data<T1&, T2&> { public: Data(const T1& d1, const T2& d2) :_d1(d1) ,_d2(d2) { cout << "Data<T1&, T2&>" << endl; } private: const T1& _d1; const T2& _d2; }; void test() { Data<int, int> d1; // 调用基础的类模板 // 不管显示实例化什么类型的指针都可以 Data<int*, char*> d4; // 调用特化的指针版本 Data<int*, int*> d5; // 调用特化的指针版本 // 不管显示实例化什么类型的引用都可以 Data<int&, int&> d6(1, 2); // 调用特化的引用版本 }

    6.模板的分离编译 6.1 模板分离编译概念

    一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有 目标文件链接起来形成单一的可执行文件的过程称为分离编译模式


    6.2 模板的分离编译

    假如有以下场景,模板的声明与定义分离开,在头文件中进行声明,源文件中完成定义:

    // a.h template<class T> T Add(const T& left, const T& right); // a.cpp #include"func.h" template<class T> T Add(const T& left, const T& right) { return left + right; } // main.cpp #include"a.h" int main() { Add(1, 2); Add(1.0, 2.0); return 0; }

    此段代码运行会报链接错误(链接错误一般是指在链接阶段找不到该函数的定义):

    无法解析的外部符号 “int __cdecl Add(int const &,int const &)” (??$Add@H@@YAHABH0@Z),函数 _main 中引用了该符号

    这是为什么呢?—这就需要先了解下程序编译的过程

    下图是对于编译过程和分离编译的思考:

    结论:模板不支持分离编译


    6.3 模板不支持分离编译的解决方法

    解决方法:

  • 不分离编译,将声明和定义放到一个头文件 “xxx.h” 中,这样头文件展开后,main.cpp 中就有函数的定义,链接时就不需要去找函数的地址了,推荐使用这种
  • 在模板定义位置显式指定实例化。用一个类型就得显式实例化一个类型,很麻烦,不实用,不推荐使用
  • //在模板定义位置显示指定实例化 // a.h template<class T> T Add(const T& left, const T& right); // 函数模板的声明 // a.cpp #include"func.h" template<class T> T Add(const T& left, const T& right) // 函数模板的实现 { return left + right; } template int Add(const int& left, const int& right); // 显示实例化函数模板 template double Add(const double& left, const double& right); // 显示实例化函数模板 // main.cpp #include"a.h" int main() { Add(1, 2); // call Add<int> Add(1.0, 2.0); // call Add<double> return 0; }
    7.模板的优缺点

    优点:

  • 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生
  • 增强了代码的灵活性
  • 缺点:

  • 模板会导致代码膨胀问题,也会导致编译时间变长
  • 出现模板编译错误时,错误信非常凌乱,不易定位错误
  • 本文标签: 模板