Bootstrap

深述C++模板类

1、前言

函数模板是通用函数的描述,类模板是通用类的描述,使用任意类型来描述类的定义。和函数模板有很多相似的地方,关于函数模板可以看我之前写过的一篇文章:简述C++函数模板。这里就不过多赘述。

2、模板类的基本概念

模板类的基本语法如下:

template <class T>
class MyClass {
private:
    T data;
public:
    MyClass(T value) : data(value) {}
    T getData()  { return data; }
};

和函数模板一样,定义的时候在前面加一个template声明就行。

1、显式调用

不过需要注意的是,和函数模板不一样,在创建具体对象的时候,必须指定具体的数据类型,也就是显式调用,声明具体的类型参数,没有自动转化。比如:

MyClass<int> myIntObj(5);
MyClass<double> myDoubleObj(3.14);

2、模板类缺省数据类型 

模板类可以指定缺省的数据类型,比如:

#include<iostream>

using namespace std;

template <class T1,class T2 = char>
class MyClass {
private:
    T1 data1;
    T2 data2;
public:
    MyClass() : data1(), data2() {}
    MyClass(T1 value1,T2 value2 ) : data1(value1), data2(value2) {}
    void getData() { cout << "data1 is " << data1 << " data2 is " << data2 << endl; }
};

int main()
{
	MyClass<int> text(97,97);
    text.getData();
}

3、数据类型匹配 

我们指定了T2的默认数据类型为char,所以我们显式定义text的时候,传入一个类型参数也不会报错,如果不指定默认的数据类型,仅传入一个类型参数,那么程序会报错:

所以使用类模板的时候,数据类型必须要适应模板类中的代码 。

4、模板类成员函数的类外实现

关于模板类成员函数的类外实现,也和正常的类外实现差不多,只不过需要多写一个模板名,拿上述代码举例:

template <class T1, class T2 >
void MyClass<T1,T2>::getData()
{ cout << "data1 is " << data1 << " data2 is " << data2 << endl; }

在类外实现成员函数的时候,最好不要写默认参数,有的编译器可能会把他当成错误。

5、用new创建模板类对象

看到模板类的代码,大家要改变一下习惯,普通类的类名是标识符,干干净净的,模板类不一样,它是类模板名,不是一种具体的数据类型, 只有当他加上<>符号,<>里面填上具体的数据类型,这个整体才能算是类名,才算是一种具体的数据类型。

比如用new 来创建模板类的指针对象的时候,我们就需要这么写:

MyClass<int, char>* a = new MyClass<int, char>;

MyClass<int, char>这一大块才算是完整的数据类型。

6、模板类的成员函数只有使用的时候才会创建 

 C++ 是一种静态类型语言,编译器在编译阶段需要知道所有类型相关的信息来生成正确的机器代码。对于模板类,其代码并不是在定义模板类的时候就完全生成的。这是因为模板类是一种泛型编程工具,在定义时并不知道最终会使用哪些具体的数据类型来实例化它。

模板类的定义更像是一个蓝图或者模板,它描述了如何根据特定的数据类型来构建一个类。编译器在遇到这个模板定义时,无法为所有可能的类型生成代码,因为可能的类型组合太多了。

 在编译阶段,当模板类的成员函数被使用时,编译器会根据具体的实例化类型生成对应的函数定义。这些生成的函数定义会被放在目标文件中。在链接阶段,如果多个目标文件中都包含了对同一个模板类成员函数(针对相同的实例化类型)的引用,那么链接器会将这些定义合并起来,确保程序的正确性。

 3、模板类具体化

模板类具体化有两种,一种是完全具体化,一种是部分具体化。

具体化程度高的类优先于具体程度低的类,具体化的类优先于没有具体化的类。

例如如下代码:

#include<iostream>

using namespace std;

template <class T1,class T2>
class MyClass {
private:
    T1 data1;
    T2 data2;
public:
    MyClass() : data1(), data2() { cout << "无具体化构造函数调用:" << endl; }
    MyClass(T1 value1, T2 value2) : data1(value1), data2(value2) {}
    void getData();
};

template <class T1, class T2 >
void MyClass<T1,T2>::getData()
{
    cout << "无具体化调用:" << endl;
    //cout << "data1 is " << data1 << " data2 is " << data2 << endl; 
}

