C语言-- 深入理解指针(3)
- 一、字符指针变量
- 二、数组指针变量
- 三、二维数组传参的本质
- 四、函数指针变量
-
- [4.1 函数指针变量的创建](#4.1 函数指针变量的创建)
- [4.2 函数指针变量的使用](#4.2 函数指针变量的使用)
- [4.3 两段奇葩代码](#4.3 两段奇葩代码)
- 五、typedef关键字
-
- [5.1 typedef的使用](#5.1 typedef的使用)
-
- [5.1.1 重定义普通变量类型](#5.1.1 重定义普通变量类型)
- [5.1.2 重定义指针变量类型](#5.1.2 重定义指针变量类型)
- [5.1.3 重定义数组指针变量类型](#5.1.3 重定义数组指针变量类型)
- [5.1.4 重定义函数指针变量类型](#5.1.4 重定义函数指针变量类型)
- [5.2 typedef 和 define 的区别](#5.2 typedef 和 define 的区别)
- 六、函数指针数组
- 七、转移表
前言: 回顾上篇,我们学习了一维数组传参的本质,二级指针和指针数组等指针有关知识:
C语言-- 深入理解指针(2)今天我们继续深入了解指针世界:
一、字符指针变量
字符指针变量就是用来存放字符类型变量地址的。
c
int main()
{
char ch = 'w';
char* pc = &ch;//字符指针变量
return 0;
}
我们知道通过指针访问数组可以通过数组名的方式,因为数组名是数组首元素的地址。字符数组也是如此:
c
int main()
{
char str[10] = "abcdefg";
char* p = str;//str表示第一个字符的地址
*p = 'w';
printf("%s\n", str);
return 0;
}
结果:str表示数组首元素的地址,也就是第一个字符的地址,解引用赋值可以改变为其他值。
那么如果我们使用指针变量来存放字符串的地址呢?是否也可以改变?
c
int main()
{
char* p = "hello world!";
*p = 'w';
return 0;
}
结果却是错误的,调试也会报错:
因为这种字符串是常量字符串,不是放在字符数组中的, 常量字符串的内容不能够被改变。指针变量p指向字符串,存放的是字符串中第一个字符的地址。
所以一般使用常量字符串会在前面加上const 修饰指针变量,来更好地约束
c
const char* p = "hello world!";
下面我们来看一道有趣的题目:
问下面的输出结果是什么?
c
int main()
{
char str1[] = "hello world.";
char str2[] = "hello world.";
const char* str3 = "hello world.";
const char* str4 = "hello world.";
if (str1 == str2)
printf("str1 and str2 are same\n");
else
printf("str1 and str2 are not same\n");
if (str3 == str4)
printf("str3 and str4 are same\n");
else
printf("str3 and str4 are not same\n");
return 0;
}
首先对于str1
和str2
是两个不同的数组 ,数组名代表首元素的地址,虽然它们的内容存放的一样,但是地址是肯定不一样的;
然后是str3
和str4
,这两个字符指针变量都指向常量字符串 "hello world."
,因为常量字符串是不能够被修改的 ,所以在 内存中一个常量字符串只会存在唯一的一个 ,没有必要存放多个浪费空间。所以存放在字符指针变量str3和str4中的内存是一样的,都是指向第一个字符的地址。
二、数组指针变量
我们之前学习了指针数组,是一个数组,数组中的每一个元素是指针变量;那么数组指针就是一个指针变量,存放的是数组的地址。
c
int main()
{
int a[10] = { 0 };
int(*p)[10] = &a;// p -> 数组指针
return 0;
}
一定要加括号,不要和指针数组弄混了!
解释:[ ]
的优先级比*
高,所以一定要加括号,让p先与*
结合,说明p是一个指针,指针指向的是一个存放10个元素的数组[10]
,每个元素的类型是int
类型。
c
int(*p1)[10];// p1 -> 数组指针
int* p2[10];// p2 -> 指针数组
数组指针变量把变量名去掉就是它的类型:
c
int(*p1)[10];// p1-> 数组指针变量
int(*)[10];// p1数组指针 的类型
三、二维数组传参的本质
在上一篇文章中,我们学习了一维数组传参的本质,知道了一维数组传参本质上是传递数组首元素的地址,所以一维数组传参既可以写成指针的形式,也可以写成一维数组的形式。
那么二维数组传参和一位数组传参是否类似呢?
首先我们刚开始学习二维数组时,就知道二维数组传参可以直接写成二维数组的形式:
c
void print(int a[3][5], int row, int col)
{
int i = 0;
for (i = 0; i < row; i++)
{
int j = 0;
for (j = 0; j < col; j++)
{
printf("%d ", a[i][j]);
}
printf("\n");
}
}
int main()
{
int arr[3][5] = { {1,1,1,1,1}, {2,2,2,2,2}, {3,3,3,3,3} };
print(arr, 3, 5);
return 0;
}
那么是否可以写成其他形式呢?
我们可以把二维数组看作是一维数组,原本二维数组中的每一行的一维数组就是一维数组中的一个元素,数组名代表首元素的地址,那么二维数组的的首元素是就是第一行的一维数组,那么 二维数组的数组名就表示第一行一维数组的地址。 类型是int(*)[5]
所以二维数组传参的本质就是传递的是第一行的一维数组的地址。
所以二维数组传参可以写成二维数组的形式,也可以写成一维数组的数组指针的形式
c
void print(int(*a)[5], int row, int col)
{
int i = 0;
for (i = 0; i < row; i++)
{
int j = 0;
for (j = 0; j < col; j++)
{
printf("%d ", *(*(a + i) + j));
}
printf("\n");
}
}
int main()
{
int arr[3][5] = { {1,1,1,1,1}, {2,2,2,2,2}, {3,3,3,3,3} };
print(arr, 3, 5);
return 0;
}
a是二维数组首元素的地址,也就是第一行一维数组a[0]的地址;
a + i
可以分别找到每一行的一维数组的地址;
解引用*(a + i)
就等价于分别得到了每一行的一维数组的数组名a[i]
;
一维数组的数组名相当于一维数组首元素的地址,一维数组的首元素是一个int类型的整型数组元素;
所以+j就可以每一次跳过一个int类型的大小,得到每一个数组元素的地址(*(a + i) + j)
;
最后再解引用,就可以依次遍历得到二维数组的每一个数组元素*(*(a + i) + j)
。
四、函数指针变量
4.1 函数指针变量的创建
既然指针可以用来存放数组的地址,用来指向和访问数组,那么函数自然也"难逃一死"。
既然叫做函数指针,那么本质上就是一个指针变量,存放的是一个函数的地址,可以通过函数指针来调用函数。
在数组中,数组名和&数组名,这两者是虽然直接输出的地址可能都是第一个元素的地址,但是还是有区别的,但是对于函数来说, 函数名和&函数名,这两者都是函数的地址,没有任何分别。
c
void test()
{
printf("hello world!\n");
}
int main()
{
printf("%p\n", test);
printf("%p\n", &test);
return 0;
}
结果:
既然函数指针变量是指向函数的,存放函数的地址,那么变量的类型就和函数的参数和返回类型等有关:
c
int Add(int x, int y)
{
return x + y;
}
int main()
{
int (*p)(int, int) = Add;
//int (*p)(int x, int y) = Add; -- 形参名x 和 y可以省略不写
return 0;
}
Add是一个有两个形参(int x, int y)
,返回类型是int
类型的函数。
那么函数指针变量首先是一个指针,因为函数的括号比*
优先级要高,所以要 先使用括号让变量名与*
号结合,说明是一个指针变量(*p)
;
在(*p)
的后面加上(int x, int y)
,在前面加上int
,说明指针指向的是一个参数为(int x, int y)
(参数名x和y可以省略不写),返回类型是int
类型的函数。
如果函数指针变量指向的是一个没有参数,返回类型是void的函数,就可以按照下面这样写:
c
void test()
{
printf("hello world!\n");
}
int main()
{
void (*p)() = test;
return 0;
}
函数指针变量把函数名去掉就是函数指针变量的类型:
c
int (*p)(int, int)//函数指针变量
int (*)(int, int)//类型
4.2 函数指针变量的使用
函数指针变量是指向函数的,所以我们可以通过它来调用函数。
c
int Add(int x, int y)
{
return x + y;
}
int main()
{
int (*pf)(int, int) = Add;
int a = 6;
int b = 8;
printf("%d\n", Add(a, b));
printf("%d\n", (&Add)(a, b));
printf("%d\n", (*pf)(a, b));
printf("%d\n", pf(a, b));
return 0;
}
- 既然Add和&Add一样,都可以表示函数的地址,所以也可以使用
&函数名
来调用函数; - 函数指针变量pf存放的就是函数Add的地址,所以解引用可以得到Add函数名 ,然后进行调用,因为函数括号的优先级比
*
高,所以注意要先使用括号让变量名与*
结合进行解引用(*pf)(a, b)
; - 因为Add和&Add等价,所以使用(*pf)可以,直接使用函数指针变量pf也可以调用函数
pf(a, b)
。
结果:
4.3 两段奇葩代码
NO.1 :请问下面这段代码什么含义?
c
(*(void (*)())0)();
很多人第一眼看到这都是懵的,这是什么奇葩代码啊?
分析:
- 首先,我们在这里面最熟悉的数字0开始下手,0是一个数字然后前面有一个括号,括号中放的是一个
void (*)()
; void (*)()
这不就是我们上面学习的函数指针变量的类型吗,所以这就是将数字0强制类型转换 为void (*)()
这样一个函数指针类型,也就是说拿到了地址为0的一个函数的地址;- 然后对将这个地址与
*
号使用括号结合起来,进行解引用(*(void (*)())0)
,得到函数名,对函数调用 ,函数没有参数 ,所以最后面的单独的一个括号中什么也没有()
,函数也返回类型是void,所以不需要使用变量来接收返回值;
类似于:
c
(*pf)();//pf是函数指针变量,存放函数的地址
- 所以总的来说就是调用一个函数 :地址为0处的,返回类型为
void
,没有参数的函数。
NO.2 :请问下面这段代码什么含义?
c
void (*signal(int, void(*)(int)))(int);
分析:
-
首先我们可以看到
int
和void(*)(int)
分别是整型和函数指针类型,共同放在一个括号里面,很明显这两个是函数的参数 ,函数名是signal
,组成才一起就是signal(int, void(*)(int))
; -
但是缺少函数返回类型;
-
然后我们把整个函数从中去掉,发现剩下的是
void (*)(int);
,这不也是一个函数指针类型吗,其实这就是函数的返回类型。
c
void (*)(int);
- 所以总的来说这句代码的含义就是声明函数 ,声明了一个函数名为
signal
,参数是(int, void(*)(int))
,返回类型是void (*)(int)
的一个函数。
所以要注意:
如果一个函数的返回类型是函数指针变量类型,不能直接按照常规函数的声明一样,将类型直接放在函数名前面,要放在函数指针变量内部,放在*
号的右边。
比如:
返回类型是void (*)(int)
:
这种写法是错误的:
c
void (*)(int)函数名(参数1, 参数2,......,(或者没有参数));
这种写法是正确的:
c
void (* 函数名(参数1, 参数2,......,(或者没有参数)) )(int);
当然可以使用typedef来对函数指针类型进行重定义,使得代码简化易懂,具体会在后面讲解typedef关键字中讲到。
五、typedef关键字
我们再来学习一个C语言中的关键字typedef
,是用来进行类型重定义的,简化代码。
5.1 typedef的使用
5.1.1 重定义普通变量类型
如果我们创建一个无符号的整型可以使用unsigned int
c
unsigned int n = 10;
但是我们有时可能嫌这个类型太长了,使用起来需要敲的代码量有点大,我们想简化一下,就可以使用typedef来对这个类型重定义:
c
typedef unsigned int u_int;
int main()
{
u_int n = 10;
return 0;
}
这就将unsigned int
类型重定义为了u_int
,在代码中就可以直接使用u_int
来创建无符号整型变量了。
5.1.2 重定义指针变量类型
同理也可以重定义指针类型:
c
typedef unsigned int u_int;
typedef int* p_int;
int main()
{
u_int n = 10;//等价于 unsigned int n = 10;
p_int pn = &n;//等价于 int* pn = &n;
printf("%d\n", n);
printf("%d\n", *pn);
return 0;
}
5.1.3 重定义数组指针变量类型
重定义数组指针变量类型和普通指针变量类型有所不同,比如我们知道有整型数组变量类型是:
c
int(*)[5]
如果对其重定义,就要将新的类型名放在*
号的右边,括号()
的里面:
c
typedef int(*p_arr)[5];
int main()
{
int arr[5] = { 1,2,3,4,5 };
int(*p1)[5] = &arr;
p_arr p2 = &arr;//p1和p2类型等价
return 0;
}
5.1.4 重定义函数指针变量类型
在上面我们提到了,如果一个函数的返回类型是函数指针变量类型,不能直接按照常规函数的声明一样,将类型直接放在函数名前面,要放在函数指针变量内部,放在*
号的右边。
但是我们这样不便于我们编写函数和理解函数,所以我们可以对函数指针变量类型进行重定义:
比如对void(*)(int)
类型进行重定义:
c
typedef void(*p_fun)(int);//p_fun就是新的类型名
上面的4.3中的第二个代码就可以换成下面这种方式写:
c
typedef void(*p_fun)(int);
p_fun signal(int, p_fun);
这样就可以避免一些复杂的代码,使函数更加清晰易懂了。
5.2 typedef 和 define 的区别
详见文章:
typedef 和 #define 的区别
六、函数指针数组
我们之前学习过指针数组,是存放指针的数组。那么很显然,函数指针数组就是存放函数指针的数组。
c
int(*parr[3])(int, int) = { 0 };
[]
的优先级比*
高,所以parr先与[3]结合,说明这是这是一个存放三个数据的一个数组,数组名是parr;- 然后把
parr[3]
去掉,剩下int(*)(int, int)
,就是数组元素类型,表示数组中存放的都是函数指针。
函数指针数组也是一个数组,所以数组元素类型都相同,都是。那么函数指针数组中存放的函数指针的类型都必须一样 。
比如:
下面这段代码中,函数指针数组parr中存放的函数指针类型都是int(*)(int, int)
,即指向的都是返回类型是int,参数是两个int类型的整型的函数
c
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
int Mul(int x, int y)
{
return x * y;
}
int main()
{
int(*parr[3])(int, int) = { Add, Sub, Mul };
return 0;
}
七、转移表
我们学习了函数指针数组,那么它到底有何用处呢?我们可以使用它来实现转移表。
比如我们来实现一个计算器,可以按照下面这段代码写:
c
#include <stdio.h>
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
int Mul(int x, int y)
{
return x * y;
}
int Div(int x, int y)
{
return x / y;
}
void menu()
{
printf("************计算器***********\n");
printf("****** 1. Add 2. Sub ******\n");
printf("****** 3. Mul 4. Div ******\n");
printf("****** 0. exit *******\n");
printf("*****************************\n");
}
int main()
{
int input = 0;
do
{
menu();
printf("请选择您的操作:\n");
scanf("%d", &input);
int x = 0;
int y = 0;
int ret = 0;
switch (input)
{
case 1:
printf("请输入两个操作数:\n");
scanf("%d %d", &x, &y);
ret = Add(x, y);
printf("%d\n", ret);
break;
case 2:
printf("请输入两个操作数:\n");
scanf("%d %d", &x, &y);
ret = Sub(x, y);
printf("%d\n", ret);
break;
case 3:
printf("请输入两个操作数:\n");
scanf("%d %d", &x, &y);
ret = Mul(x, y);
printf("%d\n", ret);
break;
case 4:
printf("请输入两个操作数:\n");
scanf("%d %d", &x, &y);
ret = Div(x, y);
printf("%d\n", ret);
break;
case 0:
printf("退出计算器\n");
break;
default:
printf("选择错误请重新选择:\n");
}
} while (input);
return 0;
}
但是我们会发现,在switch语句中,涉及到计算的case 1
到case 4
中的除了调用的函数不一样其他都是相同的,感觉上过于冗余。
思考:
- 既然只有调用的函数不一样,那么我们就可以将函数放在一个数组里,用到哪个就可以通过下标来访问到哪一个函数,拿到函数名然后进行调用,这就可以使用我们的函数指针数组了。
- 将加减乘除四个函数的函数指针(可以直接是函数名)放入函数指针数组中
int(*parr[5])(int, int) = { 0, Add, Sub, Mul, Div };
,但是为了和我们菜单页面中的选择(1:Add 2:Sub 3 :Mul 4 :Div)保持一致,所以要在数组的最前面加上一个0;- 然后就可以直接使用
if-else
语句来实现:
输入的input为
1~4:根据输入的input的具体值,进行具体的运算,得出计算结果,再进入循环重新选择;
0:退出计算机,程序结束;
其他:输入的是错误值,打印错误信息,再进入循环重新选择。
方法一:使用函数指针数组,完整代码:
c
#include <stdio.h>
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
int Mul(int x, int y)
{
return x * y;
}
int Div(int x, int y)
{
return x / y;
}
void menu()
{
printf("************计算器***********\n");
printf("****** 1. Add 2. Sub ******\n");
printf("****** 3. Mul 4. Div ******\n");
printf("****** 0. exit *******\n");
printf("*****************************\n");
}
int main()
{
int input = 0;
int x = 0;
int y = 0;
int ret = 0;//放在循环外面,只需创建一次,节省内存
int(*parr[5])(int, int) = { 0, Add, Sub, Mul, Div };//放在循环外面
do
{
menu();
printf("请选择您的操作:\n");
scanf("%d", &input);
if (input >= 1 && input<= 4)
{
printf("请输入两个操作数:\n");
scanf("%d %d", &x, &y);
ret = parr[input](x, y);
printf("%d\n", ret);
}
else if (input == 0)
{
printf("退出计算器\n");
}
else
{
printf("选择错误,请重新选择:\n");
}
} while (input);
return 0;
}
方法二:使用回调函数
会在下一章节中讲到。
结语:typedef 和 #define 的区别 章节到这里就结束了。
本人才疏学浅,文章中有错误和有待改进的地方欢迎大家批评和指正,非常感谢您的阅读!如果本文对您又帮助,可以高抬贵手点点赞和关注哦!
