C++——模版(二)

news/2025/2/24 9:28:30

前言

我们前面讲过模版的一,不知道大家还有没有所印象,如果大家不太能回忆起来可以再去前面看一下,那通过我们讲解了几个容器之后,相信大家现在应该已经对模版很熟悉了,那模版还剩下一些其他的内容我们就在这里进行讲解了,那我们就正式开始本篇文章啦


一、class和typenama的区别

在之前我们讲的时候,我们说class和typename定义模版参数的关键字,用哪个都可以,没有区别

template<class T>
void swap1(T& x, T& y)
{
	T tmp = x;
	x = y;
	y = tmp;
}

template<typename T>
void swap2(T& x, T& y)
{
	T tmp = x;
	x = y;
	y = tmp;
}

对于上面这种情况来说确实没有任何的区别,但是在一个地方他们是有区别的,下面看一个例子

void Print(const vector<int>& v)
{
	vector<int>::const_iterator it = v.begin();
	while (it != v.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
}

int main()
{
	vector<int> v = { 1, 2, 3, 4, 5 };
	Print(v);
	return 0;
}

 当我们这样写去调用Print函数要去打印这个vector的时候是没有任何问题的,注意一下这里要用const迭代器,因为这个形参v就是const类型的,但是这样写又只能去打印vector的数据,那如果想做到任意类型的容器都能打印就得用模版,那我们来改一下

template<class Container>
void Print(const Container& c)
{
	Container::const_iterator cit = c.begin();
	while (cit != c.end())
	{
		cout << *cit << " ";
		++cit;
	}
	cout << endl;
}

int main()
{
	vector<int> v = { 1, 2, 3, 4, 5 };
	Print(v);
	return 0;
}

那现在Container会被实例化成一个容器,去它里面取它的const迭代器,然后去遍历,看着好像没什么问题呀,但是这段代码会编译不通过

 编译器报错说const_iterator前必须以typename做前缀,这是什么意思呢?实际上这是因为编译器分不清这里的Container::const_iterator了,因为这里分两种情况,第一种是类型,第二种是对象,如果是类型这样写没问题,如果是对象就是错的,像上面的第一种写法,明确告诉编译器了这是vector<int>,这里已经实例化了,就直接去取const_iterator就可以,但是除了内嵌类型以外,静态成员变量也可以去这样取,编译器在这里就是不知道是哪种情况了,我们得告诉它一下,所以我们需要加上一个typename告诉编译器这是一个类型,等到模版实例化了再去找,然后去取里面的const_iterator

template<class Container>
void Print(const Container& c)
{
    // 告诉编译器这是一个类型
	typename Container::const_iterator cit = c.begin();
	while (cit != c.end())
	{
		cout << *cit << " ";
		++cit;
	}
	cout << endl;
}

int main()
{
	vector<int> v = { 1, 2, 3, 4, 5 };
	Print(v);
	return 0;
}

如果是静态成员变量就这样写

class A
{
public:
	int begin()
	{
		return 0;
	}

	static int const_iterator;
};

int A::const_iterator = 1;

int main()
{
	A aa;
	A::const_iterator cit = aa.begin();

	return 0;
}

编译器分不清的就是这两种,所以我们才要去告诉编译器,那总结起来就是:类模版中取内嵌类型要加上关键字typename

上篇文章我们讲到的优先级队列这里就用到了


二、非类型模版参数

我们先来看一个静态栈

#define N 10

// 静态栈
template<class T>
class Stack
{
private:
	T _a[N];
	int _top;
};
int main()
{
	//做不到
	Stack<int> st1; // 10
	Stack<int> st2; // 100

	return 0;
}

 这就是一个静态栈,栈里面最多存N个数据,那现在是无法做到一个栈存10个,一个栈存100个的,就像typedef一样,只能保证所有的栈中存的都是一个类型的数据,如果要存其他类型那就得去改typedef,要保证同时存不同的类型那就要定义多个栈,不满足复用的思想,对此,引入了模版,那模版参数除了可以是类型,还可以是非类型的

template<class T, size_t N = 10>
class Stack
{
private:
	T _a[N];
	int _top;
};

int main()
{
	//可以做到
	Stack<int> st1; // 10
	Stack<int, 100> st2; // 100

	return 0;
}

那我们把元素个数也写成模版的,这就是非类型模版参数,可以给上一个缺省值,没传就开小一点,如果知道自己要用多少就直接开好空间,这样是不是就比以前好多了,所以我们说模版的本质就是去复用

需要注意的是:1、非类型模版参数必须是整形

                         2、非类型模版参数是常量,不能修改

// 浮点数不可以
//template<class T, double N>
template<class T, size_t N = 10>
class Stack
{
public:
	void func()
	{
        // 常量,不能修改
		N++;
	}
private:
	T _a[N];
	int _top;
};

array

非类型模版参数的一个典型应用就是array,array是C++11新增加的一个容器,是一个定长数组

那我们来试一下它的使用,它支持迭代器,所以可以用范围for,存储5个数据,打印出来看一下

 可以看到的是,array并不会初始化,那它有什么用呢?实际上它就是没有多大的用处,array是一个非常鸡肋的设计,那我们为什么不可以直接定义一个数组?为什么不能用vector?vector还可以用n个val初始化,要是真的说跟普通数组有什么区别的话,那就是越界的检查,普通数组对于越界读是检查不出来的,对于越界写是一种抽查,可能能检查出来,也可能检查不出来,我们实现了string和vector应该就知道了,[]在实现前会去断言一下有效性,要小于size,这也就是array越界读写都报错的机制,array在实践中是几乎没有什么用处的


三、模版的特化

通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些
错误的结果,需要特殊处理,就像上一篇文章的日期类对象的指针,那现在比较的就是指针,而不是对象的大小
template<class T>
bool Less(const T& left, const T& right)
{
	return left < right;
}

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

	cout << Less(&a, &b) << endl;

	return 0;
}

