【C++】6、泛型编程

一、泛型编程

1、什么是泛型编程

忽略类型的一种编程,代码不因为类型的改变而改变。

2、为什么要用到泛型编程

回顾一下前面函数重载的内容:

#include <iostream>
using namespace std;

int Max(int a, int b)
{
    return a > b ? a : b;
}
string Max(string a, string b)
{
    return a > b ? a : b;
}
double Max(double a, double b)
{
    return a > b ? a : b;
}

int main()
{
    int a = 1, b = 2;
    cout << Max(a, b) << endl;

    string hello, world;
    cout << Max("hello", "world") << endl; // 这里比较大小本质上就是比较首字母ASCII大小

    double c = 2.2, d = 1.1;
    cout << Max(c, d) << endl;

    return 0;
}

系统会自动判断重载函数的类型来判断用哪个,相较于C语言的只能重新改函数名,无疑好了许多,但重新copy下来改类型也是挺麻烦的,C++中针对这专门设计了一种机制,叫做泛型编程,借助于此,我们只用定义一次即可:

#include <iostream>
using namespace std;

template <class _Ty>
_Ty Max(_Ty a, _Ty b)
{
    return a > b ? a : b;
}

int main()
{
    int a = 1, b = 2;
    cout << Max(a, b) << endl;

    string hello, world;
    cout << Max("hello", "world") << endl; // 这里比较大小本质上就是比较首字母ASCII大小

    double c = 2.2, d = 1.1;
    cout << Max(c, d) << endl;

    return 0;
}

从上面的例子可以看出,在泛型编程出现之前,就重载来说还是存在C语言的固有缺点的:

  • 重载的多个函数仅仅只是类型不同,代码的复用率比较低,只要出现新的类型需要交换,就需要新增对应的重载函数
  • 代码的可维护性比较低

所以,这些玩意自然也就成了泛型编程的优点了哈哈哈

二、函数模板

可以比对一下上面的例子,可以看出我们其实是创建了一个类似于模板的东西,我们也直接把 template <class _Ty> 中的关键字 template 叫做模板关键字,而 _Ty Max(_Ty a, _Ty b); 中的函数 Max 被叫做模板函数。

总之就是C帮我们干了更多的事情,我们需要做的也只是调用罢了,关于调用,在C中,模板函数有两种调用方式,分别是隐式调用和显式调用。

1、隐式调用

根据数据类型自动推断模板类型

#include <iostream>
using namespace std;

template <class _Ty>
_Ty Max(_Ty a, _Ty b)
{
    return a > b ? a : b;
}

int main()
{
    cout << Max(1, 2) << endl;
    cout << Max(2.1, 1.1) << endl;
    cout << Max("A", "a") << endl;

    return 0;
}

2、显式调用

调用的时候显示传入类型:函数名<传入的类型>(参数);

#include <iostream>
using namespace std;

template <class _Ty>
_Ty Max(_Ty a, _Ty b)
{
    return a > b ? a : b;
}

int main()
{
    cout << Max<int>(1, 2) << endl;
    cout << Max<double>(2.1, 1.2) << endl;
    cout << Max<string>("A", "a") << endl;

    return 0;
}

3、模板中有多种类型

#include <iostream>
using namespace std;

template <class _Ty1, class _Ty2>
void print(_Ty1 one, _Ty2 two)
{
    cout << one << endl;
    cout << two << endl;
}

int main()
{
    print<int, string>(1, "hello");
    print<double, string>(1.23, "world");

    return 0;
}

4、模板传入变量

模板传入变量时,调用的时候必须要显式调用,并且只能传入常量

#include <iostream>
using namespace std;

template <class _Ty, int size> // _Ty:int size = 4
_Ty* newMemeory()
{
    return new _Ty[size]; // new int[4]
}
void testConstSize()
{
    int* p = newMemeory<int, 4>();
    for(int i = 0; i < 4; i++)
    {
        p[i] = i;
        cout << p[i] << "\t";
    }
    cout << endl;
}

int main()
{
    testConstSize();

    return 0;
}

5、函数模板中带有缺省

#include <iostream>
using namespace std;

template <class _T1, class _T2 = string, class _T3>
void exa1(_T1 one, _T2 two, _T3 three)
{
    cout << one << "---";
    cout << two << "---";
    cout << three << endl;
}
void fun1()
{
    exa1<int>(1, "hello", 2); // 可以只写没有缺省的,几次试验暂且认为不必虑缺省的位置,也可以尖括号内全部一种类型的时候直接写那个类型
    exa1<int, string, int>(1, "world", 2);
}

