【C++】泛型编程

目录

1.函数模板:构建通用函数的基石

1.1函数模板的定义与语法

1.2函数模板的使用方式

1.2.1自动类型推导

1.2.2显式指定类型

1.3函数模板与函数重载

1.4函数模板的特化

2.类模板:创建通用类的利器

2.1类模板的定义与语法

2.2类模板的实例化

2.3类模板的成员函数

2.3.1类模板内部定义

2.3.2类模板外部定义

2.4类模板的特化

2.4.1全特化

2.4.2偏特化

3.友元在泛型编程中的独特作用

3.1类模板与友元类

3.2类模板与友元函数

4.泛型编程的优点,缺点以及使用场景

优点

缺点

使用场景


1.函数模板:构建通用函数的基石

1.1函数模板的定义与语法

函数模板是C++泛型编程的基础之一,它允许我们定义一个通用的函数框架,能够处理不同数据类型的操作。其定义方式需要借助template关键字,后面紧跟尖括号<>,在尖括号内声明类型参数。

例如,下面是一个简单的函数模板,用于比较两个值并返回较大的那个:

template <typename T>
T max(T a, T b) {
return a > b? a : b;
}
/*
在这个例子中,template <typename T>声明了这是一个函数模板,其中typename表明T是一个类型参数,你也可以使用class关键字来代替typename,效果是一样的。在函数体中,T就如同一个占位符,可以代表任意数据类型。当调用这个函数模板时,编译器会根据传入的实际参数类型,自动生成对应的具体函数 。
*/

1.2函数模板的使用方式

函数模板的使用方式主要有两种:自动类型推导和显式指定类型

1.2.1自动类型推导

自动类型推导是最常用的方式,编译器会根据调用函数时传入的参数类型,自动确定模板参数的具体类型。

例如:

int num1 = 5, num2 = 10;
int result1 = max(num1, num2);
/*
在这个调用中,编译器通过num1和num2的类型(均为int),自动推导出T为int,从而生成一个针对int类型的max函数。
*/
1.2.2显式指定类型

显式指定类型则是在调用函数模板时,明确写出模板参数的具体类型。

例如:

double num3 = 3.14, num4 = 2.71;
double result2 = max<double>(num3, num4);
/*
这里通过<double>显式指定了T的类型为double,编译器会据此生成处理double类型的max函数。显式指定类型在某些情况下非常有用,比如当编译器无法自动推导类型,或者需要明确指定使用某种类型时 。
*/

1.3函数模板与函数重载

当函数模板与普通函数同时存在,且都能匹配函数调用时,编译器遵循一定的规则来选择合适的函数。

一般情况下,如果有一个普通函数与函数模板提供的功能相同,且函数调用时参数类型与普通函数的参数类型完全匹配,那么编译器会优先选择普通函数。例如:

int max(int a, int b)
{
    return a > b? a : b;
}
template <typename T>
T max(T a, T b) 
{
    return a > b? a : b;
}
int main() 
{
    int num1 = 5, num2 = 10;
    int result = max(num1, num2);
    return 0;

}
/*
在这个例子中,max(num1, num2)调用的是普通函数int max(int a, int b),因为它与函数调用的参数类型完全匹配,并且在这种情况下,普通函数具有更高的优先级 。

如果没有完全匹配的普通函数,或者显式指定使用函数模板(例如max<int>(num1, num2)),编译器才会考虑函数模板,并根据参数类型生成相应的具体函数。此外,如果存在多个函数模板都能匹配调用,编译器会选择最特化(即最具体)的那个函数模板 。
*/

1.4函数模板的特化

函数模板特化是指针对特定的数据类型,为函数模板提供专门的实现。有时,对于某些特殊类型,通用的函数模板实现可能并不合适,这时就需要进行特化处理。

例如,对于比较两个指针的大小,直接比较指针的值(地址)可能没有实际意义,我们可能希望比较指针所指向的内容。可以对前面的max函数模板进行特化:

template <typename T>

T max(T a, T b)
{

    return a > b? a : b;

}

template <>
const char* max<const char*>(const char* a, const char* b)
{

    return strcmp(a, b) > 0? a : b;

}
/*
在这个特化版本中,template <>表示这是一个特化的函数模板,尖括号内为空,表示对所有模板参数进行特化(这里只有一个模板参数T)。max<const char*>(const char* a, const char* b)明确指定了针对const char*类型的特化实现,使用strcmp函数来比较字符串的内容 。

通过这种方式,当调用max函数并传入const char*类型的参数时,编译器会优先使用特化版本的函数,而不是通用的函数模板,从而满足特定类型的特殊需求 。
*/

2.类模板:创建通用类的利器

2.1类模板的定义与语法

类模板允许我们创建一个通用的类框架,能够适应不同的数据类型。其定义方式与函数模板类似,使用template关键字声明模板参数。

例如,下面定义一个简单的Stack类模板,用于实现一个栈结构:

template <typename T>

class Stack
{

private:

    T* data;

    int top;

    int capacity;

public:

    Stack(int size = 10);

