[嵌入式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
*/

大家自己试试上面的实验看看。

​ 这里面复杂的理解就是什么时候是用&,什么时候直接使用指针,什么时候使用*,现在来总结一下:

  1. 不管是普通数据类型还是指针,想要获得该变量的地址就需要使用&,且使用了&后默认为指针类型。
  2. 指针int这种数据类型请勿区别对待,赋值与初始化类似(暂时忘掉*操作),只是赋值或初始化需要标记为指针的数据。标记手法有3种:&,(int *),指针数据
  3. 一个*解析一层指针内容。

那么怎么去理解上面的话呢?

//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存储器指的是什么?先来看下储存器的结构:

img

​ 可以看到无论是二维的,还是三维的,其都需要x,yx,y,z来实现,那么将x,y和并在一起可以认为是一个地址(x,y分别为字线地址和字节线地址,如果是字节寻址)

​ 那继续来看看STM32内的存储器,这里所指的存储器实际上是以flash外设SRAM的统称,ARM将所有的存储单元通过BUS总线统一线性排布的总称,来看看STM32系统结构图(任意32手册):

​ 从图上可以知道Cortex-M3使用BUS matrixflash,SRAM外设(如GPIO,TIMER)集合在一起,那么这个又是集合这些东西又按照什么样的规则呢?

​ 其实ARM已经给出答案,在Cortex-M3手册(在正点原子给的资料包中有该书)中给出指示:


ARM官网-内存模型

​ 由上图可以知道:

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个寻址方式中,我们常常使用的寄存器寻址,或者间接寻址,都是指针的实现。

​ 希望大家多学习源码,学习基础知识。这样才能走得更远!!!

posted @ 2022-06-11 21:42  邪恶法师  阅读(283)  评论(0编辑  收藏  举报