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

news2024/11/24 7:16:13

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

文章目录

  • 结构体
    • 结构体基础
      • 声明和定义结构体类型
      • 声明和定义结构体变量
      • 初始化结构体变量
        • 初始化各个成员
        • 使用列表符号初始化
      • 使用结构体变量
      • 综上
    • 结构体的简写表示法
      • 压缩表示法
      • 匿名结构体
      • typedef
        • typedef 关键字
        • typedef 用于结构体
        • typedef 用于匿名结构体
    • 嵌套结构体
    • 结构体指针
      • 声明和定义
      • 初始化
      • 指针使用
    • 结构体数组
    • 位域
      • 位域基础
      • 缺失位
      • 示例
    • 紧凑成员
  • 结构体的应用
    • 基本应用
    • 结构体嵌套
      • 程序结构
      • 注意点和重要知识点
    • 结构类型的函数指针成员
    • 结构体的仿类类型
    • 嵌套结构类型指针
    • 基类函数钩子
    • 实现多态特性
    • 计算外层对象指针的宏定义

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

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

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

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


下方内容翻译自 Embeetle 的 Embedded 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 之间仍然可能有填充。

上方内容翻译自 Embeetle 的 Embedded 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1983031.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C语言程序设计-[5] 输入输出语句

C语言提供了一些输入输出的库函数&#xff0c;使用库函数&#xff0c;必须将相应的头文件“stdio.h”包含进来。 输入输出库函数可分为三类&#xff1a;字符输入输出函数、字符串输入输出函数和格式化输入输出函数。前两类功能单一&#xff0c;使用起来相对简单&#xff0c;以…

消息队列:Kafka吞吐量为什么比RocketMQ大

根据资料显示RocketMQ每秒能处理10W量级数据&#xff0c;而Kafka能处理17W量级数据。 这两者差别主要再使用的零拷贝技术不一样。 再什么情况下零拷贝技术诞生了 为了防止消息队列中的消息因为各种意外情况丢失&#xff0c;要对消息进行持久化处理&#xff0c;将其存储在磁盘…

Dubbo未授权访问漏洞

Dubbo是阿里巴巴公司开源的一个高性能优秀的 服务框架&#xff0c;使得应用可通过高性能的 RPC 实现服务的输 出和输入功能&#xff0c;可以和 Spring框架无缝集成。dubbo 因配置不当导致未授权访问漏洞。 》》》漏洞复现《《《 步骤一&#xff1a;使用以下语句在Fofa上进行资…

STM32智能小车(循迹、跟随、避障、测速、蓝牙、wifi、4g、语音识别)总结

前言 有需要帮忙代做51和32小车或者其他单片机项目&#xff0c;课程设计&#xff0c;报告&#xff0c;PCB原理图的小伙伴&#xff0c;可以在文章最下方加我V交流咨询&#xff0c;本篇文章的小车所有功能实现的代码还有硬件清单放在资源包里&#xff0c;有需要的自行下载即可&a…

Elasticsearch(高性能分布式搜索引擎)-上篇

Elasticsearch&#xff08;高性能分布式搜索引擎&#xff09; 文章目录 Elasticsearch&#xff08;高性能分布式搜索引擎&#xff09;1 初识elasticsearch1.1 认识和安装1.2 倒排索引1.3 IK分词器1.4 基础概念1.4.1 elasticsearch与数据库对比 2 索引库的操作2.1 Mapping映射属…

FutureTask详解

FutureTask详解 1、FutureTask简介 FutureTask主要用于异步任务的执行和结果获取。其最重要的特性就是可以被提交到线程池中执行&#xff0c;同时也可以用来获取执行结果或检查任务的状态。 2、FutureTask内部结构 继承结构 public class FutureTask<V> implements …

Materialise Magics对齐实现分件对齐

零件‘对齐’三步曲之一 当我们需要对两个零件进行重叠&#xff0c;平行等对齐操作时&#xff0c;可以使用Magics->位置-> ‘对齐’ 功能。通过添加有效的约束条件&#xff0c;就可以实现自动对齐零件啦。 让我们看一下当两个单一方向的零件如何利用边线约束来对齐吧&a…

pxe自动安装linux

实验环境 1.rhel7主机 2开启主机图形&#xff08;本人最小化安装&#xff0c;先下载&#xff09; 3配置网络 4关闭VMware dhcp功能 5能够自动安装系统 完成rhedhat7图形,kickstart,启动图形化制作工具 安装kickstart 启动图形化制作工具 在ks.cfg可以添加安装时下载的包 …

C# 高级数据处理:深入解析数据分区 Join 与 GroupJoin 操作的应用与实例演示

