外部变量只在标题中意外工作,为什么?

时间:2015-03-17 11:58:56

标签: c++ avr-gcc

我目前正在为Arduino更新C ++库(特别是使用avr-gcc编译的8位AVR处理器)。

通常,默认Arduino库的作者喜欢在头部内包含类的外部变量,该变量也在类.cpp文件中定义。我认为这基本上是为了让新手作为内置对象准备就绪。

我的方案是:我更新的库不再需要.cpp文件,我已将其从库中删除。直到我进行最后一次检查才发现我发现的错误,但是没有产生链接器错误,尽管事实上没有为.cpp文件中的extern变量提供定义。

这很简单,我可以得到它(头文件):

struct Foo{
  void method() {}
};

extern Foo foo;

包含此代码并在一个或多个源文件中使用它不会导致任何链接器错误。我在两个版本的GCC中尝试过它,Arduino使用(4.3.7,4.8.1)和启用/禁用C ++ 11。

在我试图导致错误的过程中,我发现只有在执行某些操作时才能获取对象的地址或修改我添加的虚拟变量的内容。

发现这一点后,我发现它很重要:

  • 类函数只返回其他对象,例如,没有像运算符返回对自身的引用,甚至是副本。
  • 它只修改外部对象(代码中有效volatile uint8_t引用的寄存器),并返回其他类的临时对象。
  • 此标题中的所有类函数都是如此基本,以至于它们的成本低于或等于函数调用的成本,因此它们(在我的测试中)完全内联到调用者中。典型的语句可能会在调用链中创建许多临时对象,但是编译器会通过它们直接看到并输出有效的代码修改寄存器,而不是一组嵌套的函数调用。

我还记得在n3797 7.1.1 - 8 中阅读extern可以用于不完整类型,但是该类是完全定义的,而声明不是(这可能是无关紧要的。)

我认为这可能是游戏优化的结果。我已经看到了获取地址对对象的影响,否则这些对象将被视为常量并且在没有使用RAM的情况下进行编译。通过向编译器无法保证状态的对象添加任何间接层,将导致此RAM消耗行为。

所以,也许我只是通过询问来回答我的问题,但是我仍然在做假设并且困扰我。经过相当长一段时间的爱好编码C ++,字面上我不做列表中的唯一内容是做出假设

真的,我想知道的是:

  • 关于我的工作解决方案,是否记录了无法获取类的地址(导致间接)的简单案例?
  • 这只是由优化引起的边缘案例行为,而不需要链接某些内容吗?
  • 或者是简单明了的未定义行为。因为在GCC中可能有一个错误并允许在优化降低或禁用时可能失败的代码?

或者你们中的一个人可能很幸运拥有一个解码器环,可以在标准中找到一个合适的段落,概述具体细节。

这是我的第一个问题,所以如果您想了解某些细节,请告诉我。如果需要,我还可以提供GitHub链接代码。

编辑:由于库需要与现有代码兼容,我需要保持使用点语法的能力,否则我只需要一类静态函数。

为了删除现在的假设,我看到两个选项:

  • 仅为变量声明添加.cpp。
  • 在标题中使用#define foo (Foo())这样的定义,允许通过临时的点语法。

我更喜欢使用 define 的方法,社区的想法是什么?

干杯。

2 个答案:

答案 0 :(得分:2)

声明某些内容extern只是告知汇编器和链接器,无论何时使用该标签/符号,它都应该引用符号表中的条目,而不是本地分配的符号。

链接器的作用是尽可能用符号表条目替换地址空间的实际引用。

如果您在C文件中根本不使用该符号,它将不会显示在汇编代码中,因此当您的模块与其他模块链接时不会导致任何链接器错误,因为没有未定义的参考。

答案 1 :(得分:1)

这是由优化引起的边缘情况行为,或者您从未在代码中使用foo变量。我并非100%确定它在形式上不是一种不确定的行为,但我确信从实际角度来看它并不是不确定的。

extern变量以这种方式实现,用它们编译的代码产生所谓的重定位 - 应该放置变量addres的空位 - 然后由链接器填充。显然,foo从未在您的代码中以需要获取其地址的方式使用,因此链接器甚至不会尝试找到该符号。如果关闭优化(-O0),可能会出现链接器错误。

更新:如果您想保留"点符号"但是使用未定义的extern删除问题,您可以将extern替换为static(在头文件中),创建单独的"实例"每个TU的变量。因为这个变量无论如何都会被优化,这根本不会改变真正的代码,但也可以用于未经优化的构建。