当类模板中有静态成员变量时,情况与普通类的静态成员变量不同。普通类中的静态成员函数需要在某个代码文件中显式声明,以便在该代码文件编译后可以为静态成员变量留出存储空间以供之后链接使用。而类模板中的静态变量却无法如此处理。
C++标准提倡将模板的所有实现都放在头文件中以便编译器可以当场实现模板实例,这样能够避免产生跨目标链接。但是类模板静态成员变量却与这一提倡冲突。类模板的静态成员变量是所有同类型的类模板实例共享的一块数据。当多个目标文件中声明了同一类模板的同类型实例后,必然会产生跨目标文件链接。为了与标准所倡导的风格一致,C.++编译器都会对类模板静态成员变量做特殊处理。
首先,代码必须按照C++标准的要求,将类模板静态成员变量的实现与类模板实现放在同一可见范围内。通常,将静态成员变量的实现写在类模板实现之后,由于是类模板的成员,其实现也必须写成模板。如下:
template <typename T>
struct the_class{
static int id;
the_class() {++ id;}
};
template <typename T>
int the_class<T>::id = 0;
只要静态成员变量的模板与其类模板同时可见,编译器就可针对类模板的静态成员变量做特殊处理:
- 在目标文件中写入类模板实例中静态成员变量的初始值。
- 将此模板实例静态成员变量做类似外部变量处理,即在汇编代码中为该变量临时分配一个内存地址,但在目标文件中标记该地址所关联的变量名以及链接属性等,以便在随后又链接器修改地址,以正确实现多个类模板实例共享同一内存地址
在链接时同样需要对类模板静态成员变量做特殊处理。因为类模板静态成员变量的实现以及初始值是写在头文件中,故而在每个包含了该头文件的代码文件中,都会存在若干个该类实例的静态成员变量“副本”。如果在不同文件中都生成了同一模板参数值的实例,则会有多个该实例的“副本”,从而产生冲突。此时,链接器需要解决此冲突。
看个例子:
// call1.cpp
#include <iostream>
#include "the_class.h"
void call1(){
the_class<int> c;
std::cout << c.id << "\n";
};
// call2.cpp
#include <iostream>
#include "the_class.h"
void call2(){
the_class<int> c;
std::cout << c.id << "\n";
};
// main.cpp
#include <string>
#include <iostream>
void call1();
void call2();
int main(){
call1();
call2();
return 0;
}
call1.cpp和call2.cpp定义的call1()和call2()两个函数都生成了类模板the_class的实例the_class<int>
,编译器会分别在编译两个代码文件所生成的目标文件中,为其静态成员变量the_class<int>::id
分配内存地址。而按照类静态成员的概念,所有类实例应该共享同一套静态成员存储空间。在链接时,链接器将随机选择一个目标中的空间作为最终存储空间,从而使不同目标文件中的多个等价目标实例共享同一套静态成员空间。