C++ 系列 -- 模板

模板的意义

C++ 是强类型语言 ,后来 C++ 开始支持模板,主要目的是:弥补 强类型语言"不够灵活"的缺点

强类型和弱类型语言

  • 强类型语言较为严谨,在编译时就能发现很多错误,适合开发大型的、系统级的、工业级的项目;
  • 弱类型语言较为灵活,编码效率高,部署容易,学习成本低,在 Web 开发中大显身手。

另外,强类型语言的 IDE 一般都比较强大,代码感知能力好,提示信息丰富;而弱类型语言一般都是在编辑器中直接书写代码

推出模板的原因

模板所支持的类型是宽泛的,没有限制的,我们可以使用任意类型来替换,这种编程方式称为泛型编程。相应地,可以将参数 T 看做是一个泛型,而将 int、float、string 等看做是一种具体的类型。除了 C++,Java、C#、Pascal(Delphi)也都支持泛型编程

C++ 模板也是被迫推出的,最直接的动力来源于对数据结构的封装。C++ 开发者们希望为线性表、链表、图、树 等常见的数据结构都定义一个类 ,并把它们加入到标准库中,这样以后程序员就不用重复造轮子了,直接拿来使用即可

但是这个时候遇到了一个无法解决的问题,就是数据结构中每份数据的类型无法提前预测 。以链表为例,它的每个节点可以用来存储小数、整数、字符串等,也可以用来存储一名学生、教师、司机等,还可以直接存储二进制数据,这些都是可以的,没有任何限制。而 C++ 又是强类型的,数据的种类受到了严格的限制,这种矛盾是无法调和的

要想解决这个问题,C++ 必须推陈出新,跳出现有规则的限制,开发新的技术,于是模板就诞生了。模板虽然不是 C++ 的首创,但是却在 C++ 中大放异彩,后来也被 Java、C# 等其他强类型语言采用

函数模板

为了交换不同类型的变量的值,我们通过函数重载定义了四个名字相同、参数列表不同的函数,如下所示:

cpp 复制代码
// 交换 int 变量的值
void Swap(int *a, int *b){
    int temp = *a;
    *a = *b;
    *b = temp;
}

// 交换 float 变量的值
void Swap(float *a, float *b){
    float temp = *a;
    *a = *b;
    *b = temp;
}

// 交换 char 变量的值
void Swap(char *a, char *b){
    char temp = *a;
    *a = *b;
    *b = temp;
}

// 交换 bool 变量的值
void Swap(bool *a, bool *b){
    char temp = *a;
    *a = *b;
    *b = temp;
}

能不能把它们压缩成一个函数呢?能!用到的就是函数模板:

cpp 复制代码
#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;
}

运行结果:

txt 复制代码
200, 100  
56.93, 12.5  
B, A  
1, 0

重点关注第 4 行代码:

  • template是定义函数模板的关键字,它后面紧跟尖括号<>,尖括号包围的是类型参数(也可以说是虚拟的类型,或者说是类型占位符)
  • typename是另外一个关键字,用来声明具体的类型参数,这里的类型参数就是 T
  • template<typename T>则被称为模板头

我们发现:原来使用 int、float、char 等内置类型的地方,都用类型参数 T 来代替

之所以能这么做,是因为函数模板除了支持值的参数化,还支持类型的参数化 ,也就是说 T 被当成是参数,所以才可以在发生函数调用时编译器才根据传入的实参来推演形参的值和类型

由此引出函数模板的本质和作用是:

函数模板的本质和作用

通过把类型 T 当做参数 传进函数,实现多种类型的参数都能使用的通用函数

多个类型参数

cpp 复制代码
#include <iostream>

template <typename T1, typename T2>
void print_sum(const T1& a, const T2& b) {
    std::cout << "两数之和是: " << a + b << std::endl;
}

int main() {
    int int_num = 1;
    float float_num = 2.5f;
    double double_num = 3.5;

    print_sum(int_num, float_num);    // 两数之和是: 3.5
    print_sum(int_num, double_num);   // 两数之和是: 4.5
    print_sum(float_num, double_num); // 两数之和是: 6

    return 0;
}