int main()
{
    fun1();

    return 0;
}

6、令一个类的成员函数是函数模板

#include <iostream>
using namespace std;

class MM
{
public:
    template<class _Ty>
    void print(_Ty data)
    {
        cout << data << endl;
    }
};

int main()
{
    MM mm;

    mm.print(1); // 隐式调用
    mm.print("你真坏~");

    mm.print<double>(1.23); // 也可显式调用

    return 0;
}

7、函数模板重载

函数模板和普通函数,优先调用匹配上的普通函数。

当然,若是直接显式调用该函数模板,就优先显式了。

#include <iostream>
using namespace std;

template <class _T1, class _T2, class _T3>
void printSum(_T1 one, _T2 two, _T3 three)
{
    cout << "函数模板----------\n";
    cout << one << endl;
    cout << two << endl;
    cout << three << endl;
}

void printSum(int one, int two, int three) // 函数模板和普通函数,优先调用匹配上的普通函数
{
    cout << "普通函数----------\n";
    cout << one << endl;
    cout << two << endl;
    cout << three << endl;
}

void testCallFunc()
{
    printSum(1, 2, 3);
    printSum<int>(11, 22, 33);
}

int main()
{
    testCallFunc();

    return 0;
}


8、函数模板的运行过程

在网上找了很久,无果,问了下ChatGPT,以下内容是ChatGPT的回答:

当我们使用函数模板时,实际上我们定义了一个通用的函数,该函数能够接受不同类型的参数。当我们使用该函数时,编译器会根据参数的类型来生成特定的函数实例,以便在程序中执行。下面是函数模板的运行过程:


  1. 定义函数模板:首先我们需要定义一个函数模板,可以使用template关键字来定义。函数模板的通用形式中,参数类型通常用T来表示。例如:
template <typename T>
T max(T a, T b) {
    return a > b ? a : b;
}

  1. 使用函数模板:在程序中使用函数模板时,需要将特定类型的参数传递给该函数。例如:
int a = 1, b = 2;
cout << max<int>(a, b) << endl; // 调用max函数模板并传递int类型的参数

在调用max函数模板时,我们需要指定模板参数类型,即使用尖括号<>括起来并传递模板参数类型。


  1. 编译器生成特定类型函数实例:当我们在程序中使用函数模板时,编译器会根据我们传递的参数类型生成特定类型的函数实例。例如:
int a = 1, b = 2;
double c = 1.5, d = 2.5;
cout << max<int>(a, b) << endl; // 生成int类型的max函数实例
cout << max<double>(c, d) << endl; // 生成double类型的max函数实例

在第一次调用max函数模板时,编译器会生成一个int类型的max函数实例,用于执行程序中的具体操作。在第二次调用时,编译器会生成一个double类型的max函数实例,同样用于执行具体操作。


  1. 编译器进行类型检查和参数匹配:在生成函数实例后,编译器会对参数类型进行检查,并根据参数类型进行参数匹配。如果参数类型不匹配,则编译器会报错。

  1. 执行函数:在类型检查和参数匹配之后,编译器会执行特定类型的函数实例,以便在程序中执行相应的操作。

因此,函数模板的运行过程可以简单概括为:定义函数模板 → 使用函数模板并传递特定类型的参数 → 编译器生成特定类型函数实例 → 编译器进行类型检查和参数匹配 → 执行函数。

感叹一句,ChatGPT牛批。

又想了一下,国内的GPT模型前景堪忧,要么全身只有傲慢,要么没钱,要么有心无力,要么没德行,要么着急抢地盘,要么在着急保命,要么只想着赚快钱,死水水少,诸鱼抢食,只能白白流出大数据给老外的模型添砖加瓦

三、类模板

1、定义

C的类模板为生成通用的类声明提供了一种更好的方法:模板提供参数化类型,即能够将类型名作为参数传递给接收方来建立类或函数。 ——《C Primer Plus》

2、tips

  1. 在文件中,类模板不能把声明和实现分开
  2. 类模板在调用的时候必须要显式调用,而不能隐式调用
  3. 类模板不是一个完整类,所以在用到类名的地方,必须要用 类名<数据类型> 的方式去使用

3、类模板的写法

模板类格式:

template <typename T>
class 类名
{
	类成员;
};

模板类实例化:

类名<数据类型> 对象名;

类外初始化:

template <class T>
T 类名<T>::函数名(){}

例程1:

#include <iostream>
using namespace std;

#define N 1000

template <class T>
class Array
{
    T* arr;
    int lenth;

public:
    Array(int n = N)
    {
        this->lenth = n;
        this->arr = new T[this->lenth];
    }
    ~Array()
    {
        delete []this->arr;
    }

