浅析指针与数组

指针是 C/C++ 的精华,如果未能很好的掌握指针也基本等于没学,本篇主要内容有:数组指针、指针数组、函数指针、函数指针数组、指向函数指针数组的指针、指针与数组的区别、多维数组与多级指针。暂且不要觉得这些概念比较复杂,且听我逐一道来!

数组指针

很多人开始把数组指针和指针数组搞不清楚,本人第一次听到这两个名词也是有点晕,但是要搞清楚并不难,数组指针就是一个指针,一个指向数组的指针、而指针数组就是一个数组,一个存放指针的数组。

数组指针的定义

数组指针毕竟是个指针,而且是指向数组的指针。我们知道整型叫 int,指向整型的指针就是 int*。同样的数组也是一种类型,但是准确的说数组的类型有很多种,元素个数不同或者元素类型不同那么数组的类型就不同。

1
2
3
4
5
6
7
8
9
10
11
12
#define _CRT_SECURE_NO_WARNINGS

int main(void)
{
int num = 10;
int* pNum = # // 整型变量的地址使用整型指针来保存

int arr [] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int(*pArr)[10] = &arr; // 数组的地址使用数组指针来保存

return 0;
}

其中 pArr 指向数组 arr,int 是 arr 中元素的类型,[10] 则表示了指向数组元素的个数, 数组类型是由其中存储的元素类型和元素个数共同决定的: 任何一个条件不匹配都会导致编译的时候类型不兼容!

数组指针的使用

数组指针意义就是指向一个数组,接下来看一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>

int main(void)
{

int arr [] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int(*pArr)[10] = &arr;// 数组的地址使用数组指针来保存

int i = 0;
for (i = 0; i < 10; i++)
{
printf("% d ",(* pArr)[i]);
}

return 0;
}

这个基本上看不出数组指针的意义何在,反倒变得更加麻烦了,其实数组指针多用于二维数组传参。数组指针既然是一个指针,那么就是用来接收地址,在传参时就接收数组的地址,所以数组指针在作为函数参数时对应的实参应该是二维数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
void fun(int(*pArr)[4])// 子函数中的形参,指针数组
{
//TODO...
}
int main(void)
{
int a [3][4] = { 0 }; // 主函数中定义的二维数组
fun (a); // 主函数调用子函数的实参,是二维数组的首元素首地址

system ("pause");
return 0;
}

主函数调用子函数的实参,是二维数组的首元素地址,但是二维数组首元素是个一维数组,必须传入一维数组的地址,这个时候我们发现使用 int * 类型作为函数的形参当然是不行的,我们需要的是一个一维数组的地址,也就是指向一维数组的指针,这个时候才是数组指针的用武之地!

很多人在写以一维数组传参的时候写成整型指针 int *,这一点问题都没有,因为参数是匹配的一维数组的首元素地址确实是 int * , 但是接着遇到二维数组传参就会出错,很多人把二维数组传参写成了 int **,这种做法是绝对错误的。

数组作为参数传递

我们指针,在 C 语言中只是存在值传递(地址传递也只是值拷贝),我们在传递单个值得时候就会发生值拷贝,但是如果我们在传递数组的时候也逐个拷贝,如果只是很少的元素还好,一旦元素多了那效率就太低了,于是 C 中在传递数组的时候只是将数组的首元素地址给传递过去,避免了拷贝数组,效率自然就高了!
注意:当一维数组作为函数参数的时候,编译器总是把它解析成一个指向其首元素地址的指针
所以当我们传递二维数组的时候,它的首元素是一个以为数组,传递过去的自然也就是一个指向一维数组的指针!

指针数组

指针数组的定义

指针数组 —— 本质为数组,只不过这是一个存储指针的数组,接下来看看这个存储了 4 个 int* 类型指针的数组

1
2
3
4
5
6
7
8
9
10
11
int main(void)
{
int* pInt1 = NULL;
int* pInt2 = NULL;
int* pInt3 = NULL;
int* pInt4 = NULL;

int* arr [4] = {pInt1, pInt2, pInt3, pInt4};

return 0;
}

