Chinaunix首页 | 论坛 | 博客
  • 博客访问: 158852
  • 博文数量: 45
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 273
  • 用 户 组: 普通用户
  • 注册时间: 2014-05-28 10:30
文章分类
文章存档

2017年(6)

2016年(3)

2015年(8)

2014年(28)

我的朋友

分类: C/C++

2015-01-28 20:45:37

2.6 类模板的静态成员

当类模板中有静态成员变量时,情况与普通类的静态成员变量有所不同。普通类中的静态成员变量需要在某个代码文件中显式声明,以便在该代码文件编译后可以为静态成员变量留出存储空间供之后链接使用,而类模板中的静态变量却无法如此处理。

第1章中讨论有关于模板实现代码的位置时已经提过,C++标准提倡将模板的所有实现都放在头文件中以便编译器可以当场实现模板实例,这样能够避免产生跨目标文件链接。但是类模板静态成员变量却与这一提倡冲突。类模板的静态成员变量是所有同类型的类模板实例共享的一块数据。当在多个目标文件中声明了同一类模板的同类型实例后,必然会产生跨目标文件链接。为了与标准所提倡的风格一致,C++编译器都会对类模板静态成员变量做特殊处理。

首先,代码必须按照C++标准的要求,将类模板静态成员变量的实现与类模板实现放在同一可见范围内。通常,将静态成员变量的实现写在类模板实现之后。由于是类模板的成员,其实现也必须写成模板,写法可以参考例2.6。

  1. // 文件名:the_class.hpp 
  2.  template<typename T>
  3.  template<typename T>
  4.  struct the_class {
  5.     static int id;
  6.     the_class() {++id;}
  7.  };
  8.  template<typename T> int the_class<T>::id = 0;

只要静态成员变量的模板与其类模板同时可见,编译器就可针对类模板的静态成员变量做特殊处理:

1)在目标文件中写入类模板实例中静态成员变量的初始值。

2)将此模板实例静态成员变量做类似外部变量的处理,即在汇编代码中为该变量临时分配一个内存地址,但在目标文件中标记该地址所关联的变量名及链接属性等,以便在随后由链接器修改地址,以正确实现多个类模板实例共享同一内存地址。

在链接时同样需要对类模板静态成员变量做特殊处理。因为类模板静态成员变量的实现及初始值是写在头文件中,故而在每个包含了该头文件的代码文件中,都会存在若干个该类实例的静态成员变量“副本”。如果在不同文件中都生成了同一模板参数值的实例,则会有多个该实例成员变量“副本”存在于多个目标文件中,从而产生冲突。此时,链接器需要解决此冲突。

还是以例2.6中的类模板为例,假设在两个代码文件中都生成了相同模板参数,代码见例2.7。


  1.   // 文件名:call.cpp
  2. #include <iostream>
  3. #include "the_class.hpp" 
  4.   void call()
  5.   { 
  6.      the_class<int> c; 
  7.      std::cout << c.id << std::endl; 
  8.  } 
  9.  // 文件名:call1.cpp 
  10.  #include <iostream>
  11.  #include "the_class.hpp"
  12. void call1()
  13. {
  14.  the_class<int> c;
  15.  std::cout << c.id << std::endl;
  16. }
  17.  // 文件名:main.cpp 
  18. void call();
  19. void call1();
  20. int main()
  21.  {
  22.  call();
  23. call1();
  24. }


例2.7中,代码文件call.cpp和call1.cpp中分别定义了两个函数void call()和void call1()。两函数中都生成了类模板the_class的实例the_class,编译器会分别在编译两个代码文件所生成的目标文件中,为其静态成员变量the_class::id分配内存地址。而按照类静态成员的概念,所有类实例都应该共享同一套静态成员存储空间。在链接时,链接器将随机选择一个目标中的空间作为最终存储空间,从而使不同目标文件中的多个等价模板实例共享同一套静态成员存储空间。将例2.7中的三个文件编译及链接后运行,其输出如下:


  1. $ ./a.out 
  2.  1
  3.  2


可见,不同目标文件中的the_class实例在构造时,的确会访问同一块内存地址以更新其id值。


如果对例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::id的存储空间和初始值信息,由于代码做过手脚,现在两个目标文件所存初始值并不一样。接下来链接时在命令行用不同的目标文件顺序链接后再运行,显示的结果为:


  1. $ g++ main.o call.o call1.o ; ./a.out 
  2.  1
  3.  2 
  4.  $ g++ main.o call1.o call.o ; ./a.out 
  5.  2 
  6.  3


可见,当命令行中call.o在call1.o之前时,链接器会采用call.o中的静态变量信息,而将call1.o的指令重定位至该内存空间。此时静态变量初值为0,则程序运行输出的两个类实例id分别为1和2。而当命令中call1.o在call.o之前时,情况相反,call1.o中的静态变量信息将被采用,链接后可执行程序中静态变量初值为1,则程序输出为2和3。这从另一个侧面印证了以上关于静态变量实现方式的讨论。


注意 第1章有关函数模板的讨论中也曾经涉及函数模板中的静态变量,实际上函数模板的静态变量在编译过程中的处理方式与类模板静态成员变量一致,只不过由于函数模板的静态变量天然与函数模板的实现一同可见,所以在第1章中并没有再引出有关具体实现的说明。

阅读(1664) | 评论(0) | 转发(0) |
0

上一篇:Python内建函数之getattr

下一篇:gcc0长数组

给主人留下些什么吧!~~