文章目录
前言
在我们学习C++时,常会用到函数重载。而函数重载,通常会需要我们编写较为重复的代码,这就显得臃肿,且效率低下。重载的函数仅仅只是类型不同,代码的复用率比较低,只要有新类型出现时,就需要增加对应的函数。此外,代码的可维护性比较低,一个出错可能会导致所有的重载均出错。
那么,模板的出现,就让这些问题有了解决方案,在之前的文章中已经详细的讲解了C++的 ----- 模板初认识,所以本次博客将为大家详细的讲解C++的模板进阶!!
一、C++模板是什么?
程序设计中经常会用到一些程序实体:它们的实现和所完成的功能基本相同,不同的仅仅是所涉及的数据类型不同。而模板正是一种专门处理不同数据类型的机制
模板------是泛型程序设计的基础(泛型generic type——通用类型之意)
函数、类以及类继承为程序的代码复用提供了基本手段,还有一种代码复用途径——类属类型(泛型),利用它可以给一段代码设置一些取值为类型的参数(注意:这些参数的值是类型,而不是某类型的数据),通过给这些参数提供一些类型来得到针对不同类型的代码
1.1 泛型编程思想
首先我们来看一下下面这三个函数,如果学习过了C++函数重载 和 C++引用的话,就可以知道下面这三个函数是可以共存的,而且传值会很方便
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
void Swap(double& left, double& right)
{
double temp = left;
left = right;
right = temp;
}
void Swap(char& left, char& right)
{
char temp = left;
left = right;
right = temp;
}
但是真的很方便吗?这里只有三种类型的数据需要交换,若是我们需要增加交换的数据呢?再CV然后写一个函数吗?
这肯定是不现实的,所以很明显函数重载虽然可以实现,但是有以下几个不好的地方:
- 重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应的函数
- 代码的可维护性比较低,一个出错可能所有的重载均出错
那是否能做到这么一点,告诉编译器一个模子,让编译器根据不同的类型利用该模子来生成代码
根据上面的问题,诞生了模板,告诉编译器一个模子,然后其余的工作交给它来完成,根据不同的需求生成不同的代码
这就是泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础
1.2 C++模板的分类
1、函数模板(function tempalte):使用泛型参数的函数(function with generic parameters)
2、类模板(class template):使用泛型参数的类(class with generic parameters)
更加具体 模板初阶知识,可以看之前的文章 ----- 模板初识
本篇文章主要讲解 模板的高阶操作:非类型模板参数、全特化、偏特化等,以及关于模板声明与定义不能分离(在两个不同的文件中)的问题。
二、非类型模板参数
模板参数可分为类型形参和非类型形参。
类型形参: 出现在模板参数列表中,跟在class或typename关键字之后的参数类型名称。
template <class T> // T 为模板参数中的 ---------- 类型模板形参
非类型形参: 用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。
template <size_t N> // N 为模板参数中的 ------- 非类型模板形参
之前所使用的模板参数都是用来匹配不同的类型,如 int、double、Date 等,模板参数除了可以匹配类型外,还可以匹配常量(非类型),完成如数组、位图等结构的大小确定
2.1 问题引入
假设我现在自定义了一个静态栈,栈的大小设置为100。然后我构建了一个int 类型的栈st1,和一个double 类型的栈st2。那么我希望stl 的大小为100,st2 的大小为500,能不能实现呢
#define N 100
// 静态栈
template<class T>
class Stack
{
private:
int _a[N];
int _top;
};
int main()
{
Stack<int> st1;
Stack<double> st1;
return 0;
}
是不可能实现的,哪有什么办法可以解决这个问题呢?
这个时候,就要引出 ---- 非类型模板参数
2.2 非类型模板参数的使用
2.2.1 非类型模板参数的定义
在定义模板参数时,可以不再使用 class 或 typename,而是直接使用具体的类型,如 size_t,此时称为 非类型模板参数
注:非类型模板参数必须为常量,即在编译阶段确定值
2.2.2 非类型模板参数的两种类型
1、利用 非类型模板参数 定义一个大小可以自由调整的 整型数组 类
#include<iostream>
#include<assert.h>
using namespace std;
template<size_t N>
class arr
{
public:
int& operator[](size_t pos)
{
assert(pos >= 0 && pos < N);
return _arr[pos];
}
size_t size() const
{
return N;
}
private:
int _arr[N]; //创建大小为 N 的整型数组
};
int main()
{
arr<10> a1; // 大小为 10
arr<20> a2; // 大小为 20
arr<100> a3; // 大小为 100
// 使用[]运算符访问元素
a3[0] = 90;
a3[1] = 85;
a3[2] = 92;
cout << "a1.size(): " << a1.size() << endl;
cout << "a2.size(): " << a2.size() << endl;
cout << "a3.size(): " << a3.size() << endl;
cout << "a3第一个分数: " << a3[0] << endl; // 输出: 90
}