上面这个指针的数组的大小在 32 位平台下就是 16 个字节,64 位平台下是 32 字节,无论指针指向什么内容,指针的大小只是和平台有关,所以数组的大小是非常容易确定的!

1
2
3
4
5
6
7
8
9
10
11
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>

int main(void)
{
char* arr [4] = {"AAA", "BBB", "CCC", "DDD"};

// TODO...
return 0;
}

有时候我们常见这种写法,其实也不难理解,只要想明白它们在内存中的布局就可以轻松解决问题

内存 内容 权限
栈区 函数中的普通变量 可读 可写
堆区 动态申请的内存 可读 可写
静态变量区 static 修饰的变量 可读 可写
数据区 用于初始化变量的常量 只读

指针数组的使用

指针数组常用在 main 函数传参,在写主函数时,参数有两个,一个确定参数个数,一个这是指针数组用来接收每个参数(字符串)的地址

1
2
3
4
5
int main(int argc,char* argv [])
{
// TODO...
return 0;
}

此时可以想象内存映像图,主函数的栈区有一个叫 argv 的数组,这个数组的元素是你输入的参数的地址,指向着只读数据区。

指针数组对应着二级指针

这个其实也不难理解,我们看看 main 函数的另一种写法:

1
2
3
4
5
int main(int argc,char** argv)
{
// TODO...
return 0;
}

这和传递一个普通数组的思想一样,不能传递整个数组过去,如果数组很大,这样内存利用率很低,所以应该传递数组的首地址,用一个指针接收这个地址。因此,指针数组对应着二级指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>

void fun(char* arr [])
{
// TODO...
}

void fun2(char** arr)
{
// TODO...
}
int main(void)
{
char* arr [4] = {"AAA", "BBB", "CCC", "DDD"};

fun (arr);
fun2 (arr);

return 0;
}

数组名当参数传递表示首元素地址,而指针数组的元素本来就是指针,所以接受参数的时候使用二级指针是完全正确的!!!

函数指针

函数指针也是一个指针,只不过这个指针是指向函数的指针。C 在编译时,每一个函数都有一个 入口地址 ,该入口地址就是函数指针所指向的地址。有了指向函数的指针变量后,可用该指针变量调用函数,就如同用指针变量可引用其他类型变量一样,在这些概念上是大体一致的。函数指针有两个用途:调用函数和做函数的参数。

普通函数指针

普通函数指针的简单使用呢,参考示例:

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
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>

int add(int num1,int num2)
{
return num1 + num2;
}

