C++泛型编程之模板的使用

1.模板的概念

C++中的模板是一种实现泛型编程的机制,它允许程序员编写与类型无关的代码,从而提高了代码的重用性、灵活性和抽象级别。模板可以用来创建泛型类(类模板)和泛型函数(函数模板),使得这些类和函数能够应用于多种数据类型。

1.函数模板

函数模板是创建通用函数的方式,它允许函数操作不同类型的参数,而无需为每种类型重复编写相同的函数代码。函数模板通过类型参数(通常用typenameclass关键字声明)来指定函数可以接受的任何数据类型。编译器会在实际调用函数时根据传入的参数类型自动实例化具体的函数版本。

示例:

template <typename T>
T max(T a, T b) {
    return (a > b) ? a : b;
}

在这个例子中,T是一个类型参数,编译器会根据实际调用时的参数类型(如intdouble等)生成相应类型的max函数。

2.类模板

类模板与函数模板类似,但它是应用于类的,允许类的定义参数化。类模板允许创建可以处理各种数据类型的类,从而增加了类的通用性和适应性。

示例:

template <typename T>
class Vector {
private:
    T* elements;
    int size;
public:
    Vector(int s);
    ~Vector();
    void push_back(T element);
    // ... 其他成员函数
};

这里,Vector类是一个模板类,它可以用任何数据类型T实例化,创建特定类型的向量,如Vector<int>Vector<double>

3.模板特化

模板特化允许为模板提供特定类型的特殊实现。这可以是全特化(为所有类型参数提供具体类型)或偏特化(只为部分类型参数提供具体类型)。

4.模板元编程

模板元编程是利用模板在编译时期进行计算的技术,它可以生成高效的静态(编译时计算)代码。这是通过模板实例化和递归模板实例化实现的,常用于计算编译时常量表达式、类型检查等。

总的来说,C++模板机制极大地增强了语言的能力,使得编写高效、灵活且可重用的代码成为可能。

2.模板的使用

C++模板的使用广泛涵盖了函数模板和类模板两大类别,下面分别简要介绍它们的使用方法:

1.函数模板的使用

函数模板允许你编写一个通用的函数,它能接受多种数据类型的参数。使用函数模板的基本步骤包括定义和调用:

定义:

template <typename T>
T add(T a, T b) {
    return a + b;
}

在这个例子中,T是一个类型参数,表示函数参数和返回类型可以是任何类型。编译器会根据实际调用时提供的参数类型自动实例化相应的函数版本。

调用:

int resultInt = add<int>(5, 3);    // 显式指定类型
double resultDouble = add(2.5, 3.7); // 编译器自动推导类型

//
// Created by 86189 on 2024/6/24.
//
#include <iostream>
using namespace std;

template<typename T>
T add(T a, T b) {
    return a + b;
}

int main() {
    int a = 10, b = 20;
    cout << add<int>(a, b) << endl;  //显式指定类型
    cout << add(a, b) << endl; //模板推导类型

    char c = 'a';
     //cout << add(a, c) << endl;  模板函数无法自动强制转换类型
     cout << add<int>(a, c) << endl;  //显示指定类型
    return 0;
}
2.类模板的使用

类模板允许你创建可以处理任意数据类型的类。定义和使用类模板的步骤如下:

定义:

template <typename T>
class Box {
private:
    T value;
public:
    Box(T val) : value(val) {}
    T getValue() const { return value; }
    void setValue(T val) { value = val; }
};

在这个例子中,Box类是一个模板类,它有一个私有成员value,其类型由类型参数T决定。

实例化与使用:

Box<int> intBox(10);    // 创建一个Box实例,T被实例化为int
std::cout << intBox.getValue() << std::endl;

Box<std::string> stringBox("Hello"); // T被实例化为std::string
std::cout << stringBox.getValue() << std::endl;
3.模板特化

有时你可能需要为特定类型提供一个不同的实现,这就是模板特化。例如,为整数类型优化add函数:

template <>
int add<int>(int a, int b) {
    return a + b; // 假设这里有一个特别的优化实现
}
4.模板参数推导

在大多数情况下,编译器能够自动推导出模板参数的类型,因此你不需要显式指定类型。但在某些复杂情况或需要明确指定类型以避免歧义时,可以使用尖括号语法来显式指定模板参数。

5.普通函数和函数模板的调用规则

在C++中,普通函数和函数模板的调用遵循一定的规则,这些规则决定了编译器如何解析和选择合适的函数版本来执行。以下是几个关键点:

1. 非模板函数优先

当一个普通函数和一个函数模板都可以作为候选函数时,非模板函数具有更高的优先级。如果一个非模板函数和一个函数模板都能匹配给定的调用,编译器会优先选择非模板函数,这一规则称为“非模板函数优先”。

