[嵌入式C语言基础] 指针
基础篇
指针是什么
相信很多C语言初学者都会头疼指针是什么,但我认为,初学者简单使用的话的确可以将其视为一种数据类型,就是指向地址的数据类型,和int,char
一样。
指针的理解就是:在指针的地方放了一个地址记录,通过该记录找到真实数据地址。
但是往往只是知道这些,还不足以取熟练应用,我们能够做的只有这些:
//声明
int *P;
char *p;
//初始化
int value = 1;
int *p = &value;
//赋值
int value = 1;
int variable = 2
int *p = &value;
p = &variable;
//作为函数参数传送值
int func( int *p, int *new_p)
深入探究
既然是指向地址的数据类型,那么我们就来看看指针的地址究竟有什么性质?
这里使用函数printf("%p,%x\r\n", a, b)
,占位符%p
可以帮助我们看到指针指向的地址。
实验准备
在keil
中,使用一个带串口的项目工程,将调试改到Use Simulator
软件模拟仿真:
然后在Debug中打开串口窗口。
注:有实机仿真的可以用实机。
实验1
//查看指针地址
int value = 1;
int *p = &value;
printf("Pointer point Address=0x%p, value=%d\r\n", p, *p);
printf("value Address=0x%p, value=%d\r\n", &value, value);
/*结果*/
/*
Start Debug
Pointer point Address=0x20000758, value=1
value Address=0x20000758, value=1
*/
实验2
//查看作为一种数据类型的指针需要占多少字节
char c = 1;
short sh = 2;
int i = 3;
long l = 4;
long long ll = 5;
char *p_c = &c;
short *p_sh = &sh;
int *p_i = &i;
long *p_l = ≪
printf("sizeof:\tchar=%d\tshort=%d\tint=%d\tlong=%d\tlong long=%d\r\n",sizeof(c), sizeof(sh),sizeof(i),sizeof(l),sizeof(ll));
printf("sizeof: Pchar=%d\tPshort=%d Pint=%d\tPlong=%d\r\n",sizeof(p_c), sizeof(p_sh),sizeof(p_i),sizeof(p_l));
/*结果*/
/*
Start Debug
sizeof: char=1 short=2 int=4 long=4 long long=8
sizeof: Pchar=4 Pshort=4 Pint=4 Plong=4
*/
为什么都是4字节?指向char的指针也是4。
其实这里有个概念:字长
.STM32
的字长为32bit
,恰为4字节,这是巧合么。不,因为有些CPU的地址长度刚好是机器字长。也就是说地址长度有32位,也就是2^32
个字节(STM32
是按字节地址寻址)地址。
那么指针是4字节也不足为奇,因为要存储一个地址,那么就需要32bit
,而不是指向char就是1字节,指向short为2字节。指针始终占4字节(严格来说是地址位数)。
实验3
现在我们知道如何打印地址,知道指针占4字节(在
STM32
中,51可能占1字节或者2字节,实际情况看地址),接下来看看指针的一些简单且常用的使用。
//简单使用同数组
//基本类型赋值给指针
int value = 1;
int *p = &value;
//指针给指针赋值
int *p2 = p;
//数组与指针
int arrary[10] = {0,1,2,3};
int *p_arrary = arrary;
printf("Arrary: Address=0x%p, arrary[0]=%d, arrary[1]=%d\r\n", arrary, arrary[0], *(arrary+1));
printf("Point Arrary: Address=0x%p, p_arrary[0]=%d, p_arrary[1]=%d\r\n", p_arrary, p_arrary[0], *(p_arrary+1));
printf("arrary[1]=0x%p, parrary[1]=0x%p\r\n", &arrary[1], p_arrary+1);
//结果
/*
Start Debug
Arrary: Address=0x20000734, arrary[0]=0, arrary[1]=1
Point Arrary: Address=0x20000734, p_arrary[0]=0, p_arrary[1]=1
arrary[1]=0x20000738, parrary[1]=0x20000738
*/
在这里有没有跟上?
可以这样去理解:广义上的指针,实际上是一串内存地址的开头。那么实际上arrary
可以认为是一串数组的开头,那么就是指针,但有一点不同的是,arrary
指向的地址是不变的,也就是恒为0x20000734
(在该实验中),而p_arrary
可以切换到其他地址。
那么指针的用法=数组头的用法,那么p_arrary
就是头指针,是否可以*p_arrary或*arrary
来取数组第一个值呢?可以预见的是,完全可以。
这里我使用指针的经验是:
1.将数组头与指针挂钩起来。
2.指针在直接使用地址操作时,就不用*。获得其中值就使用*来获得。
3.普通数据类型(int,char,short,long,double,float)等,要操作地址使用&,取值就什么都不用。
int a;
int *p;
a = *p;(层次上等于)
&a = p;(层次上等于)
注:实际上是约等于,差别在加粗文字
实验3.5
//指针移动
char c[3] = {'a','b','c'};
int i[3] = {8,9,10};
char *p_c = c; //这里是用地址,则什么都不加
int *p_i = i;
printf("c=0x%p\r\n", c);
printf("i=0x%p\r\n", i);
printf("[char]\tp=0x%p, p+1=0x%p, p+2=0x%p\r\n", p_c, p_c+1, p_c+2);
printf("[int]\tp=0x%p, p+1=0x%p, p+2=0x%p\r\n", p_i, p_i+1, p_i+2);
//结果
/*
Start Debug
c=0x20000754
i=0x20000748
[char] p=0x20000754, p+1=0x20000755, p+2=0x20000756
[int] p=0x20000748, p+1=0x2000074c, p+2=0x20000750
*/
我们发现char指针的移动和int指针的移动不一样,一个+1字节,一个+4字节。那有没有办法让char(+1)移动四字节呢?在实验5会有
实际上指针+1(单位)移动,是按照代码指定的方式取移动:
char = 1字节
short = 2字节
int = 4字节
//严格意义上以上是不对的,但对于初学者只需要记住:上面的数据类型作为指针单位移动宽度是根据芯片位数来改变的
//平时接触的32位机器都可以按以上来移动
实验4
//函数与指针
//指针作为参数
int func_1( int *p_i, char *p_c)
{
printf("[func_1]:i=0x%p,c=0x%p\r\n", p_i, p_c);
*p_i = 9999; //操作内容,用*
(*p_c)++; //操作内容,用*
//对p_i,p_c地址内的值进行改变
}
void main()
{
char c = 'g';
int i = 9548;
printf("i=0x%p,c=0x%p\r\n", i, c);
func_1( &c, &i);
printf("[value] i=%d,c=%c\r\n", i, c);
//最终i,c值被改变了
}
//结果
/*
Start Debug
i=0x20000750,c=0x20000754
[func_1]:i=0x20000750,c=0x20000754
[value] i=9999,c=h
*/
//函数指针
char func_c( int a)
{
printf("running %s: a=%d\r\n", __func__, a);
}
void main()
{
char c = 'g';
int i = 9548;
int (*func_p)(int*,char*);//要与目标函数的返回值,参数列表一致
int (*func_p2)( int *p_i, char *p_c);//要与目标函数的返回值,参数列表一致
char (*func_pc)(int);//要与目标函数的返回值,参数列表一致
char (*func_pc2)(int a); //要与目标函数的返回值,参数列表一致
//赋值
func_p = func_1;
func_p2 = func_1;
func_pc = func_c;
func_pc2 = func_c;
//执行
func_p( &i, &c);
func_p2( &i, &c);
func_pc(c);
func_pc2(i);
}
//结果
/*
Start Debug
[func_1]:i=0x20000750,c=0x20000754
[func_1]:i=0x20000750,c=0x20000754
running func_c: a=105
running func_c: a=9999
*/
实际上函数也是名也是一个指针头,或者通俗的讲为一个地址。
实验5
//比较狂野使用(主要是利用强转为指针)
int a = *(int *)(0x40010808) //翻译:将内存0x40010808看做int型指针,然后*p解析里面的内容。再放到STM32中来看,实际上是读取寄存器GPIOA_IDR,就是查看GPIOA端口的16个引脚输入状态。
int b = 0x40010808;
int GPIOA_input_val = *(int *)b; //想想这个是做什么的?其实也是一样和第一句代码
int *GPIOA_input_val_p = (int *)b; //这个实际上就是将指针GPIOA_input_val绑定到0x40010808,方便使用
//让char指针一次移动4字节
char *p_c = (int *)b;
((int *)p_c)++; //强行转换为int型指针后,一次移动便是4字节,自己试试让int型指针移动1字节。
//void指针
void *p_v = (int *)b; //空指针赋值,因为指针占用4个字节,所以不管是int,char型指针都是可以相互转换
printf("void Point= %d\r\n", *((int *)p_v)); //空指针不可直接使用,得转换后使用
#define NULL ((void*)0) //一般也可以使用0位置作为NULL
//认识指针数组与数组指针
int *p[10]; //按照结合优先级,[]先结合,那么p首先大方向是数组,接下来的可视为修饰词,修饰词为指针,那么就是p为10个 int* 组成的数组。
int (*p)[10];//试试理解这个?
//指向指针的指针
int d = 7788;
int *p_1 = &d;
int **p_2 = &p_1;
printf("p_1=0x%p,&d=%d\r\n", p_1, &d);
printf("*p_2=%p,**p_2=%d\r\n", *p_2, **p_2);
//结果
/*
Start Debug
p_1=0x20000754,&d=0x20000754
*p_2=20000754,**p_2=7788
*/
大家自己试试上面的实验看看。
这里面复杂的理解就是什么时候是用&
,什么时候直接使用指针,什么时候使用*
,现在来总结一下:
- 不管是普通数据类型还是指针,想要获得该变量的地址就需要使用
&
,且使用了&
后默认为指针类型。 - 将
指针
与int
这种数据类型请勿区别对待,赋值与初始化类似(暂时忘掉*
操作),只是赋值或初始化需要标记
为指针的数据。标记手法有3种:&
,(int *)
,指针数据
- 一个
*
解析一层指针内容。
那么怎么去理解上面的话呢?
//1
int a = 618;
int b = *&a; //这是合理的,普通数据类型,通过&符号将a提升为指针;(&a)=(int* const a');
printf("a = %d\r\n", b);
int *c;
&c; //这个是什么? 实际上可以理解为指针c被提升一层指针,变为(int **c'),同时也是int *c 的地址。
//2,伪代码,供理解,将618与*618区别,*618表示地址618。
int a = 618; //假设a地址为0x10000
int *b = *618;
b = *1234; //我们将*隐藏起来,发现实际上指针与整型赋值没有任何区别,那么这个标记怎么来的?
&a = *0x10000;
(int *0x10000) = *0x10000;
int *p = *0x10000; //假设p指向0x10000地址
这里告诉我们再使用数据时,如果想告诉编译器,这个数字(如:123456)为指针,可使用上述方式打上标记。
//3
int *a = (int *)0x10000; //假设0x10000地址存了1234
int **p = &a;
*p = (a) = *0x10000; //这里的*0x10000指示为地址
**p = *a = 1234;
如何使用指针(程序结构或者小技巧)
字节数组取出后int值
//使用场景:在串口或者接受数据时,都是以字节接收,接收缓冲区为Buffer[512];
//发过来的数据前4字节为帧长,如何快速取出帧长,总Buffer中。
int char BUffer[512];
......接收代码.......
int frame_size;
frame_size = *((int *)BUffer); //这里注意大小端传输
//或者
int* frame_p = Buffer;
frame_size = *frame_p; //这里注意大小端传输
小端是反向排序的,比如
int a = 0x12345678
,那么小端传输是:78 56 34 12
,高在地址高位。
作为队列或者栈的头或栈顶
//代码略
分层思想下的代码,上层给下层预留自定义接口
//使用场景:编写屏幕驱动
//有两层:上层是绘制图像层,下层是屏幕驱动层
//想同时兼容多个屏幕,又想重用代码。
//graphy.c(绘制图像层), tft.c(tft屏幕驱动),oled(oled屏幕驱动)
//在graphy.c中
struct graphy{
int resolution; //分辨率
int id; //屏幕ID编号
....
void (*draw_point)( int x, int y);
} graphy;
inline void register_draw_point_func( void (*func)( int x, int y)){ //给驱动注册的接口
graphy.draw_point = func;
}
//在tft.c中实现上层给的接口
void tft_draw_point( int x, int y){
//code
}
register_draw_point_func( tft_draw_point);
//同理oled.c也可做同样实现,这样可以实现先写上层代码,再构建下层驱动
使用void *来占位
//这里可以参考qsort函数使用
void qsort(void*base,size_t num,size_t width,int(*compare)(const void*,const void*));
//qsort 有三个参数
base:排序数组
num:排序个数
width:数组中每个元素的大小,字节为单位
compare:比较函数
//该函数可比较int,char,short等大部分数据
多返回值的函数
//使用场景:在计算某个屏幕长宽时,我们希望能够有两个返回值,但是函数一般只带一个返回值
bool CaculateResultion( int *width, int *heigh){
//deploy your code
*width = 123;
*heigh = 456;
}
int w,h;
bool res = CaculateResultion( &w, &h);
printf( "width = %d, heigh = %d\r\n", w, h);
链式表
//常见的链表都是需要指针来实现
typedef struct node {
int vaule;
node* privious;
node* next;
}node;
其他用法
熟练使用指针,根据情况使用
提高篇(加深理解)
在我的理解:一切皆为数据。有数据就有地址,知道地址,数据就很容易解析出来
探究其本质
可是对于某些复杂应用,比如指针函数
,指针数组
,指向指针的指针
,结构体指针
等这些,我们如果能够有一些通用的理解,达到一劳永逸?
我们使用Keil
来编程看看,芯片选择大家熟悉的STM32F103
,通过实验的方式来理解它,大家在看的时候希望能够跟着做,同时学习如何使用Keil
。
地址与指针关系
谈到指针,我们都会联系到地址,那么地址是什么,怎样取理解地址?
计算机里面的地址
实际上地址是储存器中的存储单元位置,那么在STM32
存储器指的是什么?先来看下储存器的结构:
可以看到无论是二维的,还是三维的,其都需要x,y
或x,y,z
来实现,那么将x,y
和并在一起可以认为是一个地址(x,y
分别为字线地址和字节线地址,如果是字节寻址)
那继续来看看STM32
内的存储器,这里所指的存储器实际上是以flash
、外设
和SRAM
的统称,ARM将所有的存储单元通过BUS
总线统一线性排布的总称,来看看STM32
系统结构图(任意32手册):
从图上可以知道Cortex-M3
使用BUS matrix
将flash
,SRAM
和外设(如GPIO,TIMER)
集合在一起,那么这个又是集合这些东西又按照什么样的规则呢?
其实ARM已经给出答案,在Cortex-M3
手册(在正点原子给的资料包中有该书)中给出指示:
由上图可以知道:
Peripheral(外设)
地址:0x40000000-0x9FFFFFFF,共0.5G
SRAM
地址:0x20000000-0x3FFFFFFF,共0.5G
CODE
地址:0x00000000-0x1FFFFFFF,共0.5G
那么知道了这些,对我们理解地址有什么帮助?继续往下看。
我们再看看STM32
手册,找到如下表:
发现表在0x40000000
位置戛然而止:
正好对应了外设的初始地址。(其他几个,大家可以自行验证)
地址数据的操作
实现地址与数据的操作,得借助于指针
又上图可以知道,不管是写的bin
文件(存在code
),还是变量(变量在sram
),还是外设,都有一个地址,那么是不是可以使用地址就能访问这些呢?
答案当然是:YES
//实验5已经使用过了
//这次我们来看看库函数与寄存器的读取GPIOA输入数值的源码(STM32F103)
//寄存器版本
int value = GPIOA->IDR;
#define GPIOA ((GPIO_TypeDef *) GPIOA_BASE) //stm32f10x.h
#define GPIOA_BASE (APB2PERIPH_BASE + 0x0800)
typedef struct
{
__IO uint32_t CRL;
__IO uint32_t CRH;
__IO uint32_t IDR;
__IO uint32_t ODR;
__IO uint32_t BSRR;
__IO uint32_t BRR;
__IO uint32_t LCKR;
} GPIO_TypeDef;
#define APB2PERIPH_BASE (PERIPH_BASE + 0x10000)
#define PERIPH_BASE ((uint32_t)0x40000000)
/*分析
GPIOA是GPIO_TypeDef *(指针),那么GPIOA_BASE = PERIPH_BASE(0x40000000)+0x10000+0x0800=0x40010800
那么翻译过来就是:
GPIOA = ((GPIO_TypeDef *)0x40010800),将一个地址强行转换为结构体指针
*/
//库函数版本
GPIO_ReadInputDataBit( GPIOA, 1);
uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{
uint8_t bitstatus = 0x00;
/* Check the parameters */
assert_param(IS_GPIO_ALL_PERIPH(GPIOx));
assert_param(IS_GET_GPIO_PIN(GPIO_Pin));
if ((GPIOx->IDR & GPIO_Pin) != (uint32_t)Bit_RESET)
{
bitstatus = (uint8_t)Bit_SET;
}
else
{
bitstatus = (uint8_t)Bit_RESET;
}
return bitstatus;
}
typedef struct
{
__IO uint32_t CRL;
__IO uint32_t CRH;
__IO uint32_t IDR;
__IO uint32_t ODR;
__IO uint32_t BSRR;
__IO uint32_t BRR;
__IO uint32_t LCKR;
} GPIO_TypeDef;
//GPIOA的宏定义与上面一致
/*分析
实际上与上面没什么区别
*/
这里告诉我们一切都是地址,只要有地址,就可以转换为指针,通过指针获取其数据。
其他
实际上在10个寻址方式中,我们常常使用的寄存器寻址,或者间接寻址,都是指针的实现。
希望大家多学习源码,学习基础知识。这样才能走得更远!!!