0%

1、基础内容

状态

  • 运行态:任务正在运行的状态。如果使用的是核处理器的话那么不管在任何时刻永远都只有一个任务处于运行态。
  • 就绪态:处于就绪态的任务是那些已经准备就绪(这些任务没有被阻塞或者挂起 可以运行的任务,但是处于就绪态的任务还没有运行,因为有一个同优先级或者更高优先级的任务正在运行)
  • 阻塞态:如果一个任务当前正在等待某个外部事件的话就说它处于阻塞态,比如说如果某个任务调用了函数 vTaskDelay()的话就会进入阻塞态, 直到延时周期完成。(任务在等待队列、信号量、事件组、通知或互斥信号量的时候也会进入阻塞态 。任务进入阻塞态会有一个超时时间,当超过这个超时时间任务就会退出阻塞态,即使所等待的事件还没有来临
  • 挂起态:任务进入挂起态以后也不能被调度器调用进入运行态,但是进入挂起态的任务没有超时时间。

需要注意的是,当任务从阻塞/挂起态退出时是进入的就绪态而非直接进入运行态。

优先级

  • 优先级数字越表示任务优先级越低 (除使用Cortex内核的情况!!!!!!!!!)

    例如当一个任务A正在运行,另外一个任务B(优先级高于A)阻塞时间到或者事件触发处于就绪态,那么B会从A那抢占处理器,B开始运行,A停止运行

    ※※※※※当使用cortex内核时相反(例如STM32)※※※※※

  • 时间片轮转

    当宏configUSE_TIME_SLICING定义为1 的时候多个任务可以共用一个优先级,数量不限。此时处于就绪态的优先级相同的任务就会使用时间片轮转调度器获取运行时间

任务堆栈

FreeRTOS之所以能正确的恢复一个任务的运行就是因为有任务堆栈在保驾护航,任务调度器在进行任务切换的时候会将当前任务的现场 (CPU寄存器值等 )保存在此任务的任务堆栈中,等到此任务下次运行的时候就会先用堆栈中保存的值来恢复现场 ,恢复现场以后任务就会接着从上次中断的地方开始运行。

2、机制

任务通知

任务通知来代替信号量、消息队列、事件标志组等这些东西。使用任务通知的话效率会更高。

STM32支持的低功耗模式

  • sleep睡眠模式

    在 SLEEP 模式下,只有内核停止了工作,而外设仍然在运行。
    在进入 SLEEP 模式后,所有中断(‘外部中断、串口中断、定时器中断等’)均可唤醒 MCU,从而退出 SLEEP 模式。

  • stop停止模式

    在进入 STOP 模式后,此时 SYSTICK 也会被停掉,只有外部中断(EXTI)才能唤醒 MCU由于 RTC 中断挂在外部中断线上,所以 RTC 中断也能唤醒 MCU)

  • standby待机模式

    在 STANDBY 模式下,内核、所有的时钟、以及后备 1.2V 电源全部停止工作。

    从 STANDBY 模式中唤醒后,系统相当于执行了一次复位操作,程序会从头来过。

Tickless 模式

FreeRTOS系统提供的低功耗模式,当处理器进入空闲任务周期以后就关闭系统节拍中断(滴答定时器中断),只有当其他中断发生或者其他任务需要处理的时侯处理器才会从低功耗模式中唤醒。

