了解嵌入式 C:什么是结构?
介绍完结构之后,我们将看看这个强大的数据对象的一些重要应用。然后,我们将检查 C 语言语法来声明一个结构。最后,我们将简要介绍数据对齐要求。我们将看到,我们可以通过简单地重新排列其成员的顺序来减小结构的大小。
本文提供了有关嵌入式 C 编程中结构的一些基本信息。
介绍完结构之后,我们将看看这个强大的数据对象的一些重要应用。然后,我们将检查 C 语言语法来声明一个结构。最后,我们将简要介绍数据对齐要求。我们将看到,我们可以通过简单地重新排列其成员的顺序来减小结构的大小。
结构
可以将多个在逻辑上相互关联的相同类型的变量分组为一个数组。处理一组而不是一组自变量使我们能够更方便地排列数据并使用它。例如,我们可以定义以下数组来存储数字化语音输入的 ADC 的最后 50 个样本:
uint16_t voice[50];
请注意 uint16_t 是宽度正好为 16 位的无符号整数类型。这是在 C 标准库 stdint.h 中定义的 ,提供与系统规范无关的特定位长的数据类型。
数组可用于对具有相同数据类型的多个变量进行分组。如果不同的变量之间存在联系怎么办 数据类型?我们可以在程序中将这些变量视为一个组吗?例如,假设我们需要指定生成 voice 的 ADC 的采样率 上面的数组。我们可以定义一个浮点变量来存储采样率:
float sample_rate;
虽然变量voice 和sample_rate 相互关联,它们被定义为两个独立变量。为了将这两个变量相互关联,我们可以使用 C 语言中称为结构的强大数据结构。结构允许我们对不同的数据类型进行分组并将它们作为单个数据对象处理。一个结构体可以包括不同种类的变量类型,例如其他结构体、函数指针、结构体指针等。对于语音示例,我们可以使用以下结构:
struct record { uint16_t voice[50];浮动采样率;};
在本例中,我们有一个名为 record 的结构 有两个不同的成员或字段:第一个成员是 uint16_t 的数组 元素,第二个成员是一个 float 类型的变量。语法以关键字 struct 开头 . struct 关键字后面的单词是一个可选名称,用于稍后引用该结构。我们将在本文的其余部分讨论定义和使用结构的其他细节。
为什么结构很重要?
上面的例子指出了结构的一个重要应用,即定义依赖于应用程序的数据对象,这些对象可以将不同类型的各个变量相互关联。这不仅导致了一种处理数据的有效方式,而且还允许我们实现称为数据结构的特殊结构。
数据结构可用于各种应用,例如两个嵌入式系统之间的消息传递以及将从传感器收集的数据存储在不连续的内存位置。
图 1. 结构可用于实现链表。
此外,当程序需要访问内存映射微控制器外设的寄存器时,结构体是有用的数据对象。我们将在下一篇文章中介绍结构应用。
图 2. STM32 MCU 的内存映射。图片由采用 ARM 的嵌入式系统提供。
声明结构
要使用结构,我们首先需要指定一个结构模板。考虑下面的示例代码:
struct record { uint16_t voice[4];浮动采样率;};
这指定了用于创建此类型的未来变量的布局或模板。该模板包含一组 uint16_t 和一个 float 类型的变量。模板的名称是 record , 这在关键字 struct 之后 .值得一提的是,没有用于存储结构模板的内存分配。只有在定义了基于此布局的结构变量之后,才会进行内存分配。以下代码声明变量 mic1 以上模板:
struct record mic1;
现在,为变量 mic1 分配了一段内存 .它有空间来存储四个 uint16_t 数组元素和一个浮点变量。
可以使用成员运算符 (.) 访问结构的成员。例如,以下代码将 100 分配给数组的第一个元素并复制 sample_rate 的值 到 fs 变量(必须是 float 类型)。
mic1.voice[0]=100;fs=mic1.sample_rate;
声明结构的其他方式
我们在上一节中研究了一种声明结构的方法。 C 语言支持的一些其他格式将在本节中介绍。您可能会在整个程序中坚持使用一种格式,但熟悉其他格式有时会有所帮助。
声明结构模板的一般语法是:
struct tag_name { type_1 member_1; type_2 成员_2; ... type_n member_n;} variable_name;
标签名称 和变量名 是可选的标识符。我们通常会看到这两个标识符中的至少一个,但在某些情况下,我们可以同时消除它们。
语法 1: 当两个 tag_name 和变量名 存在,我们在模板之后定义结构变量。使用这个语法,我们可以将前面的例子改写如下:
struct record { uint16_t voice[4];浮动采样率;} mic1;
现在,如果我们需要定义另一个变量 (mic2 ),我们可以写
struct record mic2;
语法 2: 只有 variable_name 已经包括了。使用此语法,我们可以将上一节中的示例重写为:
struct { uint16_t voice[4];浮动采样率;} mic1;
在这种情况下,我们必须在模板之后定义我们所有的变量,并且我们不能在我们的程序后面定义任何其他变量(因为模板没有名称,我们以后不能引用它)。
语法 3: 在这种情况下,没有 tag_name 或 variable_name .以这种方式定义的结构模板称为匿名结构。匿名结构可以在另一个结构或联合中定义。下面给出一个例子:
struct test { // 匿名结构 struct { float f;字符一个; };} test_var;
要访问上述匿名结构的成员,我们可以使用成员运算符 (.)。以下代码将 1.2 分配给成员 f .
test_var.f=1.2;
由于该结构是匿名的,因此我们仅使用成员运算符访问其成员一次。如果它有以下示例中的名称,我们将不得不使用成员运算符两次:
struct test { struct { float f;字符一个; } 嵌套;} test_var;
在这种情况下,我们应该使用以下代码将 1.2 分配给 f :
test_var.nested.f=1.2;
如您所见,匿名结构可以使代码更具可读性和更简洁。也可以使用 typedef 关键字和结构来定义新的数据类型。我们将在以后的文章中介绍这种方法。
结构的内存布局
C 标准保证结构的成员将按照成员在结构中声明的顺序一个接一个地位于内存中。第一个成员的内存地址将与结构本身的地址相同。考虑以下示例:
struct Test2{ uint8_t c; uint32_t d; uint8_t e; uint16_t f;} MyStruct;
将分配四个内存位置来存储变量 c、d、e 和 f。内存位置的顺序将与声明成员的顺序相匹配:c 的位置将具有最低地址,然后是 d、e,最后是 f。我们需要多少字节来存储这个结构?考虑到变量的大小,我们知道,存储这个结构至少需要1+4+1+2=8个字节。但是,如果我们为 32 位机器编译此代码,我们会惊讶地观察到 MyStruct 的大小 是 12 个字节而不是 8 个!这是因为编译器在为结构的不同成员分配内存时有一定的限制。例如,一个 32 位整数只能存储在地址可以被 4 整除的内存位置。此类约束称为数据对齐要求,旨在让处理器更有效地访问变量。数据对齐会导致内存布局中的一些空间(或填充)浪费。本主题仅在此介绍;我们将在本系列的下一篇文章中详细介绍。
图 3。 数据对齐会导致内存布局中的一些空间(或填充)浪费。
了解数据对齐要求后,我们或许能够重新排列结构中成员的顺序,从而提高内存使用效率。例如,如果我们按照下面给出的方式重写上述结构,它的大小将在 32 位机器上减少到 8 个字节。
struct Test2{ uint32_t d; uint16_t f; uint8_t c; uint8_t e;} MyStruct;
对于内存受限的嵌入式系统,将数据对象的大小从 12 字节减少到 8 字节是一项显着的节省,特别是当程序需要许多这些数据对象时。
下一篇文章将更详细地讨论数据对齐,并研究在嵌入式系统中使用结构的一些示例。
总结
- 结构允许我们定义依赖于应用程序的数据对象,这些对象可以将不同类型的各个变量相互关联。这导致了一种处理数据的有效方法。
- 称为数据结构的专用结构可用于各种应用,例如两个嵌入式系统之间的消息传递以及将从传感器收集的数据存储在不连续的内存位置。
- 当我们需要访问内存映射微控制器外设的寄存器时,结构很有用。
- 我们可以通过重新排列结构中成员的顺序来提高内存使用效率。
要查看我的文章的完整列表,请访问此页面。
嵌入式