Skip to content

1. 泛型编程

所谓的泛型编程,指的是我们编写程序的时候,是独立于任何特定的类型来编程程序。那么什么叫做独立于任何特定的类型呢?

如:

  • 设计一个数组类型,这个数组有可能只能存放int类型的元素。但是我们在代码中使用这个数组的时候,却不一定总是int类型的元素。可能需要数组能够存放任意类型的元素也就是说,数组只是一个通用的概念,不应该局限于某一种特定的类型。
  • 所以数组元素的类型,应该可以使用某一种方式独立表示、类似于函数的参数。可以在实例化数组的时候,指定数组中的元素类型。

1.1 泛型编程的定义

泛型编程是计算机科学中的一个分支,它允许一个值取不同的数据类型,并强调使用这种技术的编程风格。泛型编程的目标是推出一种针对算法、数据结构和内存分配机制的分类方法,以及其他能够带来高度可重用性、模块化和可用性的软件工具。

泛型编程强调算法的重要性,它使用最少的有关数据抽象的假设,并尽可能将具体算法提升到抽象层次,同时保持效率。通过这种方式,泛型编程表示成高通用和抽象的组件集合,这些组件可以多种方式组合在一起,形成高效、具体的程序。

泛型编程在多个场景中有广泛应用,包括集合类和数据结构、自定义数据结构、泛型方法、接口和抽象类以及异常处理等。在集合类和数据结构中,泛型允许创建存储特定类型元素的集合,并在编译时捕获类型错误。在自定义数据结构中,泛型可以创建适应不同类型数据的通用数据结构。泛型方法则允许在方法级别使用泛型,为只需要在特定方法中使用泛型的情况提供便利。

总的来说,泛型编程提供了一种灵活且高效的方式来处理不同数据类型,提高了软件的可重用性和模块化程度。

1.2 泛型编程的优点

代码重用性:泛型编程可以编写通用的代码,这些代码可以在不同类型上进行操作,而无需为每种类型单独编写代码。这大大提高了代码的重用性,减少了代码的冗余,从而降低了开发和维护成本。

类型安全:泛型编程在编译时执行类型检查,这有助于捕获大部分类型相关的错误。这意味着运行时,由于类型不匹配而引发的异常会大大减少,从而提高了代码的安全性和稳定性。

性能优化:泛型编程允许编译时进行类型优化,减少运行时的类型转换和装箱/拆箱操作。这对于性能敏感的应用程序尤为重要,因为它可以显著提高代码的执行效率。

抽象性:泛型编程提供了一种更高层次的抽象,它隐藏了实现细节,使得代码易于理解和维护。通过使用泛型,程序员可以专注于算法和数据结构的本质,而不是陷入具体的类型细节中。

灵活性:泛型编程提供了更大的灵活性,允许程序员在编译时定义和修改类型,从而根据需求调整程序的行为。这使得泛型编程成为处理多种数据类型和适应不同场景的强大工具。

简化代码:泛型代码通常更加清晰和易读,因为它减少了不必要的类型转换和类型检查,使得代码的结构更加简洁和直观。

泛型编程的核心思想是将数据类型视为参数,通过参数化类型来编写代码,使得代码能够在多种数据类型上操作。这种技术使得程序员能够编写完全一般化并可重复使用的算法,而不需要针对各种数据类型都编写特定的代码。

泛型编程如何实现呢?使用模板实现。

2. 模板

在C++中使用模板实现泛型编程,或者说使用模板来表示一个通用的概念。

模板是一种泛型编程的工具,它允许程序员创建一种可以处理任何数据类型的类或函数,而无需针对每种数据类型分别编写代码。通过模板,C++程序可以以一种统一的方式处理多种数据类型,提高了代码的重用性和灵活性。

模板允许我们在定义函数或者类的时候,将类型作为一个参数,编译器后面可以根据你提供的类型自动生成特定的函数或者类。

简单的说,模板就是用来创建一个类或者一个函数的公式(蓝图)

模板可以分为两种:

  • 函数模板,可以通过这个模板创建具体类型的函数,其参数和返回值类型也可以是任意的。
  • 类模板,可以用来创建具体类型的类,其成员变量和成员函数可以使用任意类型。

在定义模板时,需要使用template关键字,并指定一个或多个类型参数。这些类型参数在模板实例化时会被具体的类型所替换。