template<class T>
class MyClass<T, char>
{
private:
    T data1;
    char data2;
public:
    MyClass() : data1(), data2() { cout << "部分具体化构造函数调用:" << endl; }
    MyClass(T value1, char value2) : data1(value1), data2(value2) {}
    void getData();
};

template<class T>
void MyClass<T,char>::getData()
{
    cout << "部分具体化调用:" << endl;
    //cout << "data1 is " << data1 << " data2 is " << data2 << endl;
}

template <>
class MyClass<int, char>
{
private:
    int data1;
    char data2;
public:
    MyClass() : data1(), data2() { cout << "完全具体化构造函数调用:" << endl; }
    MyClass(int value1, char value2) : data1(value1), data2(value2) {}
    void getData();
};

void MyClass<int, char>::getData()
{
    cout << "完全具体化调用:" << endl;
    //cout << "data1 is " << data1 << " data2 is " << data2 << endl;
}


int main()
{
    MyClass<int, char> text1;
    text1.getData();

    MyClass<char, char>text2;
    text2.getData();

    MyClass<int, int> text3;
    text3.getData();
}

程序运行结果如下: 

我们这里要注意一下写法,例如类外成员函数的实现:

template <class T1, class T2 >
void MyClass<T1, T2>::getData()
{
    cout << "无具体化调用:" << endl;
}

template<class T>
void MyClass<T, char>::getData()
{
    cout << "部分具体化调用:" << endl;
}

void MyClass<int, char>::getData()
{
    cout << "完全具体化调用:" << endl;
}

当你对一个类模板进行完全具体化时,你是在告诉编译器:“对于这些特定的类型,不要使用通用的模板定义,而是使用我提供的这个专门的定义。”这相当于已经是一个实际方案了,类外我们并没有写复杂的模板作为他的名字。

部分具体化则是在保持模板通用性的基础上,对某些特定的类型模式进行优化或定制。 

4、模板类与继承 

模板类太灵活了,所以他的继承可以玩出很多花样。看上去情况很多,但是并不复杂,只是有一点繁琐。

如果对继承的相关知识有所忘却,可以查看:简述C++类继承

1、模板类继承普通类

模板类继承普通类很简单,只需要把基类的构造函数安排好就行了。继承中有说过用初始化列表的方式来初始化,基类的成员在基类中初始,派生类的成员在派生类中初始。

例如如下代码,我们特意不写基类的默认构造函数,只提供有参构造:

#include<iostream>

using namespace std;

class text
{
private:
    int num;
public:
    //text() { cout << "调用了text的默认构造!" << endl; };
    text(int a) :num(a) { cout << "调用了text的有参构造!" << endl; }
};

template <class T1,class T2>
class MyClass :public text{
private:
    T1 data1;
    T2 data2;
public:
    MyClass() : data1(), data2() { cout << "模板构造函数调用:" << endl; }
    MyClass(T1 value1, T2 value2) : data1(value1), data2(value2) {}   
};

int main()
{
    MyClass<int,char> text1;
}

因为没有初始化的问题,代码并不能运行,派生类中并没有初始化基类的有参构造。要在派生类的构造函数中调用基类的构造函数。代码应该这么改:

#include<iostream>

using namespace std;

class text
{
private:
    int num;
public:
    //text() { cout << "调用了text的默认构造!" << endl; };
    text(int a) :num(a) { cout << "调用了text的有参构造!" << endl; }
};

template <class T1,class T2>
class MyClass :public text{
private:
    T1 data1;
    T2 data2;
public:
    //MyClass() : data1(), data2() { cout << "模板无参构造函数调用:" << endl; }
    MyClass(T1 value1, T2 value2,int a) : data1(value1), data2(value2),text(a)
    { cout << "模板有参构造函数调用!" << endl; }
};

int main()
{
    MyClass<int,char> text1(97,97,97);
}

运行结果如下:

 2、普通类继承模板类的实例化版本

普通类继承模板类的实例化版本和普通继承很相似,例如如下代码:

#include<iostream>

using namespace std;