于是总结出函数模板的语法:

函数模板的语法

cpp 复制代码
template <typename 类型参数1 , typename 类型参数2 , ...> 返回值类型  函数名(形参列表){  
    // 函数体
}

为了加深对函数模板的理解,我们再来看一个求三个数的最大值的例子:

cpp 复制代码
#include <iostream>
using namespace std;

// 声明函数模板
template<typename T> T max(T a, T b, T c);

int main(){
    // 求三个整数的最大值
    int i1, i2, i3, i_max;
    cin >> i1 >> i2 >> i3;
    i_max = max(i1,i2,i3);
    cout << "i_max=" << i_max << endl;

    // 求三个浮点数的最大值
    double d1, d2, d3, d_max;
    cin >> d1 >> d2 >> d3;
    d_max = max(d1,d2,d3);
    cout << "d_max=" << d_max << endl;

    return 0;
}

// 定义函数模板
template<typename T>  // 模板头,这里不能有分号
T max(T a, T b, T c){
    T max_num = a;
    if(b > max_num) max_num = b;
    if(c > max_num) max_num = c;
    return max_num;
}

运行结果:

txt 复制代码
12  34  100
i_max=100  
73.234  90.2  878.23 
d_max=878.23

类模板

类模板的语法

C++ 除了支持函数模板,还支持类模板 。函数模板中定义的类型参数可以用在函数声明和函数定义中,类模板中定义的类型参数可以用在类声明和类实现中。类模板的目的同样是将数据的类型参数化。

cpp 复制代码
template<typename 类型参数1 , typename 类型参数2 , ...> class 类名{  
    // TODO:  
};

假如我们现在要定义一个类来表示坐标,要求坐标的数据类型 可以是整数、小数和字符串,例如:

  • x = 10、y = 10
  • x = 12.88、y = 129.65
  • x = "东经180度"、y = "北纬210度"

模板类的声明

这个时候类的声明就可以使用类模板,请看下面的代码:

cpp 复制代码
template<typename T1, typename 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 坐标
};

x 坐标和 y 坐标的数据类型不确定,借助类模板可以将数据类型参数化,这样就不必定义多个类了

模板类的成员函数

我们还需要在类外定义成员函数。在类外定义成员函数时仍然需要带上模板头,格式为:

cpp 复制代码
template<typename 类型参数1 , typename 类型参数2 , ...>  
返回值类型 类名<类型参数1 , 类型参数2, ...>::函数名(形参列表){  
    // TODO:  
}

第一行是模板头,第二行是函数头,它们可以合并到一行,不过为了让代码格式更加清晰,一般是将它们分成两行。

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

cpp 复制代码
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 后面的类型参数要和类声明时的一致

使用模板类创建对象

上面的两段代码完成了类的定义,接下来就可以使用该类创建对象了。使用类模板创建对象时,需要指明具体的数据类型。请看下面的代码:

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

与函数模板不同的是,类模板在实例化时必须显式地指明数据类型,编译器不能根据给定的数据推演出数据类型

除了对象变量,我们也可以使用对象指针的方式来实例化:

cpp 复制代码
Point<int, int> *p1 = new Point<int, int>(10, 20);
Point<float, float> *p2 = new Point<float, float>(10.6, 109.3);
Point<char*, char*> *p3 = new Point<char*, char*>("东经180度", "北纬210度");

完整实例

  1. 设置和获取坐标的类:
cpp 复制代码
#include <iostream>
using namespace std;

template<typename T1, typename 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<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;
}

int main(){
    Point<int, int> p1(10, 20);
    cout<<"x="<<p1.getX()<<", y="<<p1.getY()<<endl;
 
    Point<int, char*> p2(10, "东经180度");
    cout<<"x="<<p2.getX()<<", y="<<p2.getY()<<endl;
 
    Point<char*, char*> *p3 = new Point<char*, char*>("东经180度", "北纬210度");
    cout<<"x="<<p3->getX()<<", y="<<p3->getY()<<endl;

    return 0;
}

