全部博文(45)
分类: C/C++
2015-01-28 20:45:37
2.6 类模板的静态成员
当类模板中有静态成员变量时,情况与普通类的静态成员变量有所不同。普通类中的静态成员变量需要在某个代码文件中显式声明,以便在该代码文件编译后可以为静态成员变量留出存储空间供之后链接使用,而类模板中的静态变量却无法如此处理。
第1章中讨论有关于模板实现代码的位置时已经提过,C++标准提倡将模板的所有实现都放在头文件中以便编译器可以当场实现模板实例,这样能够避免产生跨目标文件链接。但是类模板静态成员变量却与这一提倡冲突。类模板的静态成员变量是所有同类型的类模板实例共享的一块数据。当在多个目标文件中声明了同一类模板的同类型实例后,必然会产生跨目标文件链接。为了与标准所提倡的风格一致,C++编译器都会对类模板静态成员变量做特殊处理。
首先,代码必须按照C++标准的要求,将类模板静态成员变量的实现与类模板实现放在同一可见范围内。通常,将静态成员变量的实现写在类模板实现之后。由于是类模板的成员,其实现也必须写成模板,写法可以参考例2.6。
只要静态成员变量的模板与其类模板同时可见,编译器就可针对类模板的静态成员变量做特殊处理:
1)在目标文件中写入类模板实例中静态成员变量的初始值。
2)将此模板实例静态成员变量做类似外部变量的处理,即在汇编代码中为该变量临时分配一个内存地址,但在目标文件中标记该地址所关联的变量名及链接属性等,以便在随后由链接器修改地址,以正确实现多个类模板实例共享同一内存地址。
在链接时同样需要对类模板静态成员变量做特殊处理。因为类模板静态成员变量的实现及初始值是写在头文件中,故而在每个包含了该头文件的代码文件中,都会存在若干个该类实例的静态成员变量“副本”。如果在不同文件中都生成了同一模板参数值的实例,则会有多个该实例成员变量“副本”存在于多个目标文件中,从而产生冲突。此时,链接器需要解决此冲突。
还是以例2.6中的类模板为例,假设在两个代码文件中都生成了相同模板参数,代码见例2.7。
例2.7中,代码文件call.cpp和call1.cpp中分别定义了两个函数void call()和void
call1()。两函数中都生成了类模板the_class的实例the_class
可见,不同目标文件中的the_class
如果对例2.7稍做修改,“骗”一下编译器再看编译结果,可更直观地理解编译器与链接器对类模板静态成员变量的处理方式。首先,另建一个头文件the_class_1.hpp,其中内容与the_class.hpp内容完全一样,再将the_class_1.hpp中对静态成员变量的初始值改为1。其次,修改call1.cpp内容,用the_class_1.hpp代替the_class.hpp包含进call1.cpp。修改后的两文件内容如下:
// 文件名:the_class_1.hpp
#pragma once
template<typename T>
struct the_class {
static int id;
the_class() {++id;}
};
template<typename T> int the_class<T>::id = 1; // 变量初值有变!
// 文件名:call1.cpp
#include <iostream>
#include "the_class_1.hpp" // 头文件定义更改!
void call1() {
the_class<int> c;
std::cout << c.id << std::endl;
}
用以上文件编译而得的程序,其运行输出将有所不同。不同编译工具的具体处理方式会略有差别,还是以笔者所用的GCC为例。将call.cpp、call1.cpp和main.cpp编译后,生成目标文件call.o、call1.o和main.o。按照之前的介绍,此时在call.o和call1.o中已经分别保存静态变量the_class
可见,当命令行中call.o在call1.o之前时,链接器会采用call.o中的静态变量信息,而将call1.o的指令重定位至该内存空间。此时静态变量初值为0,则程序运行输出的两个类实例id分别为1和2。而当命令中call1.o在call.o之前时,情况相反,call1.o中的静态变量信息将被采用,链接后可执行程序中静态变量初值为1,则程序输出为2和3。这从另一个侧面印证了以上关于静态变量实现方式的讨论。
注意 第1章有关函数模板的讨论中也曾经涉及函数模板中的静态变量,实际上函数模板的静态变量在编译过程中的处理方式与类模板静态成员变量一致,只不过由于函数模板的静态变量天然与函数模板的实现一同可见,所以在第1章中并没有再引出有关具体实现的说明。