通过使用模板,程序员可以编写更加通用和灵活的代码,减少代码冗余,提高代码的可维护性和可重用性。同时,模板也可以提高代码的性能。因为编译器在编译时会根据具体的类型生成相应的代码,避免了运行时类型转换的开销。

需要注意的是,模板不是一种运行时的特性,而是一种编译时的特性。编译器会根据模板的定义和实例化请求生成具体的代码。因此,在使用模板时,需要确保模板的定义在实例化之前已经被编译器看到。

2.1 函数模板

函数模板允许我们编写一个函数,该函数可以处理任意类型的数据。而无需针对每种数据类型分别编写函数。

函数模板实际上是建立一个通用函数,其函数类型和形参类型中的全部部分类型不具体指定,用一个虚拟的类型来代替。这个通用函数称为函数模板,函数体相同的函数都可以用这个模板来代替。

2.1.1 函数模板定义格式

cpp
template<typename T1, typename T2...>    // 模板头
返回值类型 函数名(参数列表)
{
    // 函数体;
}
  • template是C++语言中用于定义模板的关键字
  • T1,T2是一个类型参数的形参名字,在后面的函数定义中,可以直接使用这个类型参数
  • 如果该函数模板涉及到多个类型参数,只需要在括号内以逗号隔开即可
  • 在函数模板调用的时候,编译器会自动的推导类型参数去匹配函数模板

2.1.2 函数模板调用格式

cpp
函数模板名<参数类型>(实参列表);
// 其中<参数类型>是类型参数列表,它告诉编译器在实例化模板时应该使用哪种类型。在某些情况下,编译器能够自动推导出类型参数,这时可以省略

定义一个函数模板,用于计算两个对象的和:

cpp
#include<iostream>
using namespace std;

template<typename T1, typename T2> // T只是一个名字,表示一种类型
T1 sum(T1 , T2 );

template<typename T1, typename T2>
T1 sum(T1 a, T2 b)
{
    return a + b; // T类型的对象本身是可以相加的。
}

int main()
{
    cout << sum<int>(1, 2) << endl; // <int>可以省略
    cout << sum('0', 'A') << endl;
    string s1 = "abc";
    string s2 = "def";
    cout << sum(s1, s2) << endl;
    cout << sum(1, 'a') << endl;
    return 0;
}

2.1.3 多种类型参与运算

cpp
#include<iostream>
using namespace std;

template<typename T1, typename T2>
T1 sum(T1 a, T2 b)
{
    return a + b; // T1、T2类型的对象本身是可以相加的,可以是不同类型,也可以是相同类型
}

int main()
{
    cout << sum(1, 2) << endl;
    cout << sum('0', 'A') << endl;
    string s1 = "abc";
    string s2 = "def";
    cout << sum(s1, s2) << endl;
    // 如果返回值类型为T1,则结果是98
    // 如果返回值类型为T2,则结果是b
    cout << sum(1, 'a') << endl;
    return 0;
}

2.2 类模板

C++中除了支持函数模板还支持类模板(class template)

在C++中,类模板是一种特殊的类,它允许程序员定义一种可以用于多种数据类型的类,而不必为每种数据类型都重新编写一个类。通过使用类模板,我们可以在不重复编写代码的情况下创建适用于不同类型的类实例。

函数模板中定义的类型参数可以在函数的声明和定义中使用,类模板中定义的类型参数可以在类的声明和实现中使用。

类模板的目的是将数据的类型参数化让类可以更加的通用,类模板的主要优势在于其灵活性和代码重用性。通过使用类模板,我们可以编写出高度通用的代码,这些代码可以处理多种数据类型,而无需为每个数据类型编写特定的类。这不仅可以减少代码量,还可以提高代码的可读性和可维护性。

2.2.1 类模板定义格式

cpp
template<typename T1, typename T2>
class 类名
{
public:
    // 公有成员
private:
    // 私有成员
protected:
    // 保护成员
};

类模板和函数模板都是以template开头,后面跟上类型参数,类型参数不能为空。

多个类型参数可以使用逗号隔开。

一旦声明了类模板,就可以将类型参数用于类的成员变量和成员函数。

2.2.2 类模板示例

cpp
#include<iostream>
using namespace std;

