首页 理论教育 ArduinoC中的存储类及其影响

ArduinoC中的存储类及其影响

时间:2023-10-23 理论教育 版权反馈
【摘要】:Arduino C识别四个存储类:自动、注册、静态和外部。在Arduino C中使用自动存储类的实际影响似乎对生成的代码没有任何影响,因此,将其自身降级为文档功能。也就是说,代码生成器对寄存器存储类定义的变量的命运做出最终决定。使用默认值(自动)定义的变量无法实现该目标存储类,因为每次调用函数时都会重新创建它们。显然,一个解决办法是将感兴趣的变量移出函数,并使用全局范围定义它。静电存储类以一种更优雅的方式解决了这个问题。

ArduinoC中的存储类及其影响

Arduino C识别四个存储类:自动、注册、静态和外部。这四个都是Arduino C中的关键字,不能用作变量名。如果无意中定义这样的变量:

执行后,编译器发出错误消息:

error:declaration does not declare anything

显然,编译器会将两者识别为关键字,不允许将它们用作变量名。

(1)自动存储类。

自动存储类是具有本地作用域的变量的默认存储类。还可以使用块范围定义自动变量:

for(auto int k;k<MAXVAL;k++)

并且编译器会接受,不会产生错误信息。在Arduino C中使用自动存储类的实际影响似乎对生成的代码没有任何影响,因此,将其自身降级为文档功能。作者还没有看到在已发布的代码中使用的auto关键字,尽管有可能是一些例子。

(2)寄存器存储类。

寄存器存储类用于通知编译器数据项应存储在(芯片)寄存器中,而不是内存中。其思想是,这样的数据定义通过将变量保存在寄存器中来优化生成的代码的速度。使用关键字寄存器是对代码生成器的建议,而不是法令。也就是说,代码生成器对寄存器存储类定义的变量的命运做出最终决定。语法是:

register int myVal;

无论如何,编译器都会大量使用它的寄存器集,因此在数据定义中使用寄存器存储类似乎不会产生太大的影响(如果你真的喜欢这类东西,请查看工具目录中avr-objdump.exe程序的文档,以转储对象文件并允许你检查生成的代码。使用该工具超出了本书的范围)。

(3)静态存储类。

正如你所知,当你退出定义了局部范围的变量的函数时,这些变量就会消失。这意味着每次调用函数时,都会创建一组新的局部变量。这也意味着函数中先前执行的函数代码中的局部变量的任何值都将丢失。

但是,在某些情况下,如果你可以保留函数调用之间变量的值,那就太好了。例如,你可能希望维护执行特定的函数一次事件的次数计数。使用默认值(自动)定义的变量无法实现该目标存储类,因为每次调用函数时都会重新创建它们。显然,一个解决办法是将感兴趣的变量移出函数,并使用全局范围定义它。虽然这解决了丢失数值问题,但你将变量暴露于数据隐私的关键问题。静电存储类以一种更优雅的方式解决了这个问题。

看看下面的代码片段:

使用静态存储类说明符编译器会生成保留MyCounter()函数调用之间的计数器的值。首先,计数器的定义不是每次执行调用函数时创建的。实际上,计数器不是使用堆栈机制生成的(第6章有介绍)。相反,计数器是在程序开始执行时创建的,并以这种方式保持其值在整个程序的执行过程中[即在一段专门用于全局类型数据存储(称为堆)的内存中]。编译器会为你处理这些细节 。最终结果是,这个变量可以在函数调用之间保持其值,而不在定义它的函数之外公开它。

如果需要将起始值设置为0以外的值,则数据定义时应指定该值。例如,如果需要起始值为10,则定义必须为:

static int counter=10;

只能在静态变量的定义点设置其起始值。默认情况下,静态变量初始化为0。

(4)外部存储类。

有时,一个项目需要将源代码拆分为两个或多个源代码文件。也许在你拆分的这些文件中,把与输入步骤相关的函数放在了一个源文件中,把与处理流程步骤的函数放在了另一个源文件中。最后甚至可能以为显示步骤的实现过程放在第三个源代码文件中。面对这种“拆分”源文件使用全局数据,可能会很麻烦。

例如,可以将名为myPort的变量定义为全局变量,如:

在你决定拆分文件之前,一切都很顺利。如果两个文件都需要访问myPort,则拆分源文件可能会出现问题。如果在两个源文件中都定义了myPort,那么它们将不会有相同的变量(即它们将有不同的左值)。这就无法使用到相同的数据了。

让我们把第5章中的闰年程序放到这里讨论,闰年函数源代码放在第二个源文件中。让我们在这两个文件中也使用myPort,尽管代码中实际上没有使用它。

