[RTOS 学习记录] 预备知识:C语言结构体

这篇文章是我阅读《嵌入式实时操作系统μCOS-II原理及应用》后的读书笔记,记录目的是为了个人后续回顾复习使用。

《嵌入式实时操作系统μC/OS-Ⅱ原理及应用》这本书的前言部分有提到:

C 指针看起来像是一个复习的内容,其实是要重点强调 C 指针中的函数指针,因为这种数据类型在操作系统软件中使用的频率太高了,而高校的 C 语言教学又大多不把它当作重点,所以致使相当一部分高校学生甚至不知道函数指针为何物。除了 C 指针之外,C 语言中的关键字 typedef 及其常用方法也是由于上述原因而被初学者忽视,从而造成了学习上的困难,因此在第 2 章也增加了这方面的内容。当然,因为本书的宗旨不是介绍 C 语言,所以仅依靠本书的寥寥数语并不能真正使读者完全掌握函数指针,但起码能使读者知道基础的欠缺之所在,从而主动去查找和阅读文献。

之前,我们已经复习了 C 语言指针的基础知识。

因此,接下来我们需要复习 C 语言中的结构体这部分的知识内容,为接下来的实时操作系统的学习打基础。


下方内容翻译自 EmbeetleEmbedded C Tutorial,在翻译的过程中,我把自己上机实验的结果,以截图的方式插入到相应的位置。

结构体

结构体是一种复合数据类型,它用于将某些(可能不同)类型的成员组合成一种单一类型。

结构体是一种用户定义的数据类型,允许将不同类型的数据组合在一起。结构体中的各个元素称为成员

结构体可以包含任意数量的成员,这些成员可以是任何数据类型。它有点类似于数组——但数组只能包含相同类型的数据。

结构体基础

请记住上文结构体中的定义:

结构体是一种用户定义的数据类型,允许将不同类型的数据组合在一起。结构体中的各个元素称为成员

严格意义上来说,“结构体”或“结构”指的是数据类型。然而,它也常用于代指由数据类型实例化出来的变量,这时就容易引起混淆!

为了严谨地避免这种歧义,我们将从不单独使用“结构体”这个术语。我们将始终使用“结构体类型”或“结构体变量”组合词:

  • 结构体类型本质上是一种数据类型。数据类型仅在编译时存在!因此,仅仅指定结构体类型不会占用任何内存。
  • 一旦指定了结构体类型,我们就可以用它实例化出结构体变量。每个结构体变量都会消耗一部分内存——就像其他变量一样(整数、浮点数等)。

将其比作有一个食谱并用它实际烹饪菜肴:
recipe_dish

结构体类型是食谱,结构体变量是菜肴。只有菜肴才会占用餐桌上的空间。

我们将首先看看如何声明和定义结构体类型。在第 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

运行结果如下图所示:
image-20240804090618937

我知道你现在在想什么,我都快闻到味儿了:
verbose_comic

确实,我们分步骤做了每件事:

  • 声明结构体数据类型;
  • 定义结构体数据类型;
  • 声明一个结构体变量;
  • 定义结构体变量;
  • 初始化结构体变量。

当然,在实际情况下这样详细地描述是不合理的,但这是一次极好的学习经验!在下一章节中,我们将看到简写符号。换句话说,我们将学习如何将上述几个步骤糅合进单个表达式中。

结构体的简写表示法

回顾我们在上一章节中如何创建结构体:

  1. 声明结构体数据类型;
  2. 定义结构体数据类型;
  3. 声明结构体变量;
  4. 定义结构体变量;
  5. 初始化结构体变量。

这显得非常冗长,让我们看看如何简化这一过程。

压缩表示法

首先,让我们从结构体数据类型的声明和定义开始:

// 声明数据类型 '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

运行结果如下图所示:
image-20240804092958501

匿名结构体

在某些情况下,我们只希望从一个结构体数据类型中实例化一个(或几个)变量,我们不打算以后再实例化更多的变量,那么给这个结构体数据类型命名就没有意义,只需省略名称:

// 声明并定义一个匿名结构体数据类型,然后
// 从中声明并定义变量 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。这两个动作可以合并为一个表达式:

