一.指针的本质
指针的本质就是有类型的地址。地址指的是地址总线上的地址码表,而类型决定了我们操作这个指针时,要从这个地址码上读写的长度。即指针的类型决定读写一个地址块的数据长度(1字节、2字节、4字节还是结构体定义的长度)。
比如(uint8_t *)(0x12345678):C语言中*可以把他后面的内容变成地址码,所以(*)0x12345678)就是把这个常数转化为一个地址码(即指针),那这个指针的类型是什么呢?是uint8_t类型的。那么我们去操作这个指针的时候,就是读写0x12345678这个地址上一字节的内容。
*((uint8_t *)(0x12345678))= 0x55;
二.指针的大小、指针变量的值、指向指针的指针以及指针的自增
1.指针的占用的字节数就是单片机地址总线的字节数(位数/8);
2.指针p本身是个变量,只不过这个变量的值是他所指向的内存的地址,通过*p就可以读写那块内 存,读写的数据长度就是指针类型的大小
3.
如上图所示,q就是个指向指针的指针,q的值就是指针p的地址。通过*q就可以直接读写p的值,其实此时*q就是p。而相对的,p是一个指针,他的值就是buf[8]这个数组第一个元素的地址,通过*p就可以读写buf[0]。我们前面说过,指针是有类型的地址,而当前类型是int8_t,那么让p+1,地址就只会往后走一个int8_t的长度,那么p+1的数值就是buf[1]的地址,*(p+1)就是操作buf[1]了。
再如果指针p的类型是int,即int *p,如果p等于buf[0]的地址,那p+1以后的值就是往后int长度(4个),p+1的值就是&buf[4],*(p+1)访问的就是buf[4]。
总结
三.数组和指针
1.不要把数组a[n]后面的方括号当成是数组的专利
[n]的真正含义是以前面的指针(有类型的地址)为基准,向后移动长度(n*类型大小)个字节后的地址位置,取出这个位置上的值。
比如:
int a[10];
int *p;
p = a;
此时就可以直接操作p[5],p[5]就是a[5]。
四.void * 没有类型的指针
有的人把void *称作跳跃力不确定的指针,因为void *p,对p++不知道指针会指向哪里。那么void *有什么用呢?
void *包容性是很强的,我们可以把任意类型的地址赋值给他而不会有任何警告,当函数不知道传进来的参数是什么类型时,就可以使用void *
比如uart_receive_data(void *p)这个函数,他不知道传进来的是什么类型,可以先接收。然后在函数内部,用某种类型的指针强制转化以后
接收这个地址,然后再以这种类型解析,如下所示:
uart_receive_data(void *p)
{
struct xx*q;
q=(struct xx*)p;//强制转化为特定类型的指针
//后面再以struct xx的格式对数据进行解析
}
需要注意的是,void *类型的指针赋值给别的指针的时候,必须做强制转化
五.指针的初值及如何区分指针数组和数组指针
1.指针的初值
任何变量初始化的时候都会赋一个初值,如果没有指定初值,会被赋值为0(一般启动文件里面完成)。而为了避免野指针产生问题,C语言有一个保护措施,
那就是如果指针的值为0或者NULL,对这个指针操作都不会对程序产生实质的破坏
2.指针数组和数组指针的区别
可以这样,按类型(后两个字)用括号先结合
比如指针数组:首先是一个数组,那就应该是(p[5]),然后对于指针加上星号*变成*(p[5]),最后加上类型 int变成int *(p[5])
比如数组指针:首先是指针,那就是(*p),然后对于数组加上[ ]就是(*p)[10],最后加上类型就是int (*p)[10]
比如函数指针:首先是指针,那就是(*p),然后是函数,那结构就是type_t (*p)(类型1,类型1),然后再加上类型就是int (*p)(int,int)
3.补充:指向二维数组的指针的定义方法
uint32_t array[2][3]={{1,2,3},{3,4,5}};
uint32_t (*(*p))[3];//定义一个指向二维数组的指针
p=array;
printf("%d",p[0][0]);//访问内容用p[x][y]
六.结构体指针
1.结构体的基本使用
访问结构体变量的内容用'.' ,访问结构体指针里的内容用‘->’
struct student
{
int stuid;
char name[10];
int age;
int score;
}stu1;
struct student stu;
struct student *pstu;
pstu=&stu;
pstu->age = 20;//对于结构体指针,要访问里面的内容就用->
printf("%d",stu。age);//对于结构体变量,访问里面的内容就用.
再看一个结构体嵌套结构体的例子
struct stru1
{
int x;
};
struct stru2
{
struct stru1 *a;
int b;
};
int main(void) {
int i = 0;
struct stru1 stu1;
struct stru2 stu2;
struct stru1 *stu3 = &stu1;
stu2.a = &stu1;
stu2.a->x = 20;//struct stru2里包含的是stru1 *,这时是用箭头访问stru1内部的内容
printf("Result is %d",stu2.a->x);//打印结果
return 0;
}
七.联合体指针
下面举了个简单的例子,功能是实现浮点数取反
typedef union
{
float a;
uint8_t bytes[4];
} data_uni;
data_uni x = {.a = 1.0};
data_uni *p = &x;
p->bytes[3] ^= 0X80;//这里实现了浮点数的取反
printf("%.f", p->a);
八.malloc的使用
1.不要滥用malloc,malloc的5大使用原则
1)代码里尽量不用动态分配,尽量使用静态分配,也就是类似于int array[100]这样的方式。将内存在编译期间就定下来。
2)使用了malloc一定要记得对其所申请的内存进行free。
3)调用malloc之后,一定要对其返回值进行判断,而不能贸然去使用。因为可能返回个NULL
4)不要频繁调用malloc,因为malloc会产生内存的碎片,为什么会有碎片,振南后面会详细讲。
5)适当设置你的heap size,也就是堆的大小
2.malloc背后的原理
malloc分配的其实是堆里面的空间,这个空间在编译器里面可以设置。malloc就是从堆里面获取一块空的地址块,然后返回这个地址。
堆空间里剩余可用的内存会以链表的形式连接起来。
3.频繁使用malloc和free会导致内存都是碎片
当我们申请内存时,比如其中一块大小为100,然后我们申请了90,那这块内存就剩下10.下次再申请的时候,由于这块内存太小,大概率不够分配,然后就变成一块碎片内存。如果堆空间里面的大内存变成一块块小内存,虽然空余空间还有,但是malloc申请不到了。
九.关于内存溢出
上图是启动文件中的栈和堆分配的代码
1.预防内存溢出的方法(堆)
(1)写程序的时候,尽量杜绝malloc之后没有Free的情况,最大限度的保证内存分配的闭环。一个原则是:谁申请谁释放。
(2)如果是基于嵌入式操作系统,比如freeRTOS、rt thread的话,可以去看一下程序运行过程中的内存是否有持续的增长,如果呈现不断发散的趋势,那很有可能你的程序中有隐藏的内存泄露或者malloc后没free。
2.预防栈溢出
1、函数里局部变量的总大小不能太大;
2、函数的嵌套调用层数不要太多;
3、函数的规模不宜太大,一般都是建议50~200行;
十.STM32的启动流程
首先把0x08000000地址上的内容赋值给MSP(栈指针),然后取出0x08000004地址复位函数的地址并赋值给PC指针,这样程序就跳转到Reset_Handler了,在Reset_Handler中有一个SystemInit(变量等的初始化)以及_main,最终程序进入到main
十一.STM32的bootloader核心代码
typedef void (*iapfun)(void);
iapfun jump2app;
void MSR_MSP(uint32_t addr)
{
//set main stack value
_ASM volatile("MSR,MSP,r0");
_ASM volatile("BX r14");
}
void iap_load_app(uint32_t appaddr)
{
__disable_irq();
jump2app=(iapfun)(*vu32)(appaddr+4);
MSR_MSP(*vu32)(appaddr);
//初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)
jump2app();//跳转到APP.
}
十二.柔性数组
// 定义数据帧结构体,包括头部信息、数据类型以及一个指向缓冲区的指针
typedef struct
{
uint8_t header; // 数据帧头部
uint16_t data_type; // 数据帧类型
uint8_t *pbuf; // 缓冲区指针
} data_frame;
// 动态分配一个data_frame类型的内存空间
data_frame *p = (data_frame *)malloc(sizeof(data_frame));
// 再次动态分配一个datalen大小的内存空间,并将其赋值给p->buf
p->pbuf = (uint8_t *)malloc(datalen);
// 从串口1接收datalen长度的数据,并存入p->buf中
uart1_receive_data(p->buf, datalen);
//注意及时free,避免代码泄露
结构体中的pbuf只是一个指针,通过指向malloc申请的一块内存空间,使得通过p->buf可以访问一个按照需要分配的内存空间。但是这样也可能会有些问题,因为我们申请动态内存后,一般只会free(p),不会free(p->buf),所以会导致这一块内存变成僵尸内存。
typedef struct
{
unsigned char head;//1字节
short data_type; //2字节,由于结构体的对齐规格和前面之间空着1字节
unsigned char pbuf[];//占位,但是空间不在
}data_Frame;
int main()
{
data_Frame *p = (data_Frame *)malloc(sizeof(data_Frame)+10);
/* Write C code in this online editor and run it. */
printf("p->pbuf=%d,p+1=%d\n",p->pbuf,p+1);//值是一样的,说明pbuf占的位置是在下个结构体开始的位置
printf("Frame's size=%d\n",sizeof(data_Frame));//大小是4,说明pbuf占位的空间不算在结构体中
return 0;
}
通过pbuf[]占位,malloc申请的内存空间大一点,由于pbuf[]所在的地址就在p的后面,所以p->buf和p+1指向的位置是一样的。这样就住需要malloc一次,释放也只要free(p)