【问题标题】:Const static variable defined in header file has same address in different translation unit头文件中定义的 const 静态变量在不同的翻译单元中具有相同的地址
【发布时间】:2013-07-24 18:22:52
【问题描述】:

我在 /usr/include/c++/4.6/bits/ios_base.h 中检查了 std::ios::app 的定义,发现 std::ios::app 被定义为一个 const 静态变量:

typedef _Ios_Openmode openmode;

/// Seek to end before each write.
static const openmode app =     _S_app;

其中_Ios_Openmode与

在同一个头文件中定义
enum _Ios_Openmode 
{ 
  _S_app        = 1L << 0,
  _S_ate        = 1L << 1,
  _S_bin        = 1L << 2,
  _S_in         = 1L << 3,
  _S_out        = 1L << 4,
  _S_trunc      = 1L << 5,
  _S_ios_openmode_end = 1L << 16 
};

众所周知,静态变量具有内部链接,每个翻译单元都有自己的 这个静态变量的副本,这意味着不同翻译单元中的静态变量应该有不同的地址。但是,我使用了两个单独的程序来打印 std::ios::app 的地址,发现打印的地址是一样的:

源文件test1.cpp

#include <iostream>

int main() {
    std::cout << "address: " << &std::ios::app << std::endl;
    return 0;
}

结果

address: 0x804a0cc

源文件test2.cpp与test1.cpp相同,结果相同:

address: 0x804a0cc

这真的让我很困惑,不同翻译单元中的静态变量不应该有不同的地址吗?


更新:正如 cmets 中所指出的,std::ios::app 是静态数据成员而不是静态 const 变量;静态数据成员有外部链接,不同翻译单元的静态数据成员地址应该相同。第二点是我验证这个事实的方法是错误的:不同的程序并不意味着不同的翻译单元。

【问题讨论】:

  • dans3itz 刚刚指出(在对我的回答的评论中)std::ios::app 实际上是一个静态数据成员,而不是命名空间范围内的静态 const 变量。在这种情况下,它不会有内部链接。

标签: c++ static


【解决方案1】:

您的test1.cpptest2.cpp 不仅是两个独立的翻译单元,而且您将它们编译成两个完全不同的程序并作为独立的进程运行它们。每个进程的内存空间是由操作系统在每次运行时重新定义的,每个进程中地址的绝对值是无法比较的。即使您并行运行进程,它们也可能相同,因为这些地址是相对于每个进程的虚拟地址空间进行解释的。 (*)

如果要查看内部链接的效果,需要在编译后将两个翻译单元链接在一起。

您可以通过以下方式执行此操作:

定义一个标题test.h:

const static int i = 0;

test1.cpp中定义一个函数print_i_1的实现:

#include <iostream>
#include "test.h"

void print_i_1()
{ std::cout << &i << std::endl; }

test2.cpp中定义一个函数print_i_2的实现:

#include <iostream>
#include "test.h"

void print_i_2()
{ std::cout << &i << std::endl; }

请注意,这两个函数执行相同的操作,但只要分别编译它们,它们将各自引用 i 的不同实例。

还要注意,这些程序都不包含main() 的定义。我们在第三个文件test.cpp

extern void print_i_1();
extern void print_i_2();

int main()
{
  print_i_1();
  print_i_2();

  return 0;
}

现在您编译每个 .cpp 文件(所以我们有三个翻译单元)。我正在使用 GCC,但其他编译器也可以进行类似的操作:

g++ -W -Wall -g -o test1.o -c ./test1.cpp
g++ -W -Wall -g -o test2.o -c ./test2.cpp
g++ -W -Wall -g -o test.o -c ./test.cpp

然后将它们链接在一起:

g++ -W -Wall -g -o test ./test.o ./test1.o ./test2.o

运行生成的可执行文件test 时得到的输出是:

0x4009c8
0x4009d0

两个不同的地址。

请注意,C++ 中实际上并不需要关键字 static 来为命名空间范围(包括全局命名空间范围)中的 const 变量完成此操作。它们自动具有内部链接,除非明确声明 extern


(*) 事实证明,您似乎正在使用标准库中定义的类的静态成员的地址。在这种情况下,有两点需要注意:

  • 如果动态链接到标准库,对象实际上可以在两个单独的进程之间共享(然而,这并不一定意味着显示的地址将是相同的,因为每个进程仍然有自己的地址空格);
  • 但是,静态类成员具有外部链接,因此在这种情况下,您的假设从一开始就是错误的。

【讨论】:

  • 很棒的答案;我一直在走同样的路,直到我想到 std::ios::app 是什么:一个班级成员。
  • @dans3itz 很有趣。如果它是类的静态数据成员,它将具有外部链接。 (我实际上并没有注意到声明是从标准头文件中获取的。我以为提问者声明了一个全新的变量。)
【解决方案2】:

9.4.2 定义了静态数据成员的规则:

9.4.2/3 定义了一个静态常量字面量“可以指定一个大括号或相等初始化器”。这意味着,您可以将其定义为类似于下面的 X::x。

9.4.2/4 定义只能存在一个定义(`one definition rule' (odr),参见 3.2)。

最后,9.4.2/5 进一步定义了命名空间范围内类的所有静态数据成员都将具有外部链接。

例子:

// test.h
struct X {
    static const int x = 10;
};

// main.cpp

#include <iostream>
#include "test.h"
void f();
int main(int argc, const char* argv[]) {
    std::cout << &X::x << std::endl;
    f();
    return 0;
}

// test.cpp
#include <iostream>
#include "test.h"

void f() {
    std::cout << &X::x << std::endl;
}

输出:

001A31C4
001A31C4

【讨论】:

  • 我不确定我是否理解您最初的陈述。这是关于单一定义规则的吗?该规则规定每个翻译单元必须有一个确切的定义。但是你是对的,如果它们是静态类成员,它们具有外部链接(如果这就是你的意思)。不管它们是整体的还是其他类型的都无关紧要。
  • 我现在正在查看标准以找到更好的定义。
  • 我仍然认为提问者的意图是寻找内部链接的证据,而选择类成员的东西只是一个错误。 (问题中的实际问题是地址是从两个不同的进程进行比较的。)但是如果你想从标准中引用具有外部链接的静态类成员,最好使用§9.4.2/5(在 C++11 中,即)。
  • 9.4.2/5 可能是每个 C++11 的最佳定义;我很高兴不再拥有的 C++98 标准可能有一些特殊的场景覆盖。它曾经被定义为只能内联初始化整数/枚举类型,并且那里也有一些陷阱。
  • 您是否要将此信息合并到答案中?我已经准备好投票了。现在看来,单定义规则不知何故自动导致两个实例合二为一。在 C++03 中,它是 §9.4.2/6。 (整数类型和枚举的特殊规则没有改变。但这是关于在声明中包含初始化,而不是关于链接。)
猜你喜欢
  • 1970-01-01
  • 2015-10-25
  • 1970-01-01
  • 2012-03-11
  • 1970-01-01
  • 2018-06-15
  • 1970-01-01
  • 1970-01-01
  • 2016-03-29
相关资源
最近更新 更多