// 声明并定义数据类型 'struct Point',然后
// 指定 'POINT' 作为该数据类型的别名。
typedef struct Point
{
    int x;
    int y;
} POINT;

一旦结构体数据类型有了别名,就可以通过两种方式实例化变量——使用或不使用别名:

// 声明变量 a 和 b
extern struct Point a;
extern POINT b;

// 定义变量 a 和 b
struct Point a;
POINT b;

我将别名写成大写而结构体名写成小写,并没有特别的原因,只要它们是不同的标识符就可以。

typedef 用于匿名结构体

我们使用 typedefstruct Point 指定了别名 POINT。之后,我们有两种方式从结构体中实例化变量——使用或不使用别名,这有点愚蠢。更合理的做法是使用 typedef 为匿名结构体指定一个别名:

// 声明并定义一个匿名结构体数据类型,然后
// 指定 'POINT' 作为该数据类型的别名。
typedef struct
{
    int x;
    int y;
} POINT;

这样做时,我们本质上为匿名结构体去掉了匿名。现在我们只能使用别名来实例化变量:

// 声明变量 a 和 b
extern POINT a;
extern POINT b;

// 定义变量 a 和 b
POINT a;
POINT b;

很聪明,对吧?
img

无论如何,事实是大多数人更喜欢使用 typedef 的方式,这看起来更简洁。

嵌套结构体

结构体可以嵌套,例如:

// C 代码测试文件
// ================
#include <stdio.h>

// 声明并定义一个匿名结构体数据类型,然后
// 指定 'Point' 作为该数据类型的别名。
typedef struct
{
    int x;
    int y;
} Point;

// 声明并定义一个匿名结构体数据类型,然后
// 指定 'Line' 作为该数据类型的别名。
typedef struct
{
    Point a;
    Point b;
} Line;

int main()
{
    Line line;
    line.a.x = 3;
    line.a.y = 8;
    line.b.x = 7;
    line.b.y = 9;

    printf("line.a.x = %d\n", line.a.x);
    printf("line.a.y = %d\n", line.a.y);
    printf("line.b.x = %d\n", line.b.x);
    printf("line.b.y = %d\n", line.b.y);
}

运行结果如下图所示:
image-20240804094854377

在这个例子中,结构体 Line 包含两个 Point 结构体作为其成员。要访问这些点的成员,只需再使用一级点号表示法即可。

结构体指针

从理论上讲,“结构”或“结构体”指的是数据类型,尽管有时也用来指代该数据类型的结构体变量(实例)。我们在结构体基础章节中已经提到过这种歧义性。

当谈到“结构体指针”时,我们实际上指的是“指向结构体实例的指针”,请记住这一点。

声明和定义

如果我们已经指定了数据类型 struct Point,那么我们可以像下面这样声明和定义一个指向该结构体实例的指针:

// 声明指针
struct Point *p;

// 定义指针
extern struct Point *p;

通常,我们会有一个用别名引用的 typedef 结构体,如 POINT

// 声明指针
POINT *p;

// 定义指针
extern POINT *p;

如你所见,指向结构体实例的指针的声明和定义与其他指针的用法没有区别。

初始化

要初始化指针,它需要设置为其他变量的地址——在当前这种情况下是结构体实例的地址。假设 a 是这样一个变量,那么:

// 初始化指针
p = &a;

指针使用

一旦指针被声明、定义和初始化后,它就可以像它指向的变量一样使用。为此,我们需要在指针前加上 *(解引用)操作符。在这方面,没有什么特别之处。

// 通过指针访问成员 x
(*p).x = 5; // 等效于 a.x = 5;

有一种简写表示法:

// 通过指针访问成员 x
p->x = 5; // 等效于 a.x = 5;

换句话说:

p->x 等效于 (*p).x

请参考以下示例:

// C 代码测试文件
// ================
#include <stdio.h>

// 声明并定义数据类型 'struct Point'
struct Point
{
    int x;
    int y;
};

// 指定 'POINT' 为 'struct Point' 的别名
typedef struct Point POINT;

// 实例化结构体:声明并定义变量 a
POINT a;

// 声明、定义并初始化指向 a 的指针
POINT *p = &a;

