C指针笔记
从硬件开发的角度看指针
目录
前言
指针用途很多,几乎做软件硬件的都会用到。以写硬件为例,指针通常用于读取指定地址存放的数据,对硬件指定寄存器写入不同数据等等。另外,做软件方面的,例如一些复杂的数据结构也会用到指针。在这里以硬件工程师的角度用伪代码讲述指针是什么,怎么用。
1、指针是一个?
当一个定义了一个变量,就会再存储区域划分一小块地方用来存放这个数据。以一个32位单片机写程序为例子,定义一个uint32_t(32位无符号类型)类型为例,如下:
uint32_t i = 0xffffffff;
上述为:在空间中划分一块uint32_t类型的区域存放了数据:0xffffffff。然后,这个变量的名字叫i。在这时,我定义一个指针变量,如下:
uint32_t *p;
上述为:定义了一个uint32_t 指针类型的变量p。然后有下边的操作:
p = &i;
上述为:向uint32_t类型的指针变量p赋值,赋值内容为uint32_t类型变量i存放数据地址(取地址)。如果现在有一个uint32_t类型的指针变量p,里面存放的数据为i存放数据地址。想要读取这个i存放数据地址里面存放的数据时。结合前面三个步骤如下:
uint32_t i = 0xffffffff;
uint32_t *p;
uint32_t Result;
p = &i;
Result = *p;
Result读出来的数据就是uint32_t类型变量i中存放的数据0xffffffff,和i相同。程序的流程如下:
1、定义一个uint32_t类型变量i并且赋值0xffffffff;
2、定义一个uint32_t指针类型的变量p;
3、定义一个uint32_t类型变量Result;
4、对p赋值,内容为取i的地址;
5、对Result赋值,内容为变量p存放的地址数据所对应地址里面的数据(解引用)。
以硬件工程师的角度来看,指针是一个地址,指针变量是一个存放指针(地址)信息的一个容器而已。
2、指针变量大小
指针变量,分为很多种类型,以32为单片机为例,例如:uint8_t、uint16_t、uint32_t、float、double、char等等。除了名字不同,最主要时大小不一样。在32位单片机里面,一个地址里面有四位(1个字节),现在向一个地址:0x7f00写入一个uint8_t类型数据,内容为0xf1。以华大单片机的DDL库为例:
Flash_WriteByte(0x7f00, 0xf1);
上述为DDL库一个写入flash的函数,在地址0x7f00写入1字节数据,内容为0xf1。
uint8_t a;
Flash_WriteByte(0x7f00, 0xf1);
a = *(__IO uint8_t *)0x7f00;
上述为:定义一个uint8_t类型的变量a,读取在地址0x7f00的数据,读出的内容为0xf1。然而,这里写入的是1字节数据,占用了两个连续地址的空间。但是要获取该地址中存放数据,只需要读取首地址即可。同样的,uint16_t占用了连续四个地址的空间,uint32_t占用了连续八个地址的空间,但只需要读取其开头的地址即刻获取这个类型变量地址存放的数据全部。
例如uint8_t a,我们知道这个类型大小1字节,且存放了:0xf1。在单片机中,占有两个连续地址存储空间(一个地址对应4位,两个地址对应连续的8位)。
| 1 | 1 | 1 | 1 |
| 0 | 0 | 0 | 1 |
如上述所示,连续的地址从0x7f00到0x7f01两个连续空间一共8位所存放的数据,合起来就是0xf1(二进制对应:11110001)。所以只要知道先前定义好变量的数据类型(这里为uint8_t),就知道要往后读取连续几个地址的数据(uint8_t要读取2个连续地址),因此指针变量只需要存储该变量的首地址即可。同样地,uint16_t占用4个连续地址的空间,uint32_t占用连续8个地址的空间,如此类推。
一般地,在开发单片机时候如果使用到Flash功能且条件允许下,我会选择在存储数据时,在每个数据所占用地址之间空出一个地址不写入任何数据。由于外部环境等等不确定因素,空出的地址可以在一定程度上减少数据存储和读取出错的可能性。
3、指针数组(以一维举例子)
在1中,有讲到uint32_t *p是一个uint32_t 指针类型的变量p。
uint32_t *p;
如果修改成这样:
uint32_t *p[3];
这就是指针数组,实质是一个数组里面存放的元素全是指针,且指针的类型为uint32_t指针。这里是,定义了一个类型为uint32_t的指针数组p。里面的元素有:p[0]、p[1]、p[2]。于是可以这么使用:
#include <stdio.h>
#define unit32_t int
int main()
{
/*定义了六个数并且赋初值*/
uint32_t i0 = 0;
uint32_t i1 = 1;
uint32_t i2 = 2;
uint32_t Result0;
uint32_t Result1;
uint32_t Result2;
/*定义一个指针数组*/
uint32_t *p[3];
/*给指针数组里面大的每个元素赋上i0、i1、i2的地址*/
p[0] = &i0;
p[1] = &i1;
p[2] = &i2;
/*读取i0、i1、i2的地址里面的内容并且赋值给Result0、Result1、Result2*/
Result0 = *p[0];
Result1 = *p[1];
Result2 = *p[2];
printf("result[0] = %d \n",*p[0]);
printf("result[1] = %d \n",*p[1]);
printf("result[2] = %d \n",*p[2]);
return 0;
}
结果Result0为0;Result1为1;Result2为2。
4、数组指针(以一维举例子)
数组指针是一个数组里面元素的指针(数组里面各个元素的地址信息)。和指针数组不同,修改一下3中的例子。数组指针应该这样表示:
uint32_t (*p)[3];
上述为定义了一个uint32_t类型的数组指针,叫p,里面一共有3个元素。然后,这里进行如下操作:
#include <stdio.h>
#define unit32_t int
int main()
{
uint32_t p_list[3] = {3,2,1};
uint32_t (*p)[3];
uint32_t *a[3];
uint32_t result0;
uint32_t result1;
uint32_t result2;
p = &p_list;//或者写为 p = p_list; 也是可以的,因为数组名就是数组指针。
printf("p[0] = %d \n",p[0]);
printf("p[1] = %d \n",p[1]);
printf("p[2] = %d \n",p[2]);
a[0] = p[0];
a[1] = p[0]+1;
a[2] = p[0]+2;
result0 = *a[0];
result1 = *a[1];
result2 = *a[2];
printf("result0 = %d \n",result0);
printf("result1 = %d \n",result1);
printf("result2 = %d \n",result2);
return 0;
}
上述结果result[0]为0;result[1]为1;result[2]为2。首先,定义了一个数组,类型为uint32_t,名称为p_list,并且有3个元素。定义了一个uint32_t类型的数组指针,名称为p,里面一共有3个元素。定义了一个类型为uint32_t的指针数组,名称为a,里面有3个元素。定义了一个数组,类型为uint32_t,名称为result,并且有3个元素。注意:C的编译器并不是睿智,数组指针只会记录数组大的首地址,而其他元素的首地址则是数组首地址加上步长(首元素地址为:p[0],第二个元素地址为:p[0]+1,第三个元素地址为:p[0]+2,如此类推)。
然后,将数组p_list的地址信息存放到数组指针p里面。然后将数组指针p里面的存放的p_list地址信息(p_list首地址信息)逐一从0加到2并赋值给指针数组a里面的元素(a是用来存放指针的数组,这样赋值之后,相当于a中各个元素里面存储的就是数组p_list中各个元素的地址信息)。有p赋值给a[0],p+1赋值给a[1],p+2赋值给a[2]。最后,result数组对指针数组a里面的元素存储的地址信息逐一进行解引用。整个过程下来,结果和以下操作无异:
uint32_t p_list[3] = {0,1,2};
uint32_t result[3];
result[0] = p_list[0];
result[1] = p_list[1];
result[2] = p_list[2];
5、二维数组的指针
有这么一个两行三列的二维数组:
uint32_t L[2][3] = {0,1,2,3,4,5};
可以看作是这样排列的矩阵:
| 0 | 1 | 2 |
| 3 | 4 | 5 |
对应数组符号表示为:
| L[0][0] | L[0][1] | L[0][2] |
| L[1][0] | L[1][1] | L[1][2] |
上述数组可以看成L里面包含:L[0]和L[1]两个元素。然后L[0]里面又包含:L[0][0]、L[0][1]、L[0][2];L[0]里面又包含:L[1][0]、L[1][1]、L[1][2]。一般地,数组名称为数组的首地址,二维数组的数组名为二维数组的首行地址、首元素地址。如果对数组名+1,则跳转到第二行元素的首地址,如此类推。
#include <stdio.h>
#define uint32_t int
int main()
{
/*
0 1 2
3 4 5
*/
uint32_t L[2][3] = {0,1,2,3,4,5};
uint32_t (*p)[3];
uint32_t u32result[6];
p = L;
u32result[0] = *(*p);
u32result[1] = *(*p+1);
u32result[2] = *(*p+2);
u32result[3] = *(*(p+1));
u32result[4] = *(*(p+1)+1);
u32result[5] = *(*(p+1)+2);
printf("u32result[0] = %d \r\n",u32result[0]);
printf("u32result[1] = %d \r\n",u32result[1]);
printf("u32result[2] = %d \r\n",u32result[2]);
printf("u32result[3] = %d \r\n",u32result[3]);
printf("u32result[4] = %d \r\n",u32result[4]);
printf("u32result[5] = %d \r\n",u32result[5]);
printf("size u32result[2] = %d \r\n",sizeof(*(*p+2)));
printf("size 2 = %d \r\n",sizeof(*(p+1))); //第二行总长度
return 0;
}
结果:
u32result[0] = 0
u32result[1] = 1
u32result[2] = 2
u32result[3] = 3
u32result[4] = 4
u32result[5] = 5
size u32result[2] = 4
size 2 = 12
上述操作,是定义了一个uint32_t类型的数组L[2][3],定义一个uint32_t类型的数组指针p。然后把二维数组L的首地址(或者是首行地址)赋值给p。如果单纯看*p、*(p+1),它只会表示单独成行的数据。但是在等式中,是不会把整行数据放到一个变量里面,于是便转化为首地址。如果放到sizeof()这一类函数里面,例如上述操作sizeof(*(*p+2))等于,第一行第三个元素的长度。另外同时:*(p+1)代表了第二行的整行数据的首地址,然后放入sizeof函数里面,表示为算出第二行整行数据长度。
单独看u32result[1] = *(*p+1);意思为:把第1行第1个数据地址进行解引用,然后再把地址里面的数据赋值给u32result[1]。
关于数组指针uint32_t (p*)[3]定义为3维而不是2维。因为数组uint32_t L[2][3]里面有两行三列。
在编译器里面是把它分为三列的,所以p指针被定义为3维:
| 0 | 1 | 2 | ||
| 3 | 4 | 5 |
要知道目标元素地址,需要先知道行首地址,然后再根据是第几列元素加上步长即可。当知道第一行首地址的时候,要想知道第二行首地址,p+1的地址将从p向前移动3*4=12个地址。因为即使是二维数组,数据在存储空间也是连续线性的排列,并不是像矩阵一样。
该数组在存储空间实质是这样排列的:
| 0 | 1 | 2 | 3 | 4 | 5 |
6、函数指针
函数指针指的的是函数的指针,理解为存放函数地址的容器。下面定义一个函数指针和一个函数。
uint32_t (*p)(uint32_t a,uint32_t b);
uint32_t add(uint32_t i,uint32_t j)
{
uint32_t result;
result = i + j;
return result;
}
上述定义了一个类型为uint32_t的函数指针p。一个返回数据类型为uint32_t的函数add();其参数为:uint32_t i和uint32_t j。根据先前定义好的函数和函数指针,假如有这样操作:
uint32_t feedback;
p = add;// add 改写成 &add 也是可以的
feedback = p(1,1);
结果是feedback的值为2。有先前定义好了一个函数指针p,在等式p = add;中,函数名称:add表示了这个函数的地址,然后把地址赋值到p里面。然后把参数代入函数指针p,返回的值赋值给feedback。
7、指针函数
指针函数指的是,返回值为指针的函数。下面定义一个数组和指针函数。
uint32_t p_list[3] = {0,1,2};
uint32_t *feedback(uint32_t i)
{
uint32_t *list_i;
uint32_t (*p)[3];
p = &p_list;
switch(i)
{
case 0: list_i = p[0] + 0; break;
case 1: list_i = p[0] + 1; break;
case 2: list_i = p[0] + 2; break;
default: list_i = NULL; break;
}
return list_i;
}
上述,在指针函数uint32_t *feedback(uint32_t i)中,如果输入0、1、2中的一个数,则可以返回数组p_list[3]中对应的一个元素地址。
过程如下:
1、定义了数组p_list[3],内容为:0、1、2。
2、定义了一个指针函数uint32_t *feedback(uint32_t i)。
3、在指针函数中,定义局部变量:指针uint32_t *list_i;数组指针uint32_t (*p)[3];然后,把p_list的地址赋值给指针p(解引用)。然后在switch\case函数里面对指针进行运算,运算结束后,返回数组p_list[3]中任意一个元素的地址。
8、结构体指针
结构体指针就是存放这个结构体数据存储空间的首地址,这个和数组类似。以单片机控制一个电机调速PID结构体为例子:
struct pid
{
float kp;
float ki;
float kd;
}motor_pid;
这是一个定义好的结构体,里面有三个成员:float kp、float ki、float kd。当需要赋值的时候,可以这样做:
struct *pid p;
p = &motor_pid;
p -> kp = 1.1;
p -> ki = 1.2;
p -> kd = 1.3;
上述为定义了一个pid类型的结构体指针p,然后把motor_pid的地址赋值给p。然后使用符号:-> 把p分别指向成员kp、ki、kd进行赋值。在这里,p就是结构体指针(就是结构体motor_pid的首地址)。
更多推荐


所有评论(0)