下面是创建闰年函数源代码的第二个源代码文件:(www.xing528.com)

我们需要做的第一件事是创建一个文件来保存第二个源代码文件(即闰年代码)。要创建新的源文件代码窗口,在Arduino IDE环境中右侧滚动条上方的小三角形图标,然后右键单击该图标。显示效果如图7-5所示。

图7-5 创建新的源代码窗口

现在单击New Tab菜单选项,这将导致显示更改为图7-6所示。

图7-6 新建源代码窗口

注意,我们将第二个源代码文件命名为IsLeapYear.cpp。文件扩展名“CPP”与实际处理编译器任务的C++编译器相关。你还可以使用“.c”“.h”,甚至忽略文件扩展名。当单击OK按钮时,显示如图7-7所示。请注意,原始和新建的源代码选项卡显示在源代码窗口的上方。

图7-7 Arduino IDE代码窗口

现在,你可以将IsLeapYear()函数源代码从原始源文件剪切并粘贴到新的相应源代码编辑窗口中。如图7-8所示。

图7-8 新建闰年代码窗口

如果你试图按当前的状态编译文件,编译器会告诉你:

error:'IsLeapYear'was not declared in this scope

编译器说它不知道IsLeapYear()是什么。更具体地说,编译器没有关于函数返回类型或其签名的信息。我们该如何解决这个问题呢?

单击ModifiedLeapYear选项卡以显示主源代码文件。在源代码顶部,就在开始注释的下方,添加IsLeapYear()函数的原型定义。

此语句是一个函数原型。函数原型是一种数据声明,它告诉编译器需要知道在当前源文件中使用函数所需的详细信息。更具体地说,是函数原型允许编译器为数据对象创建属性列表,并将其填充到符号表。但是,由于数据对象IsLeapYear()的实际代码位于另一个文件中,因此无法执行此操作填写对象的左值。这意味着函数原型是数据声明而不是数据定义。

回顾一下第4章,我们指出许多程序员使用术语“define”和“declare”,认为它们是同义词。他们错了。Arduino C中每一个关键词都有非常具体的含义。数据声明是简单地声明变量的属性列表,没有为数据分配内存。这意味着一个数据仓库声明在符号表中有一个空的左值序列。函数原型会说:“好的,编译器,我不知道这个数据对象将在内存中的什么地方结束(左值=?),但这里文件中的对象信息已经足够了。”当所有源文件都已编译且所有的代码片段都被拉在一起(这是一个称为链接器的编程工具的工作),只有这样才能完成IsLeapYear()具有已知的左值。

好了,接下来,查找一下我们的端口在哪里?当源代码是单个文件时,我们将myPort定义为具有全局作用域的变量。现在假设第二个源文件中的某些内容需要使用myPort。因为myPort在第一个文件中定义,第二个文件对此一无所知。如何解决此问题,以便在第二个源文件中访问myPort?很简单,只需让编译器为myPort创建一个属性列表,就像函数原型为函数创建属性列表一样。

要使编译器能够在第二个源文件中创建属性列表,请在第二个文件的顶部添加以下语句行:

extern int myPort;

extern访问说明符只是告诉编译器:Hey.myPort没有在这个文件中定义。但是,此语句允许你为myPort创建属性列表,以便在此文件中使用。现在在第二个源文件中使用myPort,尽管它是在第一个文件中定义的。因此,在真正意义上extern关键字对变量的作用与函数原型对函数的作用相同。也就是说,extern允许你为在其他文件中定义的变量创建数据声明。完成这些更改后,就可以编译并运行程序了。你会发现与第5章中的函数完全相同,只是现在源代码被拆分为两个源文件。

(5)volatile关键字。

虽然很少使用,但此时我们应该讲讲volatile关键字。volatile关键字是一个变量限定符,而不是存储类或访问说明符。使用它的语法是:

volatile int lastTestValue;

volatile是对编译器的一个指令,它表示在代码引用该特定变量时,必须从内存中加载该变量。通常,当代码使用变量时,该变量的右值已经在Atmel临时寄存器中,因此无需再次从内存中重新加载它。这会导致性能的小幅度提升,因为省略了重新加载值的内存行程。编译器的优化工作就提升了程序的性能。

尽管这种优化在大多数情况下是一件好事,但有时存储在内存中的值可能与寄存器中的值不同步。这种问题最有可能发生在代码中使用中断服务例程时。通过使用volatile限定符,可以告诉编译器在程序使用变量时重新提取变量的右值。这降低了变量的右值不同步的可能性。

免责声明:以上内容源自网络,版权归原作者所有,如有侵犯您的原创版权请告知,我们将尽快删除相关内容。

我要反馈