c++11新特性

418 阅读25分钟
  1. C++新特性主要包括包含语法改进和标准库扩充两个方面,主要包括以下11点:

    1. 语法的改进

    2. 统一的初始化方法

    3. 成员变量默认初始化

    4. auto关键字 用于定义变量,编译器可以自动判断的类型(前提:定义一个变量时对其进行初始化)

    5. decltype 求表达式的类型

    6. 智能指针 shared_ptr

    7. 空指针 nullptr(原来NULL)

    8. 基于范围的for循环

    9. 右值引用和move语义 让程序员有意识减少进行深拷贝操作

    10. 可变参数模板新特性

    11. 标准库扩充(往STL里新加进一些模板类,比较好用)

    12. 无序容器(哈希表) 用法和功能同map一模一样,区别在于哈希表的效率更高

    13. 正则表达式 可以认为正则表达式实质上是一个字符串,该字符串描述了一种特定模式的字符串

    14. Lambda表达式

  2. Lambda表达式,闭包,防函数

    1. lambda 表达式(lambda expression)是一个匿名函数,lambda表达式基于数学中的 λ 演算得名。C++11中的lambda表达式用于定义并创建匿名的函数对象,以简化编程工作。
    2. lambda表达式的基本构成:
      1. [],标识一个lambda的开始,这部分必须存在,不能省略。
        1. 函数对象参数是传递给编译器自动生成的函数对象类的构造函数的。函数对象参数只能使用那些到定义lambda为止时lambda所在作用范围内可见的局部变量(包括lambda所在类的this)。
        2. 函数对象参数有以下形式:
          1. 空。没有使用任何函数对象参数。
          2. =。函数体内可以使用lambda所在作用范围内所有可见的局部变量(包括lambda所在类的this),并且是值传递方式(相当于编译器自动为我们按值传递了所有局部变量)。
          3. &。函数体内可以使用lambda所在作用范围内所有可见的局部变量(包括lambda所在类的this),并且是引用传递方式(相当于编译器自动为我们按引用传递了所有局部变量)。
          4. this。函数体内可以使用lambda所在类中的成员变量。
          5. a。将a按值进行传递。按值进行传递时,函数体内不能修改传递进来的a的拷贝,因为默认情况下函数是const的。要修改传递进来的a的拷贝,可以添加mutable修饰符。
          6. &a。将a按引用进行传递。
          7. a, &b。将a按值进行传递,b按引用进行传递。
          8. =,&a, &b。除a和b按引用进行传递外,其他参数都按值进行传递。
          9. &, a, b。除a和b按值进行传递外,其他参数都按引用进行传递。
      2. 操作符重载函数参数
        1. 标识重载的()操作符的参数,没有参数时,这部分可以省略。参数可以通过按值(如:(a,b))和按引用(如:(&a,&b))两种方式进行传递。
      3. 可修改标示符
        1. mutable声明,这部分可以省略。按值传递函数对象参数时,加上mutable修饰符后,可以修改按值传递进来的拷贝(注意是能修改拷贝,而不是值本身)。
      4. 错误抛出标示符
        1. exception声明,这部分也可以省略。exception声明用于指定函数抛出的异常,如抛出整数类型的异常,可以使用throw(int
      5. 函数返回值
        1. ->返回值类型,标识函数返回值的类型,当返回值为void,或者函数体中只有一处return的地方(此时编译器可以自动推断出返回值类型)时,这部分可以省略。
      6. 是函数体
        1. {},标识函数的实现,这部分不能省略,但函数体可以为空。
    3. lambda和仿函数。
      1. lambda和仿函数有着相同的内涵——都可以捕获一些变量作为初始化状态,并接受参数进行运行
      2. 而事实上,仿函数是编译器实现lambda的一种方式,通过编译器都是把lambda表达式转化为一个仿函数对象。因此,在C++11中,lambda可以视为仿函数的一种等价形式。
    4. 仿函数:重载 operator()*
    5. 闭包:函数是代码,状态是一组变量,将代码和一组变量捆绑 (bind) ,就形成了闭包。闭包的状态捆绑,必须发生在运行时。(仿函数,lambda表达式,#### std::bind绑定器)
    6. std::bind绑定器
      1. std::function
        1. 在C++中,可调用实体主要包括:函数、函数指针、函数引用、可以隐式转换为函数指定的对象,或者实现了opetator()的对象。
        2. C++11中,新增加了一个std::function类模板,它是对C++中现有的可调用实体的一种类型安全的包裹。通过指定它的模板参数,它可以用统一的方式处理函数、函数对象、函数指针,并允许保存和延迟执行它们。
          1. 绑定一个普通函数 function< void(void) > f1 = func;
          2. 绑定类中的静态函数 function< int(int) > f2 = Foo::foo_func;
          3. 绑定一个仿函数 cout << f2(222) << endl;
      2. std::bind
        1. std::bind是这样一种机制,它可以预先把指定可调用实体的某些参数绑定到已有的变量,产生一个新的可调用实体,这种机制在回调函数的使用过程中也颇为有用。
        2. 在C++11中,提供了std::bind,它绑定的参数的个数不受限制,绑定的具体哪些参数也不受限制,由用户指定,这个bind才是真正意义上的绑定。
        3. std::placeholders::_1是一个占位符,代表这个位置将在函数调用时,被传入的第一个参数所替代。
        4. function<void(int, int)> f1 = bind(&Test::func, &obj, _1, _2); 通过std::bind和std::function配合使用,所有的可调用对象均有了统一的操作方法。
        5. 对于类成员函数bind需要传入对象地址,因为对象的成员函数需要this指针。函数参数std::placeholders(占位符)表示,默认参数直接写值。
      3. 总结
        1. std::function可以封装任何可调用对象,直接赋值即可。
        2. std::bind可以绑定任何可调用对象,类成员函数需要传入类对象地址
  3. 智能指针

    1. C++11有unique_ptr、shared_ptr与weak_ptr等智能指针(smart pointer),定义在<memory》中。可以对动态资源进行管理,保证任何情况下,已构造的对象最终会销毁,即它的析构函数最终会被调用。
    2. unique_ptr
      1. unique_ptr持有对对象的独有权,同一时刻只能有一个unique_ptr指向给定对象(通过禁止拷贝语义、只有移动语义来实现)。
      2. unique_ptr指针本身的生命周期:从unique_ptr指针创建时开始,直到离开作用域。
      3. 开作用域时,若其指向对象,则将其所指对象销毁(默认使用delete操作符,用户可指定其他操作)。
      4. 函数接口
        1. 释放方法 std::unique_ptr::release,这里的释放并不会销毁其指向的对象,而且将其指向的对象释放出去。
        2. 重置方法 std::unique_ptr::reset
        3. 交换方法 std::unique_ptr::swap
    3. weak_ptr是为配合shared_ptr而引入的一种智能指针来协助shared_ptr工作
      1. 它可以从一个shared_ptr或另一个weak_ptr对象构造
      2. 它的构造和析构不会引起引用计数的增加或减少。
      3. 没有重载 * 和 -> 但可以使用lock获得一个可用的shared_ptr对象
      4. 函数接口
        1. operator=(); // 把shared_ptr或weak_ptr赋值给weak_ptr。
        2. expired(); // 判断它指资源是否已过期(已经被销毁)。
        3. lock(); // 返回shared_ptr,如果资源已过期,返回空的shared_ptr。
        4. reset(); // 将当前weak_ptr指针置为空。
        5. swap(); // 交换。
      5. weak_ptr解决循环引用 shared_ptr智能指针的循环引用导致的内存泄漏问题,可以通过weak_ptr解决。只需要将A或B的任意一个成员变量改为weak_ptr:
    4. shared_ptr
      1. 允许多个该智能指针共享第“拥有”同一堆分配对象的内存,这通过引用计数(reference counting)实现,会记录有多少个shared_ptr共同指向一个对象,一旦最后一个这样的指针被销毁,也就是一旦某个对象的引用计数变为0,这个对象会被自动删除。
      2. (shared_ptr)的引用计数本身是线程安全(引用计数是原子操作)。
        1. 多个线程同时读同一个shared_ptr对象是线程安全的。
        2. 如果是多个线程对同一个shared_ptr对象进行读和写,则需要加锁。
        3. 多线程读写shared_ptr所指向的同一个对象,不管是相同的shared_ptr对象,还是不同的shared_ptr对象,也需要加锁保护。
      3. 使用方法:
        1. 智能指针重载了*和->操作符,可以像使用指针一样使用shared_ptr。
        2. use_count()方法返回引用计数器的值。
        3. unique()方法,如果use_count()为1,返回true,否则返回false。
        4. shared_ptr支持赋值,左值的shared_ptr的计数器将减1,右值shared_ptr的计算器将加1。
        5. get()方法返回裸指针。
        6. 不要用同一个裸指针初始化多个shared_ptr。
        7. 不要用shared_ptr管理不是new分配的内存。
        8. 用nullptr给shared_ptr赋值将把计数减1,如果计数为0,将释放对象,空的shared_ptr==nullptr。
        9. hared_ptr 不存在释放的操作,控制权不是由一个人说了算的。
        10. std::move()可以转移对原始指针的控制权。还可以将unique_ptr转移成shared_ptr。
        11. reset()改变与资源的关联关系。
        12. swap()交换两个shared_ptr的控制权。
        13. shared_ptr也可象普通指针那样,当指向一个类继承体系的基类对象时,也具有多态性质,如同使用裸指针管理基类对象和派生类对象那样。
        14. shared_ptr不是绝对安全,如果程序中调用exit()退出,全局的shared_ptr可以自动释放,但局部的shared_ptr无法释放。
        15. shared_ptr提供了支持数组的具体化版本。数组版本的shared_ptr,重载了操作符[],操作符[]返回的是引用,可以作为左值使用。
        16. 如果unique_ptr能解决问题,就不要用shared_ptr。unique_ptr的效率更高,占用的资源更少。
      4. 智能指针的删除器
        1. 在默认情况下,智能指针]过期的时候,用delete原始指针; 释放它管理的资源。
        2. 程序员可以自定义删除器,改变智能指针释放资源的行为。
        3. 删除器可以是全局函数、仿函数和Lambda表达式,形参为原始指针。
      5. shared_ptr的构造函数也是explicit,但是,没有删除拷贝构造函数和赋值函数。初始化方法如下
        1. c p0(new AA("zq"));     // 分配内存并初始化。
        2. shared_ptr p0 = make_shared("zq"); 
        3. // 用已存在的地址初始化。
          1. AA* p = new AA("zq");
          2. shared_ptr p0(p);
        4. 用已存在的shared_ptr初始化,计数加1。
          1. shared_ptr p0(new AA("zq"));
          2. shared_ptr p1(p0);
          3. shared_ptr p2(p1);
    5. 智能指针作为函数参数使用 1. shared_ptr作为函数参数的正确用法,就是传值使用。用智能指针的引用、指针都是不对的,无法实现智能指针的引用计数效果 2. 向函数传入unique_ptr参数时,必须要使用“引用传参”
  4. 右值引用

    1. 左值、右值
      1. 一个最为典型的判别方法就是,在赋值表达式中,出现在等号左边的就是“左值”,而在等号右边的,则称为“右值”
      2. 那就是可以取地址的、有名字的就是左值,反之,不能取地址的、没有名字的就是右值
      3. 对于左值,右值表示字面常量、表达式、函数的非引用返回值等。
    2. 左值引用、右值引用
      1. 左值引用是对一个左值进行引用的类型,右值引用则是对一个右值进行引用的类型。
      2. 左值引用和右值引用都是属于引用类型。无论是声明一个左值引用还是右值引用,都必须立即进行初始化。而其原因可以理解为是引用类型本身自己并不拥有所绑定对象的内存,只是该对象的一个别名。
      3. 左值引用是具名变量值的别名,而右值引用则是不具名(匿名)变量的别名。
      4. 右值引用,使用&&表示:
    3. 移动语义
      1. 转移语义可以将资源 ( 堆,系统对象等 ) 从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,能够大幅度提高 C++ 应用程序的性能。临时对象的维护 ( 创建和销毁 ) 对性能有严重影响
      2. 在现有的 C++ 机制中,我们可以定义拷贝构造函数和赋值函数。要实现转移语义,需要定义转移构造函数,还可以定义转移赋值操作符。对于右值的拷贝和赋值会调用转移构造函数和转移赋值操作符。
      3. 如果转移构造函数和转移拷贝操作符没有定义,那么就遵循现有的机制,拷贝构造函数和赋值操作符会被调用
      4. 普通的函数和操作符也可以利用右值引用操作符实现转移语义。
      5. 转移构造函数:
        1. 参数(右值)的符号必须是右值引用符号,即“&&”。
        2. 参数(右值)不可以是常量,因为我们需要修改右值。
        3. 参数(右值)的资源链接和标记必须修改,否则,右值的析构函数就会释放资源,转移到新对象的资源也就无效了。
      6. 标准库函数 std::move
        1. 既然编译器只对右值引用才能调用转移构造函数和转移赋值函数,而所有命名对象都只能是左值引用,如果已知一个命名对象不再被使用而想对它调用转移构造函数和转移赋值函数,也就是把一个左值引用当做右值引用来使用,怎么做呢?标准库提供了函数 std::move,这个函数以非常简单的方式将左值引用转换为右值引用。
      7. 完美转发 std::forward
        1. 完美转发适用于这样的场景:需要将一组参数原封不动的传递给另一个函数。
        2. 实参如果原来是左值,到了形参中还是左值,forward是按照形参原来的类型处理,所以std::forward之后还是个左值;
        3. 实参如果原来是右值,到了形参中变成了左值,forward是按照形参原来的类型处理,所以std::forward之后还是右值;
        4. forward有强制把左值转成右值的能力,所以forward只是对原来是右值的情况有用;
  5. 新增关键字.

    1. final:
      1. final阻止类的进一步派生和虚函数的进一步重写
    2. override:
      1. override确保在派生类中声明的函数跟基类的虚函数有相同的签名
    3. =default:
      1. 程序员只需在函数声明后加上“=default;”,就可将该函数声明为 "=default"函数,编译器将为显式声明的 "=default"函数自动生成函数体。
    4. =delete: 为了能够让程序员显式的禁用某个函数,C++11 标准引入了一个新特性:"=delete"函数。程序员只需在函数声明后上“=delete;”,就可将该函数禁用。
    5. auto:
      1. auto的自动类型推导,用于从初始化表达式中推断出变量的数据类型
    6. nullptr:
      1. c++11中新增的nullptr是一种指向空地址的指针,可以用于初始化或比较任何类型的指针。nullptr也可以用于消除函数重载的二义性,与模板一起使用,进行强制类型转换,作为常量表达式,以及与智能指针一起使用,使c++程序更加安全和简洁。
  6. STL容器的线程安全性

    1. 对于C++11及以后版本,STL提供了以下两种线程安全级别的容器:
      1. 顺序容器(如 vector,deque,list,forward_list,string)的线程安全版本为其加了 _safe 后缀,例如 std::vector_safe。
      2. 关联容器(如 map,set,unordered_map,unordered_set)和无序容器(如 unordered_map,unordered_set)的线程安全版本为其加_mt 后缀,例如 std::map_mt。
    2. 一个库可以下列方式实现线程安全的容器
      1. 每次调用容器的成员函数期间都要锁定该容器
      2. 每个容器返回的迭代器的生存期内都要锁定该容器
      3. 每个容器调用算法执行期间锁定该容器
  7. 无锁编程技术 LOCK-FREE,字面解释就是不通过锁来解决多线程、多进程之间的数据同步和访问的程序设计方案。相对来说就是通过数据结构和算法来解决数据并发冲突的实现方案

    1. CAS解决多线程并行情况下使用锁造成性能损耗的一种机制
      1. CAS包含三个操作数:
        1. 内存位置 (V) 是个指针
        2. 预期原值 (A)
        3. 新值(B)
        4. 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作
      2. CAS总结
        1. CAS(Compare and Swap)比较并替换,是线程并发运行时用到的一种技术;
        2. CAS是原子操作,保证并发安全,而不能保证并发同步;
        3. CAS是CPU的一个指令;
        4. CAS是非阻塞轻量级乐观锁;
      3. C++ atomic类可以用于实现CAS的接口(compare_exchange_weak,compare_exchange_strong)。
      4. GCC编译也提供了这样的接口:bool __sync_bool_compare_and_swap (type *ptr, type oldval type newval, ...)
      5. CAS使用场景
        1. 乐观锁的实现方案:不加锁,假设没有冲突去完成某项操作,如果因为冲突失败就重试,直到成功为止。
      6. 缺点
        1. 循环开销问题。长时间更改不成功,会来带大量的CPU消耗。解决方法:需要在修改失败后执行其它逻辑, 且CAS并不适合资源大量竞争的情况。
        2. ABA问题:线程1准备用CAS将变量的值由A替换为B,在此之前,线程2将变量的值由A替换为C,又由C替换为A,然后线程1执行CAS时发现变量的值仍然为A,所以CAS成功。但实际上这时的现场已经和最初不同了。
    2. 数据Hash其实就是通过Hash算法把数据提前来确定由哪个节点进行处理或者存储,解决数据并发的思想是通过算法解决不同的数据到不同的节点。
      1. 算法:数据.hashCode() % 节点数量。据Hash其实就是通过Hash算法把数据提前来确定由哪个节点进行处理或者存储,解决数据并发的思想是通过算法解决不同的数据到不同的节点。算法:数据.hashCode() % 节点数量。
      2. 使用场景
        1. 定时任务处理数据时。例如:一个定时任务数据量较多,需要集群处理。那么就可以同时启动任务读取数据,然后根据idHash来决定当前节点是否要处理这条数据。
        2. 请求到指定服务器进行处理。例如:Nginx ipHash转发策略,Kafka hash分区保证分区有序性。
      3. 缺点
        1. 扩容相对复杂,需要进行数据迁移。例如一致性hash算法,Kafka分区再均衡策略。但是某些场景不一定支持扩容。
        2. hash算法是否散列,如果算法不够散列会出现数据倾斜问题。
    3. 单线程读写ringbuffer
      1. voliate :背景:多核cpu时的cache刷新到主存不同步带来的问题;使用它,则保证每次都从主存中读取,防止编译器对它进行优化,从cache中读;
  8. 线程局部存储

    1. 对于一个存在多个线程的进程来说,有时需要每个线程都自己操作自己的这份数据。这有点类似c++类的实例的属性,每个实例对象操作的都是自己的属性。我们把这样的数据成为线程局部存储(thread local storage,TLS)
    2. 线程局部存储是指对象内存在线程开始后分配,线程结束时回收,且每个线程有该对象自己的实例。简单的说,线程局部存储的对象都是独立于各个线程的。
    3. linux上的NTPL库提供了一套函数接口来实现线程局部存储
      1. int pthread_key_create(pthread_key_t* key, void (destructor)(void));
      2. int pthread_key_delete(pthread_key_t key);
      3. int pthread_setspecific(pthread_key_t key, const void* value);
      4. void* pthread_getspecific(pthread_key_t key);
    4. 在c++11标准中整数添加了新的tread_local说明符来声明线程局部存储变量
  9. 模板类与类模板、函数模板与模板函数

    1. 类模板: 允许用户为类定义个一种模式,使得类中的某些数据成员、默认成员函数的参数,某些成员函数的返回值,能够取任意类型
      1. template <类型形参表> //类型参数声明 class 类名{ 类模板的代码 }
      2. 类模板不能直接使用,必需先实例化为相应的模板类。定义类模板之后,创建模板类的格式如下:类模板名 <类型实参表> 对象表;
      3. 类型实参表与类模板中的类型形参表相匹配。
      4. 类模板可作为函数参数
      5. 类模板中的成员函数还可以是一个函数模板。成员函数模板只有在被调用时才会被实例化
      6. 类模板中也可以使用非类型参数,即值参数
      7. 类模板中定义静态函数,则该模板类的所有对象共享一个静态数据成员。
      8. 一个类模板中可以设计友元函数,友元函数的形参可以是类模板或类模板的引用。如果在类模板中设计与参数类型无关的友元函数,那么在类外面实现时也不能省略template类型参数声明,否则将其看成是一个普通全局函数
      9. 可变参数模板的定义
        1. C++11增强了模板功能,允许模板定义中包含0到任意个模板参数,这就是可变参数模板。
        2. 语法:可变参数模板和普通模板的语义是一样的,只是写法上稍有区别,声明可变参数模板时需要在typename或class后面带上省略号:
          1. template <class... T> void f(T... args);
          2. 声明一个参数包T... args,这个参数包中可以包含0到任意个模板参数;
      10. 可变参数模板的展开
        1. 展开可变模板参数函数的方法一般有两种:一种是通过递归函数来展开参数包,另外一种是通过逗号表达式来展开参数包。
        2. 递归函数方式展开参数包:
          1. 通过递归函数展开参数包,需要提供一个参数包展开的函数和一个递归终止函数,递归终止函数正是用来终止递归
          2. 递归函数展开参数包是一种标准做法,也比较好理解,但也有一个缺点,就是必须要一个重载的递归终止函数,即必须要有一个同名的终止函数来终止递归。
        3. 逗号表达式展开参数包:
          1. 不需要通过递归终止函数,是直接在expand函数体中展开的, printarg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。这种就地展开参数包的方式实现的关键是逗号表达式。
          2. expand函数中的逗号表达式:(printarg(args), 0),也是按照这个执行顺序,先执行printarg(args),再得到逗号表达式的结果0
      11. 可变参数模板类的展开
        1. 可变参数模板类是一个带可变模板参数的模板类,比如C++11中的元祖std::tuple就是一个可变模板类,可变参数模板类的参数包展开需要通过模板特化和继承方式去展开,展开方式比可变参数模板函数要复杂
        2. 一个基本的可变参数模板应用类由三部分组成,第一部分是:
          1. 前向声明,声明这个sum类是一个可变参数模板类
          2. 第二部分是类的定义,定义了一个部分展开的可变模参数模板类,告诉编译器如何递归展开参数包。
          3. 第三部分是特化的递归终止类
    2. 模板类: 就是类模板中的参数确定之后的产物,也就是类模板实例化后的产物。(它是一个参数已经确定好的类)
    3. 函数模板:函数模板是一个模板,其中用到通用类型参数,不能直接执行
      1. 编译器从函数模板通过不同类型产生不同函数
      2. 编译器会对函数模板进行两次编译
        1. 对模板代码本身进行编译
        2. 对参数替换后的代码进行编译
      3. 注意事项
        1. 函数模板本身不允许隐式类型转换
        2. 自动推导类型时,必须严格匹配
        3. 显式类型指定时,能够进行隐式类型转换
      4. 函数模板多参数
        1. 无法自动推导返回值类型;
        2. 可以从左向右部分指定类型参数;
        3. 工程中将返回值参数作为第一个类型参数;
      5. 函数模板重载
        1. 优先匹配普通函数,其次匹配函数模板
        2. 如果函数模板可以产生一个更好的匹配,那么选择模板
        3. 可以通过空模板实参列表,限定只匹配模板
      6. 总结
        1. 函数模板通过具体类型产生不同的函数
        2. 函数模板可以定义任意多个不同的类型参数
        3. 函数模板中的返回值类型必须是显示指定
        4. 函数模板可以像普通函数一样重载
    4. 模板函数:模板函数的重点是函数。表示的是由一个模板生成而来的函数
  10. C++ 模板特化与偏特化

    1. 模板特化(template specialization)不同于模板实例化,模板参数在某种特定类型下的具体实现称为模板特化。
    2. 模板特化有时也称之为模板的具体化,分函数模板特化和类模板特化。
      1. 函数模板特化:函数模板特化指函数模板在为特定类型下的特定实现。
      2. 使用普通函数重载和使用模板特化还是有不同之处:
        1. 如果使用普通重载函数,那么不管是否发生实际的函数调用,都会在目标文件中生成该函数的二进制代码。而如果使用模板的特化版本,除非发生函数调用,否则不会在目标文件中包含特化模板函数的二进制代码。这符合函数模板的“惰性实例化”准则。
        2. 如果使用普通重载函数,那么在分离编译模式下,需要在各个源文件中包含重载函数的申明,否则在某些源文件中就会使用模板函数,而不是重载函数。
      3. 类模板特化类似于函数模板的特化,即类模板参数在某种特定类型下的具体实现
        1. 模板偏特化(Template Partitial Specialization)是模板特化的一种特殊情况,指显示指定部分模板参数而非全部模板参数,或者指定模板参数的部分特性分而非全部特性,也称为模板部分特化。与模板偏特化相对的是模板全特化,指对所有模板参数进行特化。模板全特化与模板偏特化共同组成模板特化。
        2. 模板偏特化主要分为两种,一种是指对部分模板参数进行全特化,另一种是对模板参数特性进行特化,包括将模板参数特化为指针、引用或是另外一个模板类。
        3. 对主版本模板类、全特化类、偏特化类的调用优先级从高到低进行排序是:全特化类>偏特化类>主版本模板类