每日一课:探索嵌入式C语言的结构概念

发表时间: 2019-05-25 07:57

本文提供了有关嵌入式C编程中结构的一些基本信息。

在介绍结构之后,我们将看一下这个强大数据对象的一些重要应用。 然后,我们将检查C语言语法以声明结构。 最后,我们将简要介绍数据对齐要求。 我们将看到,我们可以通过简单地重新排列其成员的顺序来减小结构的大小。

结构(Structures)

可以将在逻辑上彼此相关的许多相同类型的变量分组为数组。 处理数组而不是自变量集合允许我们安排数据并更方便地使用它。 例如,我们可以定义以下数组来存储数字化语音输入的ADC的最后50个样本:

请注意,uint16_t是无符号整数类型,宽度恰好为16位。 这在C标准库stdint.h中定义的,它提供了与系统规范无关的特定位长度的数据类型。

数组可用于对多个具有相同数据类型的变量进行分组。 如果不同数据类型的变量之间存在连接怎么办? 我们可以在程序中将这些变量视为一个组吗? 例如,假设我们需要指定生成上述语音阵列的ADC的采样率, 我们可以定义一个浮点变量来存储采样率:

尽管变量voice和sample_rate彼此相关,但它们被定义为两个独立变量。 为了将这两个变量相互关联,我们可以使用称为结构的C语言的强大数据结构。 结构(Structures)允许我们对不同的数据类型进行分组,并将它们作为单个数据对象处理。 结构可以包括不同种类的变量类型,例如其他结构,指向函数的指针,指向结构的指针等。对于语音示例,我们可以使用以下结构:

在这种情况下,我们有一个名为record的结构,它有两个不同的成员或字段:第一个成员是uint16_t元素的数组,第二个成员是float类型的变量。 语法以关键字struct开头。 struct关键字后面的单词是一个可选名称,用于稍后引用该结构。 我们将在本文的其余部分讨论定义和使用结构的其他细节。

为什么结构(Structures)很重要?

上述示例指出了结构的重要应用,即,定义了可以将不同类型的各个变量彼此相关联的依赖于应用的数据对象。 这不仅可以实现操作数据的有效方式,还可以实现称为数据结构的专用结构。

数据结构可用于各种应用,例如两个嵌入式系统之间的消息传递,以及将从传感器收集的数据存储在非连续的存储器位置中。

图1.可用于实现链表的结构

另外,当程序需要访问存储器映射的微控制器外围设备的寄存器时,结构是有用的数据对象。 我们将在下一篇文章中介绍结构应用程序。

图2. STM32 MCU的存储器映射。 图片由ARM嵌入式系统公司提供。

声明一个结构

要使用结构,我们首先需要指定结构模板。 请考虑以下示例代码:

这指定了用于创建此类型的未来变量的布局或模板。 该模板包含一个uint16_t数组和一个float类型的变量。 模板的名称是record,它位于关键字struct之后。 值得一提的是,没有用于存储结构模板的内存分配。 仅在定义了基于此布局的结构变量之后才会进行内存分配。 以下代码声明了上述模板的变量mic1:

现在,为变量mic1分配了一段内存。 它有空间来存储数组的四个uint16_t元素和一个float变量。

可以使用成员运算符(.)访问结构的成员。 例如,以下代码将100分配给数组的第一个元素,并将sample_rate的值复制到fs变量(必须是float类型)。

声明结构的其他方法

我们在前一节中研究了一种声明结构的方法。本节将要讨论C语言支持的一些其他格式。您可能会在整个程序中坚持使用一种格式,但熟悉其他格式有时会有所帮助。

声明结构模板的一般语法是:

标记名和变量名是可选的标识符。我们通常会看到这两个标识符中的至少一个,但在某些情况下,我们可以消除这两个标识符。

语法1:当标记名和变量名都存在时,我们在模板后面定义结构变量。使用此语法,我们可以将前面的示例改写为:

标记名(tag_name )和变量名(variable_name)是可选的标识符。我们通常会看到这两个标识符中的至少一个,但在某些情况下,我们可以消除这两个标识符。

语法1:当标记名(tag_name )和变量名(variable_name)都存在时,我们在模板后面定义结构变量。使用此语法,我们可以将前面的示例改写为:

现在,如果我们需要定义另一个变量(mic2),我们可以编写为:

语法2:仅包含variable_name。 使用这种语法,我们可以重写上一节中的示例,如下所示:

在这种情况下,我们必须在模板之后定义所有变量,并且我们不能在程序中稍后定义任何其他变量(因为模板没有名称,我们以后也不能引用它)。

语法3:在这种情况下,没有tag_name或variable_name。 以这种方式定义的结构模板称为匿名结构。 可以在另一个结构或联合中定义匿名结构。 下面给出一个例子:

要访问上述匿名结构的成员,我们可以使用成员运算符(.), 以下代码将1.2分配给成员f:

由于结构是匿名的,我们只使用成员运算符访问其成员一次。 如果它有一个名称,如下例所示,我们必须使用成员运算符两次:

在这种情况下,我们应该使用以下代码将1.2分配给f:

如您所见,匿名结构可以使代码更具可读性和更简洁。 也可以使用typedef关键字和结构来定义新的数据类型。 我们将在以后的文章中介绍这种方法。

结构的内存布局

C标准保证结构的成员将按照在结构中声明成员的顺序一个接一个地位于存储器中。 第一个成员的内存地址将与结构本身的地址相同。 请考虑以下示例:

将分配四个存储器位置来存储变量c,d,e和f。内存位置的顺序将与声明成员的顺序相匹配:c的位置将具有最低地址,然后是d,e,最后,f将出现。我们需要多少字节来存储这个结构?考虑到变量的大小,我们知道,至少需要1 + 4 + 1 + 2 = 8个字节来存储这个结构。但是,如果我们为32位机器编译这个代码,我们会惊奇地发现MyStruct的大小是12个字节而不是8个!这是因为编译器在为结构的不同成员分配内存时具有某些约束。例如,32位整数只能存储在地址可被4整除的存储单元中。实现这种称为数据对齐要求的约束,以使处理器更有效地访问变量。数据对齐会导致内存布局中的一些浪费空间(或填充)。这个主题只在这里介绍;我们将在本系列的下一篇文章中详细介绍。

图3.数据对齐导致内存布局中的一些浪费空间(或填充)。

了解数据对齐要求后,我们可能能够重新排列结构中成员的顺序,并使内存使用更加高效。 例如,如果我们重写上面给出的结构,它的大小将减少到32位机器上的8个字节。

对于内存受限的嵌入式系统,将数据对象的大小从12个字节减少到8个字节可以节省大量成本,特别是当程序需要许多这些数据对象时。

下一篇文章将更详细地讨论数据对齐,并研究在嵌入式系统中使用结构的一些示例。

总结

  • 结构允许我们定义依赖于应用程序的数据对象,这些对象可以将不同类型的各个变量相互关联。 这导致了一种操纵数据的有效方法。
  • 称为数据结构的专用结构可用于各种应用,例如两个嵌入式系统之间的消息传递,以及将从传感器收集的数据存储在非连续存储器位置中。
  • 当我们需要访问存储器映射的微控制器外设的寄存器时,结构非常有用。
  • 我们可以通过重新排列结构中成员的顺序来提高内存的使用效率。