2、可以再加入一个模板参数:类型,此时就可以得到一个 泛型、大小可自定义 的数组
template<class T, size_t N>
class arr
{
public:
T& operator[](size_t pos)
{
assert(pos >= 0 && pos < N);
return _arr[pos];
}
size_t size() const
{
return N;
}
private:
int _arr[N]; //创建大小为 N 的整型数组
};
int main()
{
arr<int, 10> a1; // 大小为 10
a1[0] = 90;
a1[1] = 85;
a1[2] = 92;
arr<double, 20> a2; // 大小为 20
arr<char, 100> a3; // 大小为 100
// 输出它们的 类型
cout << typeid(a1).name() << endl;
cout << typeid(a2).name() << endl;
cout << typeid(a3).name() << endl;
}

注意:非类型模板参数支持缺省,因此写成这样也是合法的
2.2.3 非类型模板参数的使用规则
非类型模板参数要求类型为 整型家族,其他类型是不行的
- 比如下面这些 非类型模板参数 都是标准之内的
//整型家族(部分)
template<class T, int N>
class arr1 { /*……*/ };
template<class T, long N>
class arr2 { /*……*/ };
template<class T, char N>
class arr3 { /*……*/ };
- 而一旦使用其他家族类型作为 非类型模板参数,就会引发报错
//浮点型,非标准
template<class T, double N>
class arr4 { /*……*/ };

因此可以总结出,非类型模板参数 的使用要求为
- 只能将 整型家族 类型作为非类型模板参数,其他类型不在标准之内
- 非类型模板参数必须为常量(不可被修改),且需要在编译阶段确定结果,因为编译器在编译阶段就需要根据传入的非类型模板参数生成对应的类或函数
- 整型家族:char、short、bool、int、long、long long 等
2.3 问题的解决
此时我们已经知道了非类型模板参数的用法,只需要给上面的栈添加非类型模板参数,这样就实现了 st1 和 st2 构造不同的大小
// 静态栈
template<class T, size_t N>
class Stack
{
private:
int _a[N];
int _top;
};
int main()
{
Stack<int, 100> st1;
Stack<double, 500> st2;
return 0;
}
2.4 非类型模板参数的实例应用
在 C++11 标准中,引入了一个新容器 array,它就使用了 非类型模板参数,为一个真正意义上的 泛型数组,这个数组是用来对标传统数组的

