目录
非类型模版参数
类型模板参数
非类型模板参数
非类型模板参数的使用
模板的特化
函数模板的特化
类模板的特化
全特化与偏特化
偏特化的其它情况
模板的分离编译
什么是分离编译
为什么要分离编译
为什么模板不能分离编译
普通的类和函数都是可以分离编译的。
同样是分离编译,普通函数/类可以,函数模板、类模板为什么不行???
编译链接有哪些过程?
链接的时候到底做了什么??
解决模板不能分离编译的方法
显示实例化
不要分离编译
非类型模版参数
在 C++ 中,非类型模板参数是一种在模板定义中使用的参数类型,它不是一个数据类型,而是一个具体的值或对象引用。
类型模板参数
//类型模板参数
template<class T>
class A
{};
非类型模板参数
那么什么是非类型模板参数呢?首先,我们来观察以下代码
假设我想要一个数组的大小一个是100,一个是1000,只能把这个 N 要么给 100 要么给1000,或者再定义一个类出来一个N 是100一个 N 是1000,如果这样改的话代码过,代码处理不同类型不够灵活,所以我们就有了非类型模板参数
#define N 100
template<class T>
class Array
{
private:
T _a[N];
};
int main()
{
Array<int> a1; // 100
Array<int> a2; // 1000
return 0;
}
非类型模板参数的使用
非类型模板参数的使用和类型模板参数的使用类似,也是一样的传参。
#include <iostream>
using namespace std;
//#define N 100
//#define N 1000
//类型模板参数,非类型模板参数
template<class T, int N>
class Array
{
private:
T _a[N];
};
int main()
{
Array<int, 100> a1; // 100
Array<int, 1000> a2; // 1000
cout << sizeof(a1) << endl;
cout << sizeof(a2) << endl;
return 0;
}
这里的N是常量,不能修改的,因为数组的大小是常量。
template<class T, int N>
class Array
{
public:
Array()
{
N = 10; //不能修改
}
private:
T _a[N];
};
int main()
{
Array<int, 100> a1; // 100
Array<int, 1000> a2; // 1000
cout << sizeof(a1) << endl;
cout << sizeof(a2) << endl;
return 0;
}
注意点:
记住一点:模板参数不一定全部传类型,也有可能传整型来固定大小啊。非类型模板参数是一个参数,不是什么类型都能做非类型模板参数的。
double 和 自定义类型等不能作为非类型模板参数。
#include <iostream>
using namespace std;
template<class T, char ch>
//char short可以做非类型模板参数,还有long long / long / int / short / char 整型家族
//以下类型不能作为非类型模板参数
template<class T, double D>
template<class T, string s>
class B
{};
模板的特化
模板的特化:针对某些类型的特殊化处理
函数模板的特化
针对某一种模板的具体类型,要根原来的模板做不一样的处理,就写一个特化,以下是函数模板的写法,针对const char* 类型进行特殊处理。
#include <iostream>
#include <string>
using namespace std;
//原模板
template<class T>
bool IsEqual(T& left, T& right)
{
return left == right;
}
//模板的特化:针对某些类型的特殊化处理
template<>
bool IsEqual<const char*>(const char*& left, const char*& right)
{
return strcmp(left, right) == 0;
}
int main()
{
int a = 0, b = 1;
cout << IsEqual(a, b) << endl;
//这里是指针在比较,而是想比较ASCII码的值,这种情况下就需要使用到特化
const char* p1 = "hello";
const char* p2 = "world";
cout << IsEqual(p1, p2) << endl;
return 0;
}
类模板的特化
#include <iostream>
using namespace std;
//原类模板
template<class T1, class T2>
class Date
{
public:
Date()
{
cout << "Date<T1, T2>" << endl;
}
private:
T1 _d1;
T2 _d2;
};
//特化的类模板
template<>
class Date<int, char>
{
public:
Date()
{
cout << "特化的:Date<int, char>" << endl;
}
private:
int _d1;
char _d2;
};
int main()
{
Date<int, int> d1;
Date<int, char> d2;
return 0;
}
全特化与偏特化
全特化:就是针对特定的一组完整的模板参数,进行完全特殊的处理,为其提供专门的实现,不再使用通用模板的实现方式。
偏特化:就是对部分模板参数进行特殊处理,比如只针对其中一个或几个参数进行特化,或者对参数满足特定条件时进行特殊处理,而其他未特化的参数仍然保持一定的通用性。
注意:第一个参数匹配上了,优先使用偏特化,再看第二个参数。
#include <iostream>
using namespace std;
//原模板
template<class T1, class T2>
class Date
{
public:
Date()
{
cout << "原模板:Date<T1, T2>" << endl;
}
};
//全特化
template<>
class Date<int, char>
{
public:
Date()
{
cout << "全特化:Date<int, char>" << endl;
}
};
//偏特化(半特化)
template<class T2>
class Date<int, T2>
{
public:
Date()
{
cout << "偏特化:Date<int, T2>" << endl;
}
};
int main()
{
Date<int, int> d1; //偏特化
Date<int, char> d2; // 全特化
Date<int, double> d3; //偏特化
Date<char, double> d4; //原模板
return 0;
}
偏特化的其它情况
还有一种情况的特化,对指针,或者引用的特化,也是一种特化。
#include <iostream>
using namespace std;
//原模板
template<class T1, class T2>
class Date
{
public:
Date()
{
cout << "原模板: Date(T1, T2)" << endl;
}
private:
T1 _d1;
T2 _d2;
};
//特化模板,两个指针的特化,不管什么类型,只要是指针就调用该模板
template<class T1, class T2>
class Date<T1*, T2*>
{
public:
Date()
{
cout << "特化都是指针:Date(T1*, T2*)" << endl;
}
};
//特化的都是引用
template<class T1, class T2>
class Date<T1&, T2&>
{
public:
Date()
{
cout << "特化都是引用:Date(T1&, T2&)" << endl;
}
};
int main()
{
Date<int, int> d1;
Date <int*, char*> d2;
Date <char*, double*> d3;
Date<int&, int&> d4;
Date<double&, char&> d5;
return 0;
}
模板的分离编译
什么是分离编译
- 项目工程中一般将函数或者类的声明放到.h,将函数或者类的定义放到.cpp
为什么要分离编译
在一个项目工程中,代码不仅仅是我们平时写的几百行代码,而是几万甚至十几万行代码,分离编译的好处就是方便查看和维护。
为什么模板不能分离编译
普通的类和函数都是可以分离编译的。
这就是我们在实现前面所学的容器的时候,类模板和函数模板都写在一个.h文件中的,因为不能分离编译
同样是分离编译,普通函数/类可以,函数模板、类模板为什么不行???
和编译链接有关系,实际项目中有很多文件,Func.h Func.cpp Test.cpp 以三个文件为例。.
编译链接有哪些过程?
Linux环境下为例,生成的后缀都是Linux中的文件后缀
1.预处理:展开头文件,宏替换,条件编译,去掉注释
生成 Func.i Test.i
2.编译:检查语法,生成汇编代码
生成Func.s Test.s
3.汇编:将汇编代码转成二进制的机器码
生成Func.o Test.o
4.链接:将两个obj目标文件链接起来
生成 a.out,也可以指定名字,没有指定生成的就是a.out
链接的时候到底做了什么??
汇编代码和二进制的机器码几乎是一一对应的,只不过我们看不懂二进制的机器码,所以我这里就看汇编代码,来进行理解。这里 call,机器码会用一段指令一一对应,只不过我们这里用汇编方便理解。
call之前填不上地址,为什么填不上地址
因为只包含了声明,没有具体实现,只有当定义(cpp文件)的时候才会有地址。
编译的过程从i -> s -> o,从头到尾只有声明,都没有它的地址,参数都是能匹配上的,所以这里填问号。
一、函数调用的一般过程
在 C++ 中,函数的调用通常经历预处理、编译、汇编和链接等阶段。
对于普通函数,如 F1
,在预处理阶段,头文件被展开。在编译阶段,当看到对F1
的调用时,编译器会在符号表中记录这个未解析的符号,同时根据函数声明确定函数的参数和返回值类型等信息。在汇编阶段,会生成相应的汇编代码,但此时函数的具体地址还未确定。在链接阶段,链接器会在所有的目标文件和库文件中查找F1
的实现,如果找到了,就会将其地址填入符号表中的相应位置,完成链接。
二、函数模板的特殊情况
对于函数模板F2
,情况则有所不同。
- 头文件中的声明:在头文件(如
Func.h
)中,只包含了函数模板的声明:在头文件中,只是包含了模板函数的声明,不会把具体调用时的参数值(比如这里的 10)传递给头文件。
头文件只是提供了一个模板的框架和接口声明,当在某个源文件(如
test.cpp
)中进行实际调用时,编译器根据调用时传入的具体参数来确定模板参数的类型,但这个具体的参数值并不会传递到头文件中去。
这里只是提供了一个模板的框架,并不针对特定的类型进行实例化,也不知道具体的类型参数是什么。template<class T> void F2(const T& x);
-
在
test.cpp
中的调用:当在test.cpp
中看到对F2(10)
的调用时,在预处理阶段,头文件被展开,引入函数模板的声明。在编译阶段,编译器知道这里调用了一个模板函数,并记录下这个调用以及传入的参数类型(这里是int
),但此时它并不能确定最终的函数实现。因为模板的本质是一种代码生成机制,只有在确定了具体的类型参数后,才能生成真正的函数代码。在汇编阶段,由于不知道具体类型,所以无法确定函数的具体地址,只能在符号表中记录一个未解析的符号,标记这个地方需要在链接阶段找到具体的函数地址。 -
在
Func.cpp
中的情况:在编译Func.cpp
时,由于不知道会有哪些具体的类型参数传入模板函数,所以无法针对特定的类型进行实例化生成具体的函数代码。 -
链接阶段的问题:在链接阶段,链接器会查找所有的目标文件和库文件,试图解析未定义的符号。对于普通函数,只要在某个编译单元中有其实现,就可以找到并填入地址。但对于函数模板,由于在编译阶段没有针对特定类型进行实例化,链接器也无法确定具体的函数地址。即使在
test.cpp
中调用了F2(10)
,链接器也不能从这个调用中推断出在Func.cpp
中应该实例化出针对int
类型的函数模板版本。
三、总结
为什么 F1 可以找到 call 所需的地址,而F2 找不到呢???
对于普通函数 F1
,在链接阶段,链接器可以在各个编译单元中找到其实现,因为普通函数的实现是确定的,不依赖于特定的类型参数,所以可以顺利地在符号表中填入正确的地址完成链接。
而对于函数模板F2
,在声明的头文件中只是提供了模板的框架,不知道具体的类型参数。在test.cpp
中调用F2(10)
时虽然实例化成了int
类型,但这个信息并不能自动传递到Func.cpp
中去进行实例化。在编译Func.cpp
时,由于不知道具体的类型参数,无法实例化出具体的函数,所以在链接阶段,链接器找不到针对特定类型实例化后的函数地址,最终在符号表中没有 F2
的有效地址,从而出现链接错误。
所以,虽然在test.cpp
中调用F2(10)
传入了int
类型,但这个信息在编译和链接的过程中并不会自动传递到头文件(如Func.h
)或Func.cpp
中。这是由于编译和链接过程的独立性以及头文件和链接器的局限性所决定的。函数模板通常不能像普通函数那样简单地进行分离编译,需要在包含模板定义的编译单元中进行实例化,或者通过显式实例化等特殊手段来确保在链接阶段能够找到正确的函数地址。
解决模板不能分离编译的方法
显示实例化
似于特化,int类型可以,但是double又不行了,用一个就得实例化,这种方法不常用。
double 类型的实例化
Template
Void F2<double>(const double& x);
不要分离编译
【优点】
1. 模板复用了代码,节省资源,更快的选代开发,C++的标准模板库(STL)因此而产生
2. 增强了代码的灵活性
【缺陷】
1.模板会导致代码膨胀问题,也会导致编译时间变长
2.出现模板编译错误时,错误信息非常凌乱,不易定位错误