int main()
{
    // 初始化结构体变量 a
    a.x = 3;
    a.y = 5;
    
    printf("a.x = %d\n", a.x);
    printf("a.y = %d\n", a.y);
    printf("p->x = %d\n", p->x);
    printf("p->y = %d\n", p->y);
}

这段代码展示了如何使用结构体指针访问结构体成员。

运行结果如下图所示:
image-20240804133951999

结构体数组

“结构体数组”实际上是指“结构体实例的数组”。事实上,这并没有什么特别之处,它只是一个数组,每个元素恰好是一个结构体实例。唯一特别的是,我们可以使用列表表示法来初始化数组:

// 声明、定义并初始化结构体数组
struct Point my_arr[3] = {
    {.x=4, .y=5}, // 初始化第一个元素
    {.x=6, .y=7}, // 初始化第二个元素
    {.x=8, .y=9}  // 初始化第三个元素
};

然后我们可以这样访问成员:my_arr[0].x

初始化甚至可以更简短:

// 声明、定义并初始化结构体数组
struct Point my_arr[3] = { {4, 5}, {6, 7}, {8, 9} };

位域

MCU 在其内存中保留了一部分用于特殊功能寄存器(SFRs)。这些寄存器的详细信息可以在 MCU 的数据手册中找到。例如,以下是 dsPIC33FJ256MC710A 的 PORTA 和 LATA 寄存器:
image-20240804135214932

它们在 RAM 中有固定的位置:地址分别是 0x02C20x02C4。由于这是一个 16 位处理器,它们中的每一个都是 16 位宽的。通过 LATA 寄存器,可以强制 MCU 的引脚电平是高或低;PORTA 寄存器用于感知它们的状态。但如何从(往)这些寄存器中读(写)单个位呢?

我们可以使用位掩码来实现。位掩码是一种巧妙的使用按位操作符(例如 &|)来操纵变量中某些位的方法,而不影响其他位。然而,这种方法很快会导致代码难以阅读。

更优雅的方法是使用位域

位域基础

位域是结构体中的无符号整数成员,占据指定数量的相邻位。例如:

typedef struct
{
    unsigned int lo: 1;
    unsigned int mid: 6;
    unsigned int hi: 1;
} FooBits;

在这个例子中,指定了一个匿名结构体,并给它取名为 FooBits。结构体有三个位域:lomidhi。它们每一个都是无符号整数类型,并且指定了各自的位数。无符号整数类型甚至可以写成 unsigned

typedef struct
{
    unsigned lo: 1;
    unsigned mid: 6;
    unsigned hi: 1;
} FooBits;

现在用这个结构体实例化一个变量:

// 声明并定义来自结构体 'FooBits' 的变量 'foo'
FooBits foo;

// 初始化位域
foo.lo = 1;
foo.mid = 8;
foo.hi = 0;

本质上,这与普通的结构体变量非常相似。我们可以使用点号表示法来访问位域成员。结构体变量也可以使用列表表示法进行初始化(如果结构体变量的定义和初始化在同一条语句中进行,则可以省略类型转换 (FooBits)):

// 声明并定义来自结构体 'FooBits' 的变量 'foo'
FooBits foo;

// 初始化位域
foo = (FooBits){1, 8, 0};

缺失位

许多 MCU 的寄存器都有未使用的位。本章节开头的例子显示了 PORTA 和 LATA 寄存器中第 8 位和第 11-13 位是未使用的。这些未使用的位域在结构体定义中为简化起见不命名:

typedef struct
{
    unsigned lo: 1; // 位域命名为 'lo'
    unsigned : 6;   // 匿名位域
    unsigned hi: 1; // 位域命名为 'hi'
} FooBits;

在这个例子中,位域 lohi 之间的 6 位是不可达的——这正是我们所需要的。初始化结构体变量时,也应忽略这些位:

// 声明并定义来自结构体 'FooBits' 的变量 'foo'
FooBits foo;

// 初始化位域,忽略匿名位域
foo = (FooBits){1, 0};

示例

这是 Microchip 定义的结构体 LATAbits 的样子:

// 声明变量 LATA
extern volatile uint16_t LATA __attribute__((__sfr__));