int main(void)
{

int(*pAdd)(int, int) = NULL;

pAdd = &add;
pAdd = add;

int ret = 0;

ret = pAdd (2, 3);
printf("ret = % d\n",ret);

ret = (* pAdd)(2, 3);
printf("ret = % d\n", ret);


system ("pause");
return 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
25
26
27
28
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<stdarg.h>

void fun(int num,...)
{
// TODO...
}

int main(void)
{

void(*pFun)(int, ...) = NULL;

pFun = &fun;
pFun = fun;

pFun (3, 11, 22, 33);
(* pFun)(3, 11, 22, 33);

pFun (4, 11, 22, 33, 44);
(* pFun)(4, 11, 22, 33, 44);


system ("pause");
return 0;
}

可变参数函数指针与普通函数指针用法是一致的!

指针函数和函数指针

指针函数是指返回值是指针的函数,即本质是一个函数。我们知道函数都有返回类型,只不过指针函数返回类型是某一类型的指针。返回类型可以是任何基本类型和复合类型。

函数指针的重要作用:回调函数

回调函数:回调函数是一个通过函数指针调用的函数,回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方进行调用,用于对该事件或条件进行响应。

示例一

这个示例演示了非常简单的回调函数机制,首先我们定义了一个求 3 个数字加法的函数 add,接着定义了求平均值的函数 get_average,由于 get_average 的函数参数列表中有一个参数类型是函数指针,并且我们在使用的时候将 add 函数放入了这个函数指针中,所以导致的必然结果就是调用 get_average 函数必然会去调用 add 函数,这就是回调函数机制!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>

int add(int a, int b, int c)
{
return a + b + c;
}

double get_average(int a, int b, int c, int(*pAdd)(int, int, int) )
{
int ret = pAdd (a, b, c);
return ret / 3;
}

int main(void)
{
double ret = get_average (12, 15, 17, add);
return 0;
}

示例二

qsort 函数的使用

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
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>

typedef struct Stu{
int age;
char name [20];
}Stu;

// 通过年龄来比较
int cmp_age(const void * e1, const void * e2)
{
if ((((Stu*) e1)->age) > (((Stu*) e2)->age))
{
return (((Stu*) e1)->age) - (((Stu*) e2)->age);
}
return 0;
}
// 通过名字来排序
int cmp_name(const void * e1, const void * e2)
{
return strcmp(((Stu*) e1)->name, ((Stu*) e2)->name);
}

void print_Stu(Stu arr [], int len)
{
int i = 0;
for (i = 0; i < len; i++)
{
printf("[age = % d,name = % s] \n", arr [i].age, arr [i].name );
}
}

int main(void)
{

Stu arr [] = { { 21, "tim" }, { 16, "lilililala" }, { 20, "joker" }, { 18, "avinla" } };
int len = sizeof(arr) / sizeof(arr [0]);

qsort (arr, len, sizeof(Stu), cmp_age);
print_Stu (arr, len);

printf("-------------------------\n");

qsort (arr, len, sizeof(Stu), cmp_name);
print_Stu (arr, len);

system ("pause");
return 0;
}

很显然我们是对结构体进行排序,并不是简单地 int 或者 double 型,自定义的结构体类型当然只有我们自己才知道排序规则,调用的两次比较函数一次是通过年龄比较、一次是通过名称比较,然后同一个函数是怎样明确我们的比较规则的呢?这当然就是回调函数的功劳了,我们只需要改变函数指针的值就可以吧我们定义的规则传进去,这样对再复杂的结构体只要我自定义排序规则,将这个 “规则” 函数传入即可完成排序!

示例三

自定义实现 qsort,首先我们看看 qsort 的参数说明:

void qsort ( void base, size_t num, size_t size, int ( comparator ) ( const void , const void ) );

1、第一个参数 base 是需要排序的目标数组名 (或者也可以理解成开始排序的地址,因为可以写 & s [i] 这样的表达式)
2、第二个参数 num 是 参与排序的目标数组元素个数
3、第三个参数 width 是单个元素的大小 (或者目标数组中每一个元素长度),推荐使用 sizeof (s [0]) 这样的表达式)

4、第四个参数 compare 比较函数,自定义的算法

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
76
77
78
79
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>

typedef struct Stu{
int age;
char name [20];
}Stu;

// 通过年龄来比较
int cmp_age(const void * e1, const void * e2)
{
if ((((Stu*) e1)->age) > (((Stu*) e2)->age))
{
return (((Stu*) e1)->age) - (((Stu*) e2)->age);
}
return 0;
}
// 通过名字来排序
int cmp_name(const void * e1, const void * e2)
{
return strcmp(((Stu*) e1)->name, ((Stu*) e2)->name);
}

void print_Stu(Stu arr [], int len)
{
int i = 0;
for (i = 0; i < len; i++)
{
printf("[age = % d,name = % s] \n", arr [i].age, arr [i].name );
}
}
void _swap (char *e1,char *e2,int size)
{
assert ((e1 != NULL) && (e2 != NULL));
while (size--)
{
char tmp = *e1;
*e1 = *e2;
*e2 = tmp;
e1++;
e2++;
}
}
void my_qsort(void * base, int num, int size, int(*cmparator) (const void *, const void *))
{
int i = 0;
int j = 0;
assert (base != NULL);
for (i = 0; i < num-1; i++)
{
for (j = 0; j < num - i -1; j++)
{
if (cmparator ((char*) base+j*size,(char*) base+(j+1)*size)>0)
{
// 执行交换
_swap ((char*) base + j*size, (char*) base + (j + 1)*size ,size);
}
}
}
}
int main(void)
{

Stu arr [] = { { 21, "tim" }, { 16, "lilililala" }, { 20, "joker" }, { 18, "avinla" } };
int len = sizeof(arr) / sizeof(arr [0]);

my_qsort (arr, len, sizeof(Stu), cmp_age);
print_Stu (arr, len);

printf("-------------------------\n");

my_qsort (arr, len, sizeof(Stu), cmp_name);
print_Stu (arr, len);
system ("pause");
return 0;
}