template <class T1,class T2>
class MyClass {
private:
    T1 data1;
    T2 data2;
public:
    MyClass(T1 value1, T2 value2) : data1(value1), data2(value2)
    { 
        //cout << "模板有参构造函数调用!" << endl;
    }
    void prints()
    {
        cout << "data1 is " << data1 << endl << "data2 is " << data2 << endl;
    }
};

class text:public MyClass<int,char>
{
private:
    int num;
public:
    text(int a,int b,char c) :num(a),MyClass(b,c)
    { 
        //cout << "调用了text的有参构造!" << endl; 
    }
    void print()
    {
        prints();
        cout << "num is " << num;
    }
};

int main()
{
    text a(97, 97, 97);
    a.print();
}

输出结果如下:

3、普通类继承模板类

 这是更常见的作法,通常用普通类去继承一个通用的模板。我们在上述例子的代码基础上做一点改动。

#include<iostream>

using namespace std;

template <class T1,class T2>
class MyClass {
private:
    T1 data1;
    T2 data2;
public:
    MyClass(T1 value1, T2 value2) : data1(value1), data2(value2)
    { 
        //cout << "模板有参构造函数调用!" << endl;
    }
    void prints()
    {
        cout << "data1 is " << data1 << endl << "data2 is " << data2 << endl;
    }
};

template <class T1, class T2>
class text:public MyClass<T1,T2>
{
private:
    int num;
public:
    text(int a,T1 b,T2 c) :MyClass<T1,T2>(b,c),num(a)
    { 
        //cout << "调用了text的有参构造!" << endl; 
    }
    void print()
    {       
        MyClass<T1, T2>::prints();
        cout << "num is " << num;
    }
};

int main()
{
    text<int,char>a(97, 97, 97);
    a.print();
}

这里有几个讲究,不写代码可能会运行不了,但是编译器也不会报错。

1、具体化构造类型

在派生类调用基类模板的构造函数的时候,必须要在类名后面加上<>以及类型参数的名称,例如上面代码是这么写的:

    text(int a,T1 b,T2 c) :MyClass<T1,T2>(b,c),num(a)
    { 
        //cout << "调用了text的有参构造!" << endl; 
    }

如果只写一个MyClass(T1,T2),代码会运行不了,我们之前说过,模板它不是具体的类型,必须加上具体的模板参数才算是一个具体的类型 。

2、初始化构造的顺序

按照 C++ 的规定,成员变量的初始化顺序是按照它们在类中声明的顺序进行的,而不是按照初始化列表中的顺序。在 text 类中,先声明的是 int num,然后才是继承自基类 MyClass<T1, T2> 的部分(虽然在语法上它不是一个普通的成员变量,但在初始化顺序上有类似的考虑)。

所以,如果 num 的初始化依赖于基类 MyClass 的正确初始化(例如,假设 num 的值是通过某种方式基于 data1 或 data2 来确定的),就可能会出现错误。

一种更好的做法是尽量保证成员变量的初始化顺序与它们在类中的声明顺序一致,或者确保它们之间不存在这种相互依赖的初始化关系。这是一个需要注意的细节点。

3、基类模板函数在派生类作用域明确
void print()
{       
    MyClass<T1, T2>::prints();
    cout << "num is " << num;
}

这段代码中,我们加上了基类模板的作用域,如果不加上可能会报错(具体还是要看编译器处理,我的vs 2022就出错了)。

原来的prints调用可能会因为编译器的查找规则而产生混淆。通过使用MyClass<T1, T2>::prints();这种作用域限定符的方式,你明确地告诉编译器,你要调用的是MyClass<T1, T2>这个类模板实例化后的prints函数,这样就避免了潜在的命名冲突和错误的函数调用。

所以普通类继承函数通用模板的核心就在于把普通类也化为模板类。因为他要继承模板的通用特性,所以普通类变成模板类才能继承。

4、模板类继承模板类

 这个也简单,在派生类中扩展通用类型参数就可以了,直接上代码:

#include<iostream>

using namespace std;

template <class T1,class T2>
class MyClass {
private:
    T1 data1;
    T2 data2;
public:
    MyClass(T1 value1, T2 value2) : data1(value1), data2(value2)
    { 
        //cout << "模板有参构造函数调用!" << endl;
    }
    void fun1()
    {
        cout << "data1 is " << data1 << endl << "data2 is " << data2 << endl;
    }
};