//
// Created by 86189 on 2024/6/24.
//
#include <iostream>

using namespace std;

void printSay(){
    cout << "普通函数调用" << endl;
}

template<typename T>
void printSay(){
    cout << "函数模板调用" << endl;
}

int main()
{
    printSay();  //模板函数优先
    
    printSay<int>();  //通过空参函数列表调用模板函数
    
    return 0;
}
2. 最佳匹配原则

对于函数模板,编译器会尝试根据传入的实际参数类型来实例化模板,寻找最佳匹配。这意味着编译器会选择那个使得转换成本最小,最特化的模板实例。如果存在多个同样特化的模板实例,且转换成本相同,则会导致编译错误(重载模糊不清)。

#include <iostream>

// 通用模板函数
template <typename T>
void display(T value) {
    std::cout << "Generic template with type: " << typeid(T).name() << ", value: " << value << std::endl;
}

// 对int类型的特化模板
template <>
void display<int>(int value) {
    std::cout << "Specialized template for int, value: " << value << std::endl;
}

// 非模板函数,接受double类型,但限制为非负数,以与int特化有所区别
void display(double value) {
    if (value >= 0) {
        std::cout << "Non-template function for non-negative double, value: " << value << std::endl;
    } else {
        std::cout << "Invalid call for negative double value" << std::endl;
    }
}

int main() {
    display(5);      // 调用特化模板 display<int>
    display(3.14);   // 调用非模板函数 display(double),因为传入的是正浮点数
    display(-3.14);  // 也会调用非模板函数 display(double),但输出提示无效调用
    display('A');    // 调用通用模板 display<char>
    
    // 显示指定模板参数来调用通用模板,尽管对于整数这不是必需的,但展示了显式指定的方法
    display<int>(3);  // 明确指定为int类型,调用特化模板 display<int>
    
    return 0;
}
3. 显式模板参数指定

用户可以显式地指定模板参数,强制编译器使用特定的类型实例化模板函数。这种方式可以解决重载模糊的问题,或是在自动类型推导不足以表达意图时使用。

template <typename T>
void foo(T x);

void foo(int x);

foo(5);    // 调用非模板函数 foo(int)
foo<>(5.0); // 显式指定模板参数,调用模板函数 foo<double>
4. 函数模板特化

如果存在针对特定类型的模板特化,当该特化版本和通用模板都匹配时,编译器会优先选择特化版本。

5. 重载决议

在涉及函数模板和普通函数的重载时,编译器将根据重载决议规则(包括匹配度、转换序列的长度和类型转换的类型等)来决定调用哪个函数。如果模板实例化后的函数和非模板函数都适合作为候选者,非模板函数优先;如果都是模板函数,选择最特化或转换成本最低的那个。

示例

考虑以下代码:

void print(int x) { std::cout << "int: " << x << std::endl; }

template <typename T>
void print(T x) { std::cout << "template: " << x << std::endl; }

int main() {
    print(5);    // 调用非模板函数 print(int)
    print(3.14); // 调用模板函数 print<double>
}

在这个例子中,当调用print(5)时,非模板函数print(int)由于优先级更高而被调用;而print(3.14)则匹配到模板函数,实例化为print<double>

7.注意事项
  • 类型兼容性:模板函数或类中的操作需要确保对所有可能的类型参数都是合法的。
  • 模板实例化:模板只有在被使用时才会被实例化,这意味着如果模板中有错误,可能只在实例化时被发现。
  • 编译时间增加:大量或复杂的模板使用可能会增加编译时间。

模板是C++的强大特性,正确使用它们可以显著提升代码的效率和可维护性。

8.类模板成员函数类外实现

在C++中,类模板的成员函数可以在类定义内声明,也可以在类外实现。如果选择在类外实现成员函数,需要注意保持模板参数列表的一致性。

1.类模板声明及成员函数声明

首先,定义一个类模板及其成员函数的声明:

template<typename T>
class MyClass {
public:
    MyClass(T initVal);
    void display();
private:
    T value;
};
2.类模板成员函数的类外实现

接下来,在类定义之外实现这些成员函数。注意,在实现成员函数时,需要包含完整的模板参数列表:

// 构造函数的实现
template<typename T>
MyClass<T>::MyClass(T initVal) : value(initVal) {
    // 初始化逻辑
}

// 成员函数display的实现
template<typename T>
void MyClass<T>::display() {
    cout << "Value: " << value << endl;
}
3.使用类模板

最后,可以在main函数或其他地方实例化并使用这个模板类:

int main() {
    MyClass<int> objInt(10);
    objInt.display(); // 输出 Value: 10

    MyClass<double> objDouble(3.14);
    objDouble.display(); // 输出 Value: 3.14

    return 0;
}
4.注意事项
  • 模板参数一致性:在类外实现模板成员函数时,确保函数前的模板参数列表与类模板的参数列表完全一致。
  • 实现文件分离:虽然上述示例将模板的声明和实现放在了同一文件中,但实践中,为了组织清晰,有时会将声明放在头文件(.h.hpp),而将实现放在源文件(.cpp)。不过,由于模板的特例化是在使用时完成的,编译器需要看到模板的完整定义才能生成特定类型的代码,因此通常推荐将模板的声明和实现都放在同一个头文件中。
  • 避免链接错误:遵循上述做法可以防止因模板定义不完整导致的链接错误。
9.类模板的分文件编写

将模板的声明和实现都放在同一个头文件中,命名后缀为.hpp

10.模板案例

实现数组排序:

//
// Created by 86189 on 2024/6/24.
//
#include <iostream>

using namespace std;

template<typename T>
void mySort(T arr[], int len) {
    for (int i = 0; i < len - 1; ++i) {
        for (int j = 0; j < len - 1 - i; ++j) {
            if (arr[j] > arr[j + 1]) {
                T temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

int main() {
    int arr[] = {1,2,3,4,6,5};
    mySort<int>(arr,sizeof(arr)/sizeof(arr[0]));
    for (int i : arr) {
        cout << i << " ";
    }
    cout << endl;
    char str[] = "andbhsc"; // 确保数组只包含你想要排序的内容
    int len = sizeof(str)/sizeof(char) - 1; // 减1是为了排除末尾的空字符'\0'
    mySort<char>(str, len);
    for(char j : str){
        if(j != '\0') { // 排序后的数组可能把'\0'排到了前面,这里过滤掉
            cout << j << " ";
        }
    }
    cout << endl;
    return 0;
}

3.类模板与继承和友元

1.类模板与继承

当类模板与继承结合使用时,你可以创建一个泛型的基类,然后从这个基类派生出具体的类。这意味着派生类也可以是泛型的,同时继承了基类的泛型特性。这在实现一些设计模式(如策略模式、工厂模式等)或者需要处理多种数据类型的类层次结构时非常有用。

例如:

template<typename T>
class Base {
public:
    T value;
    void setValue(T val) { value = val; }
};

// 从Base<T>派生出Derived,继承其泛型特性
template<typename T>
class Derived : public Base<T> {
public:
    void printValue() { cout << "Value: " << this->value << endl; }
};

在这个例子中,Derived类继承了Base类,并且保留了其泛型性。这意味着你可以为Derived指定具体类型,比如Derived<int>Derived<string>,同时利用和扩展基类的泛型功能。

2.类模板与友元

类模板与友元功能结合使用时,可以让一个函数或另一个类成为模板类的友元,从而能够访问该模板类的私有或保护成员。这对于需要外部函数或类来协助模板类完成某些操作,而又不希望公开所有内部细节的情况非常有用。下面是一个简单的示例来说明这一点:

1.类模板与非模板友元函数
template<typename T>
class MyClass {
private:
    T data;

public:
    MyClass(T d) : data(d) {}

    // 声明一个非模板的友元函数
    friend void displayMyClass(const MyClass<T>& obj);
};

// 定义友元函数
template<typename T>
void displayMyClass(const MyClass<T>& obj) {
    cout << "Data: " << obj.data << endl;
}

int main() {
    MyClass<int> obj(10);
    displayMyClass(obj); // 输出 Data: 10
    return 0;
}

在这个例子中,displayMyClass是一个非模板的友元函数,但它能够访问模板类MyClass<T>的私有成员data

2.类模板与模板友元类

如果需要一个模板类成为另一个模板类的友元,那么友元声明也需要模板化:

template<typename T>
class MyClass {
private:
    T data;

public:
    MyClass(T d) : data(d) {}

    // 声明一个模板友元类
    template<typename U>
    friend class FriendClass;
};

// 友元类定义
template<typename U>
class FriendClass {
public:
    void displayData(const MyClass<U>& obj) {
        cout << "FriendClass accessing Data: " << obj.data << endl;
    }
};

int main() {
    MyClass<int> obj(20);
    FriendClass<int> friendObj;
    friendObj.displayData(obj); // 输出 FriendClass accessing Data: 20
    return 0;
}

这里,FriendClass是一个模板类,并且被声明为MyClass<T>的模板友元。因此,它可以访问所有MyClass<T>实例的私有成员。

总结来说,通过将函数或类声明为模板类的友元,可以在保持封装性的同时,允许外部代码以一种受控的方式访问模板类的内部数据。这种机制在设计复杂系统时特别有用,特别是当模板类需要与其他组件紧密协作时。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

FightingLod

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值