模版与泛型

发布于:2023-01-14 ⋅ 阅读:(190) ⋅ 点赞:(0)


一、函数模板

定义一个函数模版:

template<typename T>  // 定义函数模板
T funadd(T a, T b) {
	T addhe = a + b;
	return addhe;
}

尖括号里面是模板参数列表,如果这里的模板参数有多个,则用逗号分开,尖括号里至少要有一个模板参数。模板参数列表里面表示在函教定义中用到的“类型”或者“值”,使用的时候有时得指定模板实参,指定的时候也得用<>把模板实参包起来,有时又不需要指定模板实参,系统能够根据一些信息推断出来。函数模版调用的时候,先不用看函数模板定义中template<>这里有多少个模板参数,看的还是函数模板定义里面的函数名后面的参数数量

int main() {
    {
        int he = funadd(3, 1);
    }
    {
        float he = funadd(3.1f, 1.2f);
    }
    {
        // 下面这行编译出错,不知道模板参数类型应该推断为int类型还是float类型
        // float he = funadd(3, 1.2f);
    }
    return 0;
}			

看一看上述案例的模板参数列表template<typename T>,这里的T,因为前面是用typename来修饰,所以T代表一个类型,是类型参数。在这个模板参数列表里,还可以定义非类型参数。非类型参数表示的是一个值,不能用typename/class来修饰,例如非类型参数s是一个整型,那就写成int s

template<int a, int b>  // 定义函数模板
int funcaddv2() {
	int addhe = a + b;
	return addhe;
}

这里没有类型模板参数,只有非类型模板参数。调用这个函数模版:

int main() {
    {
        // 要通过<>来传递参数,就得看模板函数的<>里有几个参数
        // 这种<>写法就是显式指定模板参数,在尖括号中提供额外信息
        int result = funcaddv2<12, 13>();
        cout << result << endl;  // 25
    }
    {
        int a = 12;
        // 这不可以,非类型模板参数必须是常量表达式,值必须是在编译的时候就能确定
        // 因为实例化模板是在编译的时候做的事
        // int result = funcaddv2<a, 14>();
	}
    return 0;
}

再定义一个函数模版:

template<typename T, int a, int b>
int funcaddv3(T c) {
	int addhe = (int)c + a + b;
	return addhe;
}

int main() {
    {
        // 类型参数为int,实参为13
        int result = funcaddv3<int, 11, 12>(13);
        cout << result << endl;  // 36
    }
    {
        // 类型参数为double,实参为13,系统会以"<>"传递进去的类型为准
        // 而不是以13推断出来的类型为准
        int result = funcaddv3<double, 11, 12>(13);
        cout << result << endl;  // 36
    }
    return 0;
}

再定义一个函数模版:

template<unsigned L1, unsigned L2>  // 本例依旧没有类型参数
int charscomp(const char(&p1)[L1], const char(&p2)[L2]) {
	return strcmp(p1, p2);
}

int main() {
    {
        // 根据test2能推断出大小是6个(算末尾的\0)取代L1,L2同理,推断出大小是5个	
        int result = charscomp("test2", "test");
        cout << result << endl;  // 1
    }
    return 0;
}

上面针对charscomp()的调用,编译器实例化出来的版本是:

int charscomp(const char(&p1)[6], const char(&p2)[5]) {...}

再次提醒,非类型模板参数必须是一个常量表达式,否则编译会出错。函数模版也可以写成inlineinline的位置放在模版参数列表之后:

template<unsigned L1, unsigned L2>
inline int charscomp(const char(&p1)[L1], const char(&p2)[L2]) {
    return strcmp(p1, p2);
}

函数模板的定义并不会导致编译器生成相关代码,只有调用这个函数模板时,编译器才会实例化一个特定版本的函数并生成函数相关代码。总结一下模板参数:
模板参数分类总结

二、类模板

编译器不能为类模板推断模板参数,所以使用类模板必须在模板名后面用尖括号<>提供额外信息,这些信息其实就是对应着模板参数列表里的参数。例如vector<int>这里面的vector是类模板,尖括号里的int就理解成模板参数,通过这个模板参数指出容器vector中所保存的元素类型。类模版(也称模板类)定义的一般形式如下:

template<typename 形参名1, typename 形参名2, ..., typename 形参名n>
class 类名 {
    // ......
};