3、一些内核配置

  • configUSE_PREEMPTION

    为1时RTOS使用抢占式调度器,为0时RTOS使用协作式调度器(时间片)。(协作式操作系统是任务主动释放CPU后,切换到下一个任务。任务切换的时机完全取决于正在运行的任务)

  • configUSE_PORT_OPTIMISED_TASK_SELECTION

    某些运行FreeRTOS的硬件有两种方法选择下一个要执行的任务:通用方法和特定于硬件(硬件计算前导零指令)的方法

    • 设置为0 通用办法,可以用于所有FreeRTOS支持的硬件
    • 设置为1 硬件计算前导零指令,并非所有硬件都支持
  • configUSE_IDLE_HOOK

    设置为1使用空闲钩子(Idle Hook类似于回调函数),0忽略空闲钩子。

    空闲任务钩子是一个函数,这个函数由用户来实现,RTOS规定了函数的名字和参数,这个函数在每个空闲任务周期都会被调用。

    1
    2
    void vApplicationIdleHook(void);
    //这个钩子函数不可以调用会引起空闲任务阻塞的API函数(例如:vTaskDelay()、带有阻塞时间的队列和信号量函数

    使用空闲钩子函数设置CPU进入省电模式是很常见的。

  • configUSE_MALLOC_FAILED_HOOK

    如果定义并正确配置malloc()失败钩子函数,则这个函数会在pvPortMalloc()函数返回NULL时被调用。只有FreeRTOS在响应内存分配请求时发现堆内存不足才会返回NULL

    1
    void vApplicationMallocFailedHook( void);
  • configUSE_TICK_HOOK

    设置为1使用时间片钩子(Tick Hook),0忽略时间片钩子.

    时间片中断可以周期性的调用一个被称为钩子函数(回调函数)的应用程序。时间片钩子函数可以很方便的实现一个定时器功能。

    1
    2
    void vApplicationTickHook( void );
    //vApplicationTickHook()函数在中断服务程序中执行,因此这个函数必须非常短小,不能大量使用堆栈,只能调用以”FromISR" 或 "FROM_ISR”结尾的API函数。
  • 一些顾名思义的配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #define configCPU_CLOCK_HZ				( SystemCoreClock ) //CPU频率
    #define configTICK_RATE_HZ ( ( TickType_t ) 1000 ) //时钟节拍频率,这里设置为1000,周期就是1ms
    #define configMAX_PRIORITIES ( 32 ) //可使用的最大优先级
    #define configMINIMAL_STACK_SIZE ( ( unsigned short ) 128 )//空闲任务使用的堆栈大小
    #define configTOTAL_HEAP_SIZE ( ( size_t ) ( 64 * 1024 ) )//系统所有总的堆大小
    #define configMAX_TASK_NAME_LEN ( 16 ) //任务名字字符串长度

    #define configUSE_16_BIT_TICKS 0 //系统节拍计数器变量数据类型,
    //1表示为16位无符号整形,0表示为32位无符号整形
  • configIDLE_SHOULD_YIELD

    这个参数控制任务在空闲优先级中的行为。仅在满足下列条件后,才会起作用。

    • 使用抢占式内核调度(见第一点)
    • 用户任务使用空闲优先级

    使用同一优先级的多个任务,且该优先级大于空闲优先级时,这些任务反映在时间片上会获得相同的处理器时间。单当多个任务(不止空闲任务,还包括用户任务)共享空闲优先级时,如果configIDLE_SHOULD_YIELD为1,当用户任务运行时,空闲任务立刻让出CPU,但是空闲任务仍然会占据时间片中的一段时间,就会导致空闲任务与接下来的用户任务会共享一个时间片,即该用户任务占有时间片少于正常分配的时间片

    设置configIDLE_SHOULD_YIELD为0将阻止空闲任务为用户任务让出CPU,直到空闲任务的时间片结束。这确保所有处在空闲优先级的任务分配到相同多的处理器时间,但是,这是以分配给空闲任务更高比例的处理器时间为代价的。

  • configUSE_TASK_NOTIFICATIONS(很有用)

    置1将会开启任务通知功能,每个RTOS任务具有一个32位的通知值,RTOS任务通知相当于直接向任务发送一个事件,接收到通知的任务可以解除任务的阻塞状态(因等待任务通知而进入阻塞状态)。相对于以前必须分别创建队列、二进制信号量、计数信号量或事件组的情况,使用任务通知显然更灵活。更好的是,相比于使用信号量解除任务阻塞,使用任务通知可以快45%

  • configGENERATE_RUN_TIME_STATS

    设置宏configGENERATE_RUN_TIME_STATS为1使能运行时间统计功能。一旦设置为1,则下面两个宏必须被定义:

    • portCONFIGURE_TIMER_FOR_RUN_TIME_STATS()

      使用一个比运行时间更精准的基准定时器使统计更加精确,基准定时器中断频率要比统节拍中断快10~100倍。基准定时器中断频率越快,统计越精准,但能统计的运行时间也越短(比如,基准定时器10ms中断一次,8位无符号整形变量可以计到2.55秒,但如果是1秒中断一次,8位无符号整形变量可以统计到255秒)

    • portGET_RUN_TIME_COUNTER_VALUE()

      返回基准时钟的值以供计数(在定时器中使使长整形变量ulHighFrequencyTimerTicks自增)

    当我们配置了一个定时器中断且要使用时间统计时,需要在config.h中添加

    1
    2
    3
    extern volatile unsigned long ulHighFrequencyTimerTicks;
    #define portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() ( ulHighFrequencyTimerTicks = 0UL )
    #define portGET_RUN_TIME_COUNTER_VALUE() ulHighFrequencyTimerTicks

    C语言中的0UL和1UL

    • 0UL :无符号长整型0
    • 1UL :无符号长整型1
  • LIBRARY_LOWEST_INTERRUPT_PRIORITY & LIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY

    由于在使用cortex内核的硬件设备中优先级数值为越小,逻辑优先级越高,所以这里的最小优先级即为硬件的最低逻辑优先级,表现在数值上即为最大数值;而此处的configMAX_SYSCALL_INTERRUPT_PRIORITY是用来设置可以在中断服务程序中安全调用FreeRTOS API函数的最高中断优先级。优先级小于等于这个宏所代表的优先级时,程序可以在中断服务程序中安全的调用FreeRTOS API函数;如果优先级大于这个宏所代表的优先级,表示FreeRTOS无法禁止这个中断,在这个中断服务程序中绝不可以调用任何API函数。

    运行在大于configMAX_SYSCALL_INTERRUPT_PRIORITY的优先级中断是不会被RTOS内核所屏蔽的,因此也不受RTOS内核功能影响。这主要用于非常高的实时需求中。比如执行电机转向。但是,这类中断的中断服务例程中绝不可以调用FreeRTOS的API函数。

    4.一些报错

    configASSERT( ( portNVIC_INT_CTRL_REG & portVECTACTIVE_MASK ) == 0 )报错。

    此处报错,第一种是因为高于configMAX_SYSCALL_INTERRUPT_PRIORITY优先级的中断调用了RTOS的API导致的,解决办法,将中断优先级调低,比configMAX_SYSCALL_INTERRUPT_PRIORITY要低,就可以调用RTOS的API了。

    第二种是因为中断发送消息队列,发送信号量等操作使用了不带ISR结尾的API,而是调用了普通不带ISR的API导致的,解决办法,将API替换为带ISR结尾的API便可以解决问题。

