c++静态初始化失败示例

c++ static-init-fiasco example

本文关键字:失败 初始化 静态 c++      更新时间:2023-10-16

借助"用C++思考";Bruce Eckel著,第10章练习32。问题是如何更改链接顺序,为对象m5调用的Mirror::test()返回false。这是我的密码。

镜像.h:

#ifndef MIRROR_H_
#define MIRROR_H_
class Mirror {
 public:
  Mirror() {logic_ = true; self_ = 0;};
  Mirror(Mirror *ptr) {self_ = ptr; logic_ = false;};
  bool test() {
    if (self_ != 0) {
      return self_->test();
    } else {
      return logic_;
    }
  };
 private:
  bool logic_;
  Mirror *self_;
};

#endif // MIRROR_H_

任务

one.cpp

#include "mirror.h"
Mirror m1;

两个.cpp

#include "mirror.h"
extern Mirror m1;
Mirror m2 (&m1);

三个.cpp

#include "mirror.h"
extern Mirror m2;
Mirror m3 (&m2);

等等。最后,

五个.cpp

#include "mirror.h"
#include <iostream>
extern Mirror m4;
Mirror m5 (&m4);
int main(int argc, char* argv[]) {
  std::cout << m5.test() << std::endl;
}

m5.test()返回true。任务说,我应该更改链接顺序,m5.test()返回false。我尝试过使用:

init_优先级(优先级)

在标准C++中,在命名空间范围内定义的对象保证按照严格按照其给定翻译单位中的定义。不保证跨翻译单元的初始化。然而,GNU C++允许用户控制在中定义的对象的初始化顺序通过指定相对优先级,当前有界的常数积分表达式介于101和65535之间(包括101和65536)。数字越低表示越高优先事项

但没有运气。

练习全文:

在头文件中,创建一个包含两个数据的类Mirror成员:指向镜像对象的指针和布尔。给它两个构造函数:默认构造函数将bool初始化为true镜像指针为零。第二个构造函数将参数指向镜像对象的指针,它将该对象分配给对象的内部指针;它将bool设置为false。添加成员函数测试():如果对象的指针为非零,则返回通过指针调用的test()的值。如果指针为零,它返回bool。现在创建五个cpp文件,每个文件包括镜像标头。第一个cpp文件定义全局镜像对象使用默认构造函数。第二个文件在中声明对象第一个文件为extern,并使用第二个构造函数,带有指向第一个对象的指针。继续做下去直到到达最后一个文件,该文件还将包含全局对象定义。在该文件中,main()应该调用test()函数并报告结果。如果结果是真的,请了解如何更改链接器的链接顺序并将其更改,直到结果是错误的。

将对象文件传递到链接器时,需要更改对象文件的顺序。这对于顶层代码来说是合理的,尽管不同的编译器使用不同的方法,即它不可移植。此外,对于库,您通常无法控制对象的包含顺序。例如,如果你有

// file1.cpp
int main() {
}
// file2.cpp
#include <iostream>
static bool value = std::cout << "file2.cppn";
// file3.cpp
#include <iostream>
static bool value = std::cout << "file3.cppn";

然后你把两个程序链接起来:

g++ -o tst1 file1.cpp file2.cpp file3.cpp
g++ -o tst2 file1.cpp file3.cpp file2.cpp

您将获得tst1tst2的不同输出,例如:

$ ./tst1
file2.cpp
file3.cpp
$ ./tst2
file3.cpp
file2.cpp

总的寓意是:不要这样做。也就是说:不要使用全局对象。如果您觉得绝对需要使用全局对象,请将它们封装到函数中,例如:

Type& global_value() {
    static Type value; // possibly with constructor arguments
    return value;
}

通过这种方式,value在第一次访问时被初始化,并且在尚未构建时还无法访问它。如果你像这样封装所有对象,你可以保证它们是按照适当的顺序构建的(除非你有一个循环依赖项,在这种情况下它无法工作,你应该认真思考你的设计)。不幸的是,在C++2003中,将对象封装到函数中的上述方法不是线程安全的。不过,它在C++2011中是线程安全的。尽管如此,全局变量的使用通常是有问题的,您肯定希望尽量减少它们的使用。

我也在为这个练习而挣扎。

我设法编写了一个小Python脚本来准备makefile条目,这些条目使用所有可能的对象文件排列来链接和测试最终可执行文件:

import itertools
for perm in itertools.permutations([1, 2, 3, 4, 5]):
    print 'tg++ u0{0}.o u0{1}.o u0{2}.o u0{3}.o u0{4}.o -o $@ && ./main.exe'.format(*perm)

在执行了我的make过程之后,发现所有可能的配置都产生了true值。

这是因为所有全局(即静态)变量都保证在进入main函数之前进行初始化。

我定义了一个全局bool变量,它在main之前保存test()函数的结果,类似于以下内容:

#include "mirror.h"
#include <iostream>
extern Mirror m4;
Mirror m5 (&m4);
bool result = m5.test();
int main(int argc, char* argv[]) {
  std::cout << result << std::endl;
}

宾果!一些对象的排列在程序的输出中产生了false

在调用任何可能的构造函数之前,所有静态变量都用零初始化。在本练习中,调用构造函数的顺序是线索。

如果在result变量的值建立时,依赖链中的任何对象都没有被构造函数初始化,则结果为false值(self_值为0,logic_值为false,因此测试函数返回false)。

当在进入main函数之前对result变量进行求值时,存在这种可能性,并且链接器命令中对象文件的顺序与结果有关。