文章目录 一、概述二. 数据分区 (Partitioning)三、Join 操作符1. Join 操作符的基本用法2. Join 操作符示例 四、GroupJoin 操作符1. GroupJoin 操作符的基本用法2. GroupJoin 操作符示例 总结 在数据处理中&#xff0c;联接&#xff08;Join&#xff09;操作是一种非常常见的…

Unity:Camera 对象操作的技术指南

请关注微信公众号&#xff1a;拾荒的小海螺 博客地址&#xff1a;http://lsk-ww.cn/ 1、简述 在Unity中&#xff0c;Camera 是一个至关重要的组件&#xff0c;用于渲染场景中的图像。无论是3D游戏还是2D游戏&#xff0c;Camera 都是必不可少的元素。通过合理配置和操作 Camer…

2024华数杯全国大学生数学建模竞赛B题思路-VLSI电路单元的自动布局-关键路径优化的多层划分算法

在粗化过程中&#xff0c;只考虑了如何匹配以使得后续划分 中有尽可能少的割边数&#xff0c;但没有将关键路径和割边的时延视为划分信息的一部分&#xff0c; 这可能导致关键路径较多地被切割&#xff0c;增加了关键路径时延&#xff0c;影响了并行度。另外&#xff0c; 初始划…

卡码网--数组篇(有序数组的平方)

系列文章目录 卡码网–数组篇(二分法) 卡码网–数组篇(移除元素) 文章目录 系列文章目录前言977.有序数组的平方 前言 代码随想录详情链接 977.有序数组的平方 力扣链接&#xff1a;https://leetcode.cn/problems/squares-of-a-sorted-array/description/ Step 1: 读题&…

Xinstall全链路数据统计,助力推广者破解社交分享难题

在数字营销的时代&#xff0c;社交分享推广已成为App运营的重要手段。然而&#xff0c;推广者们在进行社交分享推广时&#xff0c;往往面临着诸多痛点。其中&#xff0c;最关键的问题便是如何准确、高效地统计推广效果。今天&#xff0c;我们就来聊聊Xinstall这一神奇工具&…

【优秀python大屏】基于python flask的广州历史天气数据应用与可视化大屏

摘要 气象数据分析在各行各业中扮演着重要的角色&#xff0c;尤其对于农业、航空、海洋、军事、资源环境等领域。在这些领域中&#xff0c;准确的气象数据可以对预测未来的自然环境变化和采取行动来减轻负面影响的决策起到至关重要的作用。 本系统基于Python Flask框架&#…

五种IO模型(阻塞,非阻塞,多路复用[select, poll, epoll],信号驱动,异步IO)

五种IO模型&#xff08;阻塞&#xff0c;非阻塞&#xff0c;信号驱动[select, poll, epoll]&#xff0c;多路复用&#xff0c;异步IO&#xff09; 本章节代码&#xff1a;一&#xff0c;五种IO模型阻塞IO非阻塞IO多路复用&#xff08;也叫多路转接&#xff09;信号驱动异步IO例…

Solaris10(SPARC/x86)源码编译安装64位Python

Solaris10(SPARC/x86)源码编译安装64位Python 系统自带的Python版本为32位&#xff0c;需要安装64位版本Python。 solariskalami>python Python 3.3.6 (default, Mar 18 2016, 14:34:49) [GCC 5.2.0] on sunos5 Type "help", "copyright", "cred…

redis在Dokcer的安装使用

1 redis 安装和配置 # redis 是什么 开源&#xff1a;基于c编写的&#xff0c;早起版本2w3千行 基于键值对的存储系统&#xff1a;字典形式 多种数据结构&#xff1a;字符串&#xff0c;hash&#xff0c;列表&#xff0c;集合&#xff0c;有序集合 高性能&#xff0c;功能丰富…

ORB-SLAM2运行环境搭建

操作系统&#xff1a;Ubuntu20.04 1.安装Eigen3 推荐大家安装版本 3.2.10 链接&#xff1a;https://eigen.tuxfamily.org/index.php?titleMain_Page mkdir build cd build cmake .. sudo make install2.安装Pangolin 推荐安装0.5版本 链接&#xff1a;https://github.com…

【生成式AI-二-强大的AI下我们可以做什么】

强大的AI下我们可以做什么 人工智能的厉害之处我们可以作什么评估模型好坏的难度prompt engineering微调fine tune 人工智能的厉害之处 人工智能并不是忽然就爆火的&#xff0c;事实上&#xff0c;很久以前就已经有深度学习、机器学习这些概念了&#xff0c;那现在的人工智能和…

MybatisPlus常见注解及配置

什么是MybatisPlus? MybatisPlus&#xff08;简称MP&#xff09;是一个基于MyBatis的增强工具&#xff0c;它在MyBatis的基础上进行了扩展&#xff0c;旨在简化MyBatis的操作&#xff0c;提高开发效率。MybatisPlus继承了MyBatis原生的所有特性&#xff0c;并添加了一些额外的…