class A
{
public:
    A(int num = 0) : m_num(num) {}
    friend ostream& operator<<(ostream& os, A& a)
    {
        os << a.m_num;
        return os;
    }
private:
    int m_num;
};

// 使用模板类定义一个数组
template<typename T>
class Array
{
public:
    Array(int cnt) : m_parr(new T[cnt]), m_cnt(cnt) {}
    ~Array()
    {
        delete[] m_parr;
    }
    
    // 获取数组大小
    int getSize()
    {
        return m_cnt;
    }
    
    // 访问元素
    T& operator[](int index)
    {
        if (index < 0 || index >= m_cnt)
        {
            cout << "下标输入有误" << endl;
            exit(-1);
        }
        return m_parr[index];
    }
    
    // 拷贝构造
    Array(const Array& that)
    {
        m_cnt = that.m_cnt;
        m_parr = new T[m_cnt];
        for (int i = 0; i < m_cnt; i++)
        {
            m_parr[i] = that.m_parr[i];
        }
    }
    
    // 拷贝赋值
    Array& operator=(const Array& that)
    {
        if (this != &that)
        {
            // 释放旧内存
            delete[] m_parr;
            
            // 分配新内存
            m_cnt = that.m_cnt;
            m_parr = new T[m_cnt];
            for (int i = 0; i < m_cnt; i++)
            {
                m_parr[i] = that.m_parr[i];
            }
        }
        return *this;
    }

private:
    T* m_parr; // 数组首地址
    int m_cnt;  // 数组元素个数
};

2.2.3 类模板的使用方式

调用函数模板只需要提供实际参数即可,编译器会自动的根据用户提供的值去推导值的类型在自动的生成相应的函数版本,然后调用。

但是我们在使用类模板的时候,必须显示的指定类型参数。

格式:

cpp
模板类名<实际类型1,实际类型2> 对象名;

一旦类模板被实例化,就可以像使用普通类一样创建对象,并调用其成员函数和访问其成员变量。

每次使用不同的类型参数实例化类模板时,都会生成一个独立的类。

cpp
int main()
{
    Array<int> intArray(5); // 类模板的使用
    for (int i = 0; i < intArray.getSize(); i++)
    {
        intArray[i] = i;
    }
    
    // 输出整型数组的内容
    for (int i = 0; i < intArray.getSize(); i++)
    {
        std::cout << intArray[i] << " ";
    }
    std::cout << endl;

    Array<int> intArray1 = intArray;
    for (int i = 0; i < intArray1.getSize(); ++i)
    {
        std::cout << intArray1[i] << " ";
    }
    std::cout << endl;

    Array<int> intArray2(3);
    intArray2 = intArray;
    for (int i = 0; i < intArray2.getSize(); ++i)
    {
        std::cout << intArray2[i] << " ";
    }
    std::cout << endl;
    std::cout << "---" << endl;

    Array<A> a(5);
    for (int i = 0; i < a.getSize(); i++)
    {
        a[i] = i;
    }
    for (int i = 0; i < a.getSize(); i++)
    {
        std::cout << a[i] << " ";
    }
    return 0;
}

2.2.4 类模板成员函数的类外实现

如果模板类的成员函数在类外实现,则该成员函数必须有模板声明,并且作用域也必须指定模板参数。

cpp
// 访问元素
T& operator[](int index); // 在类中声明

// 类外实现
template<typename T> // 模板声明
T& Array<T>::operator[](int index) // Array<T>作用域也必须指定模板参数
{
    if (index < 0 || index >= m_cnt)
    {
        cout << "下标输入有误" << endl;
        exit(-1);
    }
    return m_parr[index];
}

2.2.5 类模板的分文件编写

如果模板函数和类模板的声明和定义分开,可以将模板函数和类模板的声明写在.h文件中,而将定义写在.cpp文件中。

test_1.h

cpp
#ifndef __TEST_1_H__
#define __TEST_1_H__
#include<iostream>

class A
{
public:
    A(int num = 0) : m_num(num) {}
    friend std::ostream& operator<<(std::ostream& os, A& a);

private:
    int m_num;
};

// 使用模板类定义一个数组
template<typename T>
class Array
{
public:
    Array(int cnt);
    ~Array();
    
    // 获取数组大小
    int getSize();
    
    // 访问元素
    T& operator[](int index);
    
    // 拷贝构造
    Array(const Array& that);
    