    ~Stack();

    void push(T value);

    T pop();

    bool isEmpty();

    bool isFull();

};

在这个类模板中,template <typename T>声明了T为类型参数,它可以代表任意数据类型。类中的成员变量data是一个指向T类型的指针,用于存储栈中的元素。成员函数的参数和返回值也可以使用T类型,使得这个类模板能够处理各种数据类型的栈操作 。

2.2类模板的实例化

类模板的实例化是指根据具体的数据类型创建类对象的过程。与函数模板不同,类模板不能自动推导类型,需要显式指定模板参数的具体类型。

例如,要创建一个存储整数的栈对象,可以这样实例化:

Stack<int> intStack(5);

这里Stack<int>表示将T指定为int类型,从而创建了一个专门处理整数的栈对象intStack,初始容量为5。同样,要创建一个存储浮点数的栈对象,可以:

Stack<double> doubleStack(10);

通过这种方式,我们可以根据需要创建不同数据类型的栈,而无需为每种数据类型单独编写一个类 。

2.3类模板的成员函数

类模板的成员函数定义有两种方式:在类模板内部定义和在类模板外部定义。

2.3.1类模板内部定义

在类模板内部定义成员函数时,其语法与普通类成员函数类似,但可以使用模板参数。例如,前面Stack类模板的push函数在类内定义如下:

template <typename T>
class Stack
{

private:
    T *data;

    int top;

    int capacity;

public:
    Stack(int size = 10)
    {

        data = new T[size];

        top = -1;

        capacity = size;
    }

    ~Stack()
    {

        delete[] data;
    }

    void push(T value)
    {

        if (isFull())
        {

            // 可以进行扩容操作,这里简单省略

            return;
        }

        data[++top] = value;
    }

    T pop()
    {

        if (isEmpty())
        {

            // 可以抛出异常,这里简单返回默认值

            return T();
        }

        return data[top--];
    }

    bool isEmpty()
    {

        return top == -1;
    }

    bool isFull()
    {

        return top == capacity - 1;
    }
};
2.3.2类模板外部定义

在类模板外部定义成员函数时,需要使用模板参数列表来指定模板类型。例如,Stack类模板的push函数在类外定义如下:


template <typename T>

Stack<T>::Stack(int size)
{

    data = new T[size];

    top = -1;

    capacity = size;
}

template <typename T>

Stack<T>::~Stack()
{

    delete[] data;
}

template <typename T>

void Stack<T>::push(T value)
{

    if (isFull())
    {

        // 可以进行扩容操作,这里简单省略

        return;
    }

    data[++top] = value;
}

template <typename T>

T Stack<T>::pop()
{

    if (isEmpty())
    {

        // 可以抛出异常,这里简单返回默认值

        return T();
    }

    return data[top--];
}

template <typename T>

bool Stack<T>::isEmpty()
{

    return top == -1;
}

template <typename T>

bool Stack<T>::isFull()
{

    return top == capacity - 1;
}

在类外定义成员函数时,template <typename T>声明不能省略,并且在类名后面要使用<T>来指定模板参数,以表明这是类模板的成员函数 。

2.4类模板的特化

类模板的特化分为全特化和偏特化。

2.4.1全特化

全特化是指将类模板的所有模板参数都指定为具体类型。例如,对于前面的Stack类模板,如果我们希望针对const char*类型进行特殊处理,使其比较的是字符串内容而不是指针地址,可以进行全特化:

template <>

class Stack<const char *>
{

private:
    const char **data;

    int top;

    int capacity;

public:
    Stack(int size = 10);

    ~Stack();

    void push(const char *value);

    const char *pop();

    bool isEmpty();

    bool isFull();
};

template <>

Stack<const char *>::Stack(int size)
{

    data = new const char *[size];

    top = -1;

    capacity = size;
}

template <>

Stack<const char *>::~Stack()
{

    delete[] data;
}

template <>

void Stack<const char *>::push(const char *value)
{

    if (isFull())
    {

        // 可以进行扩容操作,这里简单省略

        return;
    }

    data[++top] = value;
}

template <>

const char *Stack<const char *>::pop()
{

    if (isEmpty())
    {

        // 可以抛出异常,这里简单返回默认值

        return nullptr;
    }

    return data[top--];
}

template <>

bool Stack<const char *>::isEmpty()
{

    return top == -1;
}

template <>

bool Stack<const char *>::isFull()
{

    return top == capacity - 1;
}

/*
在这个全特化版本中,template <>表示这是一个全特化的类模板,所有模板参数都被指定为const char*。类的成员变量和成员函数都针对const char*类型进行了重新定义 。
*/

2.4.2偏特化

偏特化则是对类模板的部分模板参数进行指定,或者对模板参数进行某种限制。例如,对于一个包含两个模板参数的类模板:

template <typename T1, typename T2>

class Pair
{

public:
    T1 first;

    T2 second;

    Pair(T1 a, T2 b) : first(a), second(b) {}
};

