第7章 C++模板总结

C++函数模板(Function Template)

值(Value)和类型(Type)是数据的两个主要特征,它们在C++中都可以被参数化

所谓函数模板

  • 实际上是建立一个通用函数,(个人:也就是说这个函数要执行的逻辑操作是通用的,框架是一致的,对于里面用到的类型,切换到另一种类型,逻辑操作依然正确的,依然是一样的,所以我们可以借助于一个虚拟的占位符来表示类型,把这个通用的函数逻辑操作定义出来,就是所谓的模板函数)
  • 它所用到的数据的类型(包括返回值类型、形参类型、局部变量类型)可以不具体指定,而是用一个虚拟的类型来代替(实际上是用一个标识符来占位),(个人:这里的占位符可以看成是实际类型的形参,对实际类型进行了参数化)
  • 等发生函数调用时编译器再根据传入的实参来逆推出真正的类型。(个人:也就是模板函数类型的实例化发生在函数调用时,编译器根据传入的实参来推断)

这个通用函数就称为函数模板(Function Template)。换个角度说,函数模板除了支持值的参数化,还支持类型的参数化

#include <iostream>
using namespace std;
template<typename T> void Swap(T *a, T *b){
    T temp = *a;
    *a = *b;
    *b = temp;
}
int main(){
    //交换 int 变量的值
    int n1 = 100, n2 = 200;
    Swap(&n1, &n2);
    cout<<n1<<", "<<n2<<endl;
   
    //交换 float 变量的值
    float f1 = 12.5, f2 = 56.93;
    Swap(&f1, &f2);
    cout<<f1<<", "<<f2<<endl;
   
    //交换 char 变量的值
    char c1 = 'A', c2 = 'B';
    Swap(&c1, &c2);
    cout<<c1<<", "<<c2<<endl;
   
    //交换 bool 变量的值
    bool b1 = false, b2 = true;
    Swap(&b1, &b2);
    cout<<b1<<", "<<b2<<endl;
    return 0;
}

  • template是定义函数模板的关键字,它后面紧跟尖括号<>
  • 尖括号包围的是类型参数(也可以说是虚拟的类型,或者说是类型占位符)。
  • typename是另外一个关键字,用来声明具体的类型参数,这里的类型参数就是T。类型参数的命名规则跟其他标识符的命名规则一样,不过使用 T、T1、T2、Type 等已经成为了一种惯例。 
  • 从整体上看,template<typename T>被称为模板头模板头中包含的类型参数可以用在函数定义的各个位置,包括返回值、形参列表和函数体

下面我们来总结一下定义模板函数的语法

template <typename 类型参数1 , typename 类型参数2 , ...> 返回值类型  函数名(形参列表){
    //在函数体中可以使用类型参数
}  

函数模板也可以提前声明,不过声明时需要带上模板头并且模板头和函数定义(声明)是一个不可分割的整体,它们可以换行,但中间不能有分号。 

(个人:函数模板定义和普通的函数定义不一样,普通的函数声明放在头文件中,定义放在源文件中,防止链接时重复定义,但是函数模板定义一般放在头文件中,代码中遇到函数模板的调用时,函数模板才会实例化,而函数模板的实例化需要知道函数模板的定义,没有遇到的类型调用,函数模板不会实例化,因为对于函数模板的类型,从理论上来说,有无数种,如果每一种都实例化,这是不可能的,所以函数模板只会对发生函数调用时的类型进行实例化。要显式实例化模板函数,请在 template 关键字后接函数的声明(不是定义),且函数标识符后接模板参数。

template float twice<float>(float original);

在编译器可以推断出模板参数时,模板参数可以省略。

template int twice(int original); 

而将函数模板放在头文件中,不会发生重复定义的问题,是因为函数模板实例化之后是弱符号,链接时编译器随机选择一个)

一般情况下模板的定义要放在头文件中,但是如果你就是执拗,非要像普通函数一样将模板函数声明放在头文件中,定义放在源文件中,那么会报错,

你要非执拗的这么做不可,那么只有在函数模板的定义处显示的进行我们需要类型的模板函数的实例化,

此时,运行结果为:

(个人:下面为更详细从其他资料看到的的论述)

符号的概念

符号 (symbol) 是在 ELF 格式中会遇到的概念. 也就是在写汇编代码的时候会遇到, 而在更高级的语言 (比如 C 或 C++) 中不会直接遇到这个概念.

比如像下面这段 C 程序,

int func() { return 1; }  

会生成对应的汇编代码:

可以看到,函数名 func 就是一个符号. 如果要调用这个函数, 就可以利用这个函数的符号.

符号的绑定 (Symbol Binding)

符号和符号之间是不一样的. 首先我们明确, 链接器的任务是把很多个 .o 文件 (object file) 组合到一起. 有时候, 一个符号可能在某一个 object file 中定义了, 而在另一个 object file 中引用了. 链接器的任务就是把这些跨文件的引用都分析清楚.

常见的Symbol Binding类型有三种:

  • Local Symbol:只有当前 object file 文件都能看见, 别的 object file 看不到.
  • Global Symbol : 所有object file 里都能看见, 全局只能有一个.
  • Weak Symbol: 所有 object file 里都能看见, 全局可以有很多, 但最后只保留一个. 如果有同名 Global Symbol, 就只留下 Global Symbol.

从上面的 .globl func 可以看出来, func 最后生成了一个 Global Symbol. 这样的话你就可以在别的 object file 引用这个 Global Symbol 了. 

如果我们在不同的object file里面声明了同样的Global Symbol, 链接的时候就会报错redefinition of symbol .

C++ 中的处理

对于一般函数, 生成的都是Global Symbol. 如果你编译两个 .cpp 文件(每个 .cpp 是一个独立的编译单元), 都给出了同一个函数的定义, 那么就会在两个 object file 之中分别生成 Global Symbol. 这样链接这两个 object file 就会报错.
解决方案一般有三种:
  • 只在一个编译单元中给出函数定义, 这样的话就只有一个 Global Symbol 生成. 不会有问题.
  • 变成两个 Local Symbol. 这样做最后的二进制文件会变大一些. 具体在 C++ 中有两种做法, 拿我们的 func 函数示例. 可以加上 static.