函数指针的小练习

((void (*)()) 0)();
把 0 强制类型转换为函数指针类型,具体类型为 (void (*)), 然后对这个函数指针解引用就把它当成了函数使用。

void ( \*signal (int,void (\* )(int)))(int)
signal 是一个函数声明,signal 有两个参数,一个是 int,一个是函数指针。该函数指针指向的函数有一个 int 参数,返回值类型为 void,signal 函数的返回值类型为一个函数指针,该函数指针指向的函数有一个 int 参数,返回类型为 void。

函数指针数组的应用:转移表

函数指针数组的概念其实不难理解,本质还是一个数组,这个数组中存放的元素类型是函数指针。

转移表其实就是和状态相关,我们在实际应用中使用 if-else 结构或者 switch 语句进行一些状态的切换。但是如果遇到比较复杂情况,转移次数达到数百次或者数千次,如果再使用 if-else 结构或者 switch 语句,维护起这个软件系统,工作量将会相当大。这个时候可以采用” 转移表” 来避免这个情况。

我们要实现一个计算器程序,我们可能就会用到这样的 switch 分支结构:

1
2
3
4
5
6
7
8
9
10
11
12
switch(operation)  
{
case ADD:
result=add (a,b);break;
case SUB:
result=sub (a,b);break;
case MUL:
result=mul (a,b);break;
case DIV:
result=div (a,b);break;
.....
}

如果这个计算器要实现的功能很多,那么将有很多这样的语句,可维护性很差。如果我们将具体的数值操作与选择操作的代码分开将会提高代码的可读性。这种情况下,我们需要建立一个 “转移表”。在建立转移表之间需要对涉及到的函数提前声明,然后建立转移表,对于上面的可以这么修改:

那么建立的转移表如下:

1
double (*operation_fun [])(double,double)={add,sub,mul,div,......};

在调用的时候可以这样操作:

1
2
double result;
result=operation_fun [operation](a,b);

上面两句可以替换 switch 语句,使得程序的可维护性大大增强!

指向函数指针数组的指针

指向函数指针数组的指针是一个指针 ,指针指向一个数组 ,数组的元素都是函数指针,如 void (\*(\*p)[10]) )(void) 这样的形式它表示:一个指向有 10 个元素 、每个元素为指向一个返回值为空的函数的数组的这样一个指针!

指针与数组的区别

两种情况:
①定义为数组、声明为指针,错误
②定义为指针、声明为数组,错误
注意:声明一个变量时是不会创建空间的!

二维数组和二级指针参数

二维数组和二级指针参数的等效关系

数组参数 等效的指针参数
数组的数组:char arr [3][4] 数组的指针:char (*p)[10]
指针数组:char *arr [5] 指针的指针:char ** p

在 C 语言中,当一位数组作为函数参数的时候,编译器总是把它解析成为一个指向其首元素地址的指针。但是这条规则并不是递归的,也就是说只有一维数组才是如此,当数组超过一维时,将第一维改写为指向数组元素首地址的指针之后,后面的维再也不可改写。比如:a [3][4][5] 作为参数时可以被修改为 (*p)[4][5])

至于超过二维数组和超过二级的指针,由于本身很少使用,在此不做讨论!

指针和数组的对比

在《天龙八部》八部中,乔峰血战聚贤庄,一套平凡的太祖长拳打得虎虎生威,在场英雄无不佩服至极,这是其苦练的结果!C 语言是程序员的内功,无论招式如何华丽只有内功深厚才是正道,注意这一点等于掌握编程的半壁江山!!!