Less内部并没有比较内容,而比较的是指针,这就无法达到预期而错误,这样的情况就是需要特殊处理的情况,此时,就需要对模板进行特化。即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式,模板特化中分为函数模板特化与类模板特化


函数模版的特化

先来看一下函数模版的特化的步骤

函数模板的特化步骤:
1. 必须要先有一个基础的函数模板
2. 关键字template后面接一对空的尖括号<>
3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型
4. 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误
template<class T>
bool Less(const T& left, const T& right)
{
	return left < right;
}

// 特化
template<>
bool Less<int*>(int* left, int* right)
{
	return *left < *right;
}

除了这样写还可以这样,直接不写成模版的,写成一个普通的函数

bool Less(int* left, int* right)
{
	return *left < *right;
}

这样写是不叫特化的,只是一种处理特殊情况的方式,函数模版和非函数模版是可以同名的,而且模版的匹配规则是在类型匹配的前提下有现成的就直接用,那刚才的Less(&a, &b)就会走这个普通的函数,推荐这样去写,不要像第一种那样写,这种实现简单明了,代码的可读性高,容易书写,因为对于一些参数类型复杂的函数模板,特化时特别给出,因此函数模板不建议特化。

除了这些原因以外,还有一个原因就是容易出错,我们再变一下,那现在是传值传参,我们把引用加上减少拷贝,加上引用又不改变最好加const

template<class T>
bool Less(const T& left, const T& right)
{
	return left < right;
}

// 特化 编译报错
template<>
bool Less<int*>(int* left, int* right)
{
	return *left < *right;
}

现在会编译报错,编译器说下面的Less不是特化, 那这是为什么呢?我们在上面的函数模版的步骤4中可以看到,要求特化版本要和基础版本的参数类型完全相同,现在就是类型不匹配了,不满足步骤4,这里的T就是int*,那就把参数对照着写

template<class T>
bool Less(const T& left, const T& right)
{
	return left < right;
}

// 特化 编译报错
template<>
bool Less<int*>(const int*& left, const int*& right)
{
	return *left < *right;
}

依然编译报错,模版的报错信息会很长,而且不太准,这里的错出在这个const,在基础版本中,const修饰的是left,left不能修改,而现在的特化版本中const修饰的是*left,那就变成了left指向的内容不能修改但是left本身可以修改,所以依然是类型不匹配,那我们就把const改成只修饰left,放在*的右边

template<class T>
bool Less(const T& left, const T& right)
{
	return left < right;
}

template<>
bool Less<int*>(int* const& left, int* const& right)
{
	return *left < *right;
}

这样写就可以了,所以函数模版的特化是可能出现幺蛾子的,我们就直接写普通的函数就可以了

如果要把所有指针类型的都要特殊处理也可以这么写,也就是最后一种

template<class T>
bool Less(const T& left, const T& right)
{
	return left < right;
}

