1. 基本概念
是对一批仅仅成员数据类型不同的类的抽象,程序员只要为这一批类所组成的整个类家族创建一个类模板,给出一套程序代码,就可以用来生成多种具体的类。
总结以下两点:
1)类模板用于实现类所需数据的类型参数化。
2)类模板在表示如数组、表、图等数据结构显得特别重要,这些数据结构的表示和算法不受所包含的元素类型的影响。
类模板基本语法举例:
// 参数化一个类型T,允许参数化多个类型 template<typename T> class A { public: A(T t) { this->t = t; } T &getT() { return t; } public: T t; }; // 子类从模板类继承的时候,既可以派生类模板,也可以派生非模板类 // 1. 可以从类模板派生出非模板类,在派生中,作为非模板类的基类,必须是类模板实例化后的模板类 class B : public A<int> { public: B(int i) : A<int>(i) {} void printB() { cout << "A:" << t << endl; } }; // 2. 从类模板派生类模板可以从类模板派生出新的类模板 template <class T> class C : public A<T> { public: C(T t) : A<T>(t) {} }; int main() { A<int> a(100); // 需要提供类型参数 int x = a.getT(); B b(10); b.printB(); C<int> c(10); return 0; }
2. 模板的编译过程
什么是编译单元:一个编译单元(translation unit)是指一个.cpp文件以及它所#include的所有.h文件,.h文件里的代码将会被扩展到包含它的.cpp文件里,
然后编译器编译该.cpp文件为一个.obj文件,并且本身包含的就已经是二进制码,但是不一定能够执行,因为并不保证其中一定有main函数。
什么是分离式编译:一个项目由若干个源文件共同实现,而每个源文件(.cpp)单独编译成目标文件(.obj),最后将所有目标文件连接起来形成单一的可执行文件的过程。
下面举一个例子来说明:
test.h文件内容如下
void func(); // 声明一个函数 func
test.cpp文件内容如下:
#include "test.h" //这里实现出 test.h 中声明的 func 函数 void func() { … // do something }
main.cpp文件内容如下:
#include "test.h" int main() { func(); // 调用func,func具有外部连接类型 return 0; }
说明:test. cpp和main.cpp各自被编译成不同的.obj文件,在main.cpp中,调用了func函数,然而当编译器编译main.cpp时,它仅仅知道的只是
main.cpp中所包含的test.h文件中的一个关于void func();的声明,所以,编译器将这里的f看作外部连接类型,func的实现代码实际存在
于test.cpp所编译成的test.obj中。在main.obj中对f的调用只会生成一行call指令,链接器负责在其它的.obj中寻找func的实现代码,找
到以后将call func这个指令的调用地址换成实际的func的函数进入点地址。
然而,对于模板,模板函数的代码其实并不能直接编译成二进制代码,其中要有一个“实例化”的过程。举个例子(将模板的声明和实现分离):
test.h文件内容如下:
template<class T> class A { public: void func(); // 这里只是个声明 };
test.cpp文件内容如下:
#include "test.h" template<class T> void A<T>::func() // 模板的实现 { …//do something }
main.cpp文件内容如下:
#include "test.h" int main() { A<int> a; a.func(); // #1 return 0; }
说明:编译器在#1处并不知道A<int>::f的定义,因为它不在test.h里面,于是编译器只好寄希望于链接器,希望它能够在其他.obj里面找到A<int>::func的实例,
在本例中就是test.obj,然而,后者中真有A<int>::func的二进制代码吗?NO!!!因为C++标准明确表示,当一个模板不被用到的时侯它就不该被实例化出来,
test.cpp中用到了A<int>::func了吗?没有!!所以实际上test.cpp编译出来的test.obj文件中关于A::f一行二进制代码也没有,于是链接器就傻眼了,只好给
出一个链接错误。但是,如果在test.cpp中写一个函数,其中调用A<int>::func,则编译器会将其实例化出来,链接器就能够完成任务。
模板的二次编译:
1)非模板类在编译的时候就会被实例化出代码,但编译模板类则不是如此,C++标准明确表示当一个模板不被用到的时侯它就不该被实例化出来。
2)当编译到用模板类特例定义对象的代码时,如A<int> a; 此时编译器才会生成对应实例化类的二进制代码,即第二次编译。
3)第二次编译的时候如果该编译单元能访问到模板类的实现代码,则好说,否则只能等到链接,如果其它模块也没有实例化过该代码,则会链接出错。
解决办法:
1)模板类声明在test.h中,定义在main.cpp中,调用在main.cpp中,则能运行成功。
2)模板类声明在test.h中,定义在test.h中,调用在main.cpp中,则能运行成功,因为在预处理阶段会对头文件展开。
这里抛出一个问题:假如多个.cpp都定义了相同类型的对象,那编译器会在每个编译单元都会产生相同的代码吗?
答案:NO,编译器肯定不会那么蠢的,具体实现细节暂时不表。
3. 类模板中的友元函数
1)普通非模板类或函数的友元声明,将友元关系授予明确指定的类或函数。
template<class T> class A { friend void func(); friend class B; private: T _a; }; void func() { A<int> a1; // func()函数内可访问类A的任意实例 A<double> a2; a1._a = 1; a2._a = 1.0; cout << a1._a << endl; cout << a2._a << endl; }
func可访问A任意类实例中的私有和保护成员。
2)类模板或函数模板的友元声明,授予对友元所有实例的访问权。
template<typename T> class A { template<class T1> friend void func(); private: T _a; }; template<typename T1> void func() { A<int> a1; A<double> a2; a1._a = 1; a2._a = 1.0; cout << a1._a << endl; cout << a2._a << endl; } void functest() { func<int>(); // func任意实例可访问类A的任意实例 func<double>(); }
我们模板类A中声明了模板函数func,两者拥有各自的模板形参T和T1,两者是互不影响的,所以在
这里对于func的所有实例(如func<int>,func<double>)对于A的所有实例(如A<int>,A<double>)中的私有或保护成员都可以进行访问。
3)只授予对类模板或函数模板的特定实例的访问权的友元声明。
template<typename T> class A { friend void func<int>(); private: T _a; }; template<typename T1> void func() { A<int> a1; A<double> a2; a1._a = 1; a2._a = 1.0; cout << a1._a << endl; cout << a2._a << endl; } void functest() { func<int>(); // func<double>(); } int main() { functest(); return 0; }
在模板类A中我们声明了模板函数func实例化后的fun<int>的友元关系,因此我们在这里仅能够在func<int>中对模板类A中
的所有实例的私有或保护成员具有访问权限,而对于func<double>则不是模板类A的友元函数,不具有访问其实例的私有或保护成员的权限(编译会出错)。
4)对于3)的例子可以做一点修改
template<typename T> class A { friend void func<T>(); // 这里是一个泛化的T类型 private: T _a; }; template<typename T1> void func() { A<int> a1; A<double> a2; a1._a = 1; a2._a = 1.0; cout << a1._a << endl; cout << a2._a << endl; } void functest() { func<int>(); func<double>(); } int main() { functest(); return 0; }
我们发现对于func<int>仅对模板类A的实例A<int>的私有或保护成员具有访问权限,对于A<double>则没有;而对于func<double>则反之。
总之,对于func的实例只对与它的模板实参一致的A实例有友元关系。