[TOC]

零基础学C语言,深入理解指针(4)

1 字符指针变量

我们知道字符指针类型是 char * ,一般的用法是这样:

1
2
3
4
5
6
7
8
#include <stdio.h>
int main ()
{
char a='A';
char * pa = &a;
return 0;

}

还有一种情况是这样:
在这里插入图片描述
这里看着像把一个字符串存到了 p 里,但是其实是把字符串首元素的地址存到了 p 里。

《剑指offer》里面有一个题目:
在这里插入图片描述
输出结果是:

在这里插入图片描述
我们看到 str 1 和 str 2 是不一样的, str 3 和 str 4 是一样的。那这是为什么呢?

要了解这个问题,我们首先要简单知道一些内存的知识。内存有栈区,堆区,还有静态区等等。这里的数组是临时变量,储存在栈区里面,每一个临时变量的创建都需要向内存申请不同的空间,所以这里 str 1 和 str 2 指向的位置就是不一样的。

而 str 3 和 str 4,指向的是常量字符串的首元素,常量字符串是不能再被改变的,它储存在内存的代码块里,因为它是不可改变的,为了节省空间,只需要有一个就可以了。所以, str 3 和 str 4 指向的是同一个地址。

2 数组指针变量

2.1 数组指针变量的定义

顾名思义,数组指针变量就是指向一个数组的指针。在之前的讲解中,我们了解了数组名是数组首元素的地址,但是有两种特殊情况。其中 & 数组名,取出的就是整个数组的地址。用数组指针变量来存储。

如图:
在这里插入图片描述
int ( * p ) [10] 就是数组指针变量。
*是指针的标志, 说明 p 是一个指针变量。因为 [ ] 的优先级比 * 高,所以要加一个括号包起来。

2.2 数组指针变量的初始化

如图:
在这里插入图片描述
就是数组指针变量的初始化。类比一下 int * 类型的指针指针的初始化,int 是指针指向的元素的类型。 这里的 int [10],也是指针指向的元素的类型,也就是这个数组的类型。

我们怎么证明一下呢?如图:

在这里插入图片描述
我们发现 p 和 arr 指向的地址是相同的,而 arr + 1 跳过了四个字节的地址, p + 1 跳过了四十个字节的地址,正好是这个 arr 数组的地址。

3 二维数组传参的本质

我们首先要来重新理解一下二维数组,二维数组的元素除了原理的理解方式,还可以理解为:二维数组的元素是一维数组。

如图:
在这里插入图片描述
这是一个二维数组 arr [3][5],我们是不是可以把它看成是:一个二维数组,它的元素是三个元素个数为 5 的一维数组,分别是 arr [0] ,arr [1] ,arr [2]。

了解了这个以后,我们再来类比一下一位数组的传参,我们知道一位数组的传参可以写成数组的形式,也可以写成指针的形式,所以,二维数组的传参同样可以写成数组和指针的形式。
写成数组的形式:

1
void test (int arr [3] [5]) 

那么写成指针的形式呢?这个指针要指向二维数组的首元素,刚才我们理解到二维数组的元素是一维数组,所以这里的指针指向的就是第一个一维数组的地址:

1
void test (int ( * p ) [10])

4 函数指针变量

和数组指针变量类似,函数指针变量就是指向函数的指针。

函数也是有地址的,我们来验证一下:
在这里插入图片描述
既然函数也有地址,我们同样可以通过这个函数的地址来调用它,指向函数的指针变量就是函数指针变量。

4.1 函数指针变量的创建和使用

函数指针变量的创建形式是这样的,假设它指向一个函数 返回类型是 int 两个参数类型是 int 如: int test (int x,int y)
那么这个函数指针变量就要这么定义:

1
int ( * p )( int , int ) = & test;

函数名就是函数的地址,这里也可以不用取地址。

解释一下,第一个 int 是函数的返回类型,* 是指针的标志,后面括号里的两个 int 是函数的参数的类型。用括号 \ * p 包起来是因为如果不包起来的话就变成了

1
int * p (int ,int )

这是一个返回类型 int * ,名字是 p ,参数类型是 int int 的函数的声明。

