使用指针可以分离硬件层驱动程序(GPIO、串口、定时器)和应用层程序,达到程序分离、提高程序可移植性的目的,但是容易出bug。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Pointer.cpp
#include <stdio.h>
int main()
{
int a;
int *p; //将int*视为一个关键字来理解,这个关键字是为了创建指向int类型数据的指针地址
a = 10;
p = &a;
printf("//-----------------变量a地址-------------------//\r\n");
printf("&a=0x%x\r\n",&a);
printf("p=0x%x\r\n",p);
printf("&p=0x%x\r\n\r\n",&p);

printf("//----------------变量a的值--------------------//\r\n");
printf("a=%d\r\n",a);
printf("*p=%d\r\n\r\n",*p);

printf("//----------通过指针变量改变a的值---------------//\r\n");
*p = 11;
printf("a=%d\r\n",a);
printf("*p=%d\r\n\r\n",*p);

printf("//-----通过a的内存地址改变a的值(每次的地址不同)---------------//\r\n");
*(unsigned int*)(0x61fe1c) = 12;
printf("a=%d\r\n",a);
printf("*p=%d\r\n\r\n",*p);

return 0;
}

或者可以将 *p 理解为一个变量,只有当 *p 和 a 的数据类型相同时,p 中才可以存放 &a。

输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ .\Pointer.exe
//-----------------变量a地址-------------------//
&a=0x61fe1c
p=0x61fe1c
&p=0x61fe10

//----------------变量a的值--------------------//
a=10
*p=10

//----------通过指针变量改变a的值---------------//
a=11
*p=11

//-----通过a的内存地址改变a的值(每次的地址不同)---------------//
a=12
*p=12

数组与指针

编译器会分配连续地址的内存来存储数组里的元素,如果把数组地址赋值给指针变量,那么就可以通过指针变量读写数组中的元素。
以下有两种方法,均可将数组buff地址赋值给指针变量p1,p2。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// array_and_pointer_define.cpp
#include <stdio.h>
int main()
{
int buff[5] = {1,2,3,4,5};
int *p1,*p2;
p1 = buff; // 赋值方法1
p2 = &buff[0]; // 赋值方法2

printf("buff = 0x%x\r\n",buff);
printf("&buff = 0x%x\r\n",&buff);
printf("p1_addr = 0x%x\r\n",p1);
printf("p2_addr = 0x%x\r\n\r\n",p2);

printf("buff[0] = %d\r\n",buff[0]);
printf("*p1 = %d\r\n",*p1);
printf("*p2 = %d\r\n",*p2);
return 0;
}

输出结果如下:

1
2
3
4
5
6
7
8
9
$ .\array_and_pointer_define.exe
buff = 0x61fdf0
&buff = 0x61fdf0
p1_addr = 0x61fdf0
p2_addr = 0x61fdf0

buff[0] = 1
*p1 = 1
*p2 = 1

指针的自加减运算

指针变量除了可以获取内存地址的值,还可以用来进行加减运算,此时加减的是地址。
指针变量的类型也会影响自加减运算后指针所存储的地址的值。

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// array_and_pointer_++or--.cpp
#include <stdio.h>
int main()
{
unsigned char buff[5] = {1,2,3,4,5};
unsigned char *p;
p = &buff[0];
printf("&buff[0] = 0x%x buff[0] = %d\r\n",&buff[0],buff[0]);
printf("&buff[1] = 0x%x buff[1] = %d\r\n",&buff[1],buff[1]);
printf("&buff[2] = 0x%x buff[2] = %d\r\n",&buff[2],buff[2]);
printf("&buff[3] = 0x%x buff[3] = %d\r\n",&buff[3],buff[3]);
printf("&buff[4] = 0x%x buff[4] = %d\r\n\r\n",&buff[4],buff[4]);

printf("p = 0x%x *p = %d &p = 0x%x\r\n",p,*p,&p);p++;
printf("p = 0x%x *p = %d &p = 0x%x\r\n",p,*p,&p);p++;
printf("p = 0x%x *p = %d &p = 0x%x\r\n",p,*p,&p);p++;
printf("p = 0x%x *p = %d &p = 0x%x\r\n",p,*p,&p);p++;
printf("p = 0x%x *p = %d &p = 0x%x\r\n",p,*p,&p);

return 0;
}

输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
$ .\array_and_pointer_++or--.exe
&buff[0] = 0x61fe1b buff[0] = 1
&buff[1] = 0x61fe1c buff[1] = 2
&buff[2] = 0x61fe1d buff[2] = 3
&buff[3] = 0x61fe1e buff[3] = 4
&buff[4] = 0x61fe1f buff[4] = 5