// 指定类型 'struct tagLATABITS' 并提供别名
// 'LATABITS'。
typedef struct tagLATABITS {
  uint16_t LATA0:1;
  uint16_t LATA1:1;
  uint16_t LATA2:1;
  uint16_t LATA3:1;
  uint16_t LATA4:1;
  uint16_t LATA5:1;
  uint16_t LATA6:1;
  uint16_t LATA7:1;
  uint16_t :1;
  uint16_t LATA9:1;
  uint16_t LATA10:1;
  uint16_t :3;
  uint16_t LATA14:1;
  uint16_t LATA15:1;
} LATABITS;

// 声明变量 'LATAbits' 为该结构体的实例。
extern volatile LATABITS LATAbits __attribute__((__sfr__));

Microchip 在这里声明了(但没有定义!)两个变量:

  • 类型为 uint16_t(16 位整数)的 LATA
  • 类型为 LATABITS(带位域的结构体,总共 16 位)的 LATAbits

由于它们未被定义,编译器不会为它们分配内存地址,extern 关键字处理这一点。更进一步的是,__attribute__((__sfr__)) 编译器指令暗示编译器地址分配将在构建过程的最后阶段:链接步骤中进行。链接脚本提供绝对地址:

PORTA        = 0x2C2;
_PORTA       = 0x2C2;
_PORTAbits   = 0x2C2;
LATA         = 0x2C4;
_LATA        = 0x2C4;
_LATAbits    = 0x2C4;

变量 LATALATAbits 都映射到相同的地址 0x2C4。变量 LATA 是一个 16 位整数,我们可以整体操作。LATAbits 总共也是 16 位,但它的数据类型是结构体,因此它提供了对各个位的细粒度访问!

紧凑成员

当你看到这样的代码:

// 指定匿名结构体类型,并通过别名 'Foo' 提供
typedef struct
{
    char a;
    char b;
} Foo;

// 声明、定义并初始化一个结构体变量
Foo foo = {'A', 'B'};

你可能认为变量 foo 在内存中的样子如下:
image-20240804141936487

然而,这并不一定正确!编译器通常会根据 CPU 的位数对成员进行对齐。换句话说,在一个 16 位的 MCU 上,我们可能会得到这样的情况:
image-20240804142140763

变量 foo 现在消耗了两个 16 位的内存单元,每个内存单元的下半部分用于存储 char 类型的数据。

在大多数情况下,这无关紧要。但是当这很重要时,我们需要使用编译器指令来紧凑成员:

typedef struct
{
    char a;
    char b __attribute__((__packed__)); // 成员 'b' 紧跟在 'a' 之后
} Foo;

在上面的例子中,成员 b 被紧凑存放:它紧跟在前一个成员后面,没有任何填充。

如果有多个成员需要紧凑存放,我们可以将编译器指令放在结构体的顶部:

typedef struct __attribute__((__packed__))
{
    char a;
    char b;
    char c;
    char d;
} Foo;

注意嵌套结构体!例如:

typedef struct
{
    uint8_t x;
    uint8_t y;
} Point;

typedef struct __attribute__((__packed__))
{
    Point a;
    Point b;
} Line;

结构体 Line 是紧凑的,但这并不意味着结构体 Point 也是紧凑的!换句话说:成员 ab 之间不会有填充,但成员 xy 之间仍然可能有填充。

上方内容翻译自 EmbeetleEmbedded C Tutorial


结构体的应用

RTOS 中的各种资源的控制块是结构体类型对象,因此有“没有结构体类型就没有操作系统”的说法,由此可见,C 语言的结构体类型在 RTOS 中作用巨大。

基本应用

为下图所示的个人基本信息表声明一个结构体类型,然后定义这个类型的对象并为其成员赋初值,最后输出它们
image-20240804150420367

测试用程序代码如下:

#include <stdio.h>
#include <stdlib.h>

struct personal
{
  char *name;
  char *sex;
  char *birthday;
  char *nationality;
  char *health;
  int age;
  char *height;
  char *email;
  char *address;
};

int main(void)
{
  struct personal ps1;
  
  ps1.name = "ZhangSan";
  ps1.sex = "male";
  ps1.birthday = "5/1/2000";
  ps1.nationality = "han";
  ps1.health = "good";
  ps1.age = 24;
  ps1.height = "180";
  ps1.email = "ZhangSan@qq.com";
  ps1.address = "BeiJing";
  
  printf("Name: \t%s\nSex: \t%s\nAge: \t%d\n", 
          ps1.name, ps1.sex, ps1.age);

  exit(0);
}