1. auto 和 register

  • auto关键字

    • 只能修饰局部变量, 局部变量如果没有其它修饰符, 默认就是auto的
    • 特点: 随用随开, 用完即销
  • register关键字

    • 只能修饰局部变量, 原则上将内存中变量提升到CPU寄存器中存储, 这样访问速度会更快
    • 但是由于CPU寄存器数量相当有限, 通常不同平台和编译器在优化阶段会自动转换为auto

2. static

  • 对局部变量的作用

    • 延长局部变量的生命周期,从程序启动到程序退出,但是它并没有改变变量的作用域
  • 定义变量的代码在整个程序运行期间仅仅会执行一次

    原因是静态局部变量的初值是在编译时赋予的,在程序执行期间不再赋予初值

  • 对全局变量的作用

    • 默认情况下多个同名的全局变量共享一块空间, 这样会导致全局变量污染问题
    • 如果想让某个全局变量只在某个文件中使用, 并且不和其他文件中同名全局变量共享同一块存储空间, 那么就可以使用static

3. extern

  • 对局部变量的作用(无意义的)

    • extern不能用于局部变量
    • extern代表声明一个变量, 而不是定义一个变量, 变量只有定义才会开辟存储空间
    • 所以如果是局部变量, 虽然提前声明有某个局部变量, 但是局部变量只有执行到才会分配存储空间
  • 对全局变量的作用

    • 声明一个全局变量, 代表告诉编译器我在其它地方定义了这个变量, 你可以放心使用。举例来说,如果文件a.c需要引用b.c中变量int v,就可以在a.c中声明extern int v,然后就可以引用变量v。
  • 与包含头文件引用函数的区别

    对变量而言,如果你想在本源文件(例如文件名A)中使用另一个源文件(例如文件名B)的变量,方法有2种:(1)在A文件中必须用extern声明在B文件中定义的变量(当然是全局变量);(2)在A文件中添加B文件对应的头文件,当然这个头文件包含B文件中的变量声明,也即在这个头文件中必须用extern声明该变量,否则,该变量又被定义一次。

    对函数而言,如果你想在本源文件(例如文件名A)中使用另一个源文件(例如文件名B)的函数,方法有2种:(1)在A文件中用extern声明在B文件中定义的函数(其实,也可省略extern,只需在A文件中出现B文件定义函数原型即可);(2)在A文件中添加B文件对应的头文件,当然这个头文件包含B文件中的函数原型,在头文件中函数可以不用加extern。

  • 总结:

    使用extern时,会加速程序的编译(确切的说是预处理)的过程,节省时间。但是当需要对变量/函数名进行修改时需要把所有使用extern关键字的地方全部修改一次,而使用头文件包含只需更改一次,效率更高。