那么一个函数指针变量怎么使用呢?
如图:
在这里插入图片描述
因为函数指针变量的值就是函数的地址,解引用了以后还是函数的地址,所以解不解引用都可以,多解几个也可以。

4.2 两段有趣的代码

代码一:

1
(*(void (*)())0)();

代码二:

1
void (*signal(int , void(*)(int)))(int);

代码一是什么意思是呢?

void ( * ) () 是一个类型,它加上括号是强制类型转换,再加上0 意思是把0 强制类型转换成 一个函数地址,然后解引用,最后调用它。

(注)0 这个地址不允许用户访问的。

代码二是什么意思呢?

我们把它拆开看,void (*signal(int , void(*)(int)))(int); 把里面拿出来,就是
void (*)(int);
signal(int, void(*)(int))

所以它表示的就是 signal 函数,返回类型是一个指向 返回值是 void 参数是 int 的函数。

4.3 typedef 关键词

typedef 是类型重定义关键词,可以重命名类型,怎么用呢?很简单:

1
typedef int hello;

这样我们就把 int 这个类型重命名成了 hello 。以后写 int 的地方就可以写 hello 。

1
2
3
4
5
6
7
8
9
#include <stdio.h>
typedef int hello;
int main()
{
hello a=0;
a=20;
printf("%d",a);
return 0;
}

d对于数组指针类型和函数指针类型,typedef 的使用方法有一点不同,如:

1
typedef int (*hello)[10];
1
typeder int (*hello)(int,int);

把重命名后的类型放到括号里面就好了。

5 函数指针数组

函数指针数组就是一个数组里面装的都是函数指针变量,本质是数组。
那么我们怎么初始化呢?

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

int add(int x, int y)
{
return x+y;
}

int sub(int x,int y)
{
return x-y;
}

int main ()
{
int (*calcul[2])( int ,int );
return 0;

}

6 转移表

函数指针数组其实就是转移表,举例:计算器的实现:

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
#include <stdio.h>
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int mul(int a, int b)
{
return a * b;
}
int div(int a, int b)
{
return a / b;
}
int main()
{
int x, y;
int input = 1;
int ret = 0;
do
{
printf("*************************\n");
printf(" 1:add 2.sub \n");

printf(" 3:mul 4:div \n");
printf(" 0:exit \n");
printf("*************************\n");
printf("请选择:");
scanf("%d", &input);
switch (input)
{
case 1:
printf("输⼊操作数:");
scanf("%d %d", &x, &y);
ret = add(x, y);
printf("ret = %d\n", ret);
break;
case 2:
printf("输⼊操作数:");
scanf("%d %d", &x, &y);
ret = sub(x, y);
printf("ret = %d\n", ret);
break;
case 3:
printf("输⼊操作数: ");
scanf("%d %d", &x, &y);
ret = mul(x, y);
printf("ret = %d\n", ret);
break;
case 4:
printf("输⼊操作数\n");
scanf("%d %d", &x, &y);
ret = div(x, y);
printf("ret = %d\n", ret);
break;
case 0:
printf("退出计算器\n");
break;
default:
printf("选择错误\n");
break;
}
} while (input);
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
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
#include <stdio.h>
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int mul(int a, int b)
{
return a * b;
}
int div(int a, int b)
{
return a / b;
}
int main()
{
int x, y;
int input = 1;
int ret = 0;
int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表


do
{
printf("*************************\n");
printf(" 1:add 2:sub \n");
printf(" 3:mul 4:div \n");
printf(" 0:exit \n");
printf("*************************\n");
printf("请选择:" );
scanf("%d", &input);
if ((input <= 4 && input >= 1))
{
printf("输⼊操作数:" );
scanf("%d %d", &x, &y);
ret = (*p[input])(x, y);
printf("ret = %d\n", ret);
}
else if (input == 0)
{
printf("退出计算器\n");
}
else
{
printf("输⼊有误\n" );
}

} while (input);
return 0;
}

这就是转移表的实现方式,当然还涉及了回调函数,我们下一期再讲。

本篇完。