我们可以对其进行偏特化,比如将第二个模板参数固定为int类型:

    template <typename T1>

    class Pair<T1, int>
{

public:
    T1 first;

    int second;

    Pair(T1 a, int b) : first(a), second(b) {}
};

//或者将两个模板参数都限制为指针类型:

template <typename T1, typename T2>

class Pair<T1 *, T2 *>
{

public:
    T1 *first;

    T2 *second;

    Pair(T1 *a, T2 *b) : first(a), second(b) {}
};

通过类模板的特化,我们可以为特定类型或特定情况提供专门的实现,以满足不同的需求 。

3.友元在泛型编程中的独特作用

3.1类模板与友元类

在类模板的世界里,友元类扮演着特殊的角色。它能够访问类模板的私有成员和保护成员,打破了常规的访问限制 。

例如,我们有一个Data类模板,用于存储数据,还有一个Printer类模板,专门用于打印Data类模板中的数据。我们希望Printer类模板能够访问Data类模板的私有数据成员,可以将Printer声明为Data的友元类 :

template <typename T>

class Data
{

private:
    T value;

public:
    Data(T val) : value(val) {}

    template <typename U>

    friend class Printer;
};

template <typename T>

class Printer
{

public:
    void print(const Data<T> &data)
    {

        std::cout << "The value is: " << data.value << std::endl;
    }
};
/*
在这个例子中,Data类模板通过template <typename U> friend class Printer;声明了Printer类模板为其友元类。这样,Printer类模板的成员函数print就可以访问Data类模板对象的私有成员value。通过这种方式,实现了两个类模板之间的紧密协作,同时又保持了Data类模板的封装性 。
*/

3.2类模板与友元函数

友元函数在类模板中同样有着重要的应用。它可以在类模板外部定义,却能访问类模板的私有成员 。

例如,对于前面的Data类模板,我们可以定义一个友元函数display,用于显示Data中的数据:

template <typename T>

class Data
{

private:
    T value;

public:
    Data(T val) : value(val) {}

    friend void display(const Data<T> &data);
};

template <typename T>

void display(const Data<T> &data)
{

    std::cout << "The value from friend function is: " << data.value << std::endl;
}
/*
在这个例子中,Data类模板通过friend void display(const Data<T>& data);声明了display函数为其友元函数。这样,display函数虽然在Data类模板外部定义,但能够访问Data类模板对象的私有成员value,方便地实现了数据的展示功能 。
*/

友元函数还可以是函数模板。例如,我们有一个Calculator类模板,用于进行数值计算,希望定义一个通用的友元函数模板来比较两个Calculator对象的计算结果:

template <typename T>

class Calculator {

private:

T result;

public:

Calculator(T res) : result(res) {}

template <typename U>

friend bool compare(const Calculator<U>& a, const Calculator<U>& b);

};

template <typename T>

bool compare(const Calculator<T>& a, const Calculator<T>& b) {

return a.result == b.result;

}
/*
在这个例子中,Calculator类模板声明了函数模板compare为其友元。compare函数模板可以比较不同Calculator对象的结果,并且能够访问Calculator类模板的私有成员result 。
*/

通过友元类和友元函数,在类模板的设计中,我们可以实现更灵活的访问控制和功能扩展,让不同的类模板或函数之间能够共享数据和功能,提升代码的可维护性和复用性 。

4.泛型编程的优点,缺点以及使用场景

优点

  1. 代码复用性极高:一个函数模板或类模板能适配多种数据类型,极大减少重复代码。如前面提到的max函数模板和Stack类模板,无需针对每种数据类型单独编写代码。2
  2. 增强灵活性:当需要支持新的数据类型时,基于泛型的代码通常只需少量修改甚至无需修改。例如在Stack类模板中,想要存储新的自定义数据类型,只需直接实例化,无需更改类模板的实现。
  3. 提高代码质量:泛型编程促进了代码的模块化和抽象化,让开发者专注于算法和逻辑本身,减少因数据类型处理带来的错误 。

缺点

  1. 编译时间增加:由于编译器要为不同的数据类型实例化模板,生成大量代码,这会导致编译时间变长。尤其是在大型项目中,包含众多复杂的模板时,编译时间可能会显著增加。2
  2. 错误信息复杂:当模板代码出现错误时,编译器给出的错误信息往往冗长且难以理解。因为错误信息涉及到模板实例化的过程,可能包含多个层次的模板嵌套,定位和解决问题变得较为困难。

使用场景

  1. 数据结构与算法库:如STL,各种容器和算法的实现都依赖泛型编程,使其能适用于不同类型的数据。在开发自定义的数据结构库,如链表、二叉树等,使用泛型编程可以让这些数据结构更具通用性。
  2. 通用工具类开发:例如日志记录类、配置文件解析类等,使用泛型编程可以使其能够处理不同类型的数据,提高工具类的适用性 。
  3. 跨平台开发:在跨平台项目中,不同平台可能对数据类型有不同的要求。泛型编程可以让代码在不同平台上使用相同的逻辑,只需在实例化时根据平台需求选择合适的数据类型 。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值