4. 条件编译

  • 是将代码编译进可执行程序, 而不是执行代码
  • 条件编译后面的条件表达式中不能识别变量,它里面只能识别常量和宏定义

5. typedef

  • C语言不仅提供了丰富的数据类型,而且还允许由用户自己定义类型说明符,也就是说允许由用户为数据类型取“别名”。

    • 基本数据类型

      1
      2
      3
      typedef int INTEGER;
      typedef INTEGER MyInteger;
      MyInteger a; // 等价于 int a;
    • 数组

      1
      2
      typedef char NAME[20]; // 表示NAME是字符数组类型,数组长度为20。然后可用NAME 说明变量
      NAME a; // 等价于 char a[20];
    • 结构体

      1
      2
      3
      4
      5
      6
      typedef struct {
      int age;
      char *name;
      } PersonType;
      PersonType person;
      //有多种写法,此写法可读性强一些
    • 指针

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      // 定义一个sum函数,计算a跟b的和
      int sum(int a, int b)
      {
      int c = a + b;
      printf("%d + %d = %d", a, b, c);
      return c;
      }
      typedef int (*MySum)(int, int);
      int main()
      {
      MySum p = sum;// 定义一个指向sum函数的指针变量p
      printf("%d",(*p)(1,2)); //3
      return 0;
      }

      详细解释可参考FreeRTOS的任务相关 | 小董的BLOG (gitee.io)中有详细案例

      也就是说,在正常的定义前加上typedef,即把该定义的变量改为一个新的类型,要再次定义该类型的变量时,直接使用该类型的名字即可。

6. const

  • 对变量声明只读特性,保护变量值以防被修改。

使用方法

  • 修饰常量、数组

    1
    2
    const int a[5]={1, 2, 3, 4, 5};
    a[1] = 55; // 错误
  • 修饰指针

    • const int *A; //const修饰指针,A可变,A指向的值不能被修改
    • int const *A; //const修饰指向的对象,A可变,A指向的对象不可变
    • int *const A; //const修饰指针A, A不可变,A指向的对象可变
    • const int *const A;//指针A和A指向的对象都不可变