运行结果如下图所示:
image-20240804151602767

这段代码首先包含了两个头文件 #include <stdio.h>#include <stdlib.h>,分别用于输入输出操作和内存管理。

接着定义了一个名为 personal 的结构体,其中包含了成员变量 namesexbirthdaynationalityhealthageheightemailaddress,用于存储个人信息。

main 函数中,创建了一个结构体变量 ps1,并为其赋值:name 为 “ZhangSan”,sex 为 “male”,birthday 为 “5/1/2000”,nationality 为 “han”,health 为 “good”,age 为 24,height 为 “180”,email 为 “ZhangSan@qq.com”,address 为 “BeiJing”。

最后通过 printf 函数打印出 ps1 结构体变量中的 namesexage 信息,并通过 exit(0) 退出程序。

结构体嵌套

在上面个人基本信息表的基础上增加如下图所示的学习经历表,然后将这两个表格组合成一个学生信息表

image-20240804153859994

测试用程序代码如下:

#include <stdio.h>
#include <stdlib.h>

//声明个人基本信息表,即声明结构类型
struct personal
{
  char *name;
  char *sex;
  char *birthday;
  char *nationality;
  char *health;
  int age;
  char *height;
  char *email;
  char *address;
};

//声明学习经历表
struct studyExperience
{
  char *school;
  char *university;
};

//声明学生信息表
struct studentMessage
{
  struct personal ps1Tab;
  struct studyExperience stuExp;
};

int main(void)
{
  //定义个人基本信息表对象并初始化
  struct personal ps1 =
  {
    "ZhangSan",
    "male",
    "5/1/2000",
    "han",
    "good",
    24,
    "180",
    "ZhangSan@qq.com",
    "BeiJing"
  };
  //读取并输出结构体部分成员的值
  printf("Name: \t%s\nSex: \t%s\nAge: \t%d\n\n", 
          ps1.name, ps1.sex, ps1.age);
  
  //定义学习经历表对象并初始化
  struct studyExperience se =
  {
    "zhongshan 1 school",
    "guangdong university"
  };
  //读取并输出结构体成员的值
  printf("School: \t\t%s\nUniversity: \t%s\n\n", 
          se.school, se.university);
  
  //定义学生信息表对象并初始化
  struct studentMessage sm =
  {
    ps1,
    se
  };
  //输出部分成员值
  printf("Name: \t\t%s\nSchool: \t%s\n", 
          sm.ps1Tab.name, sm.stuExp.university);

  exit(0);
}

运行结果如下图所示:
image-20240804154225767

这个C语言程序定义了几个结构体类型来存储个人信息和学习经历,并在main函数中创建了这些结构体的实例,对它们进行了初始化,并打印了一些成员的值。以下是对程序的详细解释及需要注意的细节和知识点:

程序结构

  1. 结构体声明

    • struct personal:定义了一个结构体来存储个人基本信息,包括姓名、性别、生日、国籍、健康状况、年龄、身高、电子邮件和地址。这些成员中的一些(如name, sex, birthday等)是char*类型,即指向字符的指针。

    • struct studyExperience:定义了一个结构体来存储学习经历,包括学校和大学,这两个成员都是char*类型。

    • struct studentMessage:定义了一个结构体来存储学生的信息,它包含两个结构体成员,分别是personalstudyExperience类型的实例。

  2. main函数

    • 个人信息表对象初始化

      struct personal ps1 = {
        "ZhangSan",
        "male",
        "5/1/2000",
        "han",
        "good",
        24,
        "180",
        "ZhangSan@qq.com",
        "BeiJing"
      };
      

      这部分定义了一个personal类型的结构体对象ps1,并为各个成员赋了初值。需要注意的是,虽然这些值是字符串常量,但ps1结构体的成员是char*类型,这意味着它们是指向这些字符串常量的指针。这样做的好处是节省内存,但如果你需要修改这些字符串,你需要在堆上分配内存并使用strdup等函数复制字符串。

    • 学习经历表对象初始化

      struct studyExperience se = {
        "zhongshan 1 school",
        "guangdong university"
      };
      

      这部分定义了一个studyExperience类型的结构体对象se,并为学校和大学赋初值。

    • 学生信息表对象初始化

      struct studentMessage sm = {
        ps1,
        se
      };
      

      这部分定义了一个studentMessage类型的结构体对象sm,它包含了ps1se两个结构体的实例,分别对应个人信息和学习经历。

  3. 输出结构体成员

    printf("Name: \t%s\nSex: \t%s\nAge: \t%d\n\n", 
            ps1.name, ps1.sex, ps1.age);
    printf("School: \t\t%s\nUniversity: \t%s\n\n", 
            se.school, se.university);
    printf("Name: \t\t%s\nSchool: \t%s\n", 
            sm.ps1Tab.name, sm.stuExp.university);
    

    这些printf语句用于打印结构体成员的值。注意,%s格式说明符用于打印字符串,%d用于打印整数。