    template<class Ty>
    friend istream& operator >> (istream& in, Array<Ty>& array);
    template<class Ty>
    friend ostream& operator << (ostream& out, Array<Ty>& array);
};

template<class Ty>
istream& operator >> (istream& in, Array<Ty>& array)
{
    for(int i = 0; i < array.lenth; i++)
    {
        cin >> array.arr[i];
    }

	return in;
}

template<class Ty>
ostream& operator << (ostream& out, Array<Ty>& array)
{
    for(int i = 0; i < array.lenth; i++)
    {
        cout << array.arr[i] << "\t";
    }
    cout << endl;
    return out;
}

int main()
{
    Array<int> array(10);

    cin >> array;
    cout << array;

    return 0;
}

当我们使用类模板 Array 创建一个数组对象时,该对象将动态分配一个大小为 N 的数组作为其成员变量。

operator>> 友元函数中,我们重载了输入操作符 >>,使其能够将用户的输入赋值给 Array 对象的数组元素。我们使用 for 循环遍历数组,并使用输入流对象 in 读取用户输入的值,并将其存储到数组的各个位置。

operator<< 友元函数中,我们重载了输出操作符 <<,使其能够将 Array 对象的数组元素以适当的格式输出到输出流对象 out。同样,我们使用 for 循环遍历数组,并使用输出流对象 out 将数组元素输出到控制台。

main 函数中,我们创建了一个 Array<int> 类型的对象 array,并通过 cin >> arraycout << array 从用户获取输入并将结果输出到控制台。

这样,我们就实现了通过重载运算符 >><< 来实现输入和输出 Array 对象的功能。使用友元函数的方式可以让这些操作符直接访问 Array 对象的私有成员变量,以便进行输入和输出操作。

4、类模板和继承一起用

#include <iostream>
using namespace std;

// 基类模板 A
template <class T1, class T2>
class A
{
    T1 width;
    T2 height;

public:
    // 设置宽度
    T1 setWidth(T1 w)
    {
        width = w;
        return this->width;
    }

    // 获取高度
    T2 getHeight(T2 h)
    {
        height = h;
        return this->height;
    }
};

// 派生类模板 B,继承自基类 A
template <class T1, class T2, class T3>
class B : public A<T1, T2>
{
public:
    // 计算面积
    T3 getArea()
    {
        T1 a = this->setWidth(3.1);    // 调用基类模板 A 的成员函数 setWidth()
        T2 b = this->getHeight(4.2);   // 调用基类模板 A 的成员函数 getHeight()
        T3 c = a * b;                  // 计算面积

        return c;
    }
};

int main()
{
    B<double, double, int> b;   // 创建 B 类模板对象 b,类型参数为 <double, double, int>
    cout << b.getArea() << endl;    // 输出计算得到的面积

    B<double, double, double> b1;   // 创建 B 类模板对象 b1,类型参数为 <double, double, double>
    cout << b1.getArea() << endl;    // 输出计算得到的面积

    return 0;
}



需要注意的是此时在派生类中,若是想使用基类中的成员,需要加上 this->,注意,普通的继承是并不需要的

指的是这两行代码:

T1 a = this->setWidth(3.1);
T2 b = this->getHeight(4.2);

这里为什么要加上呢,若是不加的话,编译器无法确定setWidth()是在基类 A 中定义的还是在其他地方定义的,我们也可以在派生类 B 中明确指定 setWidth() 函数是从基类 A 继承的:

    // 计算面积
    T3 getArea()
    {
        // T1 a = this->setWidth(3.1);
        // T2 b = this->setHeight(4.2);
        T1 a = A<T1, T2>::setWidth(3.1);
        T2 b = A<T1, T2>::setHeight(4.2);
        T3 c = a * b;

        return c;
    }

5、类模板中的静态数据

跟不用类模板的用法其实相差不大,只需要严格遵守静态数据和类模板的规则即可,这里简单回忆一下静态变量的注意点:

  • 需要类外初始化
  • 优先级大于构造
  • 即使是静态也不能打破私有权限,依旧需要采用共有成员函数调用私有变量的方式来间接调用静态私有变量

格式:

// static成员变量需要在类外初始化
template <class 数据类型T>
数据类型T Data<数据类型T>::静态变量 = 数据;

例程1:

#include <iostream>
using namespace std;

// 基类
template <class T>
class Data
{
    static T data;
public:
    Data() {}
    void show1()
    {
        this->data = data;
        cout << this->data << endl;
    }
};