p = 0x61fe1b *p = 1 &p = 0x61fe10
p = 0x61fe1c *p = 2 &p = 0x61fe10
p = 0x61fe1d *p = 3 &p = 0x61fe10
p = 0x61fe1e *p = 4 &p = 0x61fe10
p = 0x61fe1f *p = 5 &p = 0x61fe10

此时p使得指针地址偏移1byte,因为unsigned char类型占据1byte的内存空间;假如指针变量指向值的数据类型为int,p会使指针地址偏移4byte,因为int类型占据4byte的内存空间。

二维数组与指针

对于二维数组,不能直接以"p = buff"赋值,必须使用以下格式赋值

1
p = &buff[0][0];

二维数组与指针的操作也是一样的,都是分配连续地址存储数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//two-dimensionalArrayPointer.cpp
#include <stdio.h>
int main()
{
unsigned char buff[2][3] = {{1,2,3},{4,5,6}};
unsigned char *p;

p = &buff[0][0];
printf("&buff[0][0] = 0x%x buff[0][0] = %d\r\n",&buff[0][0],buff[0][0]);
printf("&buff[0][1] = 0x%x buff[0][1] = %d\r\n",&buff[0][1],buff[0][1]);
printf("&buff[0][2] = 0x%x buff[0][2] = %d\r\n",&buff[0][2],buff[0][2]);
printf("&buff[1][0] = 0x%x buff[1][0] = %d\r\n",&buff[1][0],buff[1][0]);
printf("&buff[1][1] = 0x%x buff[1][1] = %d\r\n",&buff[1][1],buff[1][1]);
printf("&buff[1][2] = 0x%x buff[1][2] = %d\r\n\r\n",&buff[1][2],buff[1][2]);

printf("p = 0x%x *p = %d &p = 0x%x\r\n",p,*p,&p);p++;
printf("p = 0x%x *p = %d &p = 0x%x\r\n",p,*p,&p);p++;
printf("p = 0x%x *p = %d &p = 0x%x\r\n",p,*p,&p);p++;
printf("p = 0x%x *p = %d &p = 0x%x\r\n",p,*p,&p);p++;
printf("p = 0x%x *p = %d &p = 0x%x\r\n",p,*p,&p);p++;
printf("p = 0x%x *p = %d &p = 0x%x\r\n",p,*p,&p);

return 0;
}

输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ .\two-dimensionalArrayPointer.exe
&buff[0][0] = 0x61fe1a buff[0][0] = 1
&buff[0][1] = 0x61fe1b buff[0][1] = 2
&buff[0][2] = 0x61fe1c buff[0][2] = 3
&buff[1][0] = 0x61fe1d buff[1][0] = 4
&buff[1][1] = 0x61fe1e buff[1][1] = 5
&buff[1][2] = 0x61fe1f buff[1][2] = 6

p = 0x61fe1a *p = 1 &p = 0x61fe10
p = 0x61fe1b *p = 2 &p = 0x61fe10
p = 0x61fe1c *p = 3 &p = 0x61fe10
p = 0x61fe1d *p = 4 &p = 0x61fe10
p = 0x61fe1e *p = 5 &p = 0x61fe10
p = 0x61fe1f *p = 6 &p = 0x61fe10

指向指针变量所属地址的指针(双重指针)

指针变量可以指向整型变量或字符型变量,当然也可以指向指针变量的存储地址,简称为双重指针。
例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// double_pointer.cpp
#include<stdio.h>
int main()
{
unsigned char a;
unsigned char *p1;
unsigned char **p2;
a = 10;
p1 = &a;
p2 = &p1;
printf("&a = 0x%x a = %d\r\n",&a,a);
printf("&p1 = 0x%x p1 = 0x%x\r\n",&p1,p1);
printf("&p2 = 0x%x p2 = 0x%x\r\n",&p2,p2);
printf("*p1 = %d\r\n",*p1);
printf("*p2 = 0x%x\r\n",*p2);
printf("**p1 = %d\r\n",**p2);
return 0;
}

输出结果如下:

1
2
3
4
5
6
7
$ .\double_pointer.exe
&a = 0x61fe1f a = 10
&p1 = 0x61fe10 p1 = 0x61fe1f
&p2 = 0x61fe08 p2 = 0x61fe10
*p1 = 10
*p2 = 0x61fe1f
**p1 = 10

指针变量作为函数形参