注意点和重要知识点

  1. 字符串和指针

    • 在这个程序中,字符串成员是用char*来声明的。这意味着这些成员存储的是指向字符串的指针,而不是实际的字符串数据。如果在程序中修改这些字符串数据,需要分配动态内存并使用strdup函数来复制字符串。
  2. 结构体成员的初始化

    • 结构体成员的初始化需要保持数据类型一致。例如,对于char*类型的成员,需要确保字符串常量的生命周期足够长,以防止在结构体使用期间字符串数据被修改或释放。
  3. 内存管理

    • 如果要在结构体中存储动态分配的内存,记得在程序结束之前释放这些内存,避免内存泄漏。
  4. 结构体的嵌套

    • studentMessage结构体通过将personalstudyExperience结构体作为成员实现了嵌套。结构体嵌套是组织和管理复杂数据的有效方法。
  5. exit函数

    • exit(0);用于正常终止程序。在许多简单程序中,它的作用类似于return 0;,表示程序正常结束。

结构类型的函数指针成员

测试用程序代码如下:

/*
 * 结构类型的函数指针成员
 */
#include <stdio.h>
#include <stdlib.h>

struct studentMessage;  //对象前置声明
void showMessage(struct studentMessage x);  //函数前置声明
//声明个人基本信息表,即声明结构类型
struct personal
{
  char *name;
  char *sex;
  char *birthday;
  char *nationality;
  char *health;
  int age;
  char *height;
  char *email;
  char *address;
};
//声明学习经历表
struct studyExperience
{
  char *school;
  char *university;
};
//声明学生信息表
struct studentMessage
{
  struct personal *ps1Tab;
  struct studyExperience *stuExp;
  void (*disPer)(struct studentMessage x);
};

int main(void)
{
  //定义个人基本信息表对象并初始化
  struct personal ps1 =
  {
    "ZhangSan",
    "male",
    "5/1/2000",
    "han",
    "good",
    24,
    "180",
    "ZhangSan@qq.com",
    "BeiJing"
  };
  //读取并输出结构体部分成员的值
  printf("Name: \t%s\nSex: \t%s\nAge: \t%d\n\n", 
          ps1.name, ps1.sex, ps1.age);
  
  //定义学习经历表对象并初始化
  struct studyExperience se =
  {
    "zhongshan 1 school",
    "guangdong university"
  };
  //读取并输出结构体成员的值
  printf("School: \t\t%s\nUniversity: \t%s\n\n", 
          se.school, se.university);
  
  //定义学生信息表对象并初始化
  struct studentMessage sm =
  {
    .ps1Tab = &ps1,
    .stuExp = &se,
    .disPer = showMessage
  };
  //通过结构类型对象的函数指针调用了函数 showMessage
  sm.disPer(sm);

  exit(0);
}

/*
 * 打印信息 
 */
void showMessage(struct studentMessage x)
{
  //输出部分成员值
  printf("Name: \t%s\nSchool: %s\n", 
          x.ps1Tab->name, x.stuExp->school);
}

运行结果如下图所示:
image-20240805215309238

结构体的仿类类型

测试用程序代码如下:

/*
 * 结构体的仿类类型
 */
#include <stdio.h>
#include <stdlib.h>