// static成员变量需要在类外初始化
template <class T>
T Data<T>::data = 10; // 第一个 T 是模板参数,它指定了要实例化的类模板 Data 的数据类型。它将决定静态成员变量 data 的类型。

// 派生类
template <class T>
class B : public Data<int>
{
public:
    B() {}
    void show2()
    {
        show1();
    }
};

int main()
{
    B<int> b;
    b.show2();

    return 0;
}


当然,这里还是要提一下直接调用类中静态成员函数的:

例程2:

#include <iostream>
using namespace std;

template <class T>
class Data
{
    static T data;
public:
    Data() {}
    static void show1()
    {
        cout << data << endl;
    }
};

// static成员变量需要在类外初始化
template <class T>
T Data<T>::data = 10;

int main()
{
    Data<int>::show1();

    return 0;
}

6、关键字 explicit

C++中的 explicit 关键字只能用于修饰

只有一个参数的类构造函数

他的作用是表明该构造函数是一个显示调用,而非隐式调用。
与它相对的关键字是 implicit,作用是表明该构造为隐式调用

explicit 禁止构造函数的函数参数转化为对象类型。


首先看一个很简单的例程:

#include <iostream>
using namespace std;

class Data
{
    int a;

public:
    Data(int a): a(a)
    {
        cout << "Data\n";
    }
};

int main()
{
    Data d(10);
    Data data = 10;

    return 0;
}

结果是很OK的,没报错,但是需要注意,若是加了explicit

这里的 Data data = 10;是不是很眼熟,没过,这是一个转换构造函数,可以理解为 Data data = Data(10);,在转换构造函数中也是只允许一个参数的,现在遇见的explicit也是如此,个人猜测explicit其实是针对的转换构造函数。


ChatGPT:

你的理解基本正确。

在你提供的代码中,Data data = 10; 是一个对象的初始化过程,这里发生了一种隐式的类型转换,将整数值 10 转换为 Data 类型的对象。这种转换是通过转换构造函数实现的。

转换构造函数是一个带有一个参数的构造函数,用于将其他类型的数据转换为类类型的对象。在这种情况下,编译器会自动调用转换构造函数来完成类型的转换。

然而,需要注意的是,如果转换构造函数被声明为 explicit,则禁止隐式转换,只能以显式方式调用该构造函数进行转换。在你的示例代码中,Data 类的构造函数没有被声明为 explicit,因此编译器允许隐式转换。

你的个人猜测也是正确的,explicit 关键字主要用于控制转换构造函数的隐式调用,可以防止意外的类型转换和错误,提高代码的清晰性和可读性。

7、类模板实现一个顺序表

#include <iostream>
using namespace std;

#define N 1000
template <class T>
class Mvector
{
    T* arr;
    int len; // 数组长度
    int count; // 下标计数

public:
    Mvector()
    {
        this->len = N;
        this->count = 0;
        this->arr = new T[this->len];
    }
    ~Mvector()
    {
        delete []this->arr;
    }

    // 尾部输入
    void last_insert(const T& data)
    {
        // 判断是否已满
        if(this->count >= this->len)
        {
            cout << "此表已满\n";
            return;
        }

        // 不满的情况,若数据赋值,下标域+1
        this->arr[this->count] = data;
        this->count++;

    }

    // 数据的查询
    int find_data(const T& data)
    {
        // 判断是否为空,空就退出去
        if(0 == this->count)
        {
            cout << "此顺序表为空\n";
            return -1;
        }

        // 若存在数据,则循环比对
        int i;
        for(i = 0; i < this->count; i++)
        {
            if(data == this->arr[i])
            {
                cout << "该数据存在\n";
                break;
            }
        }

        // 若是查不到数据,则该顺序表中没有
        if(i == this->count)
        {
            cout << "查询无此数据\n";
            return -1;
        }

        return i;

    }

    // 打印
    void display()
    {
        // 循环输出
        for(int i; i < this->count; i++)
        {
            cout << this->arr[i] << "\t";
        }
        cout << endl;
    }

    T getData(int i)
    {
        return this->arr[i];
    }
};

int main()
{
    Mvector<int> user;

    for(int i = 0; i < 10; i++)
    {
        user.last_insert(i + 1);
    }

    user.display();

    cout << "请输入查询数据:\t";
    int data;
    cin >> data;
    int i = user.find_data(data);
    if(i >= 0)
    {
        cout << user.getData(i) << endl;
    }


    return 0;
}

posted @ 2023-07-06 17:18  fhzy  阅读(57)  评论(0)    收藏  举报