【C++】模板入门教程

C++模板是C++编程中的重要利器,能够大大提高编码效率。函数模板和类模板是模板编程中的两个重要概念,它们允许我们编写独立于数据类型的通用代码

本文将深入介绍函数模板和类模板的使用方法,以及它们的实现原理、匹配规则和注意事项,帮助读者更好地掌握C++模板知识,从而在实际编程中提高效率。

目录

  • 1. 概述
    • 1.1 泛型编程
    • 1.2 模板的引入
  • 2. 函数模板
    • 2.1 定义和使用方法
    • 2.2 原理
    • 2.3 实例化
      • 2.3.1 隐式实例化
      • 2.3.2 显式实例化
    • 2.4 匹配规则
  • 3. 类模板
    • 3.1 定义类模板
    • 3.2 类模板的实例化
  • 4. 结论

1. 概述

1.1 泛型编程

泛型编程的核心思想是编写独立于特定数据类型的代码。这意味着程序员可以定义一套算法或数据结构,而不必为每种可能的数据类型编写特定的代码实现。这种思想的实现极大地提高了代码的复用性和程序的可维护性

想象一下,如果没有泛型编程,对于简单的数据结构如列表、栈或队列,我们可能需要为整型、浮点型、字符串等每一种数据类型编写特定的实现。这不仅会导致大量重复的代码,也会使得维护和更新变得异常困难。

泛型编程的一个经典例子是排序算法。不论是整数数组还是浮点数数组,甚至是自定义类型的数组,排序算法的逻辑结构是相同的。通过泛型编程,我们可以编写一个通用的排序函数,它可以对任何类型的数组进行排序,只要这些类型支持比较操作。

1.2 模板的引入

考虑一下,如果让你编写一个函数,用于两个数的交换。在C语言中,我们会用如下方法:

// 交换两个整型
void Swapi(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
// 交换两个双精度浮点型
void Swapd(double* p1, double* p2)
{
	double tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

上面的代码为每种可能的数据类型编写特定的代码实现,这显然显得代码非常冗余。

不过C++通过引入模板机制,使得泛型编程成为可能。

template <typename T>
void swap(T& a, T& b) {
    T temp = a;
    a = b;
    b = temp;
}

这个swap函数模板可以用于任何数据类型,只要这些类型支持赋值操作。

2. 函数模板

2.1 定义和使用方法

定义一个函数模板的基本语法如下:

//template 
template <class T>     //两种等价
T functionName(T parameter1, T parameter2) {
    // 函数体
}

其中,typename T或者class T是模板参数声明,表示这是一个模板函数,T是一个占位符,代表任何数据类型。

此外,模板中可以存在多个参数,通过, 号分隔

//多参数模板
//这里实现的是val2强制类型转换为val1,并取得和
template <class T1, class T2>
T1 getTrunVal(const T1& val1, const T2& val2)
{
	const T1 tmp = (const T1)val2;
	return val1 + tmp;
}

2.2 原理

编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型也是如此。
【C++】模板入门教程_第1张图片
图示:

[ swap<T>(T&, T&)模板 ]
         |
         | 实例化
         V
[ swap<int>(int&, int&) ]  [ swap<double>(double&, double&) ]

下面又一个例子:

template <typename T>
void printRef(const T& value) {
    std::cout << value << std::endl;
}

int main() {
    int a = 5;
    printRef(a); // T被推导为int

    const double b = 3.14;
    printRef(b); // T被推导为double

    std::string c = "Hello";
    printRef(c); // T被推导为std::string

    return 0;
}

2.3 实例化

函数模板的实例化可以是隐式的,也可以是显式的。当函数模板被调用时,编译器根据提供的参数自动确定模板参数的具体类型。

2.3.1 隐式实例化

考虑以下函数模板add,它的目的是返回两个参数的和

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

当我们使用不同类型的参数调用add函数时,编译器会为每种参数类型生成一个专门的add函数
例如:

int main() {
    // 使用int类型参数调用add,编译器生成add的实例
    int sumInt = add(1, 2);
    std::cout << "Sum of ints: " << sumInt << std::endl;

    // 使用double类型参数调用add,编译器生成add的实例
    double sumDouble = add(1.1, 2.2);
    std::cout << "Sum of doubles: " << sumDouble << std::endl;

    // 使用string类型参数调用add,编译器生成add的实例
    std::string sumString = add(std::string("Hello, "), std::string("World!"));
    std::cout << "Concatenated strings: " << sumString << std::endl;

    return 0;
}

在上面的例子中,我们没有显式地指定模板参数类型;编译器根据函数调用中提供的参数类型自动推导出了模板参数的类型
这种方式极大地简化了模板的使用,使得我们可以像使用普通函数一样使用模板函数,而不必担心类型的具体细节。

2.3.2 显式实例化

尽管隐式实例化对于大多数情况已经足够使用,但在某些情况下,我们可能需要显式地指定模板参数的类型。这可以通过在函数名后添加模板参数列表来实现
如:

add<double>(1, 2)

显式实例化的一个常见用途是当函数参数的自动类型推导可能不符合我们预期时。例如,当我们想要将两个整数相加,但希望结果是double类型时,就可以使用显式实例化

int main() {
    // 显式指定模板参数为double,即使所有参数都是int类型
    double result = add<double>(3, 4);
    std::cout << "Result (double): " << result << std::endl;

    return 0;
}

在这个例子中,尽管3和4都是整数,我们通过显式实例化指定了模板参数T为double,因此add函数会将两个整数的和转换为double类型返回。

2.4 匹配规则

当多个函数模板或模板与非模板函数重载时,编译器必须选择最合适的函数进行调用。

一、一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数

#include 
using namespace std;
//专门用于int类型加法的非模板函数
int Add(const int& x, const int& y)
{
	return x + y;
}
//通用类型加法的函数模板
template<typename T>
T Add(const T& x, const T& y)
{
	return x + y;
}
int main()
{
	int a = 10, b = 20;
	int c = Add(a, b); //调用非模板函数,编译器不需要实例化
	int d = Add<int>(a, b); //调用编译器实例化的Add函数
	return 0;
}

二、对于非模板函数和同名的函数模板,如果其他条件都相同,在调用时会优先调用非模板函数,而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数,那么选择模板

#include 
using namespace std;
//专门用于int类型加法的非模板函数
int Add(const int& x, const int& y)
{
	return x + y;
}
//通用类型加法的函数模板
template<typename T1, typename T2>
T1 Add(const T1& x, const T2& y)
{
	return x + y;
}
int main()
{
	int a = Add(10, 20); //与非模板函数完全匹配,不需要函数模板实例化
	int b = Add(2.2, 2); //函数模板可以生成更加匹配的版本,编译器会根据实参生成更加匹配的Add函数
	return 0;
}

三、模板函数不允许自动类型转换,但普通函数可以进行自动类型转换

#include 
using namespace std;
template<typename T>
T Add(const T& x, const T& y)
{
	return x + y;
}
int main()
{
	int a = Add(2, 2.2); //模板函数不允许自动类型转换,不能通过编译
	return 0;
}

3. 类模板

3.1 定义类模板

定义一个类模板的基本语法如下:

template <typename T>
class ClassName {
    // 类定义
};

其中,typename T或者class T是模板参数声明,T是一个占位符,代表任何数据类型。

示例:一个简单的Array类模板

考虑以下Array类模板,它实现了一个动态数组,可以存储任意类型的元素:

template <typename T>
class Array {
private:
    T* data;
    size_t size;
public:
    Array(size_t size): size(size), data(new T[size])
     {}
      
    ~Array() 
    {
     	delete[] data; 
    }

    T& operator[](size_t index) 
    { 
    	return data[index]; 
    }
    const T& operator[](size_t index) const 
    { 
    	return data[index];
    }
    size_t getSize() const 
    { 
    	return size; 
    }
    
};

3.2 类模板的实例化

类模板的实例化是在编译时根据指定的数据类型生成具体类的过程。与函数模板不同,类模板的实例化必须是显式的。

示例:实例化Array类模板

Array<int> intArray(10); // 创建一个可以存储10个int的数组
Array<std::string> stringArray(5); // 创建一个可以存储5个std::string的数组

实例化总结为下:

[ Array<T>类模板 ]
         |
         | 实例化
         V
[ Array<int> ]  [ Array<std::string> ] 

4. 结论

C++模板是一种强大的特性,它为编写高效、灵活且类型安全的代码提供了极大的便利。通过掌握模板的基础和高级特性,程序员可以提高编程效率,同时增强代码的复用性和可维护性。

你可能感兴趣的:(c++,c++,数据结构)