C++模版初阶

发布于:2025-06-10 ⋅ 阅读:(23) ⋅ 点赞:(0)

(一)C++模版的用法

1、回顾C++ 中的函数重载

// 编译器通过形参形式的不同,实现函数重载
void swap(int& x,int& y)
{
	//....
}

void swap(double& x,double& y)
{
	//....
}

void swap(char& x,char& y)
{
	//....
}

int main()
{
	int x1 = 3, y1 = 4;
	double x2 = 3.00, y2 = 4.00;
	char x3 = '3', y3 = '4';

	swap(x1, y1);
	swap(x2, y2);
	swap(x3, y3);
	// 虽然实现了函数重载
	// 但是还是要一一写出函数的原型,才能实现重载
	// 而模版避免了这种情况的产生
    // 使用函数重载虽然可以实现,但是有一下几个不好的地方:
	// 1. 重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应的函数
  // 2. 代码的可维护性比较低,一个出错可能所有的重载均出错
	// 那能否告诉编译器一个模子,让编译器根据不同的类型利用该模子来生成代码呢?
}

2、解决函数模版的痛点–引入函数模版

1、什么是函数模版

函数模版
函数模版代表了一个函数家族,该函数模版与类型无关,在使用时被实例化,根据实参类型产生函数的特定类型版本
函数模版格式
template <typename t1,typename t2,…typename tn>
返回值类型 函数名(参数列表){}

函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。
所以其实模板就是将本来应该我们做的重复的事情交给了编译器
在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将t确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型也是如此。

// 注意:typename是用来定义模板参数关键字,也可以使用class(切记:不能使用struct代替class)
// 针对任意类型的对象交换
// template<class T>
// 换句话泛型编程
template<typename T>
void swap(T& left, T& right)
{
	T temp = left;
	left = right;
	right = temp;
}
// 这里我们定义了一个函数模版swap
// 可以看出函数的返回值为void
// 函数有2个形参,一个为T& left,一个为T& right
// 而T是形参的类型,由template<typename T>中的T定义
// 在实例化后,编译器会将T转换为具体的内置类型或者自定义类

int main()
{
	int x1 = 3, y1 = 4;
	double x2 = 3.00, y2 = 4.00;
	char x3 = '3', y3 = '4';

	swap(x1, y1); // 自动根据模版载入2个int型形参,生成形参为int的swap函数
	swap(x2, y2); // 自动根据模版载入2个double型形参,生成形参为double的swap函数
	swap(x3, y3); // 自动根据模版载入2个char型形参,生成形参为char的swap函数
}
// 在编译器编译后会根据具体的实参,生成同实参类型一直的形参swap函数

2、显式查看函数模版生成的3个函数

在linux环境下使用
g++ -fdump-tree-gimple template.cpp
GIMPLE 是 GCC 的高级中间表示,更易阅读。
结果:生成template.cpp.004t.gimple 文件,其中包含3个实例化后的函数:

int main() ()
{
    int D.20852;

    {
        int x1;
        int y1;
        double x2;
        double y2;
        char x3;
        char y3;

        try
        {
            x1 = 3;
            y1 = 4;
            x2 = 3.0e+0;
            y2 = 4.0e+0;
            x3 = 51;
            y3 = 52;
            swap<int>(&x1, &y1);
            swap<double>(&x2, &y2);
            swap<char>(&x3, &y3);
        }
        finally
        {
            x1 = { CLOBBER };
            y1 = { CLOBBER };
            x2 = { CLOBBER };
            y2 = { CLOBBER };
            x3 = { CLOBBER };
            y3 = { CLOBBER };
        }
    }
    D.20852 = 0;
    return D.20852;
}


void swap(T&, T&) [with T = int] (int & left, int & right)
{
      int D.20854;
      int temp;

      temp = *left;
      D.20854 = *right;
      *left = D.20855;
      *right = temp;
}


void swap(T&, T&) [with T = double] (double & left, double & right)
{
      double D.20855;
      double temp;

      temp = *left;
      D.20855 = *right;
      *left = D.20855;
      *right = temp;
}


void swap(T&, T&) [with T = char] (char & left, char & right)
{
      char D.20856;
      char temp;

      temp = *left;
      D.20856 = *right;
      *left = D.20856;
      *right = temp;
}

