4

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


控制器.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这些特定类型实例的指针相关联。

为了跟踪这种关联,我使用了 a std::map<int, Controller::Button*>,我使用了typedefa buttonmap_t

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


控制器.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().


主文件

#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 进行调试时,调试器在std::map调用时停止在“__tree”中__insert_node_at(),这会抛出EXC_BAD_ACCESS(code=1, address=0x0). 调用堆栈显示这是由第一个Button实例调用触发的map[type] = this;

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

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

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


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

  1. std::map::insert(...) 中的分段错误
  2. std::map 中的 [] 运算符给了我分段错误
4

3 回答 3

6

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

在这种情况下,它似乎是它们出现在命令行上的顺序。这就是为什么把你的main.cpp第一个导致段错误 -controller对象首先被实例化。

我建议将实例化移动到main的.

于 2013-07-12T20:55:48.677 回答
3

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

初始化函数技巧

您可以使用初始化函数技巧来确保在需要时初始化某些内容。我知道这在使用 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;
}
于 2013-07-12T22:03:28.667 回答
0

正如@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”的习语,我做了以下修改:


控制器.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”进行任何更改。

于 2013-07-12T21:39:53.960 回答