struct studentMessage;  //对象前置声明
void showMessage(struct studentMessage x);  //函数前置声明
void setAge(struct studentMessage x, int y);  //设置 age 值的函数前置声明
int getAge(struct studentMessage x);  //获取 age 值的函数前置声明
//声明个人基本信息表,即声明结构类型
struct personal
{
  char *name;
  char *sex;
  char *birthday;
  char *nationality;
  char *health;
  int age;
  char *height;
  char *email;
  char *address;
};
//声明学习经历表
struct studyExperience
{
  char *school;
  char *university;
};
//声明学生信息表
struct studentMessage
{
  struct personal *ps1Tab;
  struct studyExperience *stuExp;
  void (*disPer)(struct studentMessage x);
  void (*set_age)(struct studentMessage x, int y);  //set 函数指针
  int (*get_age)(struct studentMessage x);  //get 函数指针
};

int main(void)
{
  //定义个人基本信息表对象并初始化
  struct personal ps1 =
  {
    "ZhangSan",
    "male",
    "5/1/2000",
    "han",
    "good",
    24,
    "180",
    "ZhangSan@qq.com",
    "BeiJing"
  };
  //读取并输出结构体部分成员的值
  printf("Name: \t%s\nSex: \t%s\nAge: \t%d\n\n", 
          ps1.name, ps1.sex, ps1.age);
  
  //定义学习经历表对象并初始化
  struct studyExperience se =
  {
    "zhongshan 1 school",
    "guangdong university"
  };
  //读取并输出结构体成员的值
  printf("School: \t\t%s\nUniversity: \t%s\n\n", 
          se.school, se.university);
  
  //定义学生信息表对象并初始化
  struct studentMessage sm =
  {
    .ps1Tab = &ps1,
    .stuExp = &se,
    .disPer = showMessage,  //为函数指针赋值
    .set_age = setAge,
    .get_age = getAge
  };
  //通过结构类型对象的函数指针调用了函数 showMessage
  sm.disPer(sm);
  //调用 set_age 函数
  sm.set_age(sm, 35);
  //调用 get_age 函数并输出
  printf("Age: \t%d\n", sm.get_age(sm));
  
  exit(0);
}

/*
 * 打印信息 
 */
void showMessage(struct studentMessage x)
{
  //输出部分成员值
  printf("Name: \t%s\nSchool: %s\n", 
          x.ps1Tab->name, x.stuExp->school);
}
/*
 * 设置 age 值的函数
 */
void setAge(struct studentMessage x, int y)
{
  x.ps1Tab->age = y;
}
/*
 * 获取 age 值的函数
 */
int getAge(struct studentMessage x)
{
  return x.ps1Tab->age;
}

运行结果如下图所示:
image-20240805215625093

嵌套结构类型指针

测试用程序代码如下:

/*
 * 嵌套结构类型指针
 */
#include <stdio.h>
#include <stdlib.h>

//声明了一个只有一个成员的结构类型(猫猫类型)
struct cat
{
  char *color;  //猫猫的颜色
};
//声明嵌套结构类型(小猫类型),其中内嵌了 struct cat 对象
struct childCat
{
  struct cat c; //内嵌对象
  char *color;  //猫咪的颜色
};

int main(void)
{
  //定义一个包含有内嵌对象的外层对象
  struct childCat chi;
  chi.c.color = "white";  //为内嵌对象 color 成员赋值
  chi.color = "black";    //为外层对象 color 成员赋值
  
  //把首地址赋予了内嵌对象类型指针
  struct cat *p = (struct cat *)&(chi);
  //输出内嵌对象的 color 的值
  printf("Cat color: %s\n", 
          p->color);

  exit(0);
}

运行结果如下图所示:
image-20240805215745181

基类函数钩子

测试用程序代码如下:

/*
 * 基类函数钩子
 */
#include <stdio.h>
#include <stdlib.h>

//内嵌类型(基类)
struct cat
{
  char *color;  //猫猫的颜色
  void (*_show)(struct cat *p); //函数钩子
};
//外层类型(派生类)
struct childCat
{
  struct cat c; //内嵌对象
  char *color;  //猫咪的颜色
};

/*
 * 基类类型的 show 函数
 */
void showCat(struct cat *p)
{
  printf("Cat color: %s\n", p->color);
}

/*
 * 派生类类型的 show 函数
 */