static int func() { return 1; } // 会生成 Local Symbol

或者是用没有名字的 namespace 包裹着.

namespace { // 匿名的 namespace 里都是 Local Symbol
    int func() { return 1; } 
}

这个在 C++ 层面上称为 Internal Linkage.

  • 变成两个 Weak Symbol. 这个直接操作可以用编译器拓展 __attribute__((weak)).

这 3 种处理方案, 一般使用 1, 3 是比较常见的. 第 2 种的话, 每一个编译单元都有一份副本, 会增加最终生成二进制的体积和符号个数.

更常用的操作是使用 inline 修饰一个函数:

inline int func() { return 1; }

对于这个函数, 编译器会考虑要不要内联它. 如果编译器决定不内联, 就会生成一个 Weak Symbol. (对于 Linux 下的 GCC/Clang)。这样即使是在多个编译单元都定义了, 也不会有重定义的错误. 链接器则会从不同 object file 中随机选择一份 func 的副本. (具体选哪个要看链接器的实现了, 你要做的是确保每个副本是一样的)

如果你在 struct/class 中给出了函数定义, 那么它也是隐式 inline 的. 比如

struct A {
    int func() { return 0; } // 隐式 inline, 所以不会有重定义的错误
};

C++ 之中, 除了 inline 会生成 Weak Symbol, 模板生成的内容也是 Weak Symbol. 所以模板可以放在头文件中, 而不用有担心重定义的错误.

C++类模板(Class Template)  

定义类模板的语法为:

template<typename 类型参数1 , typename 类型参数2 , …>//模板头 
class 类名{
    //TODO:
};

一旦定义了类模板,就可以将类型参数用于类的成员函数和成员变量了。注意:模板头和类头是一个整体,可以换行,但是中间不能有分号。  

上面的代码仅仅是类的定义,我们还需要在类外实现成员函数。在类外实现成员函数时仍然需要带上模板头,格式为:

template<typename 类型参数1 , typename 类型参数2 , …>
返回值类型 类名<类型参数1 , 类型参数2, ...>::函数名(形参列表){
    //TODO:
}
第一行是模板头,第二行是函数头,它们可以合并到一行,不过为了让代码格式更加清晰,一般是将它们分成两行。 

下面就对Point类的成员函数进行定义:

template<typename T1, typename T2>  //模板头
T1 Point<T1, T2>::getX() const /*函数头*/ {
    return m_x;
}
template<typename T1, typename T2>
void Point<T1, T2>::setX(T1 x){
    m_x = x;
}
template<typename T1, typename T2>
T2 Point<T1, T2>::getY() const{
    return m_y;
}
template<typename T1, typename T2>
void Point<T1, T2>::setY(T2 y){
    m_y = y;
}

除了template关键字后面要指明类型参数,类名Point后面也要带上类型参数,只是不加typename关键字了。另外需要注意的是,在类外定义成员函数时,template 后面的类型参数要和类定义时的一致。  

使用类模板创建对象时,需要指明具体的数据类型与函数模板不同的是,类模板在实例化时必须显式地指明数据类型,编译器不能根据给定的数据推演出数据类型。(个人:原因是显然的,初始化时可能调用不同实现的构造函数,也可能调用默认构造函数,这时候没法根据初始化值推断,因为有些构造函数的参数的类型无法和类模板的类型对应起来),请看下面的代码:

Point<int, int> p1(10, 20);
Point<int, float> p2(10, 15.5);
Point<float, char*> p3(12.4, "东经180度"); 

需要注意的是,赋值号两边都要指明具体的数据类型,且要保持一致。下面的写法是错误的:

//赋值号两边的数据类型不一致
Point<float, float> *p = new Point<float, int>(10.6, 109);
//赋值号右边没有指明数据类型
Point<float, float> *p = new Point(10.6, 109);

将上面的类定义和类实例化的代码整合起来,构成一个完整的示例,如下所示(个人:类模板的定义以及里面成员函数的实现一般情况下都放在头文件中,普通的类的定义放在头文件中,函数模板的定义一般要放在头文件中,二者综合起来,类模板的定义以及类模板的成员函数的实现都需要放在头文中):

#include <iostream>
using namespace std;
template<class T1, class T2>  //这里不能有分号
class Point {
public:
    Point(T1 x, T2 y) : m_x(x), m_y(y) { }
public:
    T1 getX() const;  //获取x坐标
    void setX(T1 x);  //设置x坐标
    T2 getY() const;  //获取y坐标
    void setY(T2 y);  //设置y坐标
private:
    T1 m_x;  //x坐标
    T2 m_y;  //y坐标
};
template<class T1, class T2>  //模板头
T1 Point<T1, T2>::getX() const /*函数头*/ {
    return m_x;
}
template<class T1, class T2>
void Point<T1, T2>::setX(T1 x) {
    m_x = x;
}
template<class T1, class T2>
T2 Point<T1, T2>::getY() const {
    return m_y;
}
template<class T1, class T2>
void Point<T1, T2>::setY(T2 y) {
    m_y = y;
}
int main() {
    Point<int, int> p1(10, 20);
    cout << "x=" << p1.getX() << ", y=" << p1.getY() << endl;
    Point<int, const char*> p2(10, "东经180度");
    cout << "x=" << p2.getX() << ", y=" << p2.getY() << endl;
    Point<const char*, const char*>* p3 = new Point<const char*, const char*>("东经180度", "北纬210度");
    cout << "x=" << p3->getX() << ", y=" << p3->getY() << endl;
    return 0;
}

  

C++函数模板的重载

C++ 允许对函数模板进行重载,程序员可以像重载常规函数那样重载模板定义

下面是一个重载函数模板的完整示例:

#include <iostream>
using namespace std;
template<class T> void Swap(T& a, T& b);  //模板①:交换基本类型的值
template<typename T> void Swap(T a[], T b[], int len);  //模板②:交换两个数组
void printArray(int arr[], int len);  //打印数组元素
int main() {
    //交换基本类型的值
    int m = 10, n = 99;
    Swap(m, n);  //匹配模板①
    cout << m << ", " << n << endl;
    //交换两个数组
    int a[5] = { 1, 2, 3, 4, 5 };
    int b[5] = { 10, 20, 30, 40, 50 };
    int len = sizeof(a) / sizeof(int);  //数组长度
    Swap(a, b, len);  //匹配模板②
    printArray(a, len);
    printArray(b, len);
    return 0;
}
template<class T> void Swap(T& a, T& b) {
    T temp = a;
    a = b;
    b = temp;
}
template<typename T> void Swap(T a[], T b[], int len) {
    T temp;
    for (int i = 0; i < len; i++) {
        temp = a[i];
        a[i] = b[i];
        b[i] = temp;
    }
}
void printArray(int arr[], int len) {
    for (int i = 0; i < len; i++) {
        if (i == len - 1) {
            cout << arr[i] << endl;
        }
        else {
            cout << arr[i] << ", ";
        }
    }
}

C++函数模板的模板类型参数推断 

  • 在使用类模板创建对象时,程序员需要显式的指明具体的模板类型
Point<int, int> p1(10, 20);  //在栈上创建对象
Point<char*, char*> *p = new Point<char*, char*>("东京180度", "北纬210度");  //在堆上创建对象
  • 而对于函数模板,调用函数时可以不显式地指明具体的模板类型
//函数模板声明
template<typename T> void Swap(T &a, T &b);
//函数调用
int n1 = 100, n2 = 200;
Swap(n1, n2);
float f1 = 12.5, f2 = 56.93;
Swap(f1, f2);

虽然没有显式地指明T的具体类型,但是编译器会根据 n1和n2、f1和f2的类型自动推断出T的类型

这种通过函数实参来确定模板类型参数的具体类型的过程称为模板类型参数推断。在模板实参推断过程中,编译器使用函数调用中的实参类型来寻找类型参数的具体类型

对于普通函数(非模板函数),发生函数调用时会对实参的类型进行适当的转换,以适应形参的类型而对于函数模板,类型转换则受到了更多的限制,仅能进行「const 转换」和「数组或函数指针转换」,其他的都不能应用于函数模板。例如有下面几个函数模板:

template<typename T> void func1(T a, T b);
template<typename T> void func2(T *buffer);
template<typename T> void func3(const T &stu);
template<typename T> void func4(T a);
template<typename T> void func5(T &a);

它们具体的调用形式为:

int name[20];
Student stu1("张华", 20, 96.5);  //创建一个Student类型的对象
func1(12.5, 30);  //Error
func2(name);  //name的类型从 int [20] 换转换为 int *,所以 T 的真实类型为 int
func3(stu1);  //非const转换为const,T 的真实类型为 Student
func4(name);  //name的类型从 int [20] 换转换为 int *,所以 T 的真实类型为 int *
func5(name);  //name的类型依然为 int [20],不会转换为 int *,所以 T 的真实类型为 int [20]

可以发现,当函数形参是引用类型时,数组不会转换为指针。这个时候读者要注意下面这样的函数模板:

template<typename T> void func(T &a, T &b);

如果它的具体调用形式为:

int str1[20];
int str2[10];
func(str1, str2);