运行结果:

txt 复制代码
x=10, y=20  
x=10, y=东经180度  
x=东经180度, y=北纬210度
  1. 用类模板实现可变长数组
cpp 复制代码
#include <iostream>
#include <cstring>
using namespace std;
template <typename T>
class CArray
{
    int size; // 数组元素的个数
    T *ptr; // 指向动态分配的数组
public:
    CArray(int s = 0);  // s 代表数组元素的个数
    CArray(CArray & a);
    ~CArray();
    void push_back(const T & v); // 用于在数组尾部添加一个元素v
    CArray & operator=(const CArray & a); // 用于数组对象间的赋值
    T length() { return size; }
    T & operator[](int i)
    { // 用以支持根据下标访问数组元素,如a[i] = 4;和n = a[i]这样的语句
        return ptr[i];
    }
};
template<typename T>
CArray<T>::CArray(int s):size(s)
{
     if(s == 0)
         ptr = NULL;
    else
        ptr = new T[s];
}
template<typename T>
CArray<T>::CArray(CArray & a)
{
    if(!a.ptr) {
        ptr = NULL;
        size = 0;
        return;
    }
    ptr = new T[a.size];
    memcpy(ptr, a.ptr, sizeof(T ) * a.size);
    size = a.size;
}
template <typename T>
CArray<T>::~CArray()
{
     if(ptr) delete [] ptr;
}
template <typename T>
CArray<T> & CArray<T>::operator=(const CArray & a)
{ // 赋值号的作用是使"="左边对象里存放的数组,大小和内容都和右边的对象一样
    if(this == & a) // 防止a=a这样的赋值导致出错
    return * this;
    if(a.ptr == NULL) {  // 如果a里面的数组是空的
        if( ptr )
            delete [] ptr;
        ptr = NULL;
        size = 0;
        return * this;
    }
     if(size < a.size) { // 如果原有空间够大,就不用分配新的空间
         if(ptr)
            delete [] ptr;
        ptr = new T[a.size];
    }
    memcpy(ptr,a.ptr,sizeof(T)*a.size);   
    size = a.size;
     return *this;
}
template <typename T>
void CArray<T>::push_back(const T & v)
{  // 在数组尾部添加一个元素
    if(ptr) {
        T *tmpPtr = new T[size+1]; // 重新分配空间
    memcpy(tmpPtr,ptr,sizeof(T)*size); // 拷贝原数组内容
    delete []ptr;
    ptr = tmpPtr;
}
    else  // 数组本来是空的
        ptr = new T[1];
    ptr[size++] = v; // 加入新的数组元素
}
int main()
{
    CArray<int> a;
    for(int i = 0;i < 5;++i)
        a.push_back(i);
    for(int i = 0; i < a.length(); ++i)
        cout << a[i] << " ";   
    return 0;
}
相关推荐
捕鲸叉2 小时前
怎样在软件设计中选择使用GOF设计模式
c++·设计模式
捕鲸叉2 小时前
C++设计模式和编程框架两种设计元素的比较与相互关系
开发语言·c++·设计模式
未知陨落3 小时前
数据结构——二叉搜索树
开发语言·数据结构·c++·二叉搜索树
一丝晨光4 小时前
gcc 1.c和g++ 1.c编译阶段有什么区别?如何知道g++编译默认会定义_GNU_SOURCE?
c语言·开发语言·c++·gnu·clang·gcc·g++
汉克老师4 小时前
GESP4级考试语法知识(贪心算法(四))
开发语言·c++·算法·贪心算法·图论·1024程序员节
姆路5 小时前
QT中使用图表之QChart绘制动态折线图
c++·qt
秋说6 小时前
【数据结构 | C++】整型关键字的平方探测法散列
数据结构·c++·算法
槿花Hibiscus8 小时前
C++基础:Pimpl设计模式的实现
c++·设计模式
黑不拉几的小白兔9 小时前
PTA部分题目C++重练
开发语言·c++·算法
写bug的小屁孩9 小时前
websocket身份验证
开发语言·网络·c++·qt·websocket·网络协议·qt6.3