ps.以下开始不是关键字

1、宏定义

  • 对程序中用双引号扩起来的字符串内的字符,不进行宏的替换操作

  • 宏定义与变量定义不同,宏定义仅是一种机械的字符替换,并不分配内存空间

  • 宏名的有效范围是从定义位置到文件结束。如果需要终止宏定义的作用域,可以用#undef命令

  • 定义一个宏时可以引用已经定义的宏名

    1
    2
    3
    4
    #define R  3.0
    #define PI 3.14
    #define L 2*PI*R
    #define S PI*R*R
  • 可用宏定义表示数据类型,使书写方便

    1
    2
    3
    4
    5
    6
    #define String char *
    int main()
    {
    String str = "This is a string!";
    return 0;
    }
  • C语言允许宏带有参数。在宏定义中的参数称为形式参数,在宏调用中的参数称为实际参数。(类似函数的使用)

    1
    2
    3
    4
    5
    6
    7
    8
    #define average(a, b) (a+b)/2

    int main ()
    {
    int a = average(10, 4);
    printf("平均值:%d", a); //7
    return 0;
    }
    • 带参数的宏在展开时,只作简单的字符和参数的替换,不进行任何计算操作。所以在定义宏时,一般用一个小括号括住字符串的参数。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      #include <stdio.h>
      #define D1(a) (2*a)
      #define D2(a) (2*(a))
      //计算结果最好也用括号括起来,小型计算不括问题应该不大
      int main ()
      {
      int b = D1(3+4);
      int c = D2(3+4);
      printf("%d", b); //输出等价于2*3+4=10
      printf("%d", c); //2*(3+4)=14
      return 0;
      }

2、动态存储分配

此前,用于存储数据的变量和数组在定义后,C编译程序通过定义的类型来了解其所需存储空间的大小并预先为其分配适当的存储空间,这些空间一经分配,在变量或数组的生存期内是固定不变的,所以这种方式也称为“静态存储分配”

C中还有一种“动态存储分配”的方法,在程序执行期间需要空间来存储数据时,通过申请分配指定的内存空间;当有闲置不用的空间时可以随时将其释放。C标准库提供了相关函数来进行对应操作:

库函数

malloc函数

函数名 malloc(unsigned int size);
参数 size:字节数
返回值 成功则返回一个类型为void的首地址,失败为null
功能 在内存的动态存储区中分配一块长度为size字节的连续区域

例:

1
2
3
//使用时需要包含"stdlib.h"或"malloc.h"
int *pi=(int*)malloc(sizeof(int));
//malloc返回值为void*型,可以强制转化为任何类型
  • 需要注意的是,malloc只会负责分配空间。并不能对其进行初始化,所以此时*pi的值是随机的
  • 该函数常用于初始化一个指针,这样可以避免使该指针成为野指针

calloc函数

函数名 calloc(unsigned int num,unsigned int size)
参数 num:分配数据项的个数
size:每个数据项的大小
返回值 成功则返回一个类型为void的首地址,失败为null
功能 在内存的动态存储区中分配n块长度为size字节的连续区域
  • 不同于malloc,calloc会对分配的存储单元值自动初始化为0

因此,可以用以下方法开辟一个一维数组

1
2
char *p;
p=(char*)calloc(10,sizeof(char));//开辟一个可以存放十个字符串的数组

free函数

函数名 free(void *ptr);
参数 一个指针
返回值
功能 释放ptr所指向的一块内存空间
  • 需要注意的是,free释放的区域应为malloc或calloc所分配的区域
  • free函数的实际功能是告诉系统输入参数对应的内存不再使用,即不再被保护,也就是说其他变量随时会占用占用那个地址。因此,原则上不再访问已经被free的的内存

1.基本内容