template<class T1, class T2,class T3>
class Text :public MyClass<T2,T3>
{
private:
    T1 num;
public:
    Text(T1 a, T2 b, T3 c) :MyClass<T2, T3>(b,c), num(a)
    {

    }
    void fun2()
    {
        MyClass<T2,T3>::fun1();
        cout << "num is " << num;
    }
};

int main()
{    
    Text<int, int, char>a(97, 97, 'a');
    a.fun2();
}

 注意安排好基类的构造函数就行。

5、模板类继承模板参数给出的基类

先上代码:

#include<iostream>

using namespace std;

class AA
{
public:
    AA() :a() { cout << "AA构造函数调用" << endl; }
    AA(int x) :a(x)
    {
        cout << "AA有参构造函数调用" << endl;
    }
private:
    int a;
};

template <class T>
class BB
{
public:
    BB():data() { cout << "BB构造函数调用" << endl; }
    BB(T a) :data(a)
    {
        cout << "BB有参构造函数调用" << endl;
    }
private:
    T data;
};

template <class T,class V>
class CC:public T
{
public:
    CC():T(),data(){ cout << "CC构造函数调用" << endl; }
    CC(V a) :T(), data(a)
    {
        cout << "CC有参构造函数调用" << endl;
    }
private:
    V data;
};

int main()
{
    CC<AA,int> c1;
    CC<BB<int> ,int> c2(10);
}

模板类CC继承了一个模板参数T,也就是说基类是不确定的,可以改变的,可以作为参数来传递,可以用模板参数T调用基类的构造函数。

代码运行结果如下:

 

5、模板类与函数 

在函数中,如果想发挥类模板的通用性的特点,必须结合函数模板。

直接上代码,不过多做解释:

#include<iostream>

using namespace std;

template <class T>
class AA
{
public:
    AA() :a() {}
    AA(T x) :a(x)
    {
        //cout << "AA有参构造函数调用" << endl;
    }
    void show()
    {
        cout << "AA show函数调用" << endl;
        cout << "a = " << a << endl;
    }
private:
    T a;
};

template <typename T>
T func(T& a)
{
    a.show();
    cout << "T func(T& a)调用!" << endl;
    return a;
}

int main()
{
    AA<int> a(10);
    func(a);
}

运行结果如下:

模板类可以作为函数模板的参数传入 ,可以返回模板类类型。

6、模板类与友元

模板类声音也可以有友元。模板的友元分为三类。

1、非模板友元

友元函数不是模板函数,而是利用模板参数生成的函数

直接上代码演示:

#include <iostream>

using namespace std;

template <class T>
class AA {
public:
    AA() : a() {}
    AA(T x) : a(x) {}
    friend void show(AA<T>& A);
private:
    T a;
};

void show(AA<int>& A) {
    cout << "show函数调用" << endl;
    cout << "a = " << A.a << endl;
}

int main() {
    AA<int> a(10);
    show(a);
}

非模板友元函数有一定的局限性,如果模板类AA是char类型的话,编译器就会报错,这样就必须重载模板类的每一个实例化版本,都准备一个友元函数。 

所以C++提供了一种解决方案:利用模板类的参数,自动生成友元函数。

在上述代码的基础上,我们把show函数的定义放到类内进行:

#include <iostream>

using namespace std;

template <class T>
class AA {
public:
    AA() : a() {}
    AA(T x) : a(x) {}
    friend void show(AA<T>& A)
    {
        cout << "show函数调用" << endl;
        cout << "a = " << A.a << endl;
    }
private:
    T a;
};

int main() {
    AA<int> a(10);
    show(a);

    AA<char> b('a');
    show(b);
}

这种方案的本质是:编译器利用模板参数帮我们生成了友元函数。但是这个函数不是模板函数。 我们来证明一下,我们在源代码的基础上加上模板具体化版本:

void show(AA<char>& A) {
    cout << "具体 show函数调用" << endl;
    cout << "a = " << A.a << endl;
}

这时候编译器就会报错:

对于类模板 AA 的任何实例化类型 T,都有一个与之对应的友元函数 show,它能够访问该实例化类型的 AA 类模板对象的私有成员 a

然而,在类模板外部又定义了一个特定类型(AA<char>)的函数 show:这就造成了函数重载的冲突。导致编译器不知道使用哪个函数了。

所以用这种方式生成的友元函数只能用于这个模板类,不能用于其他模板类,且只能在类内实现。

2、约束模板友元

模板实例化时,每一个实例化的类对应每一个友元函数。

代码如下:

#include <iostream>

using namespace std;

template <typename T>
void show(T& A);

template <class T>
class AA {
public:
    AA() : a() {}
    AA(T x) : a(x) {}
    friend void show<>(AA<T>& A);//约束
        
private:
    T a;
};

template <typename T>
void show(T& A)
{
    cout << "通用show函数调用" << endl;
    cout << "a = " << A.a << endl;
}

template <>
void show(AA<char> &A)
{
    cout << "具体show函数调用" << endl;
    cout << "a = " << A.a << endl;
}

int main() {
    AA<int> a(10);
    show(a);

    AA<char> b('a');
    show(b);
}

约束模板友元有两个优点,第一是可以具体化,二是支持多个模板类 ,这种友元方案更有价值,就是语法麻烦了一点。

如果将 friend void show<>(AA<T>& A); 写成 friend void show(AA<T>& A); 编译器上可能不会导致明显的错误,但很可能会出现编译结果不符合预期的情况,特别是在涉及函数模板的通用版本和具体化版本共存的复杂场景下。为了确保代码在不同编译器上的一致性和可预测性,建议按照原写法 friend void show<>(AA<T>& A); 来声明模板友元函数,以便正确地实现模板参数的自动推导和函数调用的正确匹配。

那如果将 friend void show<>(AA<T>& A); 写成 friend void show<>(T& A);这里将存在不匹配的情况。在类模板中声明友元函数时,虽然形式上看起来没问题,但实际上它和全局定义的函数模板 show 并没有建立起正确的关联,导致在友元函数试图访问类模板 AA 中的私有成员 a 时会出现问题。

因为按照当前的声明方式,友元函数 show 的参数 A 只是一个普通的引用类型 T&,它并不知道这个 T 应该和类模板 AA 的模板参数 T 对应起来,也无法直接访问 AA 类模板实例中的私有成员 a

3、非约束模板友元

模板实例化的时候,如果实例化了n个类,也会实例化n个友元函数,每个实例化的类都拥有n个友元函数。

代码如下:

#include <iostream>

using namespace std;

template <typename T>
void show(T& A);

template <class T>
class AA {
public:
    AA() : a() {}
    AA(T x) : a(x) {}
    //friend void show<>(AA<T>& A);
    template <typename T> friend void show<>(T& A);

private:
    T a;
};

template <typename T>
void show(T& A)
{
    cout << "通用show函数调用" << endl;
    cout << "a = " << A.a << endl;
}

template <>
void show(AA<char>& A)
{
    cout << "具体show函数调用" << endl;
    cout << "a = " << A.a << endl;
}

int main() {
    AA<int> a(10);
    show(a);

    AA<char> b('a');
    show(b);
}

和约束友元模板相比较 ,这段带码把friend void show<>(AA<T>& A); 写成了template <typename T> friend void show<>(T& A);

这里实际上是在类模板的作用域内重新声明了一个与全局已定义的函数模板 showtemplate <typename T> void show(T& A))相关联的友元函数版本。

关键在于这个声明中的 template <typename T> 部分,它使得编译器能够将类模板 AA 的模板参数 T 与函数模板 show 的模板参数 T 在某种程度上建立起联系。具体来说,当在 main 函数中创建 AA 类模板的实例(如 AA<int> 或 AA<char> 等)并调用 show 函数时,编译器会根据传入的 AA 类模板实例的类型来自动推导函数模板 show 的模板参数 T

7、模板类的成员模板

这个说白了就是套娃,直接放代码吧~

template<class T1, class T2>
class AA              
{
public:
    T1 m_x;
    T2 m_y;

    AA(const T1 x, const T2 y) : m_x(x), m_y(y) {}
    void show() { cout << "m_x=" << m_x << ",m_y=" << m_y << endl; }