    // 拷贝赋值
    Array& operator=(const Array& that);

private:
    T* m_parr; // 数组首地址
    int m_cnt;  // 数组元素个数
};

#endif

test.cpp

cpp
#include<iostream>
#include"test_1.h"
using namespace std;

ostream& operator<<(ostream& os, A& a)
{
    os << a.m_num;
    return os;
}

template<typename T>
Array<T>::Array(int cnt) : m_parr(new T[cnt]), m_cnt(cnt) {}

template<typename T>
Array<T>::~Array()
{
    delete[] m_parr;
}

template<typename T>
int Array<T>::getSize()
{
    return m_cnt;
}

template<typename T>
T& Array<T>::operator[](int index)
{
    if (index < 0 || index >= m_cnt)
    {
        cout << "下标输入有误" << endl;
        exit(-1);
    }
    return m_parr[index];
}

template<typename T>
Array<T>::Array(const Array& that)
{
    m_cnt = that.m_cnt;
    m_parr = new T[m_cnt];
    for (int i = 0; i < m_cnt; i++)
    {
        m_parr[i] = that.m_parr[i];
    }
}

template<typename T>
Array<T>& Array<T>::operator=(const Array& that)
{
    if (this != &that)
    {
        // 释放旧内存
        delete[] m_parr;
        
        // 分配新内存
        m_cnt = that.m_cnt;
        m_parr = new T[m_cnt];
        for (int i = 0; i < m_cnt; i++)
        {
            m_parr[i] = that.m_parr[i];
        }
    }
    return *this;
}

int main()
{
    Array<int> intArray(5);
    for (int i = 0; i < intArray.getSize(); i++)
    {
        intArray[i] = i;
    }
    
    // 输出整型数组的内容
    for (int i = 0; i < intArray.getSize(); i++)
    {
        std::cout << intArray[i] << " ";
    }
    std::cout << endl;

    Array<int> intArray1 = intArray;
    for (int i = 0; i < intArray1.getSize(); ++i)
    {
        std::cout << intArray1[i] << " ";
    }
    std::cout << endl;

    Array<int> intArray2(3);
    intArray2 = intArray;
    for (int i = 0; i < intArray2.getSize(); ++i)
    {
        std::cout << intArray2[i] << " ";
    }
    std::cout << endl;
    std::cout << "---" << endl;

    Array<A> a(5);
    for (int i = 0; i < a.getSize(); i++)
    {
        a[i] = i;
    }
    for (int i = 0; i < a.getSize(); i++)
    {
        std::cout << a[i] << " ";
    }
    return 0;
}

2.2.6 文件组织注意事项

如果类模板的声明、定义、调用不再同一个文件中:

  • 声明在.h文件,定义和调用相同.cpp文件(例如声明在test.h文件,定义和调用在test.cpp文件)
  • 声明和定义在同一个.h文件,调用在cpp文件(例如声明和定义在test.h文件,调用在test.cpp文件)
  • 其他形式会报错

2.2.7 模板参数关键字

在模板声明中,使用关键字typename来说明类型参数的名字。但是在早期的C++中,曾经使用的关键字叫做class。但是在新标准中,我们建议使用typename。

cpp
template<class T1, class T2>
class 类名
{
public:
    // 公有成员
private:
    // 私有成员
protected:
    // 保护成员
};

2.3 类模板与函数模板的结合使用

类模板和函数模板在C++中可以共同使用,以实现更高级别的泛型编程。通过结合类模板和函数模板,我们可以创建出既能够处理多种数据类型,又能够执行多种操作的灵活且可重用的代码。

在上述的案例中,我们需要将给数组赋值以及打印数组的操作放在模板函数中完成。

cpp
// 类模板内
void Init(int size);

// 类模板外
template<typename T> 
void print(Array<T>& arr, int size);

// 定义
template<typename T>
void Array<T>::Init(int size)
{
    for (int i = 0; i < size; i++)
    {
        m_parr[i] = i;
    }
}

template<typename T>
void print(Array<T>& arr, int size)
{
    for (int i = 0; i < size; i++)
    {
        std::cout << arr[i] << " ";
    }
}

// 使用
Array<int> intArray(5);
intArray.Init(intArray.getSize());
print(intArray, intArray.getSize());
std::cout << endl;

知识如风,常伴吾身