几种初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct Student 
{
char *name;
int age;
};
struct Student stu = {“lnj", 27}; //定义的同时按顺序初始化
/**************************************************************************/
struct Student stu = {.age = 35, .name = “lnj"};//定义的同时不按顺序初始化
/**************************************************************************/
struct Student stu;
stu.name = "lnj";
stu.age = 35; //先定义后逐个初始化
/**************************************************************************/
struct Student stu;
stu = (struct Student){"lnj", 35}; // 先定义后,再一次性初始化

结构体数组

1
2
3
4
5
6
7
8
struct Student 
{
char *name;
int age;
};
struct Student stu[2];
stu[0] = {"lnj", 35};
stu[1] = {"zs", 18};

结构体指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
     // 定义一个结构体类型
struct Student
{
char *name;
int age;
};

// 定义一个结构体变量
struct Student stu = {“lnj", 18};

// 定义一个指向结构体的指针变量
struct Student *p;

// 指向结构体变量stu
p = &stu;

/*
这时候可以用3种方式访问结构体的成员
*/
// 方式1:结构体变量名.成员名
printf("name=%s, age = %d \n", stu.name, stu.age);

// 方式2:(*指针变量名).成员名
printf("name=%s, age = %d \n", (*p).name, (*p).age);

// 方式3:指针变量名->成员名
printf("name=%s, age = %d \n", p->name, p->age);
//->运算符只用于指针结构体
return 0;
}

结构体嵌套

1
2
3
4
5
6
7
8
9
10
11
12
struct Date{
int month;
int day;
int year;
}
struct stu{
int num;
char *name;
char sex;
struct Date birthday;
Float score;
}
  • 成员访问

    如果某个成员也是结构体变量,可以连续使用成员运算符”.”访问最低一级成员。

2.内存分析

  • 结构体变量占用的内存空间永远是所有成员中占用内存最大成员的倍数(对齐问题)

    这种强制的要求一来简化了处理器与内存之间传输系统的设计,二来可以提升读取数据的速度。比如这么一种处理器,它每次读写内存的时候都从某个8倍数的地址开始,一次读出或写入8个字节的数据,假如软件能 保证double类型的数据都从8倍数地址开始,那么读或写一个double类型数据就只需要一次内存操作。否则,我们就可能需要两次内存操作才能完成这个动作,因为数据或许恰好横跨在两个符合对齐要求的8字节。

    1
    2
    3
    4
    5
    6
    7
    8
    struct Person
    {
    int age; // 4
    char ch; // 1
    double score; // 8
    };
    struct Person p;
    printf("sizeof = %i\n", sizeof(p)); // 16
    • 占用内存最大属性是score, 占8个字节, 所以第一次会分配8个字节
    • 将第一次分配的8个字节分配给age4个,分配给ch1个, 还剩下3个字节
    • 当需要分配给score时, 发现只剩下3个字节, 所以会再次开辟8个字节存储空间
    • 一共开辟了两次8个字节空间, 所以最终p占用16个字节
    1
    2
    3
    4
    5
    6
    7
    8
    struct Person
    {
    int age; // 4
    double score; // 8
    char ch; // 1
    };
    struct Person p;
    printf("sizeof = %i\n", sizeof(p)); // 24
    • 占用内存最大属性是score, 占8个字节, 所以第一次会分配8个字节
    • 将第一次分配的8个字节分配给age4个,还剩下4个字节
    • 当需要分配给score时, 发现只剩下4个字节, 所以会再次开辟8个字节存储空间
      将新分配的8个字节分配给score, 还剩下0个字节
    • 当需要分配给ch时, 发现上一次分配的已经没有了, 所以会再次开辟8个字节存储空间
      一共开辟了3次8个字节空间, 所以最终p占用24个字节

需要注意的是,对占用内存最大的变量的判断仅限基本类型,同时不包括数组,也就是说当一个结构体同时包括

chara[5];int b;时仍然用int的大小计算。

3.结构体之间赋值