template<>
bool Less<int*>(int* const& left, int* const& right)
{
	return *left < *right;
}

bool Less(int* left, int* right)
{
	return *left < *right;
}

template<class T>
bool Less(T* left, T* right)
{
	return *left < *right;
}

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

	cout << Less(&a, &b) << endl;

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

	return 0;
}

类模版的特化

类模版的特化又分为两种,一个是全特化,一个是偏特化

全特化就是将模板参数列表中所有的参数都确定化

template<class T1, class T2>
class Data
{
public:
	Data() { cout << "Data<T1, T2>" << endl; }
};

// 全特化
template<>
class Data<int, double>
{
public:
	Data() { cout << "Data<int,double>" << endl; }
};

int main()
{
	Data<int, int> d1;  // Date<T1, T2>
	Data<int, double> d2;  // Date<int, double>

	return 0;
}

偏特化就是针对模版参数进一步进行条件限制设计的特化版本

偏特化又分为两种,第一种是 将模板参数类表中的一部分参数特化
template<class T1, class T2>
class Data
{
public:
	Data() { cout << "Data<T1, T2>" << endl; }
};

// 偏特化:特化部分参数
template<class T1>
class Data<T1, double>
{
public:
	Data() { cout << "Data<T1, double>" << endl; }
};

int main()
{
	Data<int, int> d1;  // Date<T1, T2>
	Data<int*, double> d2;  // Date<T1, double>

	return 0;
}

第二种就针对模板参数更进一步的条件限制所设计出来的一个特化版本

template<class T1, class T2>
class Data
{
public:
	Data() { cout << "Data<T1, T2>" << endl; }
};

// 偏特化:可能是对某些类型的进一步限制
template<class T1, class T2>
class Data<T1*, T2*>
{
public:
	Data() { cout << "Data<T1*, T2*>" << endl; }
};

template<class T1, class T2>
class Data<T1&, T2&>
{
public:
	Data() { cout << "Data<T1&, T2&>" << endl; }
};

int main()
{
	Data<int, int> d1;  // Date<T1, T2>
	Data<double*, double*> d2; // Date<T1*, T2*>
	Data<int&, double&> d3; // Data<T1&, T2&>

	return 0;
}

那针对于我们之前的优先级队列插入一个日期类的指针时,因为默认的仿函数比较的是指针的问题,需要在外部自己写一个传进来,但现在有了特化,所以我们可以这么写

template<class T>
struct Less
{
	bool operator()(const T& x, const T& y)
	{
		return  x < y;
	}
};

template<>
struct Less<Date*>
{
	bool operator()(Date* const& x, Date* const& y) const
	{
		return *x < *y;
	}
};

现在就不需要自己传了,在调用的时候就会走特化版本,再去调日期类的<,我们的代码就灵活了很多


三、模版的声明与定义分离

我们之前说模版是不能声明与定义分离的,会报错,那我们就看看它报错的原因是什么,会什么会报错

// func.h
template<class T>
struct A
{
	T Add(const T& x, const T& y);
};

struct B
{
	int Sub(const int& x, const int& y);
};


// func.cpp
template<class T>
T A<T>::Add(const T& x, const T& y)
{
	return x + y;
}

int B::Sub(const int& x, const int& y)
{
	return x - y;
}


// test.cpp
int main()
{
	int a = 1, b = 2;
	A<int> aa;
	aa.Add(a, b);

	B bb;
	bb.Sub(a, b);

	return 0;
}

C/C++程序要运行前需要经历预处理->编译->汇编->链接,编译就是进行词法分析,语法分析,语义分析,看有没有语法错误,如果没有错误就生成汇编代码,多个源文件是分开编译的,首先Add和Sub函数都能通过编译,这是因为编译只看声明,声明是一种承诺,等到链接的时候再去其他文件的符号表查找,但是奇怪的是,Add和Sub在func.cpp中都定义了,但是只能查找到Sub,不能查找到Add,这里的原因是,Sub因为定义了,所以在符号表中可以查找到,虽然Add也定义了,但是Add并没有实例化,也就没有生成对应的代码,那自然就没有进符号表,所以找不到,报的是链接错误


解决方法有两种,第一种是显示在定义的文件中,显示实例化

template<class T>
T A<T>::Add(const T& x, const T& y)
{
	return x + y;
}

int B::Sub(const int& x, const int& y)
{
	return x - y;
}

template
class A<int>;