对于类模板,因为实例化具体类的时候必须有类模板的全部信息,包括类模板中成员函数的函数体具体内容等,所以类模板的所有信息,不管是声明还是实现等内容,都必须写到一个.h文件中去,其他的要用到类模板的源程序文件(如. cpp文件)只要#include这个类模板的.h文件即可,例如在文件myvector.h中定义:

#ifndef __MYVECTOR__
#define __MYVECTOR__

// 自己的容器类模板
template<typename T>  // 名字为T的模板参数,用来表示myvector这个容器所保存的元素类型
class myvector {
   public:
    typedef  T* myiterator;  // 迭代器
   public:
    // 构造函数
    myvector();
    // 赋值运算符重载,在类模板内部使用模板名myvector并不需要提供模板参数,当然提供也行,可以写成myvector<T>
    myvector& operator=(const myvector&);

   public:
	// 迭代器接口
    myiterator mybegin();  // 迭代器起始位置
    myiterator myend();    // 迭代器结束位置
};
#endif

实例化这个类模板:

#include "myvector.h"
int main() {
    {
        myvector<int>  tmpvec;     // T被替换成了int
        myvector<double> tmpvec2;  // T被替换成了double
        myvector<string> tmpvec3;  // T被替换成了string
    }
    return 0;
}

myvector是类模板名,myvector<int>等才是真正的类型名(实例化了的类模板)。在上述类模板中,增加成员函数(定义和声明写在一起):

   public:
    void myfunc() {};

类模板一旦被实例化之后,这个类模板的每个实例都会有自己版本的成员函数。所以,类模板的成员函数具有和这个类模板相同的模板参数(这句话的核心意思是:类模板的成员函数是有模板参数的)。如果这个类模板的成员函数定义在类模板里面,那么这个成员函数的模板参数体现不出来,但假如把类模板的成员函数的实现写在类模板定义的外面,那么这个成员函数的模板参数就体现出来了。也就是说,定义在类模板之外的成员函数必须以关健字template开始,后面接类模板参数列表。同时在类名后面要用尖括号<>把模板参数列表里面的所有模版参数名列出来,如果是多个模板参数则用,分隔。

   public:
    void myfunc();  // 函数声明

在类模板外部定义:

template<typename T>
void myvector<T>::myfunc() {}

再写一下构造函数的实现:

template<typename T>
myvector<T>::myvector() {}

一个类模板虽然里面可能有很多成员函数,但是当实例化模板之后,如果后续没有使用到某个成员函数,则这个成员函数是不会被实例化的。再看一下类模板名字的使用,在上述类模板中有一个赋值运算符的重载代码:

myvector& operator=(const myvector&);

赋值运算符重载返回一个myvector的引用,在类模板内部可以直接使用类模板名,并不需要在类模板名后跟模板参数,当然非要在类模板名后面跟模板参数也可以:

myvector<T>& operator=(const myvector<T>&);  // 赋值运算符重载

如果在类模板定义之外实现:

template<typename T>
// 第一个<T>表示返回的是一个实例化了的myvector,第三个<T>不是必加
myvector<T>& myvector<T>::operator=(const myvector<T>&) {
	//......
	return *this;
}

模板参数并不局限于类型,普通的值也能作为模板参数,也就是非类型模板参数。前面讲函数模板有非类型模板参数,而myvector类模板中是一个类型模板参数。创建一个新的类模板来演示非类型模板参数,创建一个新的文件myarray.h:

#ifndef __MYARRAY__
#define __MYARRAY__
template<typename T, int size = 10>
class myarray {
   private:
    T arr[size];
};
#endif

存在非类型模板参数size,而且还有默认值。

#include "myarray.h"

int main() {
    {
        myarray<int, 100> tmparr;
    }
    {
        myarray<int> tmparr;
    }
    return 0;
}

在类模板增加一个成员函数的声明:

   public:
    void myfunc();

在类模板外面实现:

template<typename T, int size>
void myarray<T, size>::myfunc() {
	std::cout << size << std::endl;
	return;
}

在main函数中调用:

myarray<int> tmparr;
tmparr.myfunc();  // 10

myarray<int, 50> tmparr2;
tmparr2.myfunc();  // 50

注意,非类型的模板参数,参数的类型有一定的限制:
(1)浮点型一般不能作为非类型模板参数:

template<typename T, double size>
class myarray {...};

(2)类类型也不能作为非类型模板参数:

class a {};

template<typename T, a size>
class myarray {...};

网站公告

今日签到

点亮在社区的每一天
去签到