由于str1、str2的类型分别为int [20]和int [10]在函数调用过程中又不会转换为指针,所以编译器不知道应该将T实例化为 int [20] 还是 int [10],导致调用失败。  

    函数模板的实参推断是指「在函数调用过程中根据实参的类型来寻找类型参数的具体类型」的过程,这在大部分情况下是奏效的,但是当类型参数的个数较多时,就会有个别的类型无法推断出来,这个时候就必须显式地指明实参

    下面是一个实参推断失败的例子:

    template<typename T1, typename T2> void func(T1 a){
        T2 b;
    }
    func(10);  //函数调用
    

    func() 有两个类型参数,分别是 T1 和 T2,但是编译器只能从函数调用中推断出T1的类型来,不能推断出T2的类型来,所以这种调用是失败的,这个时候就必须显式地指明 T1、T2 的具体类型

    「为函数模板显式地指明实参」和「为类模板显式地指明实参」的形式是类似的,就是在函数名后面添加尖括号< >,里面包含具体的类型

    func<int, int>(10);
    

    显式指明的模板实参会按照从左到右的顺序与对应的模板参数匹配:第一个实参与第一个模板参数匹配,第二个实参与第二个模板参数匹配,以此类推。只有尾部(最右)的类型参数的实参可以省略,而且前提是它们可以从传递给函数的实参中推断出来。  

    template<typename T1, typename T2> void func(T2 a){
        T1 b;
    }
    //函数调用
    func<int>(10);  //省略 T2 的类型
    func<int, int>(20);  //指明 T1、T2 的类型
    

    由于 T2 的类型能够自动推断出来,并且它位于参数列表的尾部(最右),所以可以省略。  

    上面我们提到,函数模板仅能进行「const 转换」和「数组或函数指针转换」两种形式的类型转换,但是当我们显式地指明类型参数的实参(具体类型)时,就可以使用正常的类型转换(非模板函数可以使用的类型转换)了。例如,

    template<typename T> void func(T a, T b);
    

    它的具体调用形式如下:

    func(10, 23.5);  //Error
    func<float>(20, 93.7);  //Correct
    

    在第二种调用形式中,我们已经显式地指明了 T 的类型为 float,编译器不会再为「T 的类型到底是 int 还是 double」而纠结了,所以可以从容地使用正常的类型转换了。    

    C++模板特例化 

      模板是一种泛型技术

      • 它能接受的类型是宽泛的、没有限制的,
      • 并且对这些类型使用的算法都是一样的(函数体或类体一样)

      但是现在我们希望改变这种“游戏规则”,让模板能够针对某种具体的类型使用不同的算法(函数体或类体不同),这在 C++ 中是可以做到的,这种技术称为模板特例化。函数模板和类模板都可以特例化

      函数模板特例化:  

      #include <iostream>
      #include <string>
      using namespace std;
      typedef struct {
          string name;
          int age;
          float score;
      } STU;
      //函数模板声明
      template<class T> const T& Max(const T& a, const T& b);
      //函数模板的显示特例化(针对STU类型的显示特例化)声明
      template<> const STU& Max<STU>(const STU& a, const STU& b);
      //重载<<
      ostream& operator<<(ostream& out, const STU& stu);
      int main() {
          int a = 10;
          int b = 20;
          cout << Max(a, b) << endl;
      
          STU stu1 = { "王明", 16, 95.5 };
          STU stu2 = { "徐亮", 17, 90.0 };
          cout << Max(stu1, stu2) << endl;
          return 0;
      }
      template<class T> const T& Max(const T& a, const T& b) {
          return a > b ? a : b;
      }
      template<> const STU& Max<STU>(const STU& a, const STU& b) {
          return a.score > b.score ? a : b;
      }
      ostream& operator<<(ostream& out, const STU& stu) {
          out << stu.name << " , " << stu.age << " , " << stu.score;
          return out;
      }
      

      不同的比较方案最终导致了算法(函数体)的不同,我们不得不借助模板特例化技术对 STU 类型进行单独处理

      • 第 12 行代码就是模板特例化的声明,第 28 行代码进行了定义。 
      • 请读者注意第 28 行代码,Max<STU>中的STU表明了要将类型参数 T 特例化为 STU 类型,原来使用 T 的位置都应该使用 STU 替换,包括返回值类型、形参类型、局部变量的类型。 
      • Max 只有一个类型参数 T,并且已经被特例化为 STU 了,这样整个模板就不再有类型参数了,类型参数列表也就为空了,所以模板头应该写作template<>。 
      • 另外,Max<STU>中的STU是可选的,因为函数的形参已经表明,这是 STU 类型的一个特例化,编译器能够逆推出 T 的具体类型。简写后的函数声明为:
        template<> const STU& Max(const STU& a, const STU& b);
        

      函数的调用规则:  

       在C++中,对于给定的函数名,可以有非模板函数模板函数显示特例化模板函数以及它们的重载版本,在调用函数时,显示特例化优先于常规模板,而非模板函数优先于显示具体化和常规模板

      类模板的显示特例化:  

      #include <iostream>
      #include <string>
      using namespace std;
      //类模板
      template<class T1, class T2> class Point {
      public:
          Point(T1 x, T2 y) : m_x(x), m_y(y) { }
      public:
          T1 getX() const { return m_x; }
          void setX(T1 x) { m_x = x; }
          T2 getY() const { return m_y; }
          void setY(T2 y) { m_y = y; }
          void display() const;
      private:
          T1 m_x;
          T2 m_y;
      };
      template<class T1, class T2>  //这里要带上模板头
      void Point<T1, T2>::display() const {
          cout << "x=" << m_x << ", y=" << m_y << endl;
      }
      //类模板的显示特例化(针对字符串类型的显示特例化)定义
      template<> class Point<string, string> {
      public:
          Point(string x, string y) : m_x(x), m_y(y) { }
      public:
          string getX() const { return m_x; }
          void setX(string x) { m_x = x; }
          string getY() const { return m_y; }
          void setY(string y) { m_y = y; }
          void display() const;
      private:
          string m_x;  //x坐标
          string m_y;  //y坐标
      };
      //这里不能带模板头template<>
      void Point<string, string>::display() const {
          cout << "x=" << m_x << " | y=" << m_y << endl;
      }
      int main() {
          (new Point<int, int>(10, 20))->display();
          (new Point<int, string>(10, "东经180度"))->display();
          (new Point<string, string>("东经180度", "北纬210度"))->display();
          return 0;
      }
      

      • 请读者注意第 23 行代码,Point<string, string>表明了要将类型参数 T1、T2 都具体化为string类型,原来使用 T1、T2 的位置都应该使用string替换。Point类有两个类型参数T1、T2,并且都已经被具体化了,所以整个类模板就不再有类型参数了,模板头应该写作template<>
      • 再来对比第 18、37行代码,可以发现,当在类的外部定义成员函数时,普通类模板的成员函数前面要带上模板头,而具体化的类模板的成员函数前面不能带模板头

      部分特例化: 

      在上面的特例化例子中,我们为所有的类型参数都提供了实参,所以最后的模板头为空,也即template<>。另外, C++ 还允许只为一部分类型参数提供实参,这称为部分显式特例化部分显式特例化只能用于类模板,不能用于函数模板。  

      仍然以 Point 为例,假设我现在希望“只要横坐标 x 是字符串类型”就以|来分隔输出结果,而不管纵坐标 y 是什么类型,这种要求就可以使用部分显式特例化技术来满足。请看下面的代码:  

      #include <iostream>
      using namespace std;
      //类模板
      template<class T1, class T2> class Point {
      public:
          Point(T1 x, T2 y) : m_x(x), m_y(y) { }
      public:
          T1 getX() const { return m_x; }
          void setX(T1 x) { m_x = x; }
          T2 getY() const { return m_y; }
          void setY(T2 y) { m_y = y; }
          void display() const;
      private:
          T1 m_x;
          T2 m_y;
      };
      template<class T1, class T2>  //这里需要带上模板头
      void Point<T1, T2>::display() const {
          cout << "x=" << m_x << ", y=" << m_y << endl;
      }
      //类模板的部分显示特例化
      template<typename T2> class Point<string, T2> {
      public:
          Point(string x, T2 y) : m_x(x), m_y(y) { }
      public:
          string getX() const { return m_x; }
          void setX(string x) { m_x = x; }
          T2 getY() const { return m_y; }
          void setY(T2 y) { m_y = y; }
          void display() const;
      private:
          string m_x;  //x坐标
          T2 m_y;  //y坐标
      };
      template<typename T2>  //这里需要带上模板头
      void Point<string, T2>::display() const {
          cout << "x=" << m_x << " | y=" << m_y << endl;
      }
      int main() {
          (new Point<int, int>(10, 20))->display();
          (new Point<string, int>("东经180度", 10))->display();
          (new Point<string, string>("东经180度", "北纬210度"))->display();
          return 0;
      }
      

      模板头template<typename T2>中声明的是没有被具体化的类型参数;类名Point<string, T2>列出了所有类型参数,包括未被具体化的和已经被具体化的。  类名后面之所以要列出所有的类型参数,是为了让编译器确认“到底是第几个类型参数被具体化了”,如果写作template<typename T2> class Point<string>,编译器就不知道string代表的是第一个类型参数,还是第二个类型参数。  

      C++模板中的非类型参数  

      模板是一种泛型技术,目的是将数据的类型参数化,以增强 C++ 语言(强类型语言)的灵活性。C++ 对模板的支持非常自由,模板中除了可以包含类型参数,还可以包含非类型参数,例如:

        template<typename T, int N> class Demo{ };
        template<class T, int N> void func(T (&arr)[N]);
        

        当调用一个函数模板或者通过一个类模板创建对象时,非类型参数会被用户提供的、或者编译器推断出的值所取代

        在函数模板中使用非类型参数
        数组在作为函数参数时会自动转换为数组指针,而sizeof只能通过数组名求得数组长度,不能通过数组指针求得数组长度.

        T (&a)[N]表明 a 是一个引用,它引用的数据的类型是T [N],也即一个数组T (&b)[N]也是类似的道理。分析一个引用和分析一个指针的方法类似,编译器总是从它的名字开始读取,然后按照优先级顺序依次解析,  

        #include <iostream>
        using namespace std;
        template<class T> void Swap(T& a, T& b);  //模板①:交换基本类型的值
        template<typename T, unsigned N> void Swap(T(&a)[N], T(&b)[N]);  //模板②:交换两个数组
        template<typename T, unsigned N> void printArray(T(&arr)[N]);  //打印数组元素
        int main() {
            //交换基本类型的值
            int m = 10, n = 99;
            Swap(m, n);  //匹配模板①
            cout << m << ", " << n << endl;
            //交换两个数组
            int a[5] = { 1, 2, 3, 4, 5 };
            int b[5] = { 10, 20, 30, 40, 50 };
            Swap(a, b);  //匹配模板②
            printArray(a);
            printArray(b);
            return 0;
        }
        template<class T> void Swap(T& a, T& b) {
            T temp = a;
            a = b;
            b = temp;
        }
        template<typename T, unsigned N> void Swap(T(&a)[N], T(&b)[N]) {
            T temp;
            for (int i = 0; i < N; i++) {
                temp = a[i];
                a[i] = b[i];
                b[i] = temp;
            }
        }
        template<typename T, unsigned N> void printArray(T(&arr)[N]) {
            for (int i = 0; i < N; i++) {
                if (i == N - 1) {
                    cout << arr[i] << endl;
                }
                else {
                    cout << arr[i] << ", ";
                }
            }
        }
        

        在类模板中使用非类型参数:(个人:在实现时,Array<T, N>第一个传的是非类型参数的值N,而不是N的类型int) 

        #include <iostream>
        #include <cstring>
        #include <cstdlib>
        using namespace std;
        template<typename T, int N>
        class Array {
        public:
            Array();
            ~Array();
        public:
            T& operator[](int i);  //重载下标运算符[]
            int length() const { return m_length; }  //获取数组长度
            bool capacity(int n);  //改变数组容量
        private:
            int m_length;  //数组的当前长度
            int m_capacity;  //当前内存的容量(能容纳的元素的个数)
            T* m_p;  //指向数组内存的指针
        };
        template<typename T, int N>
        Array<T, N>::Array() {
            m_p = new T[N];
            m_capacity = m_length = N;
        }
        template<typename T, int N>
        Array<T, N>::~Array() {
            delete[] m_p;
        }
        template<typename T, int N>
        T& Array<T, N>::operator[](int i) {
            if (i < 0 || i >= m_length) {
                cout << "Exception: Array index out of bounds!" << endl;
            }
            return m_p[i];
        }
        template<typename T, int N>
        bool Array<T, N>::capacity(int n) {
            if (n > 0) {  //增大数组
                int len = m_length + n;  //增大后的数组长度
                if (len <= m_capacity) {  //现有内存足以容纳增大后的数组
                    m_length = len;
                    return true;
                }
                else {  //现有内存不能容纳增大后的数组
                    T* pTemp = new T[m_length + 2 * n * sizeof(T)];  //增加的内存足以容纳 2*n 个元素
                    if (pTemp == NULL) {  //内存分配失败
                        cout << "Exception: Failed to allocate memory!" << endl;
                        return false;
                    }
                    else {  //内存分配成功
                        memcpy(pTemp, m_p, m_length * sizeof(T));
                        delete[] m_p;
                        m_p = pTemp;
                        m_capacity = m_length = len;
                        return true;
                    }
                }
            }
            else {  //收缩数组
                int len = m_length - abs(n);  //收缩后的数组长度
                if (len < 0) {
                    cout << "Exception: Array length is too small!" << endl;
                    return false;
                }
                else {
                    m_length = len;
                    return true;
                }
            }
        }
        int main() {
            Array<int, 5> arr;
            //为数组元素赋值
            for (int i = 0, len = arr.length(); i < len; i++) {
                arr[i] = 2 * i;
            }
        
            //第一次打印数组
            for (int i = 0, len = arr.length(); i < len; i++) {
                cout << arr[i] << " ";
            }
            cout << endl;
        
            //扩大容量并为增加的元素赋值
            arr.capacity(8);
            for (int i = 5, len = arr.length(); i < len; i++) {
                arr[i] = 2 * i;
            }
            //第二次打印数组
            for (int i = 0, len = arr.length(); i < len; i++) {
                cout << arr[i] << " ";
            }
            cout << endl;
            //收缩容量
            arr.capacity(-4);
            //第三次打印数组
            for (int i = 0, len = arr.length(); i < len; i++) {
                cout << arr[i] << " ";
            }
            cout << endl;
            return 0;
        }

        非类型参数的限制

        非类型参数的类型不能随意指定,它受到了严格的限制,

        • 当非类型参数是一个整数时,传递给它的实参,或者由编译器推导出的实参,必须是一个常量表达式,例如102 * 3018 + 23 - 4等,但不能是nn + 10n + m等(n 和 m 都是变量)。  

        下面的调用就是错误的:

        int len;
        cin>>len;
        int a[len];
        int b[len];
        Swap(a, b);
        

        对上面的 Array 类,以下创建对象的方式是错误的,这两种情况,编译器推导出来的实参是len,是一个变量,而不是常量。  

        int len;
        cin>>len;
        Array<int, len> arr;
        
        • 当非类型参数是一个指针(引用)时,绑定到该指针的实参必须具有静态的生存期;换句话说,实参必须存储在虚拟地址空间中的静态数据区。局部变量位于栈区,动态创建的对象位于堆区,它们都不能用作实参。  

        C++模板的实例化

        由模板生成函数或类的过程叫做模板的实例化(Instantiate),相应地,针对某个类型生成的特定版本的函数或类叫做模板的一个实例(Instantiation)。 

        • 模板(Templet)并不是真正的函数或类,它仅仅是编译器用来生成函数或类的一张“图纸”。模板不会占用内存,最终生成的函数或者类才会占用内存
        • 模板的实例化是按需进行的,用到哪个类型就生成针对哪个类型的函数或类,不会提前生成过多的代码。
        • 也就是说,编译器会根据传递给类型参数的实参(也可以是编译器自己推演出来的实参)来生成一个特定版本的函数或类
        • 实例化的过程也很简单,就是将所有的类型参数用实参代替。 
        • 另外,需要注意的是类模板的实例化,通过类模板创建对象时并不会实例化所有的成员函数,只有等到真正调用它们时才会被实例化;如果一个成员函数永远不会被调用,那它就永远不会被实例化。这说明类的实例化是延迟的、局部的,编译器并不着急生成所有的代码
        • 通过类模板创建对象时,一般只需要实例化成员变量和构造函数。对象的创建过程就是分配一块大小已知的内存,并对这块内存进行初始化。
          • 成员变量被实例化后就能够知道对象的大小了(占用的字节数),
          • 构造函数被实例化后就能够知道如何初始化了;

        将C++模板应用于多文件编程 

        C++编译是针对单个源文件的,只要有函数声明,编译器就能知道函数调用是否正确;而将函数调用和函数定义对应起来的过程,可以延迟到链接时期。正是有了链接器的存在,函数声明和函数定义的分离才得以实现

        基于传统的编程思维,初学者往往也会将模板(函数模板和类模板)的声明和定义分散到不同的文件中,以期达到「模块化编程」的目的。但事实证明这种做法是不对的,程序员惯用的做法是将模板的声明和定义都放到头文件中

        模板并不是真正的函数或类,它仅仅是用来生成函数或类的一张“图纸”,在这个生成过程中有三点需要明确

        • 模板的实例化是按需进行的,用到哪个类型就生成针对哪个类型的函数或类,不会提前生成过多的代码
        • 模板的实例化是由编译器完成的,而不是由链接器完成的
        • 在实例化过程中需要知道模板的所有细节,包含定义

        ==================================================================

        • 当编译器编译main.cpp时,发现使用到了 double 版本的 Swap() 函数,于是尝试生成一个 double 版本的实例,但是由于只有声明没有定义,所以生成失败。不过这个时候编译器不会报错,而是对该函数的调用做一个记录,希望等到链接程序时在其他目标文件(.obj 文件或 .o 文件)中找到该函数的定义。很明显,本例需要到func.obj中寻找。
        • 但是遗憾的是,func.cpp中没有调用 double 版本的 Swap() 函数,编译器不会生成 double 版本的实例,所以链接器最终也找不到 double 版本的函数定义,只能抛出一个链接错误,让程序员修改代码

        函数模型正确用于多文件编程

        main.cpp

        #include <iostream>
        #include "func.h"
        using namespace std;
        
        int main(){
            int n1 = 10, n2 = 20;
            Swap(n1, n2);
        
            double f1 = 23.8, f2 = 92.6;
            Swap(f1, f2);
            int x[5] = {4,1,7,9,2};
            cout<<"the old x[5] is:"<<endl;
            for(auto val:x)
                cout<<val<<" ";
            bubble_sort(x, 5);
            cout<<"\n the new x[5] is:"<<endl;
            for(auto val:x)
                cout<<val<<" ";
        
            return 0;
        } 

        func.h

        #ifndef _FUNC_H
        #define _FUNC_H
        
        //交换两个数的值
        template<typename T> void Swap(T &a, T &b){
            T temp = a;
            a = b;
            b = temp;
        }
        
        void bubble_sort(int arr[], int n);
        
        #endif

        func.cpp

        #include "func.h"
        
        //冒泡排序算法
        void bubble_sort(int arr[], int n){
            for(int i=0; i<n-1; i++){
                bool isSorted = true;
                for(int j=0; j<n-1-i; j++){
                    if(arr[j] > arr[j+1]){
                        isSorted = false;
                        Swap(arr[j], arr[j+1]);  //调用Swap()函数
                    }
                }
                if(isSorted) break;
            }
        }

        类模型正确用于多文件编程:  

        main.cpp

        #include <iostream>
        #include "point.h"
        using namespace std;
        
        int main(){
            Point<int, int> p1(10, 20);
            p1.setX(40);
            p1.setY(50);
            cout<<"x="<<p1.getX()<<", y="<<p1.getY()<<endl;
        
            Point<char*, char*> p2("东经180度", "北纬210度");
            p2.display();
        
            return 0;
        }

        point.h

        #ifndef _POINT_H
        #define _POINT_H
        
        #include <iostream>
        using namespace std;
        
        template<class T1, class T2>
        class Point{
        public:
            Point(T1 x, T2 y): m_x(x), m_y(y){ }
        public:
            T1 getX() const{ return m_x; }
            void setX(T1 x){ m_x = x; }
            T2 getY() const{ return m_y; };
            void setY(T2 y){ m_y = y; };
            void display() const;
        private:
            T1 m_x;
            T2 m_y;
        };
        
        template<class T1, class T2>
        void Point<T1, T2>::display() const{
            cout<<"x="<<m_x<<", y="<<m_y<<endl;
        }
        
        #endif 

        C++模板的显式实例化 

        • 前面讲到的模板的实例化是在调用函数或者创建对象时由编译器自动完成的,不需要程序员引导,因此称为隐式实例化
        • 相对应的,我们也可以通过代码明确地告诉编译器需要针对哪个类型进行实例化,这称为显式实例化

        编译器在实例化的过程中需要知道模板的所有细节

        • 对于函数模板,也就是函数定义;
        • 对于类模板,需要同时知道类定义和类实现。

        我们必须将显式实例化的代码放在包含了模板定义的源文件中,而不是仅仅包含了模板声明的头文件中

        显式实例化的一个好处是,可以将模板的声明和定义(实现)分散到不同的文件中了

        函数模板的显式实例化

        上节我们展示了一个反面教材,告诉大家不能把函数模板的声明和定义分散到不同的文件中,但是现在有了显式实例化,这一点就可以做到了

        func.cpp

        //交换两个数的值
        template<typename T> void Swap(T &a, T &b){
            T temp = a;
            a = b;
            b = temp;
        }
        //冒泡排序算法
        void bubble_sort(int arr[], int n){
            for(int i=0; i<n-1; i++){
                bool isSorted = true;
                for(int j=0; j<n-1-i; j++){
                    if(arr[j] > arr[j+1]){
                        isSorted = false;
                        Swap(arr[j], arr[j+1]);  //调用Swap()函数
                    }
                }
                if(isSorted) break;
            }
        }
        template void Swap(double &a, double &b);  //显式实例化定义 

        func.h:  

        #ifndef _FUNC_H
        #define _FUNC_H
        template<typename T> void Swap(T &a, T &b);
        void bubble_sort(int arr[], int n);
        #endif

        main.cpp:  

        #include <iostream>
        #include "func.h"
        using namespace std;
        //显示实例化声明(也可以不写)
        extern template void Swap(double &a, double &b);
        extern template void Swap(int &a, int &b);
        int main(){
            int n1 = 10, n2 = 20;
            Swap(n1, n2);
          
            double f1 = 23.8, f2 = 92.6;
            Swap(f1, f2);
            return 0;
        }
        

        在包含了函数调用的源文件(main.cpp)中一条语句: 

        extern template void Swap(double &a, double &b);
        

        该语句在前面增加了extern关键字,它的作用是明确地告诉编译器,该版本的函数实例在其他文件中,请在链接期间查找。不过这条语句是多余的,即使不写,编译器发现当前文件中没有对应的模板定义,也会自动去其他文件中查找。  

        显式实例化也包括声明和定义,定义要放在模板定义(实现)所在的源文件,声明要放在模板声明所在的头文件(当然也可以不写)

        类模板的显式实例化: 

        类模板的显式实例化和函数模板类似。以上节的 Point 类为例,针对string类型的显式实例化(定义形式)代码为: 

        template class Point<string, string>;
        

        相应地,它的声明形式为:

        extern template class Point<string, string>;
        

        另外需要注意的是,显式实例化一个类模板时,会一次性实例化该类的所有成员,包括成员变量和成员函数。 

        有了类模板的显式实例化,就可以将类模板的声明和定义分散到不同的文件中了

        point.cpp

        #include <iostream>
        #include "point.h"
        using namespace std;
        template<class T1, class T2>
        void Point<T1, T2>::display() const{
            cout<<"x="<<m_x<<", y="<<m_y<<endl;
        }
        //显式实例化定义
        template class Point<string, string>;
        template class Point<int, int>;
        

        point.h:  

        #ifndef _POINT_H
        #define _POINT_H
        template<class T1, class T2>
        class Point{
        public:
            Point(T1 x, T2 y): m_x(x), m_y(y){ }
        public:
            T1 getX() const{ return m_x; }
            void setX(T1 x){ m_x = x; }
            T2 getY() const{ return m_y; };
            void setY(T2 y){ m_y = y; };
            void display() const;
        private:
            T1 m_x;
            T2 m_y;
        };
        #endif
        

        main.cpp:  

        #include <iostream>
        #include "point.h"
        using namespace std;
        //显式实例化声明(也可以不写)
        extern template class Point<string, string>;
        extern template class Point<int, int>;
        int main(){
            Point<int, int> p1(10, 20);
            p1.setX(40);
            p1.setY(50);
            cout<<"x="<<p1.getX()<<", y="<<p1.getY()<<endl;
            Point<string, string> p2("东经180度", "北纬210度");
            p2.display();
            return 0;
        }
        

        显式实例化的缺陷:  

        C++ 支持显式实例化的目的是为「模块化编程」提供一种解决方案,这种方案虽然有效,但是也有明显的缺陷:

        • 程序员必须要在模板的定义文件(实现文件)中,对所有使用到的类型进行实例化。这就意味着,每次更改了模板使用文件(调用函数模板的文件,或者通过类模板创建对象的文件),也要相应地更改模板定义文件,以增加对新类型的实例化,或者删除无用类型的实例化。 
        • 一个模板可能会在多个文件中使用到,而对于库的开发者来说,他不能提前假设用户会使用哪些类型,所以根本就无法使用显式实例化,只能将模板的声明和定义(实现)全部放到头文件中;C++ 标准库几乎都是用模板来实现的,这些模板的代码也都位于头文件中。 

        总起来说,

        • 如果我们开发的模板只有我们自己使用,那也可以勉强使用显式实例化;
        • 如果希望让其他人使用(例如库、组件等),那只能将模板的声明和定义都放到头文件中了。  

        C++类模板与继承

        类模板和类模板之间、类模板和类之间可以互相继承。它们之间的派生关系有以下四种情况。

         类模板从类模板派生
        template <class T1, class T2>
        class A
        {
            Tl v1; 
            T2 v2;
        };
        template <class T1, class T2>
        class B : public A <T2, T1>
        {
            T1 v3; 
            T2 v4;
        };
        template <class T>
        class C : public B <T, T>
        {
            T v5;
        };
        int main()
        {
            B<int, double> obj1;
            C<int> obj2;
            return 0;
        }
        
        • 编译到第 18 行,编译器用 int 替换类模板 B 中的 T1,用 double 替换 T2,生成 B<int, double> 类如下:
        class B <int, double>: public A <double, int>
        {
            int v3; 
            double v4;
        };
        
        • B <int, double> 的基类是 A <double, int>。于是编译器就要用 double 替换类模板 A 中的 T1,用 int 替换 T2,生成 A<double, int> 类如下:
        class A <double, int>
        {
            double v1; 
            int v2;
        };
        
        • 编译到第 19 行,编译器生成类 C<int>,还有 C<int> 的直接基类 B<int, int>,以及 B<int, int> 的基类 A<int, int>。(个人:编译器在代码中遇到生成子类对象的代码时,不但会实例化这个派生类模板,还会实例化对应的基类模板)  

        类模板从模板类派生:  

        template<class T1, class T2>
        class A{ 
            T1 v1; 
            T2 v2; };
        template <class T>
        class B: public A <int, double>{
            T v;};
        int main() { 
            B <char> obj1; 
            return 0; }
        
        • A<int, double> 是一个具体的类的名字,而且它是一个模板类,因此说类模板 B 是从模板类派生而来的。 
        • 编译器编译到B<char>obj1;时,会自动生成两个模板类:A<int, double> 和 B<char>。  

        类模板从普通类派生:  

        class A{ int v1; };
        template<class T>
        class B: public A{ T v; };
        int main (){ B <char> obj1; return 0; }
        

        普通类从模板类派生:(个人:普通类无法从类模板派生,因为如果这样的话,无法知道进行那个类型的类模板实例化)  

        template <class T>
        class A{ T v1; int n; };
        class B: public A <int> { double v; };
        int main() { B obj1; return 0; }

        C++类模板与友元 

        函数、类、类的成员函数作为类模板的友元:  

        void Func1() {  }
        class A {  };
        class B
        {
        public:
            void Func() { }
        };
        template <class T>
        class Tmpl
        {
            friend void Func1();
            friend class A;
            friend void B::Func();
        };
        int main()
        {
            Tmpl<int> i;
            Tmpl<double> f;
            return 0;
        }
        

        类模板实例化时,除了类型参数被替换外,其他所有内容都原样保留,因此任何从 Tmp1 实例化得到的类都包含上面三条友元声明,因而也都会把 Func1、类 A 和 B::Func 当作友元

        函数模板作为类模板的友元:  

        #include <iostream>
        #include <string>
        using namespace std;
        template <class T1, class T2>
        class Pair
        {
        private:
            T1 key;  //关键字
            T2 value;  //值
        public:
            Pair(T1 k, T2 v) : key(k), value(v) { };
            bool operator < (const Pair<T1, T2> & p) const;
            template <class T3, class T4>
            friend ostream & operator << (ostream & o, const Pair<T3, T4> & p);
        };
        template <class T1, class T2>
        bool Pair <T1, T2>::operator< (const Pair<T1, T2> & p) const
        {  //“小”的意思就是关键字小
            return key < p.key;
        }
        template <class Tl, class T2>
        ostream & operator << (ostream & o, const Pair<T1, T2> & p)
        {
            o << "(" << p.key << "," << p.value << ")";
            return o;
        }
        int main()
        {
            Pair<string, int> student("Tom", 29);
            Pair<int, double> obj(12, 3.14);
            cout << student << " " << obj;
            return 0;
        }
        

        编译本程序时,编译器自动生成了两个 operator << 函数,它们的原型分别是:  

        ostream & operator << (ostream & o, const Pair<string, int> & p);
        ostream & operator << (ostream & o, const Pair<int, double> & p);
        

        前者是 Pair <string, int> 类的友元,但不是 Pair<int, double> 类的友元;后者是 Pair<int, double> 类的友元,但不是 Pair<string, int> 类的友元

        函数模板作为类的友元:  

        实际上,类也可以将函数模板声明为友元。程序示例如下:

        #include <iostream>
        using namespace std;
        class A
        {
            int v;
        public:
            A(int n) :v(n) { }
            template <class T>
            friend void Print(const T & p);
        };
        template <class T>
        void Print(const T & p)
        {
            cout << p.v;
        }
        int main()
        {
            A a(4);
            Print(a);
            return 0;
        }
        

        编译器编译到第 19 行Print(a);时,就从 Print 模板实例化出一个 Print 函数,原型如下:

        void Print(const A & p);
        

        这个函数本来不能访问 p 的私有成员。但是编译器发现,如果将类 A 的友元声明中的 T 换成 A,就能起到将该 Print 函数声明为友元的作用,因此编译器就认为该 Print 函数是类 A 的友元

        思考题:类还可以将类模板或类模板的成员函数声明为友元。自行研究这两种情况该怎么写。

        类模板作为类模板的友元: 

        一个类模板还可以将另一个类模板声明为友元。程序示例如下:

          #include <iostream>
          using namespace std;
          template<class T>
          class A
          {
          public:
              void Func(const T & p)
              {
                  cout << p.v;
              }
          };
          template <class T>
          class B
          {
          private:
              T v;
          public:
              B(T n) : v(n) { }
              template <class T2>
              friend class A;  //把类模板A声明为友元
          };
          int main()
          {
              B<int> b(5);
              A< B<int> > a;  //用B<int>替换A模板中的 T
              a.Func(b);
              return 0;
          }
          

          在本程序中,A< B<int> > 类成为 B<int> 类的友元。

          C++类模板中的静态成员 

          #include <iostream>
          using namespace std;
          
          
          template <class T>
          class A
          {
          private:
              static int count;
          public:
              A() { count++; }
              ~A() { count--; };
              A(A&) { count++; }
              static void PrintCount() { cout << count << endl; }
              static void setCount(int x) { count = x; }
          };
          template<> int A<int>::count = 0;
          template<> int A<double>::count = 0;
          int main()
          {
              A<int> ia;
              A<double> da;
              ia.PrintCount();
              cout << "set A<int> cout:";
              ia.setCount(6);
              cout << "afer set A<int>,now the A<int> count:" << endl;
              ia.PrintCount();
              cout << "print A<double> cout:" << endl;
              da.PrintCount();
              return 0;
          }

           

          第 14 行和第 15 行,对静态成员变量在类外部加以声明是必需的。A<int> 和 A<double> 是两个不同的类。虽然它们都有静态成员变量 count,但是显然,A<int> 的对象 ia 和 A<double> 的对象 da 不会共享一份 count

            

          posted on 2022-09-17 10:23  朴素贝叶斯  阅读(148)  评论(0编辑  收藏  举报

          导航