一般情况下以字符型、整型、数组等作为函数的形参输入,指针也能作为函数的形参输入,主要目的是改变指针指向地址的值,专业术语是通过形参改变实参的值。(起到参数传递的作用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// pointer_parameter.cpp
#include<stdio.h>
unsigned char a;
int SetValue(unsigned char *p)
{
*p = 20; //将20赋值给形参所指向的的地址位
return 0;
}
int main()
{
SetValue(&a);
printf("a = %d\r\n",a); //起到传递参数的作用
return 0;
}

输出结果如下:

1
2
$ .\pointer_parameter.exe
a = 20

函数指针

如果在程序中定义了一个函数,那么在编译时系统就会为这个函数分配一段存储空间,这段存储空间的首地址称为这个函数的地址。在C语言中,函数名代表的就是函数的地址。既然是地址我们就可以定义指针变量来存放,那么这个指针变量就叫做函数指针变量,简称函数指针。
定义格式:
函数返回值类型(*指针变量名)(形参列表);

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// function_pointers.cpp
#include<stdio.h>
unsigned char (*func1)(unsigned char,unsigned char);
unsigned char (**func2)(unsigned char,unsigned char);
unsigned char sum(unsigned char v1,unsigned char v2)
{
return (v1+v2);
}
int main()
{
unsigned char a;
func2 = &func1;
func1 = &sum;
a = (*func2)(1,2); //
printf("a = %d\r\n",a);
return 0;
}

输出结果如下:

1
2
$ .\function_pointers.exe
a = 3

定义了一个指向无符号字符类型函数且有两个形参的函数指针。
在使用的时候,我们将函数的地址赋值给函数指针,然后就可以用函数指针来表示这个函数。

函数指针数组

字符型和整型都可以单独定义,也可以定义数组,同样函数指针也能定义数组。
定义格式:
函数返回值类型(*指针变量名[数组大小])(形参列表);

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// array_of_function_Pointers.cpp
#include<stdio.h>
unsigned char func1();
unsigned char func2();
unsigned char func3();
unsigned char (*func[3]) () = {func1,func2,func3};

unsigned char func1()
{
printf("func1 running!\r\n");
return 0;
}

unsigned char func2()
{
printf("func2 running!\r\n");
return 0;
}

unsigned char func3()
{
printf("func3 running!\r\n");
return 0;
}

int main()
{
func[0]();
func[1]();
func[2]();
return 0;
}

输出结果如下:

1
2
3
4
$ .\array_of_function_Pointers.exe
func1 running!
func2 running!
func3 running!

以上代码块中定义了三个函数和一个函数指针数组,这样就将函数放入到函数指针数组中,想要调用funct1时,不必写出函数,而是直接执行func[3] ();
大大提高了代码的简明性。

函数指针数组控制LED

接下来用一个例子来体现函数指针的优越性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// LEDArrayOfFuncPointers.cpp
#include<stdio.h>
enum
{
LED1,
LED2,
LED3,
LED_sum
};
unsigned char driveLED1(unsigned char sta);
unsigned char driveLED2(unsigned char sta);
unsigned char driveLED3(unsigned char sta);
unsigned char driveLEDSum(unsigned char sta);
unsigned char (*driveLED[4]) (unsigned char sta) = {driveLED1,driveLED2,driveLED3,driveLEDSum};

unsigned char driveLED1(unsigned char sta)
{
if (sta)
{
printf("LED1 ON!\r\n");
}
else
{
printf("LED1 OFF!\r\n");
}
return 0;
}

unsigned char driveLED2(unsigned char sta)
{
if (sta)
{
printf("LED2 ON!\r\n");
}
else
{
printf("LED2 OFF!\r\n");
}
return 0;
}

unsigned char driveLED3(unsigned char sta)
{
if (sta)
{
printf("LED3 ON!\r\n");
}
else
{
printf("LED3 OFF!\r\n");
}
return 0;
}

unsigned char driveLEDSum(unsigned char sta)
{
if (sta)
{
printf("LED all ON!\r\n");
}
else
{
printf("LED all OFF!\r\n");
}
return 0;
}

int main()
{
driveLED[LED1](1);
driveLED[LED2](0);
driveLED[LED3](1);
driveLED[LED_sum](0);
return 0;
}

输出结果如下:

1
2
3
4
5
$ .\LEDArrayOfFuncPointers.exe
LED1 ON!
LED2 OFF!
LED3 ON!
LED all OFF!

使用函数指针似乎使代码更加复杂,实际上增强了代码的可移植性,降低了调用的难度,所以单片机厂商常常使用这种方法来构建他们的库函数,不然难以称之为一款合格的单片机。
而单片机应用开发者也应当尽量使用这种方法,因为这样不但能使后期维护更加方便,而且还能轻易地移植到别的单片机上,开发者在开发过程中应当形成自己的编码体系,而传统的面向过程编程难以形成完整的体系,无法形成核心竞争力。形成自身编码体系之后,好似以不变应万变,开发速度将有飞跃性提升,因为绝大多数代码都是从前写过的,移植即可。