这篇文章是我阅读《嵌入式实时操作系统μCOS-II原理及应用》后的读书笔记,记录目的是为了个人后续回顾复习使用。
文章目录
《嵌入式实时操作系统μC/OS-Ⅱ原理及应用》这本书的前言部分有提到:
C 指针看起来像是一个复习的内容,其实是要重点强调 C 指针中的函数指针,因为这种数据类型在操作系统软件中使用的频率太高了,而高校的 C 语言教学又大多不把它当作重点,所以致使相当一部分高校学生甚至不知道函数指针为何物。除了 C 指针之外,C 语言中的关键字
typedef
及其常用方法也是由于上述原因而被初学者忽视,从而造成了学习上的困难,因此在第 2 章也增加了这方面的内容。当然,因为本书的宗旨不是介绍 C 语言,所以仅依靠本书的寥寥数语并不能真正使读者完全掌握函数指针,但起码能使读者知道基础的欠缺之所在,从而主动去查找和阅读文献。
之前,我们已经复习了 C 语言指针的基础知识。
因此,接下来我们需要复习 C 语言中的结构体这部分的知识内容,为接下来的实时操作系统的学习打基础。
下方内容翻译自 Embeetle 的 Embedded C Tutorial,在翻译的过程中,我把自己上机实验的结果,以截图的方式插入到相应的位置。
结构体
结构体是一种复合数据类型,它用于将某些(可能不同)类型的成员组合成一种单一类型。
结构体是一种用户定义的数据类型,允许将不同类型的数据组合在一起。结构体中的各个元素称为成员。
结构体可以包含任意数量的成员,这些成员可以是任何数据类型。它有点类似于数组——但数组只能包含相同类型的数据。
结构体基础
请记住上文结构体中的定义:
结构体是一种用户定义的数据类型,允许将不同类型的数据组合在一起。结构体中的各个元素称为成员。
为了严谨地避免这种歧义,我们将从不单独使用“结构体”这个术语。我们将始终使用“结构体类型”或“结构体变量”组合词:
- 结构体类型本质上是一种数据类型。数据类型仅在编译时存在!因此,仅仅指定结构体类型不会占用任何内存。
- 一旦指定了结构体类型,我们就可以用它实例化出结构体变量。每个结构体变量都会消耗一部分内存——就像其他变量一样(整数、浮点数等)。
将其比作有一个食谱并用它实际烹饪菜肴:
结构体类型是食谱,结构体变量是菜肴。只有菜肴才会占用餐桌上的空间。
我们将首先看看如何声明和定义结构体类型。在第 2 节中,我们将开始由这种类型实例化出变量。
声明和定义结构体类型
指定结构体类型分为两个步骤:首先声明类型,然后定义它。注意,此时没有创建任何变量!我们在此仅使用“声明”和“定义”术语来指代结构体类型本身:
结构体类型的声明只是告知编译器有一个特定的结构体数据类型存在及其名称。
结构体类型的定义 完整地指定了结构体数据类型。定义后,编译器了解了所有关于它的信息:
- 结构体的确切内存布局及其所有成员。
- 结构体实例在内存中占用的空间(当我们声明并定义一个结构体变量时,就会创建一个“结构体实例”。我们将在第 2 节中进行学习了解)。
我们来看一个例子:
// 声明数据类型 'struct Point'
struct Point;
// 定义数据类型 'struct Point'
struct Point
{
int x;
int y;
};
在声明之后,编译器知道存在一种结构体数据类型 struct Point
。没错,关键词 struct
是该数据类型名称的一部分!在定义之后,编译器也知道了关于该结构体数据类型的所有细节。注意,此时还没有实例化该结构体数据类型的变量,因此尚未分配任何内存。
通常,结构体类型的显式声明会被省略,这样的话,定义就可以同时起到声明和定义类型的双重作用:
// 声明并定义数据类型 'struct Point' struct Point { int x; int y; };
在某些情况下,我们必须事先显式声明:
// 声明数据类型 'struct Foo' struct Foo; // 声明数据类型 'struct Bar' struct Bar; // 定义数据类型 'struct Bar' struct Bar { struct Foo *foo; ... }; // 定义数据类型 'struct Foo' struct Foo { struct Bar *bar; ... };
在下一章中,我们将实例化结构体类型(从中创建变量)。
声明和定义结构体变量
最后,让我们创建变量!假设我们已经声明并定义了数据类型 struct Point
现在我们来声明和定义变量:
// 声明变量 a 和 b
extern struct Point a;
extern struct Point b;
// 定义变量 a 和 b
struct Point a;
struct Point b;
再次强调,数据类型不是 Point
而是 struct Point
!如果记住这一点,那么声明和定义结构体变量其实并没有什么特别之处。
就像普通变量一样,我们应该记住声明和定义之间的区别。我们在这里重复一遍:
变量的声明告知编译器一个特定变量存在及其名称、类型和大小(对于结构体变量,数据类型显然是我们之前声明并定义的结构体类型(见第 1 章)),编译器随后知道足够的信息来与变量交互。然而,此时不会进行任何内存的分配。
变量的定义为变量分配一个或多个内存单元,这发生在编译器将源文件转换为目标文件时,目标文件为每个定义的变量保留内存空间。大多数目标文件是可重定位的,这意味着它们从地址 0x0000 开始分配内存空间,链接器最终将所有的这些目标文件合并在一起,上下移动它们的基地址以使它们都能够塞进内存中,只有在那之后,绝对内存地址才会被知道。
初始化结构体变量
一旦声明和定义了结构体变量,就应该为其成员赋值;换句话说,结构体变量应该被初始化。这可以通过多种方式实现。
初始化各个成员
要初始化结构体变量,我们可以分别为每个成员赋值,这非常简单。假设结构体变量已经声明和定义,初始化如下所示:
// 初始化结构体变量 a
a.x = 3;
a.y = 5;
使用列表符号初始化
我们可以使用列表符号一次性为所有成员赋值,而不是分别赋值:
// 初始化结构体变量 a
a = (struct Point){
.x = 3,
.y = 5,
};
注意强制转换前缀,{..}
块内表达式的结果需要转换为数据类型 struct Point
,然后才能赋值给变量 a
。如果在定义变量 a
的同一条语句中对其进行初始化,则可以省略强制转换。
在 {..}
表达式中,.x
和 .y
明确显示了哪个成员被赋予了什么值。但是,如果你愿意,也可以省略它们:
// 初始化结构体变量 a
a = (struct Point){
3,
5,
};
使用结构体变量
一旦我们的结构体变量 a
声明、定义并初始化后,我们就可以像使用其他任何变量一样使用它。然而,有一个特殊的特性:我们可以使用点表示法访问结构体变量内部的各个成员。
// 使用结构体变量 a
a.x = a.y + 7;
综上
我们从声明和定义一个结构体类型开始;然后,我们使用该类型声明和定义结构体变量,最终进行初始化。让我们将所有这些放在这个测试文件中:
// C 代码测试文件
// ================
#include <stdio.h>
// 声明数据类型 'struct Point'
struct Point;
// 定义数据类型 'struct Point'
struct Point
{
int x;
int y;
};
// 声明变量 a 和 b
extern struct Point a;
extern struct Point b;
// 定义变量 a 和 b
struct Point a;
struct Point b;
int main()
{
// 初始化结构体变量 a
a.x = 3;
a.y = 5;
// 初始化结构体变量 b
// (不同的方法)
b = (struct Point){
.x = 7,
.y = 9,
};
printf("a.x = %d\n", a.x);
printf("a.y = %d\n", a.y);
printf("b.x = %d\n", b.x);
printf("b.y = %d\n", b.y);
}
你会得到以下输出:
> gcc test.c -Wall && a.exe
a.x = 3
a.y = 5
b.x = 7
b.y = 9
运行结果如下图所示:
我知道你现在在想什么,我都快闻到味儿了:
确实,我们分步骤做了每件事:
- 声明结构体数据类型;
- 定义结构体数据类型;
- 声明一个结构体变量;
- 定义结构体变量;
- 初始化结构体变量。
当然,在实际情况下这样详细地描述是不合理的,但这是一次极好的学习经验!在下一章节中,我们将看到简写符号。换句话说,我们将学习如何将上述几个步骤糅合进单个表达式中。
结构体的简写表示法
回顾我们在上一章节中如何创建结构体:
- 声明结构体数据类型;
- 定义结构体数据类型;
- 声明结构体变量;
- 定义结构体变量;
- 初始化结构体变量。
这显得非常冗长,让我们看看如何简化这一过程。
压缩表示法
首先,让我们从结构体数据类型的声明和定义开始:
// 声明数据类型 'struct Point'
struct Point;
// 定义数据类型 'struct Point'
struct Point
{
int x;
int y;
};
首先,我们可以将声明和定义合并为一个表达式。只需省略声明部分,使定义同时具有声明的功能:
// 声明并定义数据类型 'struct Point'
struct Point
{
int x;
int y;
};
在结构体数据类型声明和定义之后,我们可以实例化变量。我们通常在单独的表达式中进行,但也可以将其合并为一个表达式:
// 声明并定义数据类型 'struct Point',然后
// 使用该数据类型声明并定义变量 a 和 b。
struct Point
{
int x;
int y;
} a, b;
如何初始化变量呢?这也可以实现!
// 声明并定义数据类型 'struct Point',然后
// 从中声明并定义变量 a 和 b,并初始化它们。
struct Point
{
int x;
int y;
} a = {
3, 5}, b = {
7, 9};
我们把它放在一个测试文件中:
// C 代码测试文件
// ================
#include <stdio.h>
// 声明并定义数据类型 'struct Point',然后
// 从中声明并定义变量 a 和 b,并初始化它们。
struct Point
{
int x;
int y;
} a = {
3, 5}, b = {
7, 9};
int main()
{
printf("a.x = %d\n", a.x);
printf("a.y = %d\n", a.y);
printf("b.x = %d\n", b.x);
printf("b.y = %d\n", b.y);
}
你会得到以下输出:
> gcc test.c -Wall && a.exe
a.x = 3
a.y = 5
b.x = 7
b.y = 9
运行结果如下图所示:
匿名结构体
在某些情况下,我们只希望从一个结构体数据类型中实例化一个(或几个)变量,我们不打算以后再实例化更多的变量,那么给这个结构体数据类型命名就没有意义,只需省略名称:
// 声明并定义一个匿名结构体数据类型,然后
// 从中声明并定义变量 a 和 b,并初始化它们。
struct
{
int x;
int y;
} a = {
3, 5}, b = {
7, 9};
从这个结构体数据类型中,只有变量 a 和 b 存在。一旦表达式结束,就无法再创建新变量!
typedef
在处理结构体时,typedef
关键字经常被使用到。通常,该关键字用于为给定的数据类型创建一个额外的名称(别名)。让我们详细说明一下。
typedef 关键字
typedef
关键字并不会创建一个新数据类型,它只是为已有的数据类型创建了一个别名。因此,它常用于简化语法。以下是一个简单的例子:
// 将 'BYTE' 做为 'unsigned char' 的别名
typedef unsigned char BYTE;
一旦 BYTE
被指定为 unsigned char
的别名,就可以这样使用:
// 使用 'BYTE' 作为 'unsigned char' 的缩写
BYTE b1, b2;
typedef 用于结构体
typedef
关键字还可以给结构体数据类型创建别名:
// 声明并定义数据类型 'struct Point'
struct Point
{
int x;
int y;
};
// 指定 'POINT' 为 'struct Point' 的别名
typedef struct Point POINT;
糟糕,我们又使用了冗长的方式!首先我们声明并定义了数据类型 struct Point
,然后利用 typedef
关键字为结构体数据类型创建别名 POINT