    template<class T>
    class BB
    {
    public:
        T m_a;
        T m_b;
        BB() : m_a(), m_b() {}
    };

    BB<T1> m_bb1;
    BB<T2> m_bb2;

    template<typename T>
    void show(T tt);
};

8、将模板类用作参数

把模板类作为参数传递,实际上是在更高层次的模板(通常称为外层模板)中,将一个尚未完全确定类型的类模板作为参数来接收。主要用于数据结构中,部分场景很少用到这么复杂的设计。

应用场景

通用算法实现
  • 可以实现一些通用的算法,这些算法不依赖于特定的容器类型(如数组、链表、向量等),而是通过接受不同的模板类作为参数来处理各种可能的容器。例如,排序算法可以针对不同的容器模板类进行实现,只要这些容器支持必要的元素访问和修改操作(如通过重载 operator[] 等方式)。
代码复用与抽象
  • 有助于代码的复用和抽象。当有多个不同类型的模板类实现了相似的功能(比如都能存储和访问元素),可以通过将模板类作为参数的方式,编写一个通用的外层模板函数或类,来处理这些不同的模板类,避免了为每个具体的模板类重复编写相同功能的代码。

代码演示:

#include <iostream>         // 包含头文件。
using namespace std;        // 指定缺省的命名空间。

template <class T, int len>
class LinkList             // 链表类模板。
{
public:
    LinkList() :m_len(0), m_head(nullptr){}
    LinkList(int x) :m_len(x), m_head(new T(m_len)){}
    
    void insert() { cout << "向链表中插入了一条记录。\n"; }
    void ddelete() { cout << "向链表中删除了一条记录。\n"; }
    void update() { cout << "向链表中更新了一条记录。\n"; }

    ~LinkList() {
        if (m_head) {
            delete m_head;
            m_head = nullptr;
        }
    }
private:
    T* m_head;          // 链表头结点。
    int  m_len;   // 表长。
};

// 线性表模板类:tabletype-线性表类型,datatype-线性表的数据类型。
template<template<class, int >class tabletype, class datatype, int len>//len是非类型参数
class LinearList
{
public:
    tabletype<datatype, len> m_table;     // 创建线性表对象。

    void insert() { m_table.insert(); }         // 线性表插入操作。
    void ddelete() { m_table.ddelete(); }      // 线性表删除操作。
    void update() { m_table.update(); }      // 线性表更新操作。
};

int main()
{
    // 创建线性表对象,容器类型为链表,链表的数据类型为int,表长为20。
    LinearList<LinkList, int, 20>  a;
    a.insert();
    a.ddelete();
    a.update();
}

 这是一个更高层次的类模板 LinearList,它的模板参数比较特殊。

其中 template<class, int >class tabletype 表示它接受一个本身也是模板类的参数,这个模板类需要有两个模板参数(一个类型参数和一个非类型参数),这里就是要接收像 LinkList 这样的模板类作为参数;

具体来说,它表示所定义的模板(在这里是 LinearList 模板类)期望接收一个本身也是模板类的参数

class datatype 是用于指定线性表中数据的类型,它会作为参数传递给 tabletype(也就是类似 LinkList 的模板类);int len 同样是一个非类型参数,用于指定线性表(实际也就是对应的 tabletype 所表示的具体数据结构,如链表)的长度。

9、结语

在对 C++ 类模板进行了深入探讨之后,我们清晰地看到了它在提升代码复用性、灵活性以及实现泛型编程方面所展现出的强大威力。通过类模板,我们能够轻松应对各种不同类型的数据结构和算法需求,将代码从特定类型的束缚中解放出来,使其具备更广泛的适用性。

从类模板的基本概念、具体化、继承、与函数模板的关联、友元函数模板,成员模板以及复杂的将模板类作为数据结构的参数处理等等,它与继承、多态等面向对象特性相结合,能够创造出更加丰富多样且灵活多变的程序结构。

正如我们所了解的,类模板的运用也伴随着一定的复杂性,以及在不同编译器下可能出现的细微差异等。

希望这篇长达一万字的类模板文章能够帮助到你,如有不当还请指教!😎👍.

;