【问题标题】:Why does compile order sometimes cause a segmentation fault when using std::map::insert()?为什么在使用 std::map::insert() 时编译顺序有时会导致分段错误?
【发布时间】:2013-07-12 20:51:04
【问题描述】:

我有一个名为Controller 的类,在其中,我有一个名为Button 的类。一个Controller 包含多个不同类型的Button 实例(例如button_type_abutton_type_b)。


controller.h

#ifndef __controller__
#define __controller__
class Controller
{
public:
    class Button
    {
    public:
        Button(int type = -1);

    private:
        int type;
    };

    Controller();

    Button A;
    Button B;
    Button X;
    Button Y;
};
#endif


按钮类型是ints,我希望能够将某些按钮类型ints 与指向这些特定类型的Button 实例的指针相关联。

为了跟踪这种关联,我使用std::map<int, Controller::Button*>,我将typedef 转换为buttonmap_t

当我创建新的Button 实例(在Controller 构造函数中)时,Button 构造函数将这些Buttons 的类型注册到映射中。


controller.cpp

#include "controller.h"
#include <map>

typedef std::map<int, Controller::Button*> buttonmap_t;
buttonmap_t map;

Controller::Controller() :
A(0),
B(1),
X(2),
Y(3)
{ }

Controller::Button::Button(int type) :
type(type)
{
    map[type] = this;
}


然后我创建一个全局Controller 对象,并定义main()


main.cpp

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

Controller controller;

int main(int argc, const char * argv[])
{
    std::cout << "running..." << std::endl;
    return 0;
}


根据我编译源代码的顺序,程序要么运行良好,要么触发分段错误:

apogee:MapTest$ gcc controller.cpp main.cpp -o maptest -lstdc++
apogee:MapTest$ ./maptest 
running...

apogee:MapTest$ gcc main.cpp controller.cpp -o maptest -lstdc++
apogee:MapTest$ ./maptest 
Segmentation fault: 11


后一种情况似乎是在正确初始化之前尝试使用地图,这导致了段错误。当我使用 Xcode 进行调试时,调试器在“__tree”中停止,因为 std::map 正在调用 __insert_node_at(),它会抛出 EXC_BAD_ACCESS(code=1, address=0x0)。调用堆栈显示这是由第一个 Button 实例调用 map[type] = this; 触发的。

所以,这是我的多部分问题:

  1. 为什么编译顺序会导致这种情况发生?
  2. 有没有办法实现这种intButton* 的映射不受 编译顺序?
  3. 如果有,是什么?

理想情况下,我仍然希望将所有 Controller- 和 Button 相关代码放在单独的 controller.* 文件中。


这似乎与以下问题有些相关(但不完全相同):

  1. Segmentation fault in std::map::insert(...)
  2. [] operator in std::map is giving me segmentation fault

【问题讨论】:

  • 附加说明,您可能希望将 .h 文件中的代码放在编译器指令中,该指令只编译一次。使用 ifndef 会有所帮助。
  • 谢谢。为简洁起见,我已将其删除,但我只是将它们重新添加。

标签: c++ segmentation-fault stdmap


【解决方案1】:

当您有多个全局构造函数时,它们的执行顺序是不确定的。如果controller 对象在map 对象之前实例化,控制器将无法访问该映射,从而导致段错误。但是,如果首先实例化 map 对象,则控制器可以正常访问它。

在这种情况下,它似乎是它们在命令行中出现的顺序。这就是为什么将您的 main.cpp 首先放置会导致段错误 - controller 对象首先被实例化。

我建议将实例化移动到您的 main 中,因为这样您就可以准确地控制对象的实例化方式和顺序。

【讨论】:

  • +1。这个问题也被称为“静态初始化命令惨败”。 C++faq lite 几乎没有关于它的条目。
  • 您的解决方案是我最初诊断实际情况的方式。不过,理想情况下,我仍然希望将所有Controller- 和Button 相关代码放在单独的“controller.*”文件中。我刚刚更新了问题以表明这一点。
  • 在这种情况下,您只需将 static 函数添加到实例化您需要的任何对象的类中,然后您可以从 main 调用这些函数。
【解决方案2】:

静态变量的初始化顺序没有定义,所以它取决于您的特定设置,包括编译器、链接器和链接顺序。

初始化函数技巧

您可以使用初始化函数技巧来确保在需要时初始化某些内容。我知道这在使用 Microsoft 的 C++ 编译器的 Windows 上肯定有效(并且在 Linux 上对 g++ 进行了一些测试,见下文)。

初始化函数

第一步是将地图作为静态变量移动到函数中,并始终通过该函数访问地图。

buttonmap_t& buttonMap() {
  static buttonmap_t map;
  return map;
}

用法

地图是在第一次调用buttonMap() 函数时创建的。如果您通过该函数访问地图,那么您可以确定它会被创建。

Controller::Button::Button(int type) :
  type_(type) {
    buttonMap()[type] = this;
}

关键部分是全局变量的初始化:您将其替换为引用并从保存该变量的函数中对其进行初始化。

buttonmap_t& map = buttonMap();

说明

使用此设置,初始化顺序无关紧要,因为对函数的第一次调用将执行初始化,之后的每次调用都将使用已初始化的实例。

注意:这个技巧适用于全局变量,因为初始化阶段是在单个线程上完成的。即使您不知道初始化的确切顺序,也可以确定它会按顺序发生。

测试

我在我的家用电脑上用 Linux 上的 g++ 进行了测试,它似乎可以工作:

$ g++ main.cpp controller.cpp -Wall
$ ./a.out
running...

最终程序:

// controller.h
#ifndef CONTROLLER_H
#define CONTROLLER_H

class Controller {
 public:
  class Button {
   public:
    Button(int type = -1);

   private:
    int type_;
  };

  Controller();

  Button A;
  Button B;
  Button X;
  Button Y;
};

#endif

// controller.cpp
#include "controller.h"
#include <map>

typedef std::map<int, Controller::Button*> buttonmap_t;

buttonmap_t& ButtonMap() {
  static buttonmap_t map;
  return map;
}

buttonmap_t& map = ButtonMap();

Controller::Controller() :
  A(0),
  B(1),
  X(2),
  Y(3) {
}

Controller::Button::Button(int type) :
  type_(type) {
  ButtonMap()[type] = this;
}

// main.cpp
#include "controller.h"
#include <iostream>
Controller controller;

int main(int argc, const char * argv[]) {
  std::cout << "running..." << std::endl;
  return 0;
}

【讨论】:

  • 不是特定于编译器的技巧,而是语言的保证。
【解决方案3】:

按照 @Drew McGowen@jrok 的建议,我查看了“静态初始化顺序惨败”:
http://www.parashift.com/c++-faq/static-init-order.html
http://www.parashift.com/c++-faq/static-init-order-on-first-use.html

使用“construct on first use”的习语,我做了以下修改:


controller.cpp

typedef std::map<int, Controller::Button*> buttonmap_t;
static buttonmap_t& map() // was buttonmap_t map;
{
    static buttonmap_t* ans = new buttonmap_t();
    return *ans;
};

//[...]

Controller::Button::Button(int type) :
type(type)
{
    map()[type] = this;
}


这适用于任一编译顺序,并且不需要对“controller.h”或“main.cpp”进行任何更改。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-03
    • 2011-04-07
    • 2018-04-08
    • 1970-01-01
    • 1970-01-01
    • 2017-06-23
    相关资源
    最近更新 更多