(二)函数模版基础

template <typename T> // 定义一个加法模版
T Add(const T& left, const T& right)
{
	return left + right;
}

// 用户手写一份专门处理int的加法函数
int Add(int left, int right) // 有成品,优先使用成品,后使用模版
{
	return left + right + 10;
}
 
/*
template <typename T,typename T2> // 多参数的,注意模版参数定义的是类型
void func(const T& left, const T2& right)
{
	return left + right;
}*/

int main()
{

	int a1 = 10, a2 = 20;
	double d1 = 10.1, d2 = 20.2;

	// . 隐式实例化:让编译器根据实参推演模板参数的实际类型
	std::cout << Add(a1, a2) << std::endl; 
	// 因为2个变量都是int类型的
	// 所以在没有屏蔽掉我们手写专门处理int型Add函数时,使用专门Add函数

	std::cout << Add(d1, d2) << std::endl;
	// 这里使用函数模版生成的处理double类型的Add函数
	
	std::cout << Add<int>(d1, d2) << std::endl; 
	// 指定int类型的时候,使用模版去实例化,不再使用手写代码 40
	
	std::cout << Add(1.0, 2) << std::endl; // 使用模版
	// 模板参数 T 无法同时匹配 double 和 int,导致实参推导冲突,编译器报错。

	// std::cout << Add(a1, d2) << std::endl;
	// 该语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型
	// 通过实参a1将T推演为int,通过实参d1将T推演为double类型,但模板参数列表中只有
	//	一个T,编译器无法确定此处到底该将T确定为int 或者 double类型而报错
	// 注意:在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要背黑锅

	// 此时有两种处理方式:1. 隐式实例化 2. 使用显式实例化
	
	// 隐式实例化
	// 强制类型转换,使用实参类型推演模版参数
	std::cout << Add(a1, (int)d2) << std::endl; // 10 20 = 30
	std::cout << Add((double)a1, d2) << std::endl; // 10.0 20.2 = 30.2


	// 使用显示实例化,指定T的类型
	// 即指定模版参数
	std::cout << Add<int>(a1, d2) << std::endl; 
	// 显式把double类型的d2转换到 int类型
	std::cout << Add<double>(a1, a2) << std::endl;
	// 显式把int类型的a1和a2都转换为double类型

	return 0;
}

(三)类模版基础

1、为什么需要类模版


#include <iostream>

using namespace std;

typedef int DateType; // 这里定义了数据类型为int类型
namespace bit
{
	class stack
	{
	private:
		DateType* _a;
		int _top;
		int _capacity;
	};
// 因为我们定义了成员变量 _a的类型为 int*
// 所以我们在这个栈中就无法存放其他类型的变量了
}

int main()
{
	bit::stack st1; // 想让st1这个栈存int
	bit::stack st2; // 想让st2这个栈存double
	// 这样在C语言中弄不了的
	// 因为数据类型由DateType* _a; 决定
	// 而栈类型DateType由typedef int DateType;数据类型重命名而来

}

2、如何使用类模版解决上面的问题?

使用类模版

namespace bit
{
	// 类模版
	template<typename T> // 使用T替代类的类型
	class stack
	{
	public:
		stack(int n = 4)
		{
			_a = new T[n];
			_top = 0;
			_capacity = 0;
			cout << "stack(int n = 4)" << endl;

		}

		~stack()
		{
			delete[] _a;
			_a = nullptr;
			_capacity = 0;
			cout << "~stack()" << endl;
		}

		void push(const T& x)
		{
			//...
			_a[_top] = x;
			_capacity++;
		}

	private:
		T* _a;
		int _top;
		int _capacity;
	};
}
int main()
{
	try
	{
		// 类模版必须进行显示实例化
		bit::stack<int> st1;  
		// 编译器先显式实例化一个存储int类型的类
		// 创建一个存储int类型的栈
		bit::stack<double> st2; 
		// 编译器先显式实例化一个存储double类型的类
		// 创建一个存储double类型的栈

		st1.push(1);
		st1.push(2);
		st1.push(3);
		st1.push(4);

		st2.push(1.0);
		st2.push(2.0);
		st2.push(3.0);
		st2.push(4.0);

	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
}

这样就避免了需要写多个类去应对不同的数据类型的情况了