  • 结构体虽然是构造类型, 但是结构体之间赋值是值拷贝, 而不是地址传递。所以结构体变量作为函数形参时也是值传递, 在函数内修改形参, 不会影响外界实参

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    #include <stdio.h>
    struct Person
    {
    char *name;
    int age;
    };
    void test(struct Person per);
    int main()
    {
    struct Person p1 = {"lnj", 35};
    printf("p1.name = %s\n", p1.name); // lnj
    test(p1);
    printf("p1.name = %s\n", p1.name); // lnj
    return 0;
    }
    void test(struct Person per)
    {
    per.name = "zs";
    }

共用体

  • 和结构体不同的是, 结构体的每个成员都是占用一块独立的存储空间, 而共用体所有的成员都占用同一块存储空间,因此所有成员的地址都是相同的

  • 特点: 由于所有属性共享同一块内存空间, 所以只要其中一个属性发生了改变, 其它的属性都会受到影响

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    union Test{
    int age;
    char ch;
    };
    union Test t;
    printf("sizeof(p) = %i\n", sizeof(t)); //4

    t.age = 33;
    printf("t.age = %i\n", t.age); // 33
    t.ch = 'a';
    printf("t.ch = %c\n", t.ch); // a
    printf("t.age = %i\n", t.age); // 97

枚举

  • 枚举使用的注意
    • C语言编译器会将枚举元素(spring、summer等)作为整型常量处理,称为枚举常量。
    • 枚举元素的值取决于定义时各枚举元素排列的先后顺序。默认情况下,第一个枚举元素的值为0,第二个为1,依次顺序加1。
    • 也可以在定义枚举类型时改变枚举元素的值
1
2
3
4
5
6
7
enum Season {
Spring = 9,
Summer,
Autumn = 20,
Winter
};
// 也就是说spring的值为9,summer的值为10,autumn的值为20,winter的值为21
  • 关于枚举类型的内存大小:
    • 若为没有在其中赋值的操作,默认4字节
    • 若有赋值,大小取决于赋值数的类型

利用结构体实现动态链表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include"stdio.h"
#include "stdlib.h"
#include"windows.h"
typedef struct list
{
int num;
char name[30];
struct list *next;
}test;

test *creat()
{
test *linkhead,*linkend,*pt;
int i;
for(i=0;i<3;i++)
{
pt=(test*)malloc(sizeof(test));//分配动态内存
scanf("%d %s",&pt->num,pt->name);
if(i==0)
{
linkhead=pt;//第一次先将当前节点赋给链头链尾
linkend=pt;
}
else//若不是第一次,先将当前节点存入上一个节点的next中,再将linkend更新为当前节点以供下一次访问
{ //当前节点的next
linkend->next=pt;
linkend=pt;
}
}
linkend->next=NULL;
return linkhead;
}
void destroy(test *linkHead)
{
test *p;
p=linkHead;
while(p!=NULL)
{
linkHead=linkHead->next;
free(p);
printf("clear\n");
p=linkHead;
}
}
int main()
{
test *LH,*Free_item;
LH=creat();
Free_item=LH;
while(LH!=NULL)
{
printf("NUM:%d NAME:%s\n",LH->num,LH->name);
LH=LH->next;
}
destroy(Free_item);
system("pause");
return 0;
}

以下解释存在一定问题,我修改后的内容以及写在下一节!!!

  • 注:其实这里最开始打印也应该单独写个函数,但是由于我懒得去写了,因此直接在main中操作参数,并且在操作完后我意识到好像无法再以LH为参数去使用destroy函数,按照一般地址与指针的理解,在打印完成过后应该已经改变了在creat()函数中分配的内存所对应的值,但是我抱着尝试的心理用main中的Free_item作为参数使用destroy函数竟然成功了,这一度让我以为我在指针的学习中是不是疏忽了什么重要内容!之后自己以简单的函数间的指针交互写了一些程序发现自己的理解好像也并没有什么问题,最后,在以”形参”为关键字搜索我的个人博客后终于找到如下一段话:

    “结构体虽然是构造类型, 但是结构体之间赋值是值拷贝, 而不是地址传递。所以结构体变量作为函数形参时也是值传递, 在函数内修改形参, 不会影响外界实参。”(完全忘记了捏😋,所以要多多复习原来的内容!!)

    因此LH也只是拷贝了creat函数所创建的linkhead的值而已,对LH的任何操作都不会影响creat函数所创建的linkhead的值,而Free_item拷贝的也是LH改变前的值,因此LH的改变也不会影响Free_item的内容。

2022.11.17改

因为最近又在学习FreeRTOS的相关内容,再次复习了一下链表的相关原理,顺便又读了一下上面写的内容,发现对于上一节最后这里的解释好像并不正确,因为当时是按照结构体赋值解释的,但是在这个链表程序中,传递的内容明显是地址而不是值!于是我再次查阅了结构指针的相关文献,解释如下:

我们一步一步来捋一下

①creat函数

首先我们可以确定,在这个链表程序中的所有test类型变量均为指针变量,也就是说,在creat()函数中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
test *creat()
{
test *linkhead,*linkend,*pt;
int i;
for(i=0;i<3;i++)
{
pt=(test*)malloc(sizeof(test));//分配动态内存
scanf("%d %s",&pt->num,pt->name);
if(i==0)
{
linkhead=pt;//第一次先将当前节点赋给链头链尾
linkend=pt;
}
else//若不是第一次,先将当前节点存入上一个节点的next中,再将linkend更新为当前节点以供下一次访问
{ //当前节点的next
linkend->next=pt;
linkend=pt;
}
}
linkend->next=NULL;
return linkhead;
}
  • scanf("%d %s",&pt->num,pt->name);中,其实是让pt这个结构体指针指向了一个整型数字和一个字符串,按照C++的说法就是直接使指针指向了两个字符值常量。
  • 在三次循环中,分别开辟了三个动态内存区域。
  • creat函数返回的也是一个结构体指针

②main()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main()
{
test *LH,*Free_item;
LH=creat();
Free_item=LH;
while(LH!=NULL)
{
printf("NUM:%d NAME:%s\n",LH->num,LH->name);
LH=LH->next;
}
destroy(Free_item);
system("pause");
return 0;
}
  • creat函数返回的指针赋值给结构体指针LH,并且在这里,所有的指针都不是二重指针,因此,creat的返回值linkhead的成员num和name都是直接指向的字符值常量,因此LH也通过linkhead直接指向这两个成员,而不是LH指向linkhead这个指针变量的地址,可以理解为linkhead和LH是并列的,没有先后顺序的。
  • 下一步的Free_item=LH;也是同理,字面意义是Free_item指向了LH的地址,但是实际上Free_item也是直接指向的动态内存中的字符值常量,因为Free_item是一个一重指针,它不具有保存另一个指针变量地址的功能,所以它会通过LH直接指向动态内存
  • 综上,实际上Free_item和LH也是并列关系,因此循环中LH的指向的改变不会影响到Free_item的指向,即使LH已经指向null,Free_item也还是指向的链头。

总结

其实说到底,是我自己对指针理解一直存在的误区,例如如下例程:

1
2
3
4
5
6
7
8
9
int main()
{
int a=10;
int *p1,*p2;
p1=&a;
p2=p1;
printf("%d %d %d %d",p1,&p1,p2,&a); //6422036 6422024 6422036 6422036
return 0;
}

我之前一直认为一个指针指向另一个指针,该指针的便会指向另一个指针变量自己的地址,但实际上,一重指针并不具有保存另一个指针变量地址的功能,所以该指针会直接指向另一个指针指向的内容。

就像上述例程中,p1指向整型变量a的地址,再将p1赋值给p2,但打印结果中p2指向的地址是a的地址(&a),而不是p1本身的地址(&p1)。

这也从另一方面说明了二重指针的一些重要性吧。