HAL库的一些知识
断言
C语言断言是一种用于==检查程序运行时条件是否满足的技术==。它可以帮助我们在开发过程中发现和调试错误,提高程序的健壮性和可靠性。下面我们通过示例程序和各种使用情况来学习C语言断言。
当程序不满足条件时会终止
示例程序
包含assert.h
在gcc编译器下输出:
在上面的示例程序中,我们使用了断言assert
来检查变量a
和b
是否相等。由于a
和b
不相等,程序会在运行时触发断言失败,==终止程序的运行==,并输出错误信息。这样做可以帮助我们在开发过程中尽早发现错误,避免出现更严重的问题。
情况一:检查函数参数
在上面的示例程序中,我们使用了断言assert
来检查函数print_number
的参数n
是否在有效范围内。由于第二次调用print_number
时,参数n
不在有效范围内,程序会在运行时触发断言失败,终止程序的运行,并输出错误信息。这样做可以帮助我们在开发过程中避免出现非法参数的情况。
情况二:检查函数返回值
在上面的示例程序中,我们使用了断言assert
来检查函数divide
的参数b
是否为0。由于第二次调用divide
时,参数b
为0,程序会在运行时触发断言失败,终止程序的运行,并输出错误信息。这样做可以帮助我们在开发过程中避免出现非法返回值的情况。
注意事项
在使用C语言断言时,需要注意以下几点:
断言只在调试模式下有效,因为在==发布模式==下,断言会被编译器忽略。
断言应该用于检查程序运行时的条件是否满足,而不是用于处理已知错误或异常情况。
断言的条件应该是纯粹的布尔表达式,不能包含副作用或需要计算的表达式。
断言的错误信息应该尽可能地清晰和明确,以便于快速定位和解决问题
HAL库的实例
HAl库中NVIC使能中断时,使用了断言检查输入的中断参数的合理性,
NVIC使能函数如下,使用了assert_param
函数,该函数为HAL库自编写:
其中IS_NVIC_DEVICE_IRQ,定义如下
这是一个宏定义,用于判断输入的中断号是否是合法的。 宏定义的名称为IS_NVIC_DEVICE_IRQ
,其接受一个参数IRQ
,代表输入的中断号。 该宏定义使用了一个条件表达式(IRQ) >= (IRQn_Type)0x00U
,其中(IRQ)
代表输入的中断号,(IRQn_Type)0x00U
代表中断号的最小值,即中断号为0的枚举类型。 因此,当输入的中断号大于等于0时,即该中断号是合法的,宏定义返回真值;否则返回假值,表示该中断号不合法。
而assert_param
函数定义如下,一般情况没有宏定义USE_FULL_ASSERT
,也就是说会执行执行((void)0),==即什么都不做。==
要实现真正的断言需要宏定义USE_FULL_ASSERT
(可以在stm32xx_hal_conf.h文件中取消注释,如下图,也可以在IDE如MDK中宏定义)
接着编写assert_failed()函数,可在在main.c中定义
实现自己的断言形式
__FILE__
和__LINE__
是C语言中的两个预定义宏,它们可以在任何时候使用,但是它们的值会根据它们所在的代码位置而变化。
需要注意的是,在某些特殊情况下,__FILE__
和__LINE__
的值可能会出现意外的变化。例如,在代码中使用了#pragma或者__LINE__宏时,可能会导致__FILE__
和__LINE__
的值出现异常。因此,在使用__FILE__
和__LINE__
时,应该注意这些情况的可能影响,并尽量避免使用会导致异常的代码结构。
在gcc编译器下输出:
可见这种断言可以输出错误的文件与行数,==同时程序还会正常执行==
在对断言的使用中,一定要遵循这样一条规定:对来自系统内部的可靠的数据使用断言,对于外部不可靠数据不能够使用断言,而应该使用错误处理代码。换句话说,断言是用来处理不应该发生的非法情况,而对于可能会发生且必须处理的情况应该使用错误处理代码,而不是断言。
结构体
定义方法
在一般情况下,tag、member-list、variable-list 这 3 部分至少要出现 2 个。以下为实例:
最常用的是用typedef创建新类型,==一般在头文件中定义==,在C文件中可以直接调用,
HAL库中的实例
如HAL库创建GPIO的属性的结构体,在stm32f1xx_hal_gpio.h定义,==也就说一般是在头文件中进行结构体typedef==
在C文件中就可以直接使用
结构体上可以互相嵌套的
FreeRTOS的实例
在task.c中有如下程序结构体定义
在task.h中
观察上面,tskTCB
与tskTaskControlBlock
在C语言语法上的使用有所不同
tskTCB是被typedef struct重定义出来的,可以直接作为一种结构体变量类型使用,这边有使用typedef进行了重命名
tskTaskControlBlock是结构体类型,其实就是结构体标签,但在使用时需要加前缀struct,说明是一个结构体
之所以一个在.h中typedef,一个在.c中typedef是因为tskTCB与TCB_t只在task.c中使用,而tskTaskControlBlock与TaskHandle_t在其他文件也会使用,因此需要在.h中进行重定义
之所以先使用了
struct tskTaskControlBlock
而不是直接使用tskTaskControlBlock
来声明结构体,是因为在.h文件中只需要知道该结构体的存在即可,而不需要知道其具体实现细节,这样可以提高代码的封装性和可维护性。
下面给出一个简单的实例,说明tskTCB
与tskTaskControlBlock
在C语言语法上的使用的不同
结构体指针
结构体指针的基本用法
结构体指针的定义方式与普通指针相同,只是指向的类型是结构体类型。例如,下面是一个结构体类型的定义:
我们可以定义一个指向该结构体类型的指针变量 p
,如下所示:
接下来,我们可以通过该指针变量来访问结构体成员变量。例如,我们可以通过指针 p
访问结构体成员变量 name
,如下所示:
上面的代码将字符串 "Tom"
复制到了指针 p
所指向的结构体变量的 name
成员变量中。
结构体指针作为函数参数
结构体指针常常被用作函数参数,以便在函数中对结构体进行操作。下面是一个示例程序:
在上面的示例程序中,我们定义了一个名为 printPerson
的函数,它接受一个指向结构体类型 Person
的指针作为参数,并输出结构体的成员变量。在 main
函数中,我们定义了一个结构体变量 p1
,给它的成员变量赋值,并将其地址传递给 printPerson
函数来输出它的成员变量。
结构体指针数组
结构体指针还可以用于定义结构体指针数组。下面是一个示例程序:
在上面的示例程序中,我们定义了三个结构体变量 p1
、p2
和 p3
,然后定义了一个指针数组 arr
,其中每个元素都是一个指向 Person
结构体类型的指针。我们将三个结构体变量的地址分别存储到了数组的不同元素中,并使用循环遍历数组,输出结构体的成员变量。 总的来说,结构体指针是 C 语言中非常重要的数据类型之一,它可以用于访问结构体的成员变量、作为函数参数、进行动态内存分配以及定义结构体指针数组等。在学习 C 语言时,掌握结构体指针的使用方法是非常重要的。
结构体指针的动态内存分配
结构体指针还可以用于动态内存分配,以便在程序运行时动态地创建结构体对象。下面是一个示例程序:
在上面的示例程序中,我们使用 malloc
函数动态地分配了一个大小为 sizeof(struct Person)
的内存空间,并将其地址强制转换成了指向结构体类型 Person
的指针。然后,我们可以通过该指针来访问结构体的成员变量,并在程序结束时使用 free
函数释放动态分配的内存空间。
数组指针
数组指针是==指向数组的指针变量==,它可以用来访问数组元素,也可以用来定义二维数组。本文将介绍数组指针的基本用法和常见的使用情况。
数组指针的基本用法
数组指针的定义方式与普通指针相同,只是指向的类型是数组类型。例如,下面是一个数组类型的定义:
我们可以定义一个指向该数组类型的指针变量 p
,如下所示:
上面的代码将数组 arr
的地址赋值给了指针变量 p
。现在,我们可以通过指针 p
来访问数组元素。例如,我们可以通过指针 p
访问数组元素 arr[0]
,如下所示:
上面的代码将数组元素 arr[0]
的值修改为了 4
。 我们还可以使用指针运算符 ++
和 --
来访问数组的下一个或上一个元素。例如,下面的代码将指针 p
指向数组的下一个元素:
数组指针作为函数参数
数组指针常常被用作函数参数,以便在函数中对数组进行操作。下面是一个示例程序:
在上面的示例程序中,我们定义了一个名为 printArray
的函数,它接受一个指向整型数组的指针和数组的大小作为参数,并输出数组的元素。在 main
函数中,我们定义了一个整型数组 arr
,给它的元素赋值,并将其地址和数组大小传递给 printArray
函数来输出它的元素。
数组指针的动态内存分配
数组指针还可以用于动态内存分配,以便在程序运行时动态地创建数组对象。下面是一个示例程序:
在上面的示例程序中,我们使用 malloc
函数动态地分配了一个大小为 size * sizeof(int)
的内存空间,并将其地址强制转换成了指向整型数组的指针。然后,我们可以通过该指针来访问数组的元素,并在程序结束时使用 free
函数释放动态分配的内存空间。
数组指针的二维数组
数组指针还可以用于定义二维数组。下面是一个示例程序:
在上面的示例程序中,我们定义了一个二维整型数组 arr
,然后定义了一个指向含有 3
个整型元素的一维数组的指针变量 p
。我们将二维数组 arr
的地址赋值给了指针变量 p
,现在我们可以通过指针 p
来访问二维数组的元素。 总的来说,数组指针是 C 语言中非常重要的数据类型之一,它可以用于访问数组元素、作为函数参数、进行动态内存分配以及定义二维数组等。在学习 C 语言时,掌握数组指针的使用方法是非常重要的
指针数组
首先它是一个数组,数组的元素都是指针,==与数组指针相区别==,
在上面的示例程序中,我们首先定义了三个整型变量 a
、b
和 c
,然后定义了一个指针数组 ptr_arr
,并将这三个整型变量的地址存储到了这个数组中。最后,我们通过指针数组 ptr_arr
访问了这三个整型变量的值。
指针数组与数组指针的区分
下面到底哪个是数组指针,哪个是指针数组呢: A) int *p1[10]; B) int (*p2)[10];
“[]”的优先级比“*”要高。p1 先与“[]”结合,构成一个数组的定义,数组名为p1,int 修饰的是数组的内容,即数组的每个元素。那现在我们清楚,这是一个数组,其包含10 个指向int 类型数据的指针,即指针数组。至于p2 就更好理解了,在这里“()”的优先级比“[]”高,“”号和p2 构成一个指针的定义,指针变量名为p2,int 修饰的是数组的内容,即数组的每个元素。数组在这里并没有名字,是个匿名数组。那现在我们清楚p2 是一个指针,它指向一个包含10 个int 类型数据的数组,即数组指针。
我们可以借助下面的图加深理解:
指针与数组地址结合的一些知识
数组名是数组第一个元素的地址
例如,定义一个整型数组 int arr[5] = {1, 2, 3, 4, 5};
,数组名 arr
就是第一个元素的地址,即 &arr[0]
。
输出:
数组元素的地址可以通过下标访问
例如,定义一个整型数组 int arr[5] = {1, 2, 3, 4, 5};
,数组第三个元素 arr[2]
的地址可以表示为 &arr[2]
或者 (arr + 2)
。
输出:
数组元素的地址可以用指针变量来存储和访问
例如,定义一个整型指针变量 int* ptr = NULL;
,可以将数组第一个元素的地址赋值给指针变量 ptr
,即 ptr = &arr[0];
或者 ptr = arr;
,然后就可以通过指针变量 ptr
访问数组元素。
输出:
数组名可以用作指针来访问数组元素
例如,定义一个整型数组 int arr[5] = {1, 2, 3, 4, 5};
,可以通过数组名 arr
直接访问数组元素,即 *(arr + 2)
或者 arr[2]
。
指针也可以用来定义动态数组
例如,定义一个指向整型数组的指针变量 int* ptr = NULL;
,然后通过 malloc
函数动态分配一个 10 个整型元素的数组,即 ptr = (int*)malloc(10 * sizeof(int));
。分配后,就可以通过指针变量 ptr
访问动态数组元素,例如,ptr[2] = 3;
。
输出:
数组作函数参数
指针的大小由什么决定
指针大小是由当前CPU运行模式的寻址位数决定!
==所以在x86的64位PC机中是8字节,在arm架构的stm32单片机是4字节。==
字长:在同一时间中处理二进制数的位数叫字长。通常称处理字长为8位数据的CPU叫8位CPU,32位CPU就是在同一时间内处理字长为32位的二进制数据。二进制的每一个0或1是组成二进制的最小单位,称为一个比特(bit)。
一般说来,计算机在同一时间内处理的一组二进制数称为一个计算机的“字”,而这组二进制数的位数就是“字长”。字长与计算机的功能和用途有很大的关系, 是计算机的一个重要技术指标。字长直接反映了一台计算机的计算精度,为适应不同的要求及协调运算精度和硬件造价间的关系,大多数计算机均支持变字长运算, 即机内可实现半字长、全字长(或单字长)和双倍字长运算。在其他指标相同时,字长越大计算机的处理数据的速度就越快。早期的微机字长一般是8位和16 位,386以及更高的处理器大多是32位。目前市面上的计算机的处理器大部分已达到64位。
字长由微处理器(CPU)对外数据通路的数据总线条数决定。
最小可寻址单位:内存的最小可寻址单位通常都是字节。也就是说一个指针地址值可对应内存中一个字节的空间。
寻址空间:寻址空间一般指的是CPU对于内存寻址的能力。CPU最大能查找多大范围的地址叫做寻址能力 ,CPU的寻址能力以字节为单位 (字节是最小可寻址单位),如32位寻址的CPU可以寻址2的32次方大小的地址也就是4G,这也是为什么32位寻址的CPU最大能搭配4G内存的原因 ,再多的话CPU就找不到了。
这里CPU的寻址位数是由地址总线的位数决定,32位CPU的寻址位数不一定是32位,因为32位CPU中32的意义为字长。
有关寻址范围计算解释,对于32位寻址的CPU,其地址值为32位的二进制数,所以可以表示的最大地址为2的32次方(即4G,最大内存空间为4GB,这里G表示数量、GB表示容量)。同时我们不难看出,一个指针的值就是一个32位的二进制数,32位对应4字节**(Byte)。**所以,指针的大小实际上是由CPU的寻址位数决定,而不是字长。
再来分析一下如下的情况:
32位处理器上32位操作系统的32位编译器,指针大小4字节。 32位处理器上32位操作系统的16位编译器,指针大小2字节。 32位处理器上16位操作系统的16位编译器,指针大小2字节。 16位处理器上16位操作系统的16位编译器,指针大小2字节。
这从结果看起来指针的大小和编译器有关??
实际不是这样的,有这样的结果是因为以上几种情况,处理器当前运行模式的寻址位数是不一样的,如下:
Intel 32位处理器32位运行模式,逻辑寻址位数32,指针也就是32位,即4个字节 Intel 32位处理器16位虚拟机运行模式,逻辑寻址位数16,指针也就是16位,即2个字节
编译器的作用是根据目标硬件(即CPU)的特性将源程序编译为可在该硬件上运行的目标文件。如果一个编译器支持某32位的CPU,那么它就可以将源程序编译为可以在该CPU上运行的目标文件。该源程序中指针大小也会被编译器根据该CPU的寻址位数(如32位)编译选择为4字节。
指针长度由谁决定?到底是多长? - 知乎 (zhihu.com)
字符串指针
从 C 语言角度来说,字符串在内存中是以字符数组的形式存在的,它的每个字符占据一个字节的内存空间,最后一个字符必须是 '\0'(空字符)。 在 C 语言中,字符串(如 "start_task")是以字符数组的形式存储在内存中,并且在编译时就会被赋值。因此,在函数调用时,可以直接将字符串常量作为参数传递给形参,就像传递一个指向字符数组首元素的指针一样。 例如,下面的代码中,我们定义了一个字符数组 str,然后直接将其作为参数传递给了函数 print_string,而不必取地址或使用指针变量:
需要注意的是,字符串常量是不可修改的,因此不能通过形参来修改其内容。如果需要修改字符串,可以使用字符数组和指向字符数组的指针。 另外,还有一种常见的字符串指针用法,就是使用动态内存分配函数(如 malloc)来创建一个字符数组,并将其赋值给指针变量。这种方式可以在程序运行时动态地分配内存,非常灵活。例如:
FreeRTOS中的实例
"start_task"就直接被赋值给字符串指针。
指针常量与常量指针
指针常量和指向常量的指针都是C语言中的指针类型,它们的区别在于指针本身的常量性和指针所指向的变量的常量性。
指针常量-const pointer(int *const p)
指针常量是指一个==指针变量本身是一个常量==,即不能再指向其他位置,但是可以通过这个指针访问和修改所指向的变量。在声明时,需要将const关键字放在指针符号 * 后面。
常量指针-pointer to const(const int *p, int const *p)
常量指针本质上是一个指针,常量表示指针指向的内容,说明该指针指向一个“常量”。**在常量指针中,指针指向的内容是不可改变的,指针看起来好像指向了一个常量。**用法如下:
简单的区分方法
指针常量:*号在左,const在右,我们从左往右读,“*”号读作“指针”,“const”读作“常量”,所以总的读作:“指针常量”。
常量指针:常量指针中const 总是位于*号左侧,所以我们按照上面的方法依次从左往右读,合并起来就是“常量指针”。
指向常量的指针常量
最经典的就是FreeRTOS的xTaskCreate动态创建函数的参数二const char * const pcName
具体来说,它包含两个关键字 const
,第一个 const
表示 pcName
指向的字符内容是常量,不可修改,第二个 const
表示 pcName
本身是一个常量指针,即 pcName
的值(指向地址的指针)也是不可修改的。因此,该指针不能再指向其他地址,也不能修改所指向的字符内容。
综合示例
FreeRTOS的一些实例
动态创建函数的入口参数多次用到了这些知识点
第二个参数
const char * const pcName
是一个指向常量字符数组的指针,表示任务的名称。它是一个指向常量字符的指针常量,具体来说,它包含两个关键字const
,第一个const
表示pcName
指向的字符内容是常量,不可修改,第二个const
表示pcName
本身是一个常量指针,即pcName
的值(指向地址的指针)也是不可修改的。因此,该指针不能再指向其他地址,也不能修改所指向的字符内容。第四个参数
void * const pvParameters
是一个指向void类型的指针,具体来说,它包含了一个const
关键字,表示指针pvParameter
本身是一个指针常量,即它的值(指向地址的指针)是不可修改的。同时,它是一个void
指针,表示可以指向任意类型的数据。最后一个参数
TaskHandle_t * const pxCreatedTask
是一个指向TaskHandle_t类型变量的指针,这是一个==指针常量==,在这个声明中,const关键字用于指定指针本身是一个常量,即不能再指向其他位置,而*表示指针所指向的变量是可修改的。。因此,这个声明定义了一个可以修改指向内容,但不能修改指针指向的位置的常量指针。
函数指针的定义和声明
函数指针的定义和声明方式如下:
其中,括号中的*
表示指针类型,函数指针变量名
是指针变量的名称,参数列表
是函数的参数列表。例如,下面是一个函数指针的声明示例:
这个声明表示pFunc
是一个指向返回类型为int
,参数列表为(int, int)
的函数指针。
函数指针的赋值和使用
函数指针的赋值和使用方式如下:
例如,下面是一个函数指针的赋值示例:
这个示例中,我们定义了一个名为add
的函数,然后将其地址赋值给了指针变量pFunc
。现在,pFunc
就可以像一个函数一样被调用了:
这个示例中,我们通过指针变量pFunc
调用了函数add
,并且将其返回值赋给了变量result
。
函数指针的地址强制赋值
函数指针作为参数
函数指针可以作为函数的参数传递。例如,下面是一个示例程序,其中函数operate
接受一个函数指针作为参数,然后调用该函数指针指向的函数:
在这个示例程序中,我们定义了两个函数add
和multiply
,它们分别实现了加法和乘法运算。然后,我们定义了一个函数operate
,接受三个参数:两个整数a
和b
,以及一个函数指针pFunc
。该函数会调用pFunc
指向的函数,并将a
和b
作为参数传递给它。最后,我们在main
函数中调用了operate
函数,并传递了不同的函数指针作为参数,分别获得了加法和乘法的结果。
函数指针作为返回值
函数指针可以作为函数的返回值返回。例如,下面是一个示例程序,其中函数getFunction
返回一个函数指针:
在这个示例程序中,我们定义了两个函数add
和multiply
,它们分别实现了加法和乘法运算。然后,我们定义了一个函数getFunction
,接受一个字符参数op
,并根据op
的不同返回不同的函数指针。最后,我们在main
函数中调用了getFunction
函数,并将'+'
作为参数传递给它,然后将返回的函数指针赋值给了指针变量pFunc
。现在,我们可以通过pFunc
调用函数add
了。
函数指针作为回调函数
函数指针经常被用作回调函数,回调函数是指在程序运行期间,由其他函数调用的函数。回调函数可以作为参数传递给调用它的函数,并在需要的时候被调用。例如,下面是一个示例程序,其中函数filter
接受一个整型数组和一个函数指针作为参数,然后调用该函数指针指向的函数来过滤数组中的元素:
在这个示例程序中,我们定义了两个函数isEven
和isOdd
,它们分别判断一个整数是否为偶数和奇数。然后,我们定义了一个函数filter
,接受一个整型数组arr
、数组长度len
和一个函数指针pFunc
作为参数。该函数会遍历数组中的元素,并调用pFunc
指向的函数来判断每个元素是否符合条件。最后,我们在main
函数中调用了filter
函数,并将isEven
和isOdd
函数指针作为参数传递给它,分别打印出了数组中的偶数和奇数。
函数指针和typedef
为了方便使用,我们可以使用typedef
来定义函数指针类型。例如,下面是一个示例程序,其中使用了typedef
来定义了一个名为Func
的函数指针类型:
在这个示例程序中,我们使用typedef
定义了一个名为Func
的函数指针类型,它指向返回类型为int
,参数列表为(int, int)
的函数。然后,我们定义了一个函数add
,实现了加法运算。最后,我们在main
函数中声明了一个Func
类型的指针变量pFunc
,并将add
函数的地址赋值给了它。现在,我们可以通过pFunc
调用函数add
了。
FreeRTOS的函数指针实例
动态创建函数xTaskCreate() 的参数如下
TaskFunction_t
是一个typedef定义的函数指针类型,表示任务函数的类型。
void * 类型的参数
void *
类型的参数是一种指向不确定类型
的指针类型,它可以指向任意类型的数据,因为它不知道指向的数据的具体类型==,所以在使用它指向的数据时,需要进行类型转换==。 如果要使用 void *
类型的参数,需要先将它转换成实际的类型,然后再进行操作。例如,如果我们有一个 void *
类型的参数 p
,它指向一个整数变量,我们可以将它转换为 int *
类型的指针,然后再使用 *p
访问指向的整数变量的值,示例代码如下:
函数指针的地址赋值
void (*app)(void)
是一个函数指针变量的定义,它表示一个指向参数为 void
、返回值为 void
的函数的指针。这里定义的变量名为 app
,它是一个指向函数的指针变量。 (void (*)(void))
是一个强制类型转换,它将一个无类型指针 0x08040001
转换成一个指向参数为 void
、返回值为 void
的函数指针类型,即 void (*)(void)
类型。 因此,app = (void (*)(void))0x08040001;
的作用是将地址 0x08040001
强制转换成一个 void (*)(void)
类型的函数指针,并将它赋值给 app
变量,从而得到了一个指向地址 0x08040001
处的函数的指针 app
。这个函数指针可以用来调用该地址处的函数,即启动应用程序。
指向任何类型的函数指针
在C语言中,可以使用void*
类型的指针或者使用typedef
来实现指向任何类型的函数指针。需要注意的是,使用指向任何类型的函数指针时需要特别小心,确保指向的函数和实际需要调用的函数类型一致,否则可能会导致未定义的行为或者程序崩溃。
在上面的示例中,我们分别使用了void*
类型的指针和typedef
实现了指向任何类型的函数指针。在使用void*
类型的指针时,需要将其强制转换为指向具体类型的函数指针,并使用括号将函数指针转换为函数调用。在使用typedef
实现时,使用了函数指针类型别名func_ptr_t
来代表指向任何类型的函数指针,使用func_ptr_t
来声明函数指针变量,并直接使用函数指针变量来调用函数。 需要注意的是,在使用指向任何类型的函数指针时,必须确保指向的函数和实际需要调用的函数类型一致,否则可能会导致未定义的行为或者程序崩溃。
函数指针与变量指针赋值时的差异
函数指针作为形参时传入函数时不需要加 &
,是因为函数名本身就是一个指向函数的指针,它可以直接传递给形参。例如:
而变量指针作为形参时传入参数时需要加 &
,是因为函数中的形参和实参是两个不同的变量,它们的地址不同。如果不加 &
,就会把实参的值传递给形参,而不是地址,这样就无法在函数中修改实参的值。例如:
含参宏定义
语法
含参宏定义的语法形式为:
其中,macro_name
是宏的名称,parameter_list
是宏的参数列表,可以包含零个或多个参数,用逗号隔开。replacement_text
是宏的替换文本,它可以包含参数和其他C语句,但不需要加上分号。
示例程序
下面是一个简单的示例程序,演示了如何使用含参宏定义:
在这个示例程序中,我们定义了一个名为SQUARE
的含参宏,它接受一个参数x
,并返回x
的平方。在main
函数中,我们定义了一个整型变量a
,并赋值为3。然后,我们使用SQUARE
宏计算了(a+1)
的平方,并将结果赋值给了整型变量b
。最后,我们使用printf
函数输出了b
的值,结果为16。
宏定义函数
宏定义函数是 C 语言中一种宏替换的形式,它通过在代码中定义一个宏来代替函数的调用。宏定义函数不会产生函数调用的开销,可以在一定程度上提高程序的执行效率。 宏定义函数的语法格式如下:
其中,函数名
是宏定义函数的名称,参数列表
是函数的参数列表,宏体
是宏定义函数的函数体。 下面是一个使用宏定义函数的示例程序:
在上面的示例程序中,我们定义了一个名为 MAX
的宏定义函数,它接受两个参数 a
和 b
,返回它们中的最大值。在 main
函数中,我们定义了两个整数变量 x
和 y
,然后调用宏定义函数 MAX
来获取它们中的最大值,并将结果赋值给变量 max
。最后,我们通过 printf
函数输出结果。 除了上面的示例程序中所示的基本使用方法外,宏定义函数还有以下常见的使用情况:
宏定义函数可以嵌套使用:
在上面的示例程序中,我们定义了两个宏定义函数 SQUARE
和 CUBE
,分别用于计算一个数的平方和立方。在 CUBE
函数的宏体中,我们调用了 SQUARE
函数来计算一个数的平方。 总的来说,宏定义函数是 C 语言中一种非常有用的宏替换形式,它可以简化代码,提高程序的执行效率。但需要注意的是,宏定义函数在展开后会直接替换原代码,因此在使用时需要仔细考虑宏定义函数的参数和宏体,以避免出现意外的错误。
FreeRTOS的宏定义函数实例
串口库中的HAL_UART_GET_FLAG(HANDLE, FLAG)函数,其实就是一个宏定义函数
类型强制转换
位带操作
(20条消息) 快速理解STM32位带操作原理和用途_strongerHuang的博客-CSDN博客
按位与或,异,按位取反,逻辑取反
按位与运算
按位与运算符(&)的作用是将两个操作数的二进制位逐位进行与操作,只有在两个二进制位都是1时,结果才为1,否则为0。示例程序如下:
按位或运算
按位或运算符(|)的作用是将两个操作数的二进制位逐位进行或操作,只要两个二进制位中有一个为1,结果就为1,否则为0。示例程序如下:
按位异或运算
按位异或运算符(^)的作用是将两个操作数的二进制位逐位进行异或操作,如果两个二进制位不同,则结果为1,否则为0。示例程序如下:
按位取反运算
按位取反运算符(~)的作用是将操作数的二进制位逐位进行取反操作,即0变为1,1变为0。示例程序如下:
逻辑取反
逻辑非运算符(!)的作用是对一个表达式取反,即如果表达式的值为真,则返回假,否则返回真
弱定义
HAL库的头文件包含
stm32f1xx_hal_conf.h文件里包含着所有外设的.h文件,就是#include "stm32f1xx_hal_rcc.h",#include "stm32f1xx_hal_gpio.h"这样的头文件,而该文件又被包含在stm32f1xx_hal.h文件里,stm32f1xx_hal.h又被包含在stm32f1xx.h,也就是如以下的关系,
最终只需要调用stm32f1xx.h
而在正点原子的Hal库例程中,将"stm32f1xx.h"包含在sys.h中,同时sys.h还实现了typedef uint32_t u32这样的重定义,并实现了IO的位带操作,因此我们可以直接调用sys.h
Last updated