1、基本数据类型
在32位系统中,short int占2字节,int占4字节,long int或者long占4字节,long long占8字节;
2、数值在计算机中的存储
计算机一般用补码表示整数,5的补码00000101,-5的补码11111011(说白了负数十进制取负的原码00000101再按位取反11111010后+1得11111011就是负数在计算机中的存储数值);
浮点数的存储要符合IEEE-754标准,公式如下,网页版快速转换工具
(
−
1
)
S
∗
M
∗
2
E
(−1)^S∗M∗2^E
(−1)S∗M∗2E
举个栗子,-25.625;符号位-1;整数位25,二进制为11001;小数位要*2取整,因此
0.625*2=1.25 取整为`1`余0.25,
0.25*2=0.5 取整为`0`余0.5,
0.5*2=1.0 取整为`1`余0
将所有取整的值取出即为101
故结果为
11001.101
=
(
−
1
)
1
∗
1.1001101
∗
2
4
11001.101 = (-1)^1*1.1001101*2^4
11001.101=(−1)1∗1.1001101∗24S为1没毛病;
E的取值为指数部分4-1+128(double则是4-1+1024)为10000011(double为10000000011);
M只保留小数位为1001101;
最终结果就是1 10000011 10011010000000000000000;
当然若*2取整取不尽就只能存储近似值了,故(float)6.33 != (double)6.33
这个表达式为真
3、typedef关键字,语句结尾要加‘;’,常用于数据类型的别名,如
typedef unsigned int uint32_t;
typedef struct CLASS{
int num;
char name[10];
struct CLASS *next;
}Class_TypeDef; 这是个链表
typedef int (*func)(int a); 这是个函数指针
4、若定义了两个同名的变量,优先用内层的变量,{ }之间为一个代码块
5、static修饰的局部变量只会初始化一次,若没有自定义初始化值会被自动初始化为0,退出其作用域后其中的数据仍然保留,存储在全局数据区,但变量的作用域不变;
static修饰全局变量和函数,则该全局变量和函数只在当前文件中能被使用,其他文件用extern也不行
6、else语句从属于其最近的不完整的if语句,下面语句else
是if(b>0)
的
if(a>0) ...; if(b>0) ...; else ...;
7、do{...}while(0);
while后面必须加;程序必运行一次do,运行完判断while若为真再执行do,为假则往下执行
8、switch case default语句其中的case和default代表的是程序跳转至该位置执行,且()中必须为整形
下面语句若a不等于1,2,3,那a会赋值为9再赋值为1再赋值为2最终a为2;
所以这段代码,只要输入时a不等于3,a最终都会被赋值为2,只有a一开始为3时其值才不会变
switch(a){
default: a=9;
case 1: a=1;
case 2: a=2; break;
case 3: a=3; break;
}
9、& | 是按位运算符,它会将两操作数按二进制按位与或;
&& || 是逻辑运算符,当&&的左操作数为假则右操作数直接跳过运算,最终输出为假;
当||左操作数为真也会直接跳过右操作数的运算,最终输出为真;
10、++a是先对a自增后再取值,a++则是先取值(也可以理解为先拷贝一份出来)再自增;
11、对于函数定义int printf (const char *__format, ...)
来说,const char *__format
和char const *__format
是等价的,都表示常量字符的指针,表明指针指向的数据是不可更改的;
而char * const __format
表示常量指针,即指针不可更改,但指向的数据是可更改的;
12、对于下面的定义,用sizeof(mess1)的结果是6,因为字符串结尾还有个’\0’;
而mess2是指向字符串常量"Hello"的指针,因此不可修改字符串里面的内容,用sizeof(mess2)输出的和其他指针一样都是4在32位系统中;
char mess1[] = "Hello";
char *mess2 = "Hello";
13、对于一个二维数组 int mat[2][3]={0,1,2,3,4,5};
其被定义为包含2个元素的数组,每个元素又是包含3个整形元素的数组(C语言本身并不会将其定义为2行3列)。因此数组名mat是指向第一个元素的指针,因此mat是一个指向包含3个整形元素的数组的指针。
*mat+1 表示指向mat[0][1]的指针
*(mat+1) 表示指向mat[1][0]的指针,也可以理解为mat的第2行单拎出来成一维数组的数组名
*(mat + 1) + 2 表示指向mat[1][2]的指针
*(*(mat + 1) + 2) 表示元素值5
int *mp=mat; 这条语句是非法的,因为mat是一个指向整形数组的指针
int (*mp)[3]=mat; 这是合法的,()指出他是一个指针,指向有三个元素的数组,因此必须指定数组的大小
int *mp[3]=mat; 这条语句是非法的,和mat类型不匹配,下面紧接着会解释
14、对于 int *api[10];
这条语句,下标引用[]优先级高于间接访问*,因此这是一个有10个元素的数组,而数组的元素类型是指向整形的指针;
对于const char key[][6]={"up", "down", "left", "right"};
需指定二维数组宽度>=6;
const char *key[]={"up", "down", "left", "right"};
更加灵活,指针数组存储在RAM中,而二维数组存储在Flash只读区里,且指针数组相对节省空间一点;
而作为函数传参应这么定义 void fun(const char **arr)
;
15、对于char *strcpy(char *dest, const char *src)
复制字符串函数,src复制给dest,若dest长度大于src,则src全部复制过去包括结束符\0;若dest长度小于src,则src长于dest的部分仍被复制替换掉dest后面内存中的值,这种操作是危险的,因为你一定不想某块你不确定其作用的内存空间被本不属于它的值填充修改;
16、size_t strlen(const char *str)
计算字符串长度直到第一个\0结束,即便\0后还有内容也不再计算,且返回值为无符号型
17、结构体对齐
结构体在没有使用**#pragma pack(x)**指定对齐数的情况满足以下规则
1)基本元素类型或其数组,对齐数为元素类型所占字节大小(char→1,int→4,double→8)
2)若其中嵌套结构体或联合体,则为其中成员最大对齐数
3)结构体总大小为最大对齐数的整数倍
若使用**#pragma pack(x)**指定对齐数时
1)每个成员对齐数为 min(成员自身对齐数,x)
2)结构体整体对齐数为 min(成员中最大对齐数,x)
3)结构体总大小为 min(成员中最大对齐数,x)的整数倍
对于嵌入式设备而言,有以下的结构体内存优化策略
1)成员重排,大小相同的前后挨着放,减少填充无用字节
2)使用__attribute__ ((packed))
和 #pragma pack(1)
一样表示取消编译过程中的优化对齐,
使用__attribute__ ((aligned(4)))
和#pragma pack(4)
一样表示4位对齐,
__attribute__
修饰结构体或联合体紧跟着加在‘}’
后面基本都不会错,
#pragma pack(x)
作为预处理语句要加在结构体语句的开始前和‘;’
后
#pragma pack(1)
struct VAR{
int b;
}__attribute__((packed)) var1;
#pragma pack()
18、位域
位域是一种特殊的结构体,它以bit位为最小的可操作单位,‘ : ’后面的数字为所占的bit位数。
下面的代码a是无符号整形占4字节没毛病,b是一个占2bits的位域,再之后会空着3bits,再后面是占4bits的位域c。用得不多了解一下
struct BITS{
unsigned int a;
unsigned int b:2;
unsigned int :3;
unsigned int c:4;
}bits;
19、联合体union
联合体union和结构体的定义使用几乎一样,只需将关键字struct
改成union
,而联合体的特点是里面所有成员共用一个内存空间,也就是说每个成员的首地址都是相同的
,而联合体的大小由最大的成员并做内存对齐后取得
下面的例子实现了两个8位数据拼接成16位数据的工作,但若是申请两个uint8型的变量则不行
union{
uint8_t byte[2];
uint16_t halfword;
}_flag;
_flag.byte[0]
_flag.byte[1]
_flag.halfword
20、函数指针
因为函数本身也有它自己的地址,所以可以定义一个指针来指向函数
int fun(int a); 这是普通的函数声明
int *fun1(int a); 这是返回值为int型指针的函数
int (*pfun)(int a); 这是函数指针
int fun(int a){
return a*a;
}
int main(){
pfun = &fun; '&'操作符可以去掉,因为函数会被编译器转换为函数指针
printf("%d\n", pfun(5));
}
函数指针常用的地方博主总结为:程序需在不同时刻调用同一个外层函数,但在该外层函数内部需要调用不同的内层函数,但出于某些原因你不方便修改外层函数的代码时,就可以考虑使用函数指针传递想执行的业务;
那么具体有哪些常见的合理的“不方便”的地方呢?
1)STM32的中断回调函数:可以通过USE_HAL_REGISTER_CALLBACKS
宏来开启,但一般我们都是默认不开启,直接在用户代码文件中重新定义__weak
弱函数来实现
__weak void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
其实这不是一个使用函数指针解决问题的合格例子,因为HAL库已经将可能出现的中断事件在中断处理函数HAL_UART_IRQHandler(&huart1)
中用if else定义判断好了
2)应用与驱动层解耦:在FATFS的例子中有很好的表现,因为我们使用读写应用一般都会使用
FRESULT f_read (FIL* fp, void* buff, UINT btr, UINT* br);
来读取,但我们的文件系统可能挂载在SD卡、NAND、RAM或是别的存储媒介中,他们底层接口的驱动都是不同的,因此FATFS使用函数指针的方式让用户可以编写不同的底层驱动来链接到应用层
这儿简单介绍一下在FATFS中函数指针和结构体的实现,以供学习使用
先用typedef定义结构体,里面正常定义函数指针(‘.....’代表省略的代码)
typedef struct{
.....
DRESULT (*disk_read) (BYTE, BYTE*, DWORD, UINT);
DRESULT (*disk_write) (BYTE, const BYTE*, DWORD, UINT);
.....
}Diskio_drvTypeDef;
再声明一下用户自定义的驱动函数
DRESULT SD_read (BYTE, BYTE*, DWORD, UINT);
DRESULT SD_write (BYTE, const BYTE*, DWORD, UINT);
然后定义一个 Diskio_drvTypeDef 类型的结构体 SD_Driver 并初始化
const Diskio_drvTypeDef SD_Driver = {
.....
SD_read,
SD_write,
.....
};
.....这儿省略 SD_read 和 SD_write 函数的实现
然后定义一个的指针数组 drv 其所指向的元素类型为 Diskio_drvTypeDef 结构体并初始化为空,
再在初始化函数中将SD_Driver(如果你还编写了NAND_Driver也一起)的地址幅值给指针drv,
这儿使用const为了说明SD_Driver(和NAND_Driver)结构体的内容即自定义函数的地址是不允许修改的
const Diskio_drvTypeDef *drv[_VOLUMES] = {NULL};
drv[0] = &SD_Driver;
drv[1] = &NAND_Driver;
这样FATFS就可以通过 drv[pdrv] 中的 disk_read 函数来使用用户定义函数 SD_read (或NAND_read)了
若是别的存储介质的底层驱动,drv 数组下标 pdrv 会选择其他的驱动程序
res = drv[pdrv]->disk_read(disk.lun[pdrv], buff, sector, count);
res = drv[pdrv]->disk_write(disk.lun[pdrv], buff, sector, count);
3)FreeRTOS任务调度器:因为任务APP函数是用户自定义的,FreeRTOS用函数指针的方式访问这些函数并对其进行任务调度
在cmsis_os2.h头文件中有以下定义,
使用typedef定义函数指针可以比传统不加typedef的定义方法有更多的灵活性
typedef void (*osThreadFunc_t) (void *argument);
先声明一下任务函数,用户后续可自定义函数内容
void AppTask_Recorder(void *argument);
函数的形参就可以这样简单的定义
osThreadId_t osThreadNew (osThreadFunc_t func, void *argument, const osThreadAttr_t *attr)
传递的实参直接使用用户函数名就行
osThreadNew(AppTask_Recorder, NULL, &Task_Recorder_attributes);
osThreadFunc_t func1 = AppTask_Recorder; 合法
osThreadFunc_t func2 = &AppTask_Recorder; 与上一条等价
osThreadFunc_t *func3 = &func2; 二级指针
21、预处理指令
‘ # ’开头的都是预处理指令,语句结尾都不加‘ ;’
1)#define是宏定义,使用时仅是文本的替换
,对于那些多重运算的宏定义,我们应将表达式展开
来避免潜在的bug,宏定义最好全用大写比较符合命名规范,常见易错题有
#define SQR(x) (x*x)
a = SQR(3+2); 结果为a=11,要想正确求平方需
#define SQR(x) (x)*(x)
#define DOUBLE(x) (x)+(x)
a = 10 * DOUBLE(3); 结果为a=33,若想正确求10*2*3需
#define DOUBLE(x) ((x)+(x))
#define MAX(a,b) ((a)>(b)?(a):(b))
int x=3, y=5, z=0;
z = MAX(x++, y++);
printf("x=%d, y=%d, z=%d\n", x, y, z);
输出的结果为x=4, y=7, z=6;将式子展开就会发现((x++)>(y++)?(x++):(y++))
2)#include是包含头文件
#include <filename.h> 是包含函数库中的头文件
#include "filename.h" 是包含本地工程路径下的头文件
#include "./BSP/LCD/lcd.h" 如果头文件路径太多也可以使用相对路径包含头文件
3)在头文件开头的位置一般都会加上#ifndef来避免重复定义
#ifndef _LCD_H
#define _LCD_H
......
#endif
4)条件编译,会判断语句是否为非0值而选择是否编译
#if a>0
#elif a==0
#else
#endif
22、printf
printf详细介绍这儿有,就不重复介绍了,这里就简单介绍一下函数可变参数的使用规则吧
先介绍一个并不推荐的
,使用指针的方案
1)首先你定义的函数必须要有一个不可变参数,可变参数用…表示
2)根据ATPCS规则,函数的形参存储在寄存器中。但要是对形参取址&,编译器就会将所有形参压入栈中,这样函数的形参就在内存中有自己的地址了(似乎确实是这么一回事,但这套理论有待验证)
3)对指针解引用再做类型转换就能获得传入函数的正确值
#include <stdio.h>
void print(int num, ...) {
int *addr = #
char type[10] = {0};
int a = 0;
printf("value = %4d, address = %p\n", (int)*(addr + 2 * ++a), addr + 2*a);
printf("value = %4c, address = %p\n", (char)*(addr + 2 * ++a), addr + 2*a);
printf("value = %2.2f, address = %p\n", (double)*(addr + 2 * ++a), addr + 2*a);
printf("value = %2.2f, address = %p\n", (float)*(addr + 2 * ++a), addr + 2*a);
printf("value = %4d, address = %p\n", (short)*(addr + 2 * ++a), addr + 2*a);
printf("value = %4d, address = %p\n", *(short*)*(addr + 2 * ++a), addr + 2*a);
printf("address of a = %p\n", &a);
}
int main() {
short i = 6;
print(i, 1, '2', 3., 4.f, (short)5, &i);
}
value = 1, address = 000000000061FE00
value = 2, address = 000000000061FE08
value = 0.00, address = 000000000061FE10
value = 0.00, address = 000000000061FE18
value = 5, address = 000000000061FE20
value = 6, address = 000000000061FE28
address of a = 000000000061FDD8
但其实在实际编程中并不推荐用这种方法,他存在很多例如:栈的增长方向、对齐数、莫名其妙的类型转换、平台兼容性等一系列bug
所以在实际应用中还是推荐<stdarg.h>中定义的方法
1)包含<stdarg.h>
头文件,函数可变参数用…表示,创建一个va_list
类型的变量,用va_start()
设置可变参数数量,使用va_arg()
挨个读取可变参数,最后va_end()
释放va_list
类型变量
2)值得留意的是,va_arg第二个参数如果是整形或指针要用int来接再转换成对应的类型;如果是浮点型则要用double来接,但如果你实参一个float一个double即使是相同的数值也很大概率因精度差异导致判断两个==结果为假
#include <stdio.h>
#include <stdarg.h>
void print_arg(int num,...) {
va_list arg_ptr; 创建一个va_list的变量
va_start(arg_ptr, num); 设置有多少个可变参数
printf("value = %d \n", va_arg(arg_ptr, int));
printf("value = %c \n", (char)va_arg(arg_ptr, int));
double d = va_arg(arg_ptr, double);
printf("value = %f \n", d);
double f = va_arg(arg_ptr, double);
printf("value = %f \n", f);
if(d != f) printf("double and float are not equal\n");
printf("value = %d \n", (short)va_arg(arg_ptr, int));
printf("value = %d \n", *(short*)va_arg(arg_ptr, int));
va_end(arg_ptr); 用完释放一开始申请的变量
}
int main() {
short i = 6;
print_arg(i, 1, '2', 3.6255, 3.6255f, (short)5, &i);
}
value = 1
value = 2
value = 3.625500
value = 3.625500
double and float are not equal
value = 5
value = 6