array 的第二个模板参数就是 非类型模板参数
#include <iostream>
#include <cassert>
#include <array>
using namespace std;
int main()
{
int arrOld[10] = { 0 }; //传统数组
array<int, 10> arrNew; //新标准中的数组
//与传统数组一样,新数组并没有进行初始化
//新数组对于越界读、写检查更为严格
arrOld[15]; //老数组越界读,未报错
arrNew[15]; //新数组则会报错
arrOld[12] = 0; //老数组越界写,不报错,出现严重的内存问题
arrNew[12] = 10; //新数组严格检查
return 0;
}
array 是泛型编程思想中的产物,支持了许多 STL 容器的功能,比如 迭代器 和 运算符重载 等实用功能,最主要的改进是 严格检查越界行为
实际开发中,很少使用 array,因为它对标传统数组,连初始化都没有,vector 在功能和实用性上可以全面碾压,并且 array 使用的是 栈区 上的空间,存在栈溢出问题,可以说 array 是一个鸡肋的容器
三、模板的特化
3.1 概念
通常情况下,模板可以帮我们实现一些与类型无关的代码,但在某些场景中,【泛型】无法满足调用方的精准需求,此时会引发错误
比如使用 日期类 ,实现一个专门用来进行小于比较的函数模板
// 日期类
class Date
{
//友元函数
friend std::ostream& operator<<(std::ostream& out, const Date& d); // 标准流输出 --> printf
friend std::istream& operator>>(std::istream& in, Date& d); // 标准流插入 --> scanf
public:
// 构造函数
Date(int year = 1970,int month = 1,int day = 1)
:_year(year)
,_month(month)
,_day(day)
{}
// 运算符重载
bool operator<(const Date& d)const
{
return (_year < d._year) ||
(_year == d._year && _month < d._month) ||
(_year == d._year && _month == d._month && _day < d._day);
}
bool operator>(const Date& d)const
{
return (_year > d._year) ||
(_year == d._year && _month > d._month) ||
(_year == d._year && _month == d._month && _day > d._day);
}
// 打印日期
void Printf()
{
cout << _year << " / " << _month << " / " << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
// 流插入
std::ostream& operator<<(std::ostream& out, const Date& d) {
out << d._year << "-" << d._month << "-" << d._day << endl;
return out;
}
// 流提取
std::istream& operator>>(std::istream& in, Date& d) {
in >> d._year >> d._month >> d._day;
return in;
}
// 函数模板 -- 参数匹配
template<class T>
bool Less(T left, T right)
{
return left < right;
}
int main()
{
cout << Less(1, 2) << endl;
Date d1(2024, 7, 6);
Date d2(2024, 7, 8);
cout << Less(d1, d2) << endl;
return 0;
}
可以看到,不管是内置类型,还是自己实现的日期类,都可以通过Less函数模板来比较大小,而且结果都是正确的

如果我们要比较指针类型呢?

我们发现结果是0,也就是说6 小于 8 为 false,这是为什么呢?
上述示例中,p1 指向的对象显然小于p2指向的对象,但结果显示是大于,这是因为Less 内部并没有比较p1和p2指向的对象内容,而比较的是pl和p2指针地址,这就无法达到预期,而错误。
也就是说,Less 绝对多数情况下都可以正常比较,但是在特殊场景下就得到错误的结果,此时,就需要对 – 模板进行特化处理
即 : 在原模板类的基础上 , 针对特殊类型所进行特殊化的实现方式
模板特化中分为 函数模板特化 与 类模板特化
3.2 函数模板特化
函数模板的特化步骤:
- 必须要先有一个基础的函数模板
- 关键字 template 后面接 一对空的尖括号<>
- 『函数名后跟一对尖括号』,尖括号中指定需要特化的类型
- 函数形参表:必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一 些奇怪的错误
// 函数模板 -- 参数匹配
template<class T>
bool Less(T left, T right)
{
return left < right;
}
// 对Less函数模板进行特化
template<>
bool Less<Date*>(Date* left, Date* right)
{
return *left < *right;
}
int main()
{
Date* p1 = new Date(2024, 7, 6);
Date* p2 = new Date(2024, 7, 8);
cout << Less(p1, p2) << endl;
Date* p3 = new Date(2024, 7, 8);
Date* p4 = new Date(2022, 7, 6);
cout << Less(p3, p4) << endl;
return 0;
}
此时,就会调用特化之后的版本

注意:一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给
bool Less(Date* left, Date* right)
{
return *left < *right;
}
这种实现简单明了,代码的可读性高,容易写。因为对于一些参数类型复 杂的函数模板,特化时才会特别给出,因此函数模板不建议特化
3.3 类模板特化
模板特化主要用在 — 类模板,它可以在泛型思想之上解决大部分特殊问题,并且类模板特化还可以分为:全特化 和 偏特化,适用于不同场景
3.3.1 全特化
全特化指 将所有的模板参数特化为具体类型,将模板全特化后,调用时,会优先选择更为匹配的模板类。
简单一点来说:全特化 就是将模板参数列表中 所有的参数都确定化
全特化的特化步骤:
- 首先必须要有一个基础的类模板
- 关键字template后接一对空的尖括号<>
- 『类名后跟一对尖括号』,尖括号中指定需要特化的类型
假设有下面这样一个 Data 类,我希望 构造函数 打印出来的 d2 对象里面 Tl 是 int , T2 是 double,有什么办法吗?
template<class T1, class T2>
class Data
{
public:
Data()
{
cout << "Data<T1, T2>" << endl;
}
private:
T1 _d1;
T2 _d2;
};
int main()
{
Data<int, int> d1;
Data<int, double> d2;
return 0;
}
我们实例化 dl 和 d2 对象时,编译器会自动调用其默认构造函数,当我们打印的时候,可以看到实际上d2 对象里面还是 T1 和 T2 并不是我们想要的 int 和 double

那么这个时候 ,我们就可以对 T1 和 T2 进行模板的特化
template<class T1, class T2>
class Data
{
public:
Data()
{
cout << "Data<T1, T2>" << endl;
}
private:
T1 _d1;
T2 _d2;
};
// 全特化
template<>
class Data<int, double>
{
public:
Data()
{
cout << "Data<int, double>" << endl;
}
private:
int _d1;
double _d2;
};
int main()
{
Data<int, int> d1;
Data<int, double> d2;
return 0;
}
当我们运行以后,可以看到 d2 对象就去调用刚刚写好的特化类模板

注意:全特化 = 所有参数都必须精确匹配,d1 虽然第一个参数是 int,但第二个是 int 而不是 double,所以不匹配特化版本,只有 d2 的 <int, double> 才完全匹配特化版本
总结:对模板进行全特化处理后,实际调用时,会优先选择已经特化并且类型符合的模板
3.3.2 偏特化
偏特化是指任何针对模板参数进一步进行条件限制设计的特化版本。
偏特化又可分为以下两种表现形式:『 部分特化』、『 参数更进一步的限制』
偏特化的特化步骤:
- 首先必须要有一个基础的类模板
- 关键字template后接一对尖括号,尖括号中指定特定类型
- 『类名后跟一对尖括号』,尖括号中指定需要特化的类型
1、部分特化
将模板参数类表中的一部分参数特化
比如我们对 Tl 类型进行特化处理,固定其类型为 double
template<class T1, class T2>
class Data
{
public:
Data()
{
cout << "Data<T1, T2>" << endl;
}
private:
T1 _d1;
T2 _d2;
};
// 部分特化 -- 将第一个参数特化为double
template<class T2>
class Data<double, T2>
{
public:
Data()
{
cout << "Data<double, T2>" << endl;
}
private:
double _d1;
T2 _d2;
};
int main()
{
Data<int, int> _d1;
Data<double, double> _d2;
Data<double, char> _d3;
return 0;
}
可以看到,当我们指定T1 为 double 的时候,才会调用这个部分特化的类模板

2、参数更进一步的限制
偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限 制所设计出来的一个特化版本
例如,我们还可以指定当T1和T2为某种类型时,使用我们特殊化的类模板
// 基础模板
template<class T1, class T2>
class Data
{
public:
Data()
{
cout << "Data<T1, T2>" << endl;
}
private:
T1 _d1;
T2 _d2;
};
// 部分特化 -- 将第一个参数特化为double
template<class T2>
class Data<double, T2>
{
public:
Data()
{
cout << "Data<double, T2>" << endl;
}
private:
double _d1;
T2 _d2;
};
//两个参数偏特化为指针类型
template <typename T1, typename T2>
class Data <T1*, T2*>
{
public:
Data()
{
cout << "Data<T1*, T2*>" << endl;
}
private:
T1* _d1;
T2* _d2;
};
//两个参数偏特化为引用类型
template <typename T1, typename T2>
class Data <T1&, T2&>
{
public:
Data(const T1& d1, const T2& d2)
: _d1(d1)
, _d2(d2)
{
cout << "Data<T1&, T2&>" << endl;
}
private:
const T1& _d1;
const T2& _d2;
};
// 主函数
int main()
{
Data<int, int> d1; // 调用基础的版本
Data<double, double> d2; // 调用部分特化的double版本
Data<int*, int*> d3; // 调用特化的指针版本
Data<int&, int&> d4(2, 4); // 调用特化的引用版本
return 0;
}
运行以后可以看到,当我们实例化的对象为指针类型或者引用类型的时候,就会去调用这两个特化模板

3.4 模板特化的应用示例
我们还是拿日期类来举例,假设我现在要对3个实例化对象进行排序
// Less模板 --- 比较小于
template<class T>
struct Less
{
bool operator()(const T& x, const T& y) const
{
return x < y;
}
};
int main()
{
Date d1(2024, 7, 7);
Date d2(2024, 7, 6);
Date d3(2024, 7, 8);
vector<Date> v1;
v1.push_back(d1);
v1.push_back(d2);
v1.push_back(d3);
// 排序
sort(v1.begin(), v1.end(), Less<Date>());
// 打印
for (auto e : v1)
{
cout << e;
}
return 0;
}
可以看到,此时是能直接排序的,结果是日期升序

那如果 vector 里面存放的是 Date* 类型的数据,还能排序吗
int main()
{
Date d1(2024, 7, 7);
Date d2(2024, 7, 6);
Date d3(2024, 7, 8);
vector<Date*> v2;
v2.push_back(&d1);
v2.push_back(&d2);
v2.push_back(&d3);
// 排序
sort(v2.begin(), v2.end(), Less<Date*>());
// 打印
for (auto e : v2) {
cout << *e << endl;
}
// 打印
for (auto e : v2) {
cout << e << endl;
}
return 0;
}

因为v2 当中存放的地址,所以我们打印的时候要解引用,打印以后看到,日期还不是升序,但我们可以发现,不解引用,直接打印v2的每个元素可以看到,v2 中放的地址是升序的。因为此处需要在排序过程中,让sort 比较v2中存放地址指向的日期对象,但是走了Less模板,sort 在排序时实际比较的是v2中指针的地址,因此无法达到预期
通过观察上述程序的结果发现,对于日期对象可以直接排序,并且结果是正确的。但是如果待排序元素是指针,结果就不一定正确。
因为: sort 最终按照Less模板中的方式比较,所以只会比较指针,而不是比较指针指向空间中内容。
那么此时可以使用类版本特化来处理上述问题:
全特化处理:
// Less模板 --- 比较小于
template<class T>
struct Less
{
bool operator()(const T& x, const T& y) const
{
return x < y;
}
};
// 对Less类模板按照指针方式特化
template<>
struct Less<Date*>
{
bool operator()(Date* x, Date* y) const
{
return *x < *y;
}
};
int main()
{
Date d1(2024, 7, 7);
Date d2(2024, 7, 6);
Date d3(2024, 7, 8);
vector<Date*> v2;
v2.push_back(&d1);
v2.push_back(&d2);
v2.push_back(&d3);
// 排序
sort(v2.begin(), v2.end(), Less<Date*>());
// 打印
for (auto e : v2) {
cout << *e;
}
return 0;
}

偏特化处理:
// Less模板 --- 比较小于
template<class T>
struct Less
{
bool operator()(const T& x, const T& y) const
{
return x < y;
}
};
//偏特化后的比较模板
template<class T>
class Less<T*>
{
public:
bool operator()(T* x, T* y) const
{
return *x < *y;
}
};
int main()
{
Date d1(2024, 7, 7);
Date d2(2024, 7, 6);
Date d3(2024, 7, 8);
vector<Date*> v2;
v2.push_back(&d1);
v2.push_back(&d2);
v2.push_back(&d3);
// 排序
sort(v2.begin(), v2.end(), Less<Date*>());
// 打印
for (auto e : v2) {
cout << *e;
}
return 0;
}

四、模板的分离编译
4.1 什么是分离编译
一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式
4.2 模板的分离编译
在分离编译模式下,我们一般创建三个文件,一个头文件用于进行函数声明,一个源文件用于对头文件中声明的函数进行定义,最后一个源文件用于调用头文件当中的函数。
按照此方法,我们若是对一个加法函数模板进行分离编译,其三个文件当中的内容大致如下:

但是使用这三个文件生成可执行文件时,却会在链接阶段产生报错
下面我们对其进行分析:
我们都知道,程序要运行起来一般要经历以下四个步骤:
- 预处理: 头文件展开、去注释、宏替换、条件编译等。
- 编译:检查代码的规范性、是否有语法错误等,确定代码实际要做的工作,在检查无误后,将代码翻译成汇编语言。
- 汇编:把编译阶段生成的文件转成目标文件。
- 链接: 将生成的各个目标文件进行链接,生成可执行文件。
以上代码在预处理阶段需要进行头文件的包含以及去注释操作

这三个文件经过预处理后实际上就只有两个文件了,若是对应到Linux操作系统当中,此时就生成了 Add.i 和 main.i 文件了

预处理后就需要进行编译,虽然在 main.i 当中有调用Add函数的代码,但是在 main.i 里面也有Add函数模板的声明,因此在编译阶段并不会发现任何语法错误,之后便顺利将 Add.i 和 main.i 翻译成了汇编语言,对应到Linux操作系统当中就生成了 Add.s 和 main.s 文件
之后就到达了汇编阶段,此阶段利用 Add.s 和 main.s 这两个文件分别生成了两个目标文件,对应到Linux操作系统当中就是生成了 Add.o 和 main.o 两个目标文件
前面的预处理、编译和汇编都没有问题,现在就需要将生成的两个目标文件进行链接操作了,但在链接时发现,在main函数当中调用的两个Add函数实际上并没有被真正定义,主要原因是函数模板并没有生成对应的函数,因为在全过程中都没有实例化过函数模板的模板参数T,所以函数模板根本就不知道该实例化T为何类型的函数
模板分离编译失败的原因:
在函数模板定义的地方(Add.cpp)没有进行实例化,而在需要实例化函数的地方(main.cpp)没有模板函数的定义,无法进行实例化
4.3 解决办法
解决类似于上述模板分离编译失败的方法有两个,第一个就是在模板定义的位置进行显示实例化。
例如,对于上述代码解决方案如下:

在函数模板定义的地方,对T为int和double类型的函数进行了显示实例化,这样在链接时就不会找不到对应函数的定义了,也就能正确执行代码了。
虽然第一种方法能够解决模板分离编译失败的问题,但是我们这里并不推荐这种方法,因为我们需要用到一个函数模板实例化的函数,就需要自己手动显示实例化一个函数,非常麻烦。
现在就来说说解决该问题的第二个方法,也是我们所推荐的,那就是对于模板来说最好不要进行分离编译,不论是函数模板还是类模板,将模板的声明和定义都放到一个文件当中就行了。
4.4 补充命名空间
命名空间是C++用来避免命名冲突的机制,它把相关的类、函数、变量等组织在一个命名空间里,比如STL的所有内容都在std命名空间中。
在写代码时必须先用#include把头文件包含进来,因为#include是预处理指令会把整个头文件内容复制到当前位置,这样std命名空间里的stack、vector等定义才会存在。
然后才能写using namespace std让我们可以直接使用std里的名字而不用每次都写std::前缀。如果不写using namespace std就必须用std::stack这样的完整写法,其中::叫作用域解析运算符,std::stack表示std命名空间里的stack,而::stack表示从全局命名空间开始找stack。
如果两个地方都定义了同名的类或函数,比如std命名空间有stack,你自己的代码也定义了stack,那么使用using namespace std后直接写stack就会有歧义导致编译错误,这时候必须明确指定是std::stack还是::stack来告诉编译器用哪一个。
所以正确的顺序是先include头文件让定义存在,再using namespace让名字可以直接用,最后在代码里使用这些名字,如果有冲突就用完整的命名空间前缀来区分。
为什么 #include 要写在 using namespace std; 之前
正确的顺序:
#include <iostream> // ✓ 先包含头文件
#include <stack>
#include <queue>
#include "Stack.h"
using namespace std; // ✓ 再使用命名空间
void test_stack_queue()
{
stack<int> st; // 这里才能直接用 stack
}
核心知识点:编译顺序和作用域
- 预处理器的工作方式
// 编译器处理顺序:
// 第一步:预处理 - 展开 #include
#include <stack>
// 相当于把整个 stack 头文件的内容复制粘贴到这里
// 此时代码变成:
namespace std {
template<class T>
class stack {
...
};
}
// 第二步:声明命名空间
using namespace std;
// 告诉编译器:从现在开始,std命名空间里的东西可以直接用
- 如果顺序写反了会怎样?
错误示例:
using namespace std; // ✗ 先使用命名空间
#include <stack> // ✗ 后包含头文件
// 问题:此时 stack 定义被插入到 using 语句之后
// 但 using 已经生效,可能导致作用域混乱
可能的问题:
- 如果 Stack.h 中有与 std::stack 同名的类
- using namespace std; 在前会先声明使用std的stack
- 但此时std::stack还没被include进来
- 后面include时可能产生命名冲突或未定义错误
- 举例说明
假设你的 Stack.h 里也定义了一个 stack 类:
// Stack.h 文件内容
class stack { // 自定义的stack类
public:
void push(int x);
void pop();
};
正确顺序:
#include <stack> // std::stack 定义进来了
#include "Stack.h" // 自定义 stack 定义进来了
using namespace std; // 现在两个stack都在,会有命名冲突!
// 现在有两个stack:
// 1. std::stack(来自<stack>)
// 2. ::stack(来自Stack.h,全局命名空间)
stack<int> st1; // ❌ 歧义!编译器不知道用哪个
// 解决方法:
std::stack<int> st1; // 明确指定用STL的
::stack st2; // 明确指定用自定义的stack(全局的自定义stack)
错误顺序:
using namespace std; // 此时std::stack还不存在!
#include <stack> // 现在才定义std::stack
#include "Stack.h" // 又定义一个stack
stack<int> st; // 编译器懵了:用哪个stack?
- 编译器的处理流程
1. 预处理阶段(从上到下)
├─ 展开所有 #include
├─ 处理所有 #define
└─ 结果:一个完整的翻译单元
2. 编译阶段
├─ 解析 using namespace
├─ 名称查找
└─ 类型检查
3. 关键:using namespace 只影响它之后的代码
推荐写法
// 1. 系统头文件
#include <iostream>
#include <stack>
#include <queue>
// 2. 自定义头文件
#include "Stack.h"
// 3. 命名空间声明
using namespace std;
// 4. 代码实现
void test() {
stack<int> st; // 明确无歧义
}
总结:
- #include 必须在 using namespace 之前 - 因为要先有定义,才能使用
- 顺序影响编译器的名称查找 - using只对之后的代码生效
- 避免命名冲突 - 正确的顺序能让编译器清楚知道每个名字的来源
五、模板总结
模板是 STL 的基础支撑,假若没有模板、没有泛型编程思想,那么恐怕 “STL” 会变得非常大
模板的优点:
- 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生
- 增强了代码的灵活性
模板的缺点:
- 模板会导致代码膨胀问题,也会导致编译时间变长
- 出现模板编译错误时,错误信息非常凌乱,不易定位错误
总之,模板 是一把双刃剑,既有优点,也有缺点,只有把它用好了,才能使代码 更灵活、更优雅

1649

被折叠的 条评论
为什么被折叠?