void showChildCat(struct cat *p)
{
  //将 p 转换为派生类类型
  printf("Child Cat color: %s\n", 
        ((struct childCat *)p)->color);
}

int main(void)
{
  //定义基类对象并初始化
  struct cat stCat;
  stCat.color = "white";
  stCat._show = showCat;   //为钩子挂接函数 showCat
  //定义派生类对象并初始化
  struct childCat stChildCat;
  stChildCat.color = "black";  //为外层对象 color 成员赋值
  stChildCat.c._show = showChildCat;   //为钩子挂接函数 showChildCat
  
  //使用对象调用钩子
  stCat._show(&stCat);
  stChildCat.c._show((struct cat *)&stChildCat);
  
  //定义两个基类类型的指针变量
  struct cat *p1, *p2;
  p1 = &stCat;
  p2 = (struct cat *)&stChildCat;
  
  //使用基类类型的指针调用钩子(调用界面完全相同)
  p1->_show(&stCat);
  p2->_show((struct cat *)&stChildCat);

  exit(0);
}

程序运行结果如下图所示:

image-20240805215840003

实现多态特性

测试用程序代码如下:

/*
 * 实现多态特性
 */
#include <stdio.h>
#include <stdlib.h>

//内嵌类型(基类)
struct cat
{
  char *color;  //猫猫的颜色
  void (*_show)(struct cat *p); //函数钩子
};
//外层类型(派生类)
struct childCat
{
  struct cat c; //内嵌对象
  char *color;  //猫咪的颜色
};

/*
 * 基类类型的 show 函数
 */
void showCat(struct cat *p)
{
  if (NULL == p)  //指针为空返回
    return;
  printf("Cat color: %s\n", p->color);
}

/*
 * 派生类类型的 show 函数
 */
void showChildCat(struct cat *p)
{
  if (NULL == p)  //指针为空返回
    return;
  //将 p 转换为派生类类型
  printf("Child Cat color: %s\n", 
        ((struct childCat *)p)->color);
}

/*
 * 同一个函数因接收对象的类型不同,其 show 的结果也不同
 */
void show(struct cat *_cat)
{
  if (NULL == _cat)  //指针为空返回
    return;
  //调用钩子上的函数
  _cat->_show(_cat);
}

int main(void)
{
  //定义基类对象并初始化
  struct cat stCat;
  stCat.color = "white";
  stCat._show = showCat;   //为钩子挂接函数 showCat
  //定义派生类对象并初始化
  struct childCat stChildCat;
  stChildCat.color = "black";  //为外层对象 color 成员赋值
  stChildCat.c._show = showChildCat;   //为钩子挂接函数 showChildCat
  
  //以 struct childCat * 类型对象为实参调用 show()
  show((struct cat *)&stChildCat);
  //以 struct cat * 类型对象为实参调用 show()
  show(&stCat);

  exit(0);
}

程序运行结果如下图所示:
image-20240805220106669

计算外层对象指针的宏定义

测试用程序代码如下:

/*
 * 计算外层对象指针的宏定义
 */
#include <stdio.h>
#include <stdlib.h>

//offsetof 和 container_of 的定义
#define offsetof(TYPE, MEMBER)  ((size_t)&((TYPE*)0)->MEMBER)
#define container_of(ptr, type, member) ({\
        const typeof(((type *)0)->member) *__mptr = (ptr); \
        (type *)((char *)__mptr - offsetof(type, member));})

//定义了结构 struct parentStruct (本例中,它为内嵌对象的结构类型)
struct parentStruct
{
  int a;
};
//定义了结构 struct childStruct (本例中,它为外层对象的结构类型)
struct childStruct
{
  int b;
  struct parentStruct parent;
};

/*
 * 参数为内嵌结构类型指针的函数 show()
 */
void show(struct parentStruct *p)
{
  //使用宏 container_of 获得外层对象指针
  struct childStruct *pChild = container_of(p, struct childStruct, parent);
  printf("a == %d\nb == %d\n", p->a, pChild->b);
}

int main(void)
{
  //定义外层对象并初始化
  struct childStruct child;
  child.b = 53;
  child.parent.a = 77;  //给内嵌对象成员 a 赋值
  show(&child.parent);
  
  exit(0);
}

运行结果如下图所示:
image-20240805220331904

  • 21
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值