在func.cpp文件中这样写,告诉编译器实例化成int,那就有了具体的函数,链接时就可以找到,但是这种方法不实用,一般不用


第二种方法就是同一个文件内声明与定义分离

template<class T>
struct A
{
	T Add(const T& x, const T& y);
};

struct B
{
	int Sub(const int& x, const int& y);
};

template<class T>
T A<T>::Add(const T& x, const T& y)
{
	return x + y;
}

在.h文件里定义Add,这里要注意Sub函数不能在.h里实现,因为func.cpp和test.cpp都包含了func.h,那就会在两个.cpp文件中展开,就冲突了,所以Sub函数要不就直接在类内实现了,要不就放在func.cpp里实现


总结

模版会让我们的灵活性变得更强,包括之前的适配器,仿函数也可以看出来,但是模版的缺点就是出错了会非常难看,非常长,也很凌乱,但是,模版的优点还是远大于它的缺点的,如果没有模版那就要多写很多代码,那维护起来也就更加困难,本篇文章到这里就结束了,如果觉得小编写的不错,大家可以给一个三连,感谢大家支持!!!


http://www.niftyadmin.cn/n/5864179.html

相关文章

vllm部署LLM(qwen2.5,llama,deepseek)

目录 环境 qwen2.5-1.5b-instruct 模型下载 vllm 安装 验证安装 vllm 启动 查看当前模型列表 OpenAI Completions API&#xff08;文本生成&#xff09; OpenAI Chat Completions API&#xff08;chat 对话&#xff09; vllm 进程查看&#xff0c;kill llama3 deep…

2025年- G15-Lc89-383.赎金记录-java版

1.题目描述 给定两个字符串 ransomNote 和 magazine&#xff0c;如果 ransomNote 可以通过使用 magazine 中的字母构造出来&#xff0c;返回 true&#xff0c;否则返回 false。 magazine 中的每个字母只能使用一次。 示例 1&#xff1a; 输入&#xff1a;ransomNote “a”, …

Element UI日期选择器默认显示1970年解决方案

目录 问题背景 问题根源 1. 数据绑定类型错误 2. 初始化逻辑错误 解决方案 核心思路 步骤 1&#xff1a;正确初始化日期对象 步骤 2&#xff1a;处理数据交互 步骤 3&#xff1a;处理年份切换事件 完整代码示例 注意事项 1. 时区问题 2. 格式化绑定值 常见问题 1. 为什…

C++之旅-C++11的深度剖析(1)

目录 前言/背景 1.C11的发展历史 2.列表初始化 2.1 C98传统的{} 2.2 C11中的{} 2.3 C11中的std::initializer_list 3.右值引用 3.1 左值和右值 3.2 左值引用和右值引用 3.3 引用延长生命周期 3.4 左值和右值的参数匹配 结束语 前言/背景 随着现代软件开发的快速发展…

改进A*算法并用于城市无人机路径规划

独家原创&#xff01;改进A*算法进行城市无人机路径规划&#xff0c;考虑碰撞&#xff0c;飞行高度等优化启发式搜索。所有指标超过A*和A算法&#xff01;附有完整的文档说明 算法设计、毕业设计、期刊专利&#xff01;感兴趣可以联系我。 &#x1f3c6;代码获取方式1&#xff…

UE_C++ —— Logging in Unreal

目录 一&#xff0c;UE_LOG Log Verbosity Console Commands Logging Fundamental Data Types Define Your Own Log Category 二&#xff0c;UE_LOGFMT On-screen debug messages 日志是一种非常实用的调试工具&#xff0c;可以详细说明代码当前的执行逻辑&#xff1b;可…

美颜相机1.0

项目开发步骤 1 界面开发 美颜相机界面构成&#xff1a; 标题 尺寸 关闭方式 位置 可视化 2 创建主函数调用界面方法 3 添加两个面板 一个是按钮面板一个是图片面板 用JPanel 4 添加按钮到按钮面吧【注意&#xff1a;此时要用初始化按钮面板的方法initBtnPanel 并且将按钮添…

【嵌入式Linux应用开发基础】多线程编程

目录 一、基本概念 二、相关 API 2.1. 线程创建 2.2. 线程等待 2.3. 线程退出 2.4. 互斥锁 2.5. 条件变量 2.6. 使用示例 三、线程的属性设置 四、多线程编程中的问题和同步 五、多线程编程的实践 六、参考资料 在嵌入式